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
137 lines
4.2 KiB
QML
137 lines
4.2 KiB
QML
import QtQuick
|
|
import QtQuick.Shapes
|
|
import QtQuick.Layouts
|
|
import Quickshell
|
|
import Quickshell.Hyprland
|
|
import Quickshell.Wayland
|
|
import qs.hyprchrome.widgets
|
|
import qs.hyprchrome.widgets.host
|
|
import qs.hyprchrome.widgets.vitals
|
|
import qs.hyprchrome.widgets.tray
|
|
|
|
Scope {
|
|
id: root
|
|
|
|
// Monitor the rail lives on. Falls back to the FIRST connected screen when
|
|
// the name matches nothing, so the bar still appears on a single-monitor
|
|
// session or after a cable swap (DebugWindow falls back to the last one
|
|
// instead — it wants the secondary).
|
|
property string screenName: "DP-2"
|
|
|
|
// Quickshell.screens is a QML list, not a JS array — no .find() on it.
|
|
readonly property var targetScreen: {
|
|
const screens = Quickshell.screens;
|
|
if (screens.length === 0)
|
|
return null;
|
|
for (let i = 0; i < screens.length; i++) {
|
|
if (screens[i].name === root.screenName)
|
|
return screens[i];
|
|
}
|
|
return screens[0];
|
|
}
|
|
|
|
// Density is a property of the BAR: every panel follows it, so the whole rail
|
|
// expands and collapses as one. Panels keep their own animation; only the
|
|
// decision is centralised here.
|
|
property bool expanded: true
|
|
|
|
function toggle() {
|
|
root.expanded = !root.expanded;
|
|
}
|
|
|
|
// SUPER A — see hosts/terra/home/hyprland.nix.
|
|
GlobalShortcut {
|
|
name: "chrome"
|
|
description: "Expand or collapse the hyprchrome bar"
|
|
onPressed: root.toggle()
|
|
}
|
|
|
|
// Scrim first: it is a layer below the bar, so stacking does not depend on
|
|
// creation order, but keeping the declaration order the same as the visual
|
|
// order costs nothing.
|
|
ChromeBackdrop {
|
|
screen: root.targetScreen
|
|
active: root.expanded
|
|
}
|
|
|
|
PanelWindow {
|
|
id: window
|
|
|
|
screen: root.targetScreen
|
|
visible: root.targetScreen !== null
|
|
|
|
// 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
|
|
|
|
// 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
|
|
|
|
implicitHeight: Math.round(window.expandedContent)
|
|
|
|
exclusionMode: ExclusionMode.Normal
|
|
exclusiveZone: Math.round(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; }
|
|
|
|
color: "transparent"
|
|
|
|
RowLayout {
|
|
id: panelRow
|
|
x: window.margin
|
|
y: window.margin
|
|
width: window.width - window.margin * 2
|
|
spacing: 8
|
|
|
|
HostPanel {
|
|
id: hostPanel
|
|
|
|
expanded: root.expanded
|
|
toggleOnClick: false
|
|
Layout.preferredWidth: 350
|
|
Layout.fillHeight: true
|
|
}
|
|
|
|
VitalsPanel {
|
|
id: vitalsPanel
|
|
|
|
expanded: root.expanded
|
|
toggleOnClick: false
|
|
Layout.preferredWidth: 500
|
|
Layout.fillHeight: true
|
|
}
|
|
|
|
// Slack lives between the left group and the tray, so the tray sits
|
|
// flush right whatever the other panels measure.
|
|
Item { Layout.fillWidth: true }
|
|
|
|
TrayPanel {
|
|
id: trayPanel
|
|
|
|
expanded: root.expanded
|
|
toggleOnClick: false
|
|
Layout.fillHeight: true
|
|
}
|
|
}
|
|
}
|
|
}
|