At spacing 0 a row of panels read as separate tiles, each closing its own silhouette. BarPanel can now drop either cut corner — rightChamfer (top-right) and leftChamfer (bottom-left), the only two the shape cuts — so an edge that a neighbour butts against runs square into it. The bar turns off the host's right and the vitals' left; the tray keeps both, since the spacer between them is not a panel and that edge is free. A side with its chamfer off also draws a connector: the shared edge restroked in accent at outlineWidth + 2, so the join reads as a deliberate seam rather than two outlines that happen to touch. The outline's own width becomes a property so the connector can be defined against it instead of as a second literal. The lower-right accent strip takes accentLineWidth, the same slug-derived width the upper-left one already used, instead of its own Math.min(49, width / 3). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HEtXvseVb5gwAtKNhFx2PU
144 lines
4.4 KiB
QML
144 lines
4.4 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.
|
|
// 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
|
|
|
|
property int margin: 12
|
|
|
|
// The surface never resizes: it is always tall enough for the expanded
|
|
// rail, and only the exclusive zone tracks the current state. Resizing a
|
|
// layer surface makes Hyprland animate the change, which showed up as the
|
|
// panels twitching a pixel or two the moment the collapse finished.
|
|
//
|
|
// 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
|
|
|
|
implicitHeight: Math.round(window.expandedContent)
|
|
|
|
exclusionMode: ExclusionMode.Normal
|
|
exclusiveZone: Math.round(window.contentHeight)
|
|
|
|
// Only the panels take input. Without this the surface would keep eating
|
|
// clicks across its full height while the rail is collapsed.
|
|
mask: Region {
|
|
item: panelRow
|
|
}
|
|
|
|
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: 0
|
|
|
|
HostPanel {
|
|
id: hostPanel
|
|
|
|
expanded: root.expanded
|
|
// Vitals butts against its right edge; the left end of the row is free.
|
|
rightChamfer: false
|
|
|
|
toggleOnClick: false
|
|
Layout.preferredWidth: 350
|
|
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.
|
|
leftChamfer: false
|
|
|
|
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
|
|
}
|
|
}
|
|
}
|
|
}
|