From 2bf71494f4cbbda8f038b8ddf733d92aff550f50 Mon Sep 17 00:00:00 2001 From: Erik Simon Date: Mon, 31 Aug 2026 00:30:58 +0200 Subject: [PATCH] feat(quickshell): cap the rail's open chamfers and trace between them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A chamfer with no neighbour behind it now carries the corner it removed, put back OUTSIDE the panel as a detached accent triangle. capGap is the perpendicular distance from the cut, hence the per-axis shift of capGap over root 2: the cap moves along the cut's normal, not along an axis. The trace joining two caps belongs to the RAIL, not the panel — the run it draws is the gap BETWEEN two panels, which no panel can see. HyprChromeBar filters the row down to panels (a slack Item has no `rightChamfer`, so spacers drop out, and dropping them is exactly what makes a trace span them), then joins each right cap to the next left cap: straight, one 45° step at the midpoint of the gap, straight. It meets the middle of each cap's outward face rather than its tip. TestPanel is a staging slot between two spacers. It counts seconds since load, which is the cheapest proof the panel is live, and standing alone it exercises a cap and a trace at both ends — something a rail of butted panels never does. Restructures the module in the same commit, since the moves and the edits above land in the same files: folders are PascalCase, the bar and its widgets moved under Widgets/Bar, and DebugWindow takes the HyprChrome Theme instead of the legacy one. The two singletons are identical today, so that is not a visual fix — it is a palette edit reaching the debug stage in future. Rename detection needs -M40% to follow HyprChromeBar, which grew past the default similarity threshold. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01U19X5LGTxtq4pb4jdNVivv --- .../DebugWindow.qml | 2 +- .../theme => HyprChrome/Theme}/Theme.qml | 0 .../Widgets/Bar/Debug}/LoremPanel.qml | 4 +- .../Widgets/Bar/Debug/TestPanel.qml | 69 ++++++ .../Widgets/Bar/Host}/HostPanel.qml | 4 +- .../HyprChrome/Widgets/Bar/HyprChromeBar.qml | 217 ++++++++++++++++++ .../Widgets/Bar/Panels}/BarPanel.qml | 59 ++++- .../Widgets/Bar/Tray}/TrayIcon.qml | 2 +- .../Widgets/Bar/Tray}/TrayPanel.qml | 4 +- .../Widgets/Bar/Vitals}/GpuBusy.qml | 0 .../Widgets/Bar/Vitals}/SegmentMeter.qml | 2 +- .../Widgets/Bar/Vitals}/VitalRow.qml | 2 +- .../Widgets/Bar/Vitals}/VitalsPanel.qml | 4 +- .../Bar/Workspaces}/WorkspacesPanel.qml | 4 +- .../Widgets}/ChromeBackdrop.qml | 2 +- .../Widgets}/HyprChromeShell.qml | 3 +- .../hyprchrome/widgets/HyprChromeBar.qml | 119 ---------- dotfiles/quickshell/shell.qml | 10 +- 18 files changed, 366 insertions(+), 141 deletions(-) rename dotfiles/quickshell/{hyprchrome => HyprChrome}/DebugWindow.qml (99%) rename dotfiles/quickshell/{hyprchrome/theme => HyprChrome/Theme}/Theme.qml (100%) rename dotfiles/quickshell/{hyprchrome/widgets/debug => HyprChrome/Widgets/Bar/Debug}/LoremPanel.qml (95%) create mode 100644 dotfiles/quickshell/HyprChrome/Widgets/Bar/Debug/TestPanel.qml rename dotfiles/quickshell/{hyprchrome/widgets/host => HyprChrome/Widgets/Bar/Host}/HostPanel.qml (99%) create mode 100644 dotfiles/quickshell/HyprChrome/Widgets/Bar/HyprChromeBar.qml rename dotfiles/quickshell/{hyprchrome/widgets/panels => HyprChrome/Widgets/Bar/Panels}/BarPanel.qml (82%) rename dotfiles/quickshell/{hyprchrome/widgets/tray => HyprChrome/Widgets/Bar/Tray}/TrayIcon.qml (98%) rename dotfiles/quickshell/{hyprchrome/widgets/tray => HyprChrome/Widgets/Bar/Tray}/TrayPanel.qml (96%) rename dotfiles/quickshell/{hyprchrome/widgets/vitals => HyprChrome/Widgets/Bar/Vitals}/GpuBusy.qml (100%) rename dotfiles/quickshell/{hyprchrome/widgets/vitals => HyprChrome/Widgets/Bar/Vitals}/SegmentMeter.qml (97%) rename dotfiles/quickshell/{hyprchrome/widgets/vitals => HyprChrome/Widgets/Bar/Vitals}/VitalRow.qml (98%) rename dotfiles/quickshell/{hyprchrome/widgets/vitals => HyprChrome/Widgets/Bar/Vitals}/VitalsPanel.qml (98%) rename dotfiles/quickshell/{hyprchrome/widgets/workspaces => HyprChrome/Widgets/Bar/Workspaces}/WorkspacesPanel.qml (99%) rename dotfiles/quickshell/{hyprchrome/widgets => HyprChrome/Widgets}/ChromeBackdrop.qml (99%) rename dotfiles/quickshell/{hyprchrome/widgets => HyprChrome/Widgets}/HyprChromeShell.qml (98%) delete mode 100644 dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml diff --git a/dotfiles/quickshell/hyprchrome/DebugWindow.qml b/dotfiles/quickshell/HyprChrome/DebugWindow.qml similarity index 99% rename from dotfiles/quickshell/hyprchrome/DebugWindow.qml rename to dotfiles/quickshell/HyprChrome/DebugWindow.qml index 3bd543c..91aeed2 100644 --- a/dotfiles/quickshell/hyprchrome/DebugWindow.qml +++ b/dotfiles/quickshell/HyprChrome/DebugWindow.qml @@ -4,7 +4,7 @@ import Quickshell import Quickshell.Hyprland import Quickshell.Wayland import QtQuick -import qs.widgets.theme +import qs.HyprChrome.Theme // Debug stage: a bare, chrome-less staging area in the middle of ONE monitor // (the secondary by default), used to look at a widget in isolation before it diff --git a/dotfiles/quickshell/hyprchrome/theme/Theme.qml b/dotfiles/quickshell/HyprChrome/Theme/Theme.qml similarity index 100% rename from dotfiles/quickshell/hyprchrome/theme/Theme.qml rename to dotfiles/quickshell/HyprChrome/Theme/Theme.qml diff --git a/dotfiles/quickshell/hyprchrome/widgets/debug/LoremPanel.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Debug/LoremPanel.qml similarity index 95% rename from dotfiles/quickshell/hyprchrome/widgets/debug/LoremPanel.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Debug/LoremPanel.qml index 96998fb..0e1546f 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/debug/LoremPanel.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Debug/LoremPanel.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound import QtQuick -import qs.hyprchrome.theme -import qs.hyprchrome.widgets.panels +import qs.HyprChrome.Theme +import qs.HyprChrome.Widgets.Bar.Panels // Staging widget for the debug window: a BarPanel carrying filler copy in both // of the panel's densities — the full block when expanded, one elided line when diff --git a/dotfiles/quickshell/HyprChrome/Widgets/Bar/Debug/TestPanel.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Debug/TestPanel.qml new file mode 100644 index 0000000..a3eae22 --- /dev/null +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Debug/TestPanel.qml @@ -0,0 +1,69 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.HyprChrome.Theme +import qs.HyprChrome.Widgets.Bar.Panels + +// Staging slot for whatever is being worked on, sized to itself so it can be +// dropped anywhere in the rail without a width. It counts seconds since the +// shell loaded, which is the cheapest thing that proves the panel is live and +// not a still frame — a reload visibly restarts it. +// +// It also stands ALONE between two spacers, so it is the pair of caps and +// traces that a rail of butted panels never exercises: a right cap joining the +// next panel's left cap across a gap, twice over. +BarPanel { + id: test + + panelId: "TST" + title: "TEST" + meta: "STAGE" + + property int seconds: 0 + + // Sized to its content, like the tray: a staging panel has no business + // reserving a share of the rail. + implicitWidth: Math.max(test.headerMinWidth, + test.briefLeft + brief.implicitWidth + test.padding, + test.padding * 2 + body.implicitWidth) + + Timer { + interval: 1000 + running: true + repeat: true + onTriggered: test.seconds++ + } + + summary: Text { + id: brief + + text: "T+" + test.seconds + color: Theme.accent + font.family: Theme.displayFont + font.pixelSize: 13 + font.bold: true + } + + Column { + id: body + + spacing: 2 + + Text { + text: "T+" + test.seconds + color: Theme.text + font.family: Theme.displayFont + font.pixelSize: 17 + font.bold: true + font.letterSpacing: 1 + } + + Text { + text: "SECONDS SINCE LOAD" + color: Theme.muted + font.family: Theme.microFont + font.pixelSize: 9 + font.letterSpacing: 1.4 + } + } +} diff --git a/dotfiles/quickshell/hyprchrome/widgets/host/HostPanel.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Host/HostPanel.qml similarity index 99% rename from dotfiles/quickshell/hyprchrome/widgets/host/HostPanel.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Host/HostPanel.qml index ec2ad7a..4079473 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/host/HostPanel.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Host/HostPanel.qml @@ -4,8 +4,8 @@ import Quickshell import Quickshell.Io import QtQuick import QtQuick.Layouts -import qs.hyprchrome.theme -import qs.hyprchrome.widgets.panels +import qs.HyprChrome.Theme +import qs.HyprChrome.Widgets.Bar.Panels // Host identity in the hyprchrome panel chrome: the machine's name set large, // with its timezone and the current date and time. diff --git a/dotfiles/quickshell/HyprChrome/Widgets/Bar/HyprChromeBar.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/HyprChromeBar.qml new file mode 100644 index 0000000..4f5f2f2 --- /dev/null +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/HyprChromeBar.qml @@ -0,0 +1,217 @@ +import QtQuick +import QtQuick.Shapes +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland +import qs.HyprChrome.Theme +import qs.HyprChrome.Widgets.Bar.Debug +import qs.HyprChrome.Widgets.Bar.Host +import qs.HyprChrome.Widgets.Bar.Vitals +import qs.HyprChrome.Widgets.Bar.Workspaces +import qs.HyprChrome.Widgets.Bar.Tray + +// 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 + + // 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 + + // 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: window.wlrLayer + + 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, workspacesPanel.expandedHeight, vitalsPanel.expandedHeight, testPanel.expandedHeight, trayPanel.expandedHeight) + window.margin * 2 + readonly property real contentHeight: Math.max(hostPanel.targetHeight, workspacesPanel.targetHeight, vitalsPanel.targetHeight, testPanel.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: window.expanded + // Workspaces butt against its right edge; the left end of the row is free. + rightChamfer: false + + toggleOnClick: false + 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: 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 + Layout.preferredWidth: 500 + Layout.fillHeight: true + } + + // Slack on both sides of the staging panel, so it floats between the left + // group and the tray rather than butting against either. Two spacers is + // also what puts a gap on both of its sides, which is what gives it a cap + // and a trace at each end. + Item { Layout.fillWidth: true } + + TestPanel { + id: testPanel + + expanded: window.expanded + toggleOnClick: false + Layout.fillHeight: true + } + + Item { Layout.fillWidth: true } + + TrayPanel { + id: trayPanel + + expanded: window.expanded + toggleOnClick: false + Layout.fillHeight: true + } + } + + // Traces between the panels' caps. They live here rather than in BarPanel + // because the run they draw is the gap BETWEEN two panels, which is the one + // piece of this geometry no panel can see. Laid over the row, in the row's + // own coordinates, so a panel's x is directly usable. + Item { + id: traces + + x: panelRow.x + y: panelRow.y + width: panelRow.width + height: panelRow.height + + // Consecutive PANELS, with the spacers dropped — a spacer has no caps, so + // it is not something a trace can start or end at, and skipping it is + // exactly what makes the trace span it. `rightChamfer` is the tell: an + // Item put in the row for slack has no such property. + readonly property var pairs: { + const panels = []; + for (let i = 0; i < panelRow.children.length; i++) { + const child = panelRow.children[i]; + if (child.rightChamfer !== undefined) + panels.push(child); + } + + // A cap only exists on an open chamfer, so a pair that has both is + // exactly a pair with something to join. + const found = []; + for (let i = 0; i + 1 < panels.length; i++) { + if (panels[i].rightChamfer && panels[i + 1].leftChamfer) + found.push({ from: panels[i], to: panels[i + 1] }); + } + return found; + } + + Repeater { + model: traces.pairs + + CapTrace { + anchors.fill: parent + } + } + } + + // One run: out of a panel's top-right cap, along the top, one 45° step down + // at the midpoint of the gap, then along the bottom into the next panel's + // bottom-left cap. 45° means the step is as wide as it is tall, so the run + // IS the drop — clamped if the gap is too narrow to fit it, which is the + // only case where the angle gives. + component CapTrace: Item { + id: trace + + required property var modelData + + property int lineWidth: 3 + + readonly property real fromX: trace.modelData.from.x + trace.modelData.from.rightCapX + readonly property real fromY: trace.modelData.from.y + trace.modelData.from.rightCapY + readonly property real toX: trace.modelData.to.x + trace.modelData.to.leftCapX + readonly property real toY: trace.modelData.to.y + trace.modelData.to.leftCapY + + readonly property real drop: trace.toY - trace.fromY + readonly property real gap: trace.toX - trace.fromX + readonly property real step: Math.max(0, Math.min(Math.abs(trace.drop), trace.gap)) + readonly property real mid: (trace.fromX + trace.toX) / 2 + + Shape { + anchors.fill: parent + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: "transparent" + strokeColor: Theme.accent + // Nothing to draw while the panels overlap, which they do for a frame + // or two while the row is still laying itself out. + strokeWidth: trace.gap > 0 ? trace.lineWidth : 0 + startX: trace.fromX; startY: trace.fromY + PathLine { x: trace.mid - trace.step / 2; y: trace.fromY } + PathLine { x: trace.mid + trace.step / 2; y: trace.toY } + PathLine { x: trace.toX; y: trace.toY } + } + } + } +} diff --git a/dotfiles/quickshell/hyprchrome/widgets/panels/BarPanel.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Panels/BarPanel.qml similarity index 82% rename from dotfiles/quickshell/hyprchrome/widgets/panels/BarPanel.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Panels/BarPanel.qml index aee1063..fe8b88e 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/panels/BarPanel.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Panels/BarPanel.qml @@ -2,7 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Shapes -import qs.hyprchrome.theme +import qs.HyprChrome.Theme // Chamfered panel chrome for the dense status rail: outline, corner accent // lines, header strip (id chip / title / meta / tick marks) and a collapsing @@ -135,6 +135,36 @@ Item { property int outlineWidth: 1 readonly property int connectorWidth: panel.outlineWidth + 2 + // A cut corner has nothing butting against it, so it gets capped: the very + // corner the chamfer removed, put back OUTSIDE the panel as a detached + // accent triangle. Its hypotenuse faces the cut and its right angle points + // away, so the cap and the notch read as two halves of one corner. + // + // Nothing guards these — a chamfer that is off measures zero, which + // collapses its triangle to no area at all, so one piece of geometry covers + // both cases. + // + // capGap is the perpendicular distance from the chamfer, which is why the + // per-axis shift is it over root 2 rather than the gap itself: the cap + // moves along the cut's normal, not along an axis. + property real capGap: 4 + readonly property real capOffset: panel.capGap / Math.SQRT2 + + // Where a trace attaches: the MIDDLE of the cap's outward-facing edge — the + // vertical one, since a trace arrives horizontally — rather than the tip, + // so the line meets the triangle's face instead of clipping its corner. + // That edge runs from the cut's end to the corner, so its midpoint is the + // half-way point between them, carried out by the same offset as the cap. + // + // The RAIL draws those traces, between one panel's right cap and the next + // panel's left cap, because the run between two panels is the one piece of + // this that no panel can see. All a panel owes it is where its own caps + // ended up. + readonly property real rightCapX: panel.width + panel.capOffset + readonly property real rightCapY: (panel.offsetY + panel.activeChamfer) / 2 - panel.capOffset + readonly property real leftCapX: -panel.capOffset + readonly property real leftCapY: panel.height - panel.activeChamfer / 2 + panel.capOffset + Shape { id: panelShape anchors.fill: parent @@ -154,6 +184,33 @@ Item { PathLine { x: 0; y: panel.offsetY } } + // Cap on the top-right chamfer: the corner the cut removed, sitting + // just outside it. The two ends of its hypotenuse are the same points + // the outline turns on, shifted clear along the cut's normal; the third + // is the corner itself, which the outline never reaches. + ShapePath { + fillColor: Theme.accent + strokeWidth: 0 + startX: panelShape.width - panel.rightCut + panel.capOffset; startY: panel.offsetY - panel.capOffset + PathLine { + x: panelShape.width + panel.capOffset + y: (panel.rightChamfer ? panel.activeChamfer : panel.offsetY) - panel.capOffset + } + PathLine { x: panelShape.width + panel.capOffset; y: panel.offsetY - panel.capOffset } + PathLine { x: panelShape.width - panel.rightCut + panel.capOffset; y: panel.offsetY - panel.capOffset } + } + + // Cap on the bottom-left chamfer, the same triangle mirrored, clearing + // the panel in the other direction. + ShapePath { + fillColor: Theme.accent + strokeWidth: 0 + startX: panel.leftCut - panel.capOffset; startY: panelShape.height + panel.capOffset + PathLine { x: -panel.capOffset; y: panelShape.height - panel.leftCut + panel.capOffset } + PathLine { x: -panel.capOffset; y: panelShape.height + panel.capOffset } + PathLine { x: panel.leftCut - panel.capOffset; y: panelShape.height + panel.capOffset } + } + // Upper left accent line ShapePath { diff --git a/dotfiles/quickshell/hyprchrome/widgets/tray/TrayIcon.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Tray/TrayIcon.qml similarity index 98% rename from dotfiles/quickshell/hyprchrome/widgets/tray/TrayIcon.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Tray/TrayIcon.qml index 9ad4542..59fe08a 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/tray/TrayIcon.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Tray/TrayIcon.qml @@ -4,7 +4,7 @@ import Quickshell import Quickshell.Services.SystemTray import QtQuick import QtQuick.Shapes -import qs.hyprchrome.theme +import qs.HyprChrome.Theme // One tray item as a chamfered cell. Declares `modelData` required so it can be // a Repeater delegate directly, without an Item wrapper in between. diff --git a/dotfiles/quickshell/hyprchrome/widgets/tray/TrayPanel.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Tray/TrayPanel.qml similarity index 96% rename from dotfiles/quickshell/hyprchrome/widgets/tray/TrayPanel.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Tray/TrayPanel.qml index 01c80b2..649cddb 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/tray/TrayPanel.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Tray/TrayPanel.qml @@ -3,8 +3,8 @@ pragma ComponentBehavior: Bound import Quickshell import Quickshell.Services.SystemTray import QtQuick -import qs.hyprchrome.theme -import qs.hyprchrome.widgets.panels +import qs.HyprChrome.Theme +import qs.HyprChrome.Widgets.Bar.Panels // System tray in the hyprchrome panel chrome, in both densities: the same // items, drawn large enough to hit when expanded and shrunk onto the header diff --git a/dotfiles/quickshell/hyprchrome/widgets/vitals/GpuBusy.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/GpuBusy.qml similarity index 100% rename from dotfiles/quickshell/hyprchrome/widgets/vitals/GpuBusy.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/GpuBusy.qml diff --git a/dotfiles/quickshell/hyprchrome/widgets/vitals/SegmentMeter.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/SegmentMeter.qml similarity index 97% rename from dotfiles/quickshell/hyprchrome/widgets/vitals/SegmentMeter.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/SegmentMeter.qml index aee364e..95f5b53 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/vitals/SegmentMeter.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/SegmentMeter.qml @@ -1,7 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick -import qs.hyprchrome.theme +import qs.HyprChrome.Theme // Segmented horizontal meter: a row of cells lit up to `value`. // diff --git a/dotfiles/quickshell/hyprchrome/widgets/vitals/VitalRow.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/VitalRow.qml similarity index 98% rename from dotfiles/quickshell/hyprchrome/widgets/vitals/VitalRow.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/VitalRow.qml index 47069e5..caf011e 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/vitals/VitalRow.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/VitalRow.qml @@ -1,7 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick -import qs.hyprchrome.theme +import qs.HyprChrome.Theme // One metric of the expanded vitals panel: label, meter, readout on a line. // diff --git a/dotfiles/quickshell/hyprchrome/widgets/vitals/VitalsPanel.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/VitalsPanel.qml similarity index 98% rename from dotfiles/quickshell/hyprchrome/widgets/vitals/VitalsPanel.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/VitalsPanel.qml index 00c1cca..ab4e894 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/vitals/VitalsPanel.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Vitals/VitalsPanel.qml @@ -3,8 +3,8 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts -import qs.hyprchrome.theme -import qs.hyprchrome.widgets.panels +import qs.HyprChrome.Theme +import qs.HyprChrome.Widgets.Bar.Panels import qs.widgets.vitals // Host vitals in the hyprchrome panel chrome, in both of BarPanel's densities. diff --git a/dotfiles/quickshell/hyprchrome/widgets/workspaces/WorkspacesPanel.qml b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Workspaces/WorkspacesPanel.qml similarity index 99% rename from dotfiles/quickshell/hyprchrome/widgets/workspaces/WorkspacesPanel.qml rename to dotfiles/quickshell/HyprChrome/Widgets/Bar/Workspaces/WorkspacesPanel.qml index 71f9302..279ae50 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/workspaces/WorkspacesPanel.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/Bar/Workspaces/WorkspacesPanel.qml @@ -3,8 +3,8 @@ pragma ComponentBehavior: Bound import Quickshell import Quickshell.Hyprland import QtQuick -import qs.hyprchrome.theme -import qs.hyprchrome.widgets.panels +import qs.HyprChrome.Theme +import qs.HyprChrome.Widgets.Bar.Panels // Which workspace each monitor is currently showing, in the hyprchrome panel // chrome, in both of BarPanel's densities. diff --git a/dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml b/dotfiles/quickshell/HyprChrome/Widgets/ChromeBackdrop.qml similarity index 99% rename from dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml rename to dotfiles/quickshell/HyprChrome/Widgets/ChromeBackdrop.qml index b44d15c..da699c6 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/ChromeBackdrop.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/ChromeBackdrop.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import Quickshell import Quickshell.Wayland import QtQuick -import qs.hyprchrome.theme +import qs.HyprChrome.Theme // Scrim behind the bar: dims the desktop and lays the dense bar's drafting grid // over it. It is always on screen — only how far DOWN it reaches changes. diff --git a/dotfiles/quickshell/hyprchrome/widgets/HyprChromeShell.qml b/dotfiles/quickshell/HyprChrome/Widgets/HyprChromeShell.qml similarity index 98% rename from dotfiles/quickshell/hyprchrome/widgets/HyprChromeShell.qml rename to dotfiles/quickshell/HyprChrome/Widgets/HyprChromeShell.qml index b2cfbd5..2539bc2 100644 --- a/dotfiles/quickshell/hyprchrome/widgets/HyprChromeShell.qml +++ b/dotfiles/quickshell/HyprChrome/Widgets/HyprChromeShell.qml @@ -3,7 +3,8 @@ pragma ComponentBehavior: Bound import Quickshell import Quickshell.Hyprland import Quickshell.Wayland -import qs.hyprchrome.widgets +import qs.HyprChrome.Widgets.Bar +import qs.HyprChrome.Widgets // The hyprchrome shell: owns everything the rail's surfaces have to agree on, // and instantiates them. diff --git a/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml b/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml deleted file mode 100644 index 16047bb..0000000 --- a/dotfiles/quickshell/hyprchrome/widgets/HyprChromeBar.qml +++ /dev/null @@ -1,119 +0,0 @@ -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Wayland -import qs.hyprchrome.widgets.host -import qs.hyprchrome.widgets.vitals -import qs.hyprchrome.widgets.workspaces -import qs.hyprchrome.widgets.tray - -// 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 - - // 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 - - // 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: window.wlrLayer - - 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, 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) - - 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: window.expanded - // Workspaces butt against its right edge; the left end of the row is free. - rightChamfer: false - - toggleOnClick: false - 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: 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 - 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: window.expanded - toggleOnClick: false - Layout.fillHeight: true - } - } -} diff --git a/dotfiles/quickshell/shell.qml b/dotfiles/quickshell/shell.qml index da3257d..9974731 100644 --- a/dotfiles/quickshell/shell.qml +++ b/dotfiles/quickshell/shell.qml @@ -10,11 +10,11 @@ import qs.widgets.osd import qs.widgets.systray import qs.widgets.theme import qs.widgets.vitals -import qs.hyprchrome -import qs.hyprchrome.widgets -import qs.hyprchrome.widgets.debug -import qs.hyprchrome.widgets.host -import qs.hyprchrome.widgets.vitals +import qs.HyprChrome +import qs.HyprChrome.Widgets +import qs.HyprChrome.Widgets.Bar.Debug +import qs.HyprChrome.Widgets.Bar.Host +import qs.HyprChrome.Widgets.Bar.Vitals Scope { // Dense multi-monitor status rail; visual core is headlessly renderable.