fix(quickshell): stop the bar twitching when a collapse finishes
Two causes, both outside the panel animation itself. bodyBottom and bodyEndPadding are animated reals, so their 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 and targetHeight now round. The rest was the surface. Hyprland animates layer-surface resizes (animations enabled, `layers` left at its default), and the deferred shrink put that resize exactly where the panel motion ended. The bar is now sized once to the expanded rail via the new BarPanel.expandedHeight and never resizes; only exclusiveZone tracks the state, so the desktop still reflows once per toggle, at the start. That retires barHeight, the content-height handler and the 340ms shrink timer. A surface that stays tall would swallow clicks across the screen while the rail is collapsed, so input is masked to the panel row. Instrumenting BarPanel per frame ruled the panels themselves out first: slug and summary hold the same absolute y through an entire collapse. The bar and scrim also get their own layer namespaces. Nothing depends on them yet; they are the handle for a layerrule that would exempt the rail from compositor animations without catching the launchers, which share the default "quickshell" namespace and do want their fade. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
This commit is contained in:
@@ -47,6 +47,7 @@ PanelWindow {
|
|||||||
|
|
||||||
visible: scrim.opacity > 0
|
visible: scrim.opacity > 0
|
||||||
|
|
||||||
|
WlrLayershell.namespace: "hyprchrome-scrim"
|
||||||
WlrLayershell.layer: WlrLayer.Top
|
WlrLayershell.layer: WlrLayer.Top
|
||||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
exclusionMode: ExclusionMode.Ignore
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
|||||||
@@ -62,44 +62,33 @@ Scope {
|
|||||||
|
|
||||||
// One layer above the scrim, so the stacking is guaranteed rather than
|
// One layer above the scrim, so the stacking is guaranteed rather than
|
||||||
// dependent on surface creation order. See ChromeBackdrop.
|
// 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
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
|
||||||
property int margin: 12
|
property int margin: 12
|
||||||
|
|
||||||
// Where the panels will SETTLE, not where they are mid-transition. Binding
|
// The surface never resizes: it is always tall enough for the expanded
|
||||||
// the surface to the animated height instead resizes the layer surface —
|
// rail, and only the exclusive zone tracks the current state. Resizing a
|
||||||
// and, with the automatic exclusive zone, relayouts every tiled window on
|
// layer surface makes Hyprland animate the change, which showed up as the
|
||||||
// this output — on every frame of the animation.
|
// 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
|
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
|
implicitHeight: Math.round(window.expandedContent)
|
||||||
// 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
|
|
||||||
|
|
||||||
exclusionMode: ExclusionMode.Normal
|
exclusionMode: ExclusionMode.Normal
|
||||||
exclusiveZone: Math.round(window.contentHeight)
|
exclusiveZone: Math.round(window.contentHeight)
|
||||||
|
|
||||||
implicitHeight: window.barHeight
|
// Only the panels take input. Without this the surface would keep eating
|
||||||
|
// clicks across its full height while the rail is collapsed.
|
||||||
onContentHeightChanged: {
|
mask: Region {
|
||||||
if (window.contentHeight > window.barHeight)
|
item: panelRow
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
anchors { top: true; left: true; right: true; }
|
anchors { top: true; left: true; right: true; }
|
||||||
|
|||||||
@@ -93,16 +93,24 @@ Item {
|
|||||||
property real bodyBottom: detail.y + detail.height
|
property real bodyBottom: detail.y + detail.height
|
||||||
property real bodyEndPadding: panel.padding
|
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
|
// Where the panel settles in each state, skipping the values the transition
|
||||||
// the transition passes through. A window sized to this reconfigures once
|
// passes through: a host sizes its surface and its exclusive zone from these
|
||||||
// per toggle rather than once per animation frame — which, for a
|
// rather than from the animated height, so the desktop is relaid out once per
|
||||||
// layer-shell bar with an automatic exclusive zone, is the difference
|
// toggle instead of once per animation frame.
|
||||||
// between one relayout of the desktop and a dozen.
|
// Height of the expanded body regardless of the current state — what a
|
||||||
readonly property real targetHeight: Math.max(panel.minimumHeight, panel.expanded
|
// host needs to size a surface that must not resize when panels collapse.
|
||||||
? detail.y + detail.height + panel.padding
|
readonly property real expandedHeight: Math.round(Math.max(panel.minimumHeight,
|
||||||
: panel.collapsedHeight)
|
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
|
// The two chamfer cuts (top-right at y=chamfer, bottom-left at
|
||||||
|
|||||||
Reference in New Issue
Block a user