Files
homelab/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml
T
darmanandClaude Opus 5 7eb7b948e8 feat(quickshell): dim the desktop behind the expanded hyprchrome bar
A full-screen scrim carrying the dense bar's drafting grid, shown while the
rail is expanded and faded out with it.

It sits on the TOP layer while the bar moves to OVERLAY. Both on one layer
would stack by surface creation order, which is not something to rely on; a
layer apart makes "above windows, below the bar" a guarantee. The mask is an
empty Region, so the scrim takes no clicks and reserves nothing.

The grid is the dense bar's, at twice the spacing and with registration
crosses on every other intersection. Its 0.018 opacity was tuned against a
near-black panel and is invisible over a scrim on lit windows, so grid and
crosses are both properties rather than constants, and the lines take a
desaturated accent derived from the palette instead of plain text colour.

Cross geometry rounds with Math.floor on both the mark's offset and the bars
inside it. anchors.*Center halves the box unfloored, which put an even-sized
mark half a pixel off the 1px rule it registers against. crossThickness is in
steps for the same reason: 1 -> 1px, 2 -> 3px, 3 -> 5px, since only an odd
width straddles a rule symmetrically.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
2026-08-29 03:25:14 +02:00

148 lines
4.3 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.
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.
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
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
}
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
}
}
}
}