252 lines
11 KiB
QML
252 lines
11 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
|
|
import qs.HyprChrome.Widgets.Launcher
|
|
import qs.HyprChrome.Widgets.Askpass
|
|
|
|
// 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 a MODAL is open — the polkit prompt, the launcher, or the sudo
|
|
// askpass dialog. Each raises the same scrim the rail uses, freezes the
|
|
// density, lands on the focused monitor and takes the keyboard off the
|
|
// rail, so several surfaces read it. That is why they live here rather than
|
|
// as siblings of the remaining launcher variants 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 (shell.modalOpen)
|
|
return;
|
|
|
|
shell.expanded = !shell.expanded;
|
|
}
|
|
|
|
// The surfaces that take over the screen: they dim EVERY output, land on the
|
|
// focused one, and take the keyboard off the rail. Grouped because
|
|
// everything below treats them alike, so a fourth one joins by being named
|
|
// here and nowhere else.
|
|
readonly property bool modalOpen: polkit.prompting || launcher.active || askpass.active
|
|
|
|
// Whether the scrim is up, from ANY cause. This is the fact the surfaces
|
|
// actually share — the rail's density is only one of the things that can
|
|
// raise it — so the backdrop and the layer pair below key off this rather
|
|
// than off `expanded`.
|
|
//
|
|
// One backdrop instance serves all of them. A modal opening over an already
|
|
// expanded rail therefore changes nothing about the scrim on that monitor:
|
|
// it is already up, already full height, and the modal simply appears above
|
|
// it. Over a COLLAPSED rail the same scrim expands 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 || shell.modalOpen
|
|
|
|
// 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: shell.modalOpen
|
|
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
|
|
|
|
// ESC closes the rail — but only when it is the innermost thing open.
|
|
// While a prompt is up the rail gives up the keyboard so ESC dismisses
|
|
// the DIALOG; the prompt closing hands it back, and the next ESC closes
|
|
// the rail. Withheld rather than left to the compositor to arbitrate
|
|
// between two exclusive surfaces, which would decide by stacking and
|
|
// silently swap the order the day the layers change.
|
|
grabsKeyboard: shell.expanded && !shell.modalOpen
|
|
|
|
onDismissed: shell.expanded = false
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// Primary application launcher — SUPER_L. Migrated out of
|
|
// widgets/launcher/; the ten remaining variants are still evaluation copies
|
|
// and stay in shell.qml. Declared after the bar for the same reason the
|
|
// prompt is: while it is open the bar is on Overlay too, and there is no
|
|
// layer above Overlay to escape to.
|
|
AppLauncher {
|
|
id: launcher
|
|
|
|
screen: shell.focusedScreen
|
|
}
|
|
|
|
// GUI password prompt for `sudo -A`. Not the polkit agent — sudo cannot use
|
|
// one — but it renders the same dialog. See the file for the flow.
|
|
AskpassPrompt {
|
|
id: askpass
|
|
|
|
screen: shell.focusedScreen
|
|
}
|
|
}
|