diff --git a/hypr/lid_state.conf b/hypr/lid_state.conf deleted file mode 100644 index e8e4907..0000000 --- a/hypr/lid_state.conf +++ /dev/null @@ -1 +0,0 @@ -monitor=eDP-1, disable diff --git a/hypr/lua/monitors.lua b/hypr/lua/monitors.lua index 86f0083..ed3e174 100644 --- a/hypr/lua/monitors.lua +++ b/hypr/lua/monitors.lua @@ -20,7 +20,12 @@ M.external = { { output = "desc:Samsung Electric Company LC27G7xT H4ZRA00734", mode = "2560x1440@240", position = "-2560x0", scale = 1 }, } -M.builtin = { output = M.laptop, mode = "1920x1080@60", position = "0x0", scale = 1 } +-- `disabled = false` is load-bearing, not decoration. hl.monitor() merges into +-- whatever rule the output already has, so a spec that simply omits the flag +-- leaves an earlier `disabled = true` in place: the panel stays dark and the +-- mode/position are applied to a monitor nobody can see. Opening the lid while +-- docked hit exactly that. +M.builtin = { output = M.laptop, mode = "1920x1080@60", position = "0x0", scale = 1, disabled = false } -- Anything not declared above falls back to Hyprland's auto placement, so a -- borrowed projector or a monitor at the office still lights up. diff --git a/quickshell/bar/Bar.qml b/quickshell/bar/Bar.qml index 12df02b..57eca11 100644 --- a/quickshell/bar/Bar.qml +++ b/quickshell/bar/Bar.qml @@ -115,10 +115,17 @@ PanelWindow { implicitWidth: Theme.padM * 2 implicitHeight: Theme.barHeight + // A leaning hairline needs a box wide enough to hold the lean: at 20px + // tall the top edge sits 20 * skew ≈ 5px right of the bottom one, so a + // 1px-wide Skew folded into a bowtie instead. Width is the lean plus the + // stroke the rule should actually read as. + readonly property real ruleWeight: 1.5 + readonly property real ruleHeight: 20 + Skew { anchors.centerIn: parent - width: 1 - height: 20 + width: parent.ruleHeight * Theme.skew + parent.ruleWeight + height: parent.ruleHeight color: Theme.alpha(Theme.outline, 0.9) } } diff --git a/quickshell/bar/BarPill.qml b/quickshell/bar/BarPill.qml index 4c13c26..05161ee 100644 --- a/quickshell/bar/BarPill.qml +++ b/quickshell/bar/BarPill.qml @@ -37,9 +37,15 @@ Item { // crimson line struck through its bottom edge on hover. Reserving the band // and centring content in what is left is what keeps the rule under the // module instead of across it. + // The band reserved the full rule plus its inset — 7px off a 40px module, + // all of it taken from the bottom — so every module's content was centred + // 3.5px above the slab's own centre and the whole bar read as top-heavy. + // The rule is pushed closer to the edge now and the band keeps only enough + // clearance to stop the tallest content (the media pill's 24px art) from + // touching it, so content sits within a pixel of the slab's centre line. readonly property real ruleHeight: 2 - readonly property real ruleInset: 5 - readonly property real ruleBand: root.standalone ? 0 : root.ruleHeight + root.ruleInset + readonly property real ruleInset: 3 + readonly property real ruleBand: root.standalone ? 0 : 2 // Modules with a notion of progress (the player's track position) draw it in // the same band, so it can never be struck through the content. A module diff --git a/quickshell/bar/Clock.qml b/quickshell/bar/Clock.qml index 8d8fcbc..f7325dd 100644 --- a/quickshell/bar/Clock.qml +++ b/quickshell/bar/Clock.qml @@ -50,18 +50,54 @@ BarPill { } } + // Declared with room for its own lean, so the slash sits inside the box + // the Row gave it instead of hanging several px into the date column. Skew { anchors.verticalCenter: parent.verticalCenter - width: 2 + width: 22 * Theme.skew + 3 height: 22 color: Theme.alpha(Theme.accent, 0.6) } + // Archivo Black's digits are not all the same width, so a clock sized to + // its own text changed width as the minutes rolled over — 11:11 is + // several px narrower than 08:48 — and shoved the rest of the bar + // around. Reserve the widest possible reading and centre inside it. + TextMetrics { + id: timeMetrics + text: "88:88" + font.family: Theme.fontDisplay + font.pixelSize: Theme.fsTitle + font.weight: Theme.weightDisplay + font.italic: true + font.letterSpacing: 0.5 + } + + // Archivo Black reserves descender room the time never uses — "12:04" + // is all cap height — so centring the line box left the digits sitting + // above everything beside them. Centre the ink instead. + // `tightBoundingRect` is measured from the baseline, so the ink's top + // inside the line box is `ascent + ink.y`. + readonly property real timeInkOffset: { + const ink = timeMetrics.tightBoundingRect; + if (ink.height <= 0) return 0; + const inkCentre = timeFont.ascent + ink.y + ink.height / 2; + return Math.round(timeMetrics.boundingRect.height / 2 - inkCentre); + } + + FontMetrics { + id: timeFont + font: timeMetrics.font + } + SplitText { id: timeText anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: layout.timeInkOffset + width: timeMetrics.advanceWidth text: Qt.formatDateTime(clock.date, "HH:mm") pixelSize: Theme.fsTitle + horizontalAlignment: Text.AlignHCenter split: 1.4 } diff --git a/quickshell/bar/Rail.qml b/quickshell/bar/Rail.qml index 67fd5e2..3055d60 100644 --- a/quickshell/bar/Rail.qml +++ b/quickshell/bar/Rail.qml @@ -89,6 +89,7 @@ PanelWindow { id: volIcon icon: Audio.icon readout: Audio.muted ? "" : Audio.volumePercent + "%" + reserveReadout: true label: "Volume" statusText: Audio.muted ? "Muted" : Audio.volumePercent + "% · " + Audio.sinkName hint: "Scroll to adjust" @@ -162,6 +163,7 @@ PanelWindow { readout: Notifs.unread > 0 ? (Notifs.unread > 9 ? "9+" : String(Notifs.unread)) : "" + reserveReadout: true dim: Notifs.dnd alert: Notifs.hasUnread && !Notifs.dnd suppressTooltip: notifPopout.detailActive @@ -187,10 +189,17 @@ PanelWindow { implicitWidth: Theme.railWidth implicitHeight: Theme.padM + 4 + // Same correction as the bar's divider, on the other axis: a 22px rule + // rises 22 * skew ≈ 5px across its run, so the box has to be that tall + // plus the weight of the stroke. At height 1 the polygon inverted and + // the rule rendered as a stubby wedge sitting left of centre. + readonly property real ruleWeight: 1.5 + readonly property real ruleWidth: 22 + Skew { anchors.centerIn: parent - width: 22 - height: 1 + width: parent.ruleWidth + height: parent.ruleWidth * Theme.skew + parent.ruleWeight vertical: true color: Theme.alpha(Theme.outline, 0.9) } diff --git a/quickshell/bar/RailIcon.qml b/quickshell/bar/RailIcon.qml index 403d73a..abfeb62 100644 --- a/quickshell/bar/RailIcon.qml +++ b/quickshell/bar/RailIcon.qml @@ -21,6 +21,13 @@ Item { property bool dim: false property bool pulsing: false + // Hold the readout line open even while there is nothing to put in it. + // Volume drops its percentage when muted and the bell only carries a count + // when something is unread, so without this the slot collapsed from 42px to + // 32px and every icon below it jumped a third of its own height. + property bool reserveReadout: false + + readonly property bool hasReadout: root.readout !== "" || root.reserveReadout readonly property bool hovered: hover.hovered // Clearance between the wipe and the rail's left edge, chosen so the bar @@ -31,16 +38,42 @@ Item { signal scrolled(real delta) implicitWidth: Theme.railWidth - implicitHeight: root.readout !== "" ? 42 : 32 + implicitHeight: root.hasReadout ? root.glyphBand + readoutLine.height + 1 : root.glyphBand - Column { + // The glyph always sits centred in a fixed band at the top of the slot and + // the readout hangs below it, rather than the pair being centred together. + // Centring the pair meant the icon slid up and down by a few px whenever its + // number appeared or vanished — the icons stopped lining up with each other + // exactly when something was changing and you were looking at them. + readonly property real glyphBand: 32 + + // Nerd Font's status glyphs are not all centred inside their own advance — + // the muted-speaker and muted-mic marks carry their cross out to the right, + // so centring the advance box left them visibly off-axis next to the wifi + // and bluetooth glyphs. Centre the ink instead, capped so a glyph with an + // odd bounding box cannot slide far off the rail's axis. + readonly property real opticalShift: { + const ink = glyphMetrics.tightBoundingRect; + if (ink.width <= 0) return 0; + const off = glyphMetrics.advanceWidth / 2 - (ink.x + ink.width / 2); + return Math.max(-4, Math.min(4, off)); + } + + TextMetrics { + id: glyphMetrics + text: root.icon + font.family: Theme.fontIcon + font.pixelSize: Theme.fsLarge + } + + Item { id: layout - anchors.centerIn: parent - spacing: 1 + anchors.fill: parent Icon { id: glyph - anchors.horizontalCenter: parent.horizontalCenter + x: (root.width - width) / 2 + root.opticalShift + y: (root.glyphBand - height) / 2 text: root.icon pulsing: root.pulsing font.pixelSize: Theme.fsLarge @@ -57,7 +90,10 @@ Item { } Text { + id: readoutLine anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: root.glyphBand + 1 visible: root.readout !== "" text: root.readout color: root.dim ? Theme.muted : Theme.subtext @@ -88,7 +124,7 @@ Item { anchors.leftMargin: root.wipeInset anchors.verticalCenter: parent.verticalCenter width: 2 - height: root.hovered ? layout.implicitHeight : 0 + height: root.hovered ? root.height - 4 : 0 color: Theme.glow Behavior on height { diff --git a/quickshell/bar/Resources.qml b/quickshell/bar/Resources.qml index b58dd7c..1d43411 100644 --- a/quickshell/bar/Resources.qml +++ b/quickshell/bar/Resources.qml @@ -29,6 +29,7 @@ BarPill { label: "CPU" value: Sys.cpuUsage readout: Math.round(Sys.cpuUsage) + "%" + widest: "100%" } Divider {} @@ -37,6 +38,7 @@ BarPill { label: "MEM" value: Sys.memPercent readout: Sys.memUsed.toFixed(1) + "G" + widest: "88.8G" } Divider { visible: Sys.gpuAvailable } @@ -46,14 +48,23 @@ BarPill { label: "GPU" value: Sys.gpuUsage readout: Math.round(Sys.gpuUsage) + "%" + widest: "100%" } } - component Divider: Skew { + component Divider: Item { anchors.verticalCenter: parent.verticalCenter - width: 1 - height: 18 - color: Theme.alpha(Theme.outline, 0.8) + implicitWidth: rule.width + implicitHeight: 18 + + // Sized to hold its own lean — see the note on the bar's divider. + Skew { + id: rule + anchors.centerIn: parent + width: parent.height * Theme.skew + 1 + height: parent.height + color: Theme.alpha(Theme.outline, 0.8) + } } // A tiny fixed label makes every number self-describing without turning the @@ -65,14 +76,35 @@ BarPill { property real value: 0 property string readout: "" + // The widest reading this metric will ever show. The number column is + // sized from this rather than from the live text, because sizing to the + // live text is what made the whole right-hand group breathe in and out: + // "9%" and "100%" are ~14px apart in Archivo Black, so every tick of the + // CPU sampler nudged the metrics, the media pill and the clock sideways. + // Reserving the maximum costs a few px of blank and buys a bar that + // never moves. + property string widest: "100%" + + readonly property real numberWidth: Math.max(gauge.width, metrics.advanceWidth) + readonly property real headWidth: labelText.implicitWidth + head.spacing + numberWidth + anchors.verticalCenter: parent.verticalCenter spacing: 2 + width: Math.max(headWidth, 44) + + TextMetrics { + id: metrics + text: metric.widest + font: gauge.font + } Row { + id: head spacing: 4 Text { - anchors.baseline: readoutText.baseline + id: labelText + anchors.baseline: gauge.baseline text: metric.label color: Theme.muted font.family: Theme.fontMono @@ -83,7 +115,7 @@ BarPill { } Text { - id: readoutText + id: gauge text: metric.readout color: Theme.heat(metric.value) font.family: Theme.fontDisplay @@ -99,12 +131,15 @@ BarPill { } } + // Ends flush with the reading above it instead of stopping short at a + // fixed 43px, so each metric reads as one block. MeterBar { value: metric.value segments: 9 - segmentWidth: 3 spacing: 2 height: 5 + width: metric.width + segmentWidth: (width - 8 * spacing) / 9 } } diff --git a/quickshell/components/Skew.qml b/quickshell/components/Skew.qml index 9fbed52..7e61547 100644 --- a/quickshell/components/Skew.qml +++ b/quickshell/components/Skew.qml @@ -31,6 +31,20 @@ Shape { // rail slab. property bool vertical: false + // NOTE ON SIZING. The lean shifts one edge by `lean * height` (or by + // `lean * width` when vertical), and the piece that actually gets painted is + // what is left: a Skew declared 1px wide and 20px tall does not draw a 1px + // rule at an angle, it draws a rule |1 - 20 * 0.249| = 4px thick that hangs + // 4px to the *left* of the box it was given. That is why the bar's and the + // rail's hairlines came out fat and off-centre. A thin leaning rule has to be + // declared with room for its own lean — `height * Theme.skew + weight` — and + // the call sites that draw one now do exactly that. + // + // Zero-sized pieces still have a lean offset, so a collapsed hover wipe or a + // meter at 0 drew a sub-pixel wedge of pure accent — the stray red dot under + // the metrics. Nothing with no extent should paint at all. + visible: root.width > 0 && root.height > 0 + preferredRendererType: Shape.CurveRenderer asynchronous: false