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:
2026-08-29 03:44:40 +02:00
co-authored by Claude Opus 5
parent e7c30fd39d
commit c69aa4fea2
3 changed files with 35 additions and 37 deletions
@@ -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