quickshell: bar surface is the bar column, frame moves to its own background layer

The bar's PanelWindow was screen-sized because the frame band and every
panel drew into it. Hyprland blurs that namespace, and blur is recomputed
whenever what is behind the surface changes — with a game underneath, that
is every pixel every frame. Measured at 3440x1440 it cost a third of the
card; the diagnostic shrink took game-running GPU 57% -> 38% and made the
stutter unprovokable.

So the surface is now barWidth at rest and grows to the screen only while a
dropdown, the toast or the launcher is up. exclusiveZone is unchanged, so
windows never reflow on resize. chromeSdf's cutout derives from
screen.width rather than the surface width, so the shader is identical at
both sizes and the narrow surface simply clips it.

The frame band moves to its own background-layer surface, below windows and
matched by no blur rule. Its shader hides (rather than the window
unmapping, which would flicker) while the bar is wide, since chromeSdf then
draws the frame itself and melts panels into it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
rope 2026-09-02 12:25:13 +01:00
parent 978a198b43
commit 89eaeca2f9

View file

@ -1474,22 +1474,31 @@ in
// the grab — it self-clears and instantly closes the panel.
WlrLayershell.keyboardFocus: (launcherPanel.open || bar.activeDropdown !== null) && !bar.screenshotPinned ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
// Vertical bar on the left edge. The surface still covers the
// whole screen (the frame band and every panel are drawn into
// it); only the reserved strip is the bar itself.
// Vertical bar on the left edge.
anchors {
top: true
left: true
bottom: true
}
// DIAGNOSTIC — normally bar.screen.width, because the frame
// band and every panel are drawn into this one surface.
// Shrinking it to the bar column clips all of that, which
// looks broken on purpose: the point is to measure whether
// blur and the SDF shader are expensive because of this
// surface's AREA. Revert this line to restore the shell.
implicitWidth: Theme.barWidth
// True while something is drawn outside the bar column and the
// surface therefore has to span the screen.
readonly property bool surfaceWide: bar.activeDropdown !== null
|| launcherPanel.visible || toastItem.visible
// THE performance knob for this whole shell. Hyprland blurs
// this namespace, and blur behind a surface is recomputed
// whenever what is BEHIND it changes — which, with a game
// running underneath, is every pixel of the screen every
// frame. At 3440x1440 that measured ~a third of the card and
// halved framerates; at 46px it is free. So the surface is the
// bar column at rest and only grows while a panel, the toast
// or the launcher is actually on screen, where a momentary
// cost doesn't matter.
//
// exclusiveZone stays fixed at barWidth, so windows never
// reflow when this resizes.
implicitWidth: bar.surfaceWide ? bar.screen.width : Theme.barWidth
exclusiveZone: Theme.barWidth
color: "transparent"
@ -1977,9 +1986,9 @@ in
// buries a full corner radius inside the bar column.
readonly property real surfLeftX: Theme.barWidth - meltInto / 2
property vector4d cutout: Qt.vector4d(
(Theme.barWidth + bar.width - Theme.frameWidth) / 2,
(Theme.barWidth + bar.screen.width - Theme.frameWidth) / 2,
bar.height / 2,
(bar.width - Theme.frameWidth - Theme.barWidth) / 2,
(bar.screen.width - Theme.frameWidth - Theme.barWidth) / 2,
bar.height / 2 - Theme.frameWidth)
property vector4d panel: chrome.visible
? Qt.vector4d(surfLeftX + chrome.width / 2, (panelTop + panelBottom) / 2,
@ -2044,7 +2053,7 @@ in
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
}
x: Math.round((bar.width - openW) / 2)
x: Math.round((bar.screen.width - openW) / 2)
y: bar.height - Theme.frameWidth - openH
width: openW
height: openH
@ -6802,6 +6811,64 @@ in
}
}
// ── Screen frame band, on its OWN background-layer surface.
//
// This used to be drawn into the bar's surface, and it is the
// reason that surface had to be screen-sized — which is what
// made blur behind it cost a third of the GPU. The frame is
// static decoration that never takes input, so it belongs
// below windows: no blur rule matches this namespace, nothing
// ever repaints it, and gaps_out keeps it visible.
//
// Hidden (the shader, not the window — mapping and unmapping a
// layer surface on every panel open would flicker) while the
// bar's own surface is wide, because chromeSdf then draws the
// frame itself and melts panels into it. Two translucent
// copies of the same band would read as a darker strip.
PanelWindow {
id: frameWin
screen: bar.screen
WlrLayershell.namespace: "quickshell-frame"
WlrLayershell.layer: WlrLayer.Background
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
exclusionMode: ExclusionMode.Ignore
anchors { top: true; left: true; right: true; bottom: true }
color: "transparent"
mask: Region {} // decoration only, never takes a click
ShaderEffect {
// Offset past the bar column so this and the bar never
// composite on top of each other.
x: Theme.barWidth
width: frameWin.width - Theme.barWidth
height: frameWin.height
visible: !bar.surfaceWide
// Push the cutout's left edge well off-item so only
// straight edges reach the bar column — a rounded
// corner landing here would leave a nub beside the bar.
readonly property real cutLeft: -2 * Theme.radius
readonly property real cutRight: width - Theme.frameWidth
property vector4d cutout: Qt.vector4d(
(cutLeft + cutRight) / 2,
height / 2,
(cutRight - cutLeft) / 2,
height / 2 - Theme.frameWidth)
// No panels down here — the bar's surface owns those.
property vector4d panel: Qt.vector4d(0, 0, 0, 0)
property vector4d toast: Qt.vector4d(0, 0, 0, 0)
property vector4d launcher: Qt.vector4d(0, 0, 0, 0)
property vector4d fillColor: Qt.vector4d(Theme.barBg.r, Theme.barBg.g, Theme.barBg.b, Theme.barBg.a)
property vector4d borderColor: Qt.vector4d(Theme.base03.r, Theme.base03.g, Theme.base03.b, 1)
property vector2d res: Qt.vector2d(width, height)
property real cutoutR: Theme.radius
property real panelR: Theme.radius
property real meltK: Theme.radius / 2
property real borderW: Theme.borderWidth
fragmentShader: "file://${chromeShader}"
}
}
// ── OSD: volume / brightness level, shown inline in the bar's
// empty middle — the gap between the two clusters exists for
// this. Volume is reactive: Pipewire reports changes from any