feat(quickshell): add HyprChromeShell, workspaces panel, persistent backdrop
HyprChromeShell owns what the rail's surfaces have to agree on: the target screen, the density and the shortcut that toggles it, and the bar/backdrop layer PAIR. That pair is why the wrapper exists — the backdrop has to sit exactly one layer below the bar in both densities, and split across two files the two assignments drifted apart and put the scrim over the bar. HyprChromeBar is now just its own surface; ChromeBackdrop takes its layer. The backdrop renders in both densities instead of fading out: full-screen while expanded, scoped to the band the rail occupies while collapsed, with a proportional tail fading off the bottom of that band. Both gradients end on their solid color when there is no fade — with a zero-length ramp the start and end stops coincide, Qt sorts stops unstably, and the transparent one winning turned "no fade" into a ramp across the whole scrim. WorkspacesPanel, between host and vitals, shows the active workspace per monitor: one accent box per output collapsed, the full strip with the shown one filled expanded, lined up in a column behind a fixed-width name cell. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01U19X5LGTxtq4pb4jdNVivv
This commit is contained in:
@@ -1,72 +1,35 @@
|
||||
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.workspaces
|
||||
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 {
|
||||
// The dense status rail itself: one layer surface holding the panel row.
|
||||
//
|
||||
// It owns nothing shared — the screen, the density and its layer all arrive
|
||||
// from HyprChromeShell, which is also what keeps this surface and the backdrop
|
||||
// one layer apart. What it does own is its own measurement: `contentHeight` is
|
||||
// the settled height of the current density, which the shell hands to the
|
||||
// backdrop and which sizes the exclusive zone.
|
||||
PanelWindow {
|
||||
id: window
|
||||
|
||||
screen: root.targetScreen
|
||||
visible: root.targetScreen !== null
|
||||
// Density for every panel on the rail; driven by the shell.
|
||||
property bool expanded: false
|
||||
|
||||
// Which layer to sit on. An int rather than a private decision: it is half
|
||||
// of a pair with the backdrop's, so the shell derives both. See
|
||||
// HyprChromeShell.
|
||||
property int wlrLayer: WlrLayer.Overlay
|
||||
|
||||
// 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
|
||||
WlrLayershell.layer: window.wlrLayer
|
||||
|
||||
property int margin: 12
|
||||
|
||||
@@ -77,8 +40,8 @@ Scope {
|
||||
//
|
||||
// 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
|
||||
readonly property real expandedContent: Math.max(hostPanel.expandedHeight, workspacesPanel.expandedHeight, vitalsPanel.expandedHeight, trayPanel.expandedHeight) + window.margin * 2
|
||||
readonly property real contentHeight: Math.max(hostPanel.targetHeight, workspacesPanel.targetHeight, vitalsPanel.targetHeight, trayPanel.targetHeight) + window.margin * 2
|
||||
|
||||
implicitHeight: Math.round(window.expandedContent)
|
||||
|
||||
@@ -105,21 +68,35 @@ Scope {
|
||||
HostPanel {
|
||||
id: hostPanel
|
||||
|
||||
expanded: root.expanded
|
||||
// Vitals butts against its right edge; the left end of the row is free.
|
||||
expanded: window.expanded
|
||||
// Workspaces butt against its right edge; the left end of the row is free.
|
||||
rightChamfer: false
|
||||
|
||||
toggleOnClick: false
|
||||
Layout.preferredWidth: 350
|
||||
Layout.preferredWidth: 400
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
WorkspacesPanel {
|
||||
id: workspacesPanel
|
||||
|
||||
expanded: window.expanded
|
||||
// Mid-rail: a panel on either side, so neither corner is cut.
|
||||
leftChamfer: false
|
||||
rightChamfer: false
|
||||
|
||||
toggleOnClick: false
|
||||
// No preferred width: the panel sizes itself to however many outputs
|
||||
// the session has, the same way the tray sizes itself to its items.
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
|
||||
VitalsPanel {
|
||||
id: vitalsPanel
|
||||
|
||||
expanded: root.expanded
|
||||
// Host on the left, the spacer on the right — and a spacer is not a
|
||||
// panel, so that edge keeps its cut.
|
||||
expanded: window.expanded
|
||||
// Workspaces on the left, the spacer on the right — and a spacer is not
|
||||
// a panel, so that edge keeps its cut.
|
||||
leftChamfer: false
|
||||
|
||||
toggleOnClick: false
|
||||
@@ -134,10 +111,9 @@ Scope {
|
||||
TrayPanel {
|
||||
id: trayPanel
|
||||
|
||||
expanded: root.expanded
|
||||
expanded: window.expanded
|
||||
toggleOnClick: false
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user