Moves PolkitPrompt from shell.qml into HyprChromeShell. Whether a prompt is open is shell state by the same rule as the screen and the layer pair: two surfaces read it. The prompt no longer carries a backdrop of its own. There is one ChromeBackdrop per output and a prompt raises them all, so a prompt over an already-expanded rail reuses the scrim that is there rather than stacking a second one on it, and a prompt over a collapsed rail expands that same scrim from its bar-height band to the whole output. The layer pair now keys off `scrimUp` (expanded OR prompting) rather than off the density, which keeps bar and backdrop exactly one level apart in every state. A prompt over a collapsed rail raises both: BACKGROUND sits under ordinary windows so a scrim there dims nothing, and the bar has to stay one above the scrim or the shell dims its own chrome. The rail is raised but stays collapsed — its layer answers to the scrim, its height to `expanded`. Outputs the rail does not live on get a scrim only while a prompt is up; a modal that dims one monitor and leaves the others lit does not read as modal. Expanding the rail still dims only the rail's screen, which is the existing behaviour and the right one. The dialog follows Hyprland.focusedMonitor rather than the rail's screen — a password prompt belongs where the user is looking — matched by name against Quickshell.screens, falling back to the rail's screen rather than to nothing. SUPER A is frozen while a prompt is open, and dropped rather than queued, so the rail does not spring open the moment the dialog goes. Verified on two monitors via hyprctl layers, both densities, plus the toggle block with an odd number of presses (two cancel out and prove nothing). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GAq2kKCLazZmrKvkd3akud
214 lines
9.4 KiB
QML
214 lines
9.4 KiB
QML
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
|
|
}
|
|
}
|