pragma ComponentBehavior: Bound import Quickshell import Quickshell.Hyprland import Quickshell.Wayland import qs.HyprChrome.Widgets.Bar import qs.HyprChrome.Widgets import qs.HyprChrome.Widgets.Polkit // The hyprchrome shell: owns everything the rail's surfaces have to agree on, // and instantiates them. // // State lives here rather than in any one surface because more than one of them // reads it, and a second reader is what turns a local property into shared // state. Four things qualify so far: // // * which monitor the shell lives on — every surface has to pick the same one // * the density — the whole rail expands and collapses as one, so the toggle // and the shortcut that drives it belong to the shell, not to the bar // * whether an authorization prompt is up — it raises the same scrim the rail // uses and freezes the density while it is open, so two surfaces read it. // That is why the agent lives here rather than as a sibling of the // launchers in shell.qml // * the layer PAIR — the backdrop must sit exactly one layer below the bar in // both densities. Two surfaces on the same layer stack by creation order, // which is not something to rely on; one layer apart is a guarantee. Split // across two files those two assignments drifted apart and the scrim ended // up over the bar, so they are derived together here and passed down. // // A future widget joins by taking `targetScreen` and `expanded` the same way. Scope { id: shell // 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 === shell.screenName) return screens[i]; } return screens[0]; } // Every output the rail does NOT live on. They get a scrim of their own // while a prompt is up, because a modal that dims one monitor and leaves // the others lit does not read as modal at all — and the rail's backdrop // covers exactly one output. readonly property var otherScreens: { const out = []; const screens = Quickshell.screens; for (let i = 0; i < screens.length; i++) { if (screens[i] !== shell.targetScreen) out.push(screens[i]); } return out; } // Where a prompt appears: wherever the user is actually looking, which is // not necessarily where the rail lives. Hyprland reports the focused // monitor by name, and Quickshell.screens is keyed the same way, so the // two are matched by name exactly as targetScreen is above. // // Falls back to the rail's own screen rather than to nothing: a prompt that // fails to place itself would leave its caller blocked on a dialog nobody // can see. readonly property var focusedScreen: { const focused = Hyprland.focusedMonitor; if (!focused) return shell.targetScreen; const screens = Quickshell.screens; for (let i = 0; i < screens.length; i++) { if (screens[i].name === focused.name) return screens[i]; } return shell.targetScreen; } // Density is a property of the SHELL: every panel follows it, so the whole // rail expands and collapses as one. Panels keep their own animation; only // the decision is centralised here. // // A prompt does NOT change it: an authorization request leaves the rail at // whatever density it was, and only freezes it there. property bool expanded: false function toggle() { // Frozen while a prompt is up, and dropped rather than queued: SUPER A // during a prompt does nothing at all, instead of arming a change that // springs the rail open or shut the moment the dialog goes. if (polkit.prompting) return; shell.expanded = !shell.expanded; } // Whether the scrim is up, from EITHER cause. This is the fact the surfaces // actually share — the rail's density is only one of the two things that // can raise it — so the backdrop and the layer pair below key off this // rather than off `expanded`. // // One backdrop instance serves both. A prompt arriving over an already // expanded rail therefore changes nothing about the scrim: it is already up, // already full height, and the dialog simply appears above it. A prompt over // a COLLAPSED rail expands that same scrim from its bar-height band to the // whole output, using the animation it already has, and the rail stays // collapsed throughout. readonly property bool scrimUp: shell.expanded || polkit.prompting // Scrim up, the rail is over everything; scrim down, it drops below ordinary // windows. BOTTOM rather than BACKGROUND for the lowered bar: it is the // lowest level that still leaves a layer underneath for the backdrop, and // it keeps the rail off the wallpaper's own level. // // Both key off `scrimUp`, not `expanded`, so the pair stays exactly one // level apart in every state — which is the whole point of deriving them // together. A prompt over a collapsed rail raises BOTH: the scrim has to // clear ordinary windows to dim them at all (BACKGROUND sits under them), // and the bar has to stay one above the scrim or the shell would be dimming // its own chrome. The rail is raised but still collapsed: its layer answers // to the scrim, its height to `expanded`. readonly property int barLayer: shell.scrimUp ? WlrLayer.Overlay : WlrLayer.Bottom readonly property int backdropLayer: shell.scrimUp ? WlrLayer.Top : WlrLayer.Background // SUPER A — see hosts/terra/home/hyprland.nix. GlobalShortcut { name: "chrome" description: "Expand or collapse the hyprchrome bar" onPressed: shell.toggle() } // Backdrop 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: shell.targetScreen active: shell.scrimUp wlrLayer: shell.backdropLayer // Collapsed, the scrim only backs the rail, so it needs the band the // rail occupies. contentHeight is the SETTLED height for the current // state — it jumps once per toggle rather than tracking the panels // frame by frame, so the backdrop animates the change itself instead of // chasing a value that is already being animated. barHeight: bar.contentHeight } // The same scrim on every other output, up only while a prompt is. These // have no rail to back, so barHeight stays 0 and revealHeight falls to // nothing between prompts — the surfaces take themselves off screen rather // than lingering as a strip the way the rail's does. // // Deliberately NOT tied to `scrimUp`: expanding the rail dims the rail's // monitor only, which is the existing behaviour and the right one — the // rail is a thing on one screen. A prompt is the only event that concerns // every screen at once. // // TOP unconditionally: there is no bar on these outputs to keep one level // above the scrim, and BACKGROUND would put the dim under ordinary windows // where it would dim nothing. Inactive they are invisible, so the level // costs nothing between prompts. Variants { model: shell.otherScreens ChromeBackdrop { required property var modelData screen: modelData active: polkit.prompting wlrLayer: WlrLayer.Top barHeight: 0 } } HyprChromeBar { id: bar screen: shell.targetScreen // Set here, not from the window's own `screen`: reading that inside // `visible` is circular — a hidden window has no screen to report. visible: shell.targetScreen !== null expanded: shell.expanded wlrLayer: shell.barLayer } // Polkit authentication agent. It registers for this logind session on // creation, so it replaces hyprpolkitagent rather than coexisting with it — // only one agent may hold a session (see hosts/terra/home/hyprland.nix). // // It lives here rather than beside the launchers in shell.qml because its // state is shared: `prompting` raises the scrim and freezes the density, // which makes it shell state by the same rule as the screen and the layer // pair. It owns only its dialog; the scrim above is the rail's. // // Declared LAST on purpose. While a prompt is up the bar is on Overlay too, // and there is no layer above Overlay to escape to, so the dialog stays on // top by being the later surface. In practice it is later regardless — its // window only exists while a request is open, so it is always created after // the bar's — but the declaration order says so without relying on that. PolkitPrompt { id: polkit screen: shell.focusedScreen } }