diff --git a/dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml b/dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml index 4de1703..da5857a 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml +++ b/dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml @@ -47,6 +47,7 @@ PanelWindow { visible: scrim.opacity > 0 + WlrLayershell.namespace: "hyprchrome-scrim" WlrLayershell.layer: WlrLayer.Top WlrLayershell.keyboardFocus: WlrKeyboardFocus.None exclusionMode: ExclusionMode.Ignore diff --git a/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml b/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml index 1177859..341a28f 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml +++ b/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml @@ -62,44 +62,33 @@ Scope { // One layer above the scrim, so the stacking is guaranteed rather than // dependent on surface creation order. See ChromeBackdrop. + // Own namespace so a layerrule can exempt the rail from Hyprland's layer + // animation without also catching the launchers, which share the default + // "quickshell" namespace and do want their fade. + WlrLayershell.namespace: "hyprchrome-bar" WlrLayershell.layer: WlrLayer.Overlay property int margin: 12 - // Where the panels will SETTLE, not where they are mid-transition. Binding - // the surface to the animated height instead resizes the layer surface — - // and, with the automatic exclusive zone, relayouts every tiled window on - // this output — on every frame of the animation. + // The surface never resizes: it is always tall enough for the expanded + // rail, and only the exclusive zone tracks the current state. Resizing a + // layer surface makes Hyprland animate the change, which showed up as the + // panels twitching a pixel or two the moment the collapse finished. + // + // The zone still follows the target height, so tiled windows reflow once + // per toggle, at the start, and slide while the panels animate. + readonly property real expandedContent: Math.max(hostPanel.expandedHeight, vitalsPanel.expandedHeight, trayPanel.expandedHeight) + window.margin * 2 readonly property real contentHeight: Math.max(hostPanel.targetHeight, vitalsPanel.targetHeight, trayPanel.targetHeight) + window.margin * 2 - // The surface and the exclusive zone move on different clocks. The zone is - // the desktop-visible half: set it to the target immediately, so the tiled - // windows reflow ONCE, at the start, and slide while the bar animates. - // The surface itself grows before the panels do but shrinks only after they - // have finished, because a surface that shrank immediately would clip the - // panels still animating inside it. - property real barHeight: 0 + implicitHeight: Math.round(window.expandedContent) exclusionMode: ExclusionMode.Normal exclusiveZone: Math.round(window.contentHeight) - implicitHeight: window.barHeight - - onContentHeightChanged: { - if (window.contentHeight > window.barHeight) - window.barHeight = window.contentHeight; - else - shrink.restart(); - } - - Component.onCompleted: window.barHeight = window.contentHeight - - Timer { - id: shrink - - // Longer than the panel's own collapse (200ms body + 110ms fade-in). - interval: 340 - onTriggered: window.barHeight = window.contentHeight + // Only the panels take input. Without this the surface would keep eating + // clicks across its full height while the rail is collapsed. + mask: Region { + item: panelRow } anchors { top: true; left: true; right: true; } diff --git a/dotfiles/quickshell/hyprchrome/widgets/panels/BarPanel.qml b/dotfiles/quickshell/hyprchrome/widgets/panels/BarPanel.qml index 422462f..bcedb43 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/panels/BarPanel.qml +++ b/dotfiles/quickshell/hyprchrome/widgets/panels/BarPanel.qml @@ -93,16 +93,24 @@ Item { property real bodyBottom: detail.y + detail.height property real bodyEndPadding: panel.padding - implicitHeight: Math.max(panel.minimumHeight, panel.bodyBottom + panel.bodyEndPadding) + // Rounded: bodyBottom and bodyEndPadding are animated reals, so the sum + // spends the tail of every transition on a fraction. A layout rounds that + // UP, then drops a pixel the moment the animation lands on its exact + // value — a 1px hop after the motion has visibly finished. + implicitHeight: Math.round(Math.max(panel.minimumHeight, panel.bodyBottom + panel.bodyEndPadding)) - // Where the panel will settle in its current state, skipping the values - // the transition passes through. A window sized to this reconfigures once - // per toggle rather than once per animation frame — which, for a - // layer-shell bar with an automatic exclusive zone, is the difference - // between one relayout of the desktop and a dozen. - readonly property real targetHeight: Math.max(panel.minimumHeight, panel.expanded - ? detail.y + detail.height + panel.padding - : panel.collapsedHeight) + // Where the panel settles in each state, skipping the values the transition + // passes through: a host sizes its surface and its exclusive zone from these + // rather than from the animated height, so the desktop is relaid out once per + // toggle instead of once per animation frame. + // Height of the expanded body regardless of the current state — what a + // host needs to size a surface that must not resize when panels collapse. + readonly property real expandedHeight: Math.round(Math.max(panel.minimumHeight, + detail.y + detail.height + panel.padding)) + + readonly property real targetHeight: panel.expanded + ? panel.expandedHeight + : Math.round(Math.max(panel.minimumHeight, panel.collapsedHeight)) // The two chamfer cuts (top-right at y=chamfer, bottom-left at