feat(quickshell): bar height 36, invert geometry coupling, unify centering
Update PKGBUILD version / update-pkgver (push) Successful in 6s
Update PKGBUILD version / update-pkgver (push) Successful in 6s
moduleHeight (24) becomes the primary vertical token and barHeight is derived as moduleHeight + moduleMarginV * 2. Previously ModuleBox sized its pill as barHeight - moduleMarginV * 2, so lowering the bar shrank every pill by the same amount — the margin is absolute and does not scale. Bar height is now set via moduleMarginV while the pills hold. Widget implicitHeight stays on barHeight: ModuleBox has no clip, so the items overflow the pill and keep the whole bar height clickable, which matters because the display edge stops the pointer. Also fixes pre-existing vertical centering. anchors.verticalCenter centers the Text layout box (ascent + descent) and rounds down on an odd layout height, which put the network and bluetooth glyphs 1 px above the bar mid and several others off by 0.5 px. Replaced across 13 widgets with height: parent.height + verticalAlignment: Text.AlignVCenter, which centers by font metrics. Workspace dots move from a hardcoded 22 to moduleHeight. Verified by pixel measurement: all 7 pills on rows 6-29 (mid 17.50 = bar mid) and all 14 glyphs with even ink height exactly on 17.50. The remaining 7 have odd ink height, where 17.50 is not representable on the pixel grid; two of those are Image, where verticalAlignment does not apply.
This commit is contained in:
@@ -63,7 +63,13 @@ quickshell is the **default bar and notification daemon**, spawned by niri as `q
|
||||
}
|
||||
}
|
||||
```
|
||||
`ModuleBox` wraps its content in a `Row` (and self-hides when its content collapses to zero width). Do **not** anchor a direct child with `anchors.centerIn: parent` — a `Row` rejects horizontal child anchors with a runtime warning (`QML Row: Cannot specify … centerIn anchors for items inside Row`) and ignores them. There is no binding loop (the `Row` sizes from children's implicit sizes, not `childrenRect`), but the centering silently does nothing. Center vertically with `anchors.verticalCenter: parent.verticalCenter` instead.
|
||||
`ModuleBox` wraps its content in a `Row` (and self-hides when its content collapses to zero width). Do **not** anchor a direct child with `anchors.centerIn: parent` — a `Row` rejects horizontal child anchors with a runtime warning (`QML Row: Cannot specify … centerIn anchors for items inside Row`) and ignores them. There is no binding loop (the `Row` sizes from children's implicit sizes, not `childrenRect`), but the centering silently does nothing. Center vertically with the pattern described under **Geometry and centering** below.
|
||||
|
||||
- **Geometry and centering.** `Theme.moduleHeight` (24) is the primary vertical unit and `barHeight` is derived from it: `barHeight = moduleHeight + moduleMarginV * 2`. Set the **bar** height via `moduleMarginV` (6 → 36 px, 8 → 40 px); the module pills stay put. Do not reintroduce the reverse coupling (pills sized as `barHeight - moduleMarginV * 2`) — since the margin is absolute, every bar-height change then shrinks the pills by the same amount.
|
||||
- Only `ModuleBox` takes `moduleHeight`. Widgets keep `implicitHeight: Theme.barHeight` **on purpose**: `ModuleBox` has no `clip`, so the widget item overflows the pill and keeps the full bar height clickable. The bar sits at the display edge, which stops the pointer — the topmost pixels have to be live, so never size a widget item down to the pill.
|
||||
- Center glyphs with `height: parent.height` + `verticalAlignment: Text.AlignVCenter`, **not** `anchors.verticalCenter`. The latter centers the Text *layout* box (ascent + descent) and rounds down on an odd layout height, which put several bar glyphs 1 px high; `verticalAlignment` centers by font metrics. Where a widget also centers horizontally (Launcher, workspace dots) use `anchors.fill: parent` plus both alignment properties. A widget whose root is a `Row` needs an explicit `height` before its children can align inside it (see `WindowTitle.qml`).
|
||||
- `Image`-based content (tray icons, the window-title icon) has no `verticalAlignment`; residual asymmetry there lives in the icon file, not in the layout.
|
||||
- Verify with pixels, not by eye: `grim -g "<x>,0 <w>x<barHeight>"` plus an ImageMagick `-threshold … -trim` bounding box over an element's **full width** — a single-column sample is valid for a pill rectangle but not for a glyph.
|
||||
|
||||
- **qmldir** exposes the singletons/components for the cross-dir `import`. It is **generated** from the `.qml` files; regenerate when adding components:
|
||||
```sh
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# Decisions
|
||||
|
||||
## 2026-07-28 – Quickshell bar: moduleHeight as the primary vertical token, unified glyph centering
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Why**: Bar height was to drop from 40 to 36. `barHeight` was the primary geometry token and `ModuleBox` derived its pill height from it (`barHeight - moduleMarginV * 2`), so lowering the bar shrank every module pill by the same amount — the margin is an absolute value and does not scale. At 30 px that left 14 px pills for a 13 px font. Investigating the result surfaced a second, pre-existing defect: module glyphs were not vertically centered. Measured against `30c9b55`, the network and bluetooth glyphs sat 1 px above the bar mid, and the nightlight/cpugov/idle/mpris/volume/backlight glyphs were off by 0.5 px.
|
||||
- **Tradeoffs**: (1) Geometry coupling — keep `barHeight` primary and re-tune `moduleMarginV` on every height change (fragile, the exact trap that caused the shrinking), vs. invert it so `moduleHeight` is primary and `barHeight = moduleHeight + moduleMarginV * 2`. Picked the inversion: pill height is now the invariant, bar height is set via the margin, and the pills cannot silently follow the bar again. (2) Widget hit areas — the widgets' own `implicitHeight: Theme.barHeight` looked redundant next to a 24 px pill and was briefly repointed to `moduleHeight`; reverted, because `ModuleBox` has no `clip` and those items intentionally overflow the pill to keep the whole bar height clickable (screen-edge targeting: the display edge stops the pointer, so the topmost pixels must be live). Only `ModuleBox` itself takes `moduleHeight`. (3) Centering mechanism — `anchors.verticalCenter` centers the Text *layout* box (ascent + descent) and rounds down on an odd layout height; `verticalAlignment: Text.AlignVCenter` inside an explicit `height` centers by font metrics. Verified by experiment, both glyphs in the same pill in one screenshot: converted network landed on mid 17.50, untouched bluetooth stayed at 16.50. A `pixelSize` hypothesis was tested first and refuted (13 vs 14 made no difference).
|
||||
- **How**: `Theme.qml` — `moduleHeight: 24` + `moduleMarginV: 6` as inputs, `barHeight` derived (36). `ModuleBox.qml` takes `implicitHeight: Theme.moduleHeight`; all widget `implicitHeight`s stay on `barHeight`. `Workspaces.qml` dot size from hardcoded 22 to `Theme.moduleHeight`. Centering unified across 13 widgets onto one pattern — `height: parent.height` + `verticalAlignment: Text.AlignVCenter` (Launcher and the workspace dots use `anchors.fill` + both alignments as they center horizontally too); `WindowTitle`'s root `Row` gained `height: Theme.barHeight` so its Text has an independent height to align in. Verified by pixel measurement (grim + ImageMagick bounding boxes over full element width, not a single column): all 7 pills on `rows 6-29` (mid 17.50 = bar mid) and all 14 glyphs with even ink height exactly on 17.50. The remaining 7 have odd ink height (13/15 px), where 17.50 is not representable on the pixel grid — they sit on 17.0/18.0 by rounding; two of those (`window icon`, `tray icon`) are `Image`, where `verticalAlignment` does not apply and the asymmetry lives in the icon file.
|
||||
|
||||
## 2026-07-08 – nm-applet stays in autostart (secret agent, not just tray icon)
|
||||
- **Who**: Dominik, ClaudeCode
|
||||
- **Why**: With the quickshell network indicator/popout covering display and control, nm-applet's tray icon became redundant and was slated for removal from the niri autostart. Investigation stopped that: nm-applet is the session's NM **secret agent** — agent-owned VPN passwords (`password-flags = 1`, e.g. the wwwe profile, GUI-created) live in the user keyring and are delivered to NetworkManager by nm-applet on connect. Without it, `nmcli connection up` fails ("secrets were required, but not provided") from the popout, moonarch-vpn, and the CLI alike. See also 2026-03-30 (nm-applet introduced as VPN secret agent).
|
||||
|
||||
@@ -56,16 +56,20 @@ Item {
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: root.muted ? Theme.subtext0 : Theme.text
|
||||
text: root.icon
|
||||
}
|
||||
Text {
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.text
|
||||
|
||||
@@ -37,18 +37,20 @@ Item {
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
spacing: 4
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.text
|
||||
text: root.icon
|
||||
}
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.text
|
||||
|
||||
@@ -43,7 +43,8 @@ Item {
|
||||
|
||||
Text {
|
||||
id: iconText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: root.on ? Theme.text : Theme.subtext0
|
||||
|
||||
@@ -58,7 +58,8 @@ Item {
|
||||
|
||||
Text {
|
||||
id: iconText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.text
|
||||
|
||||
@@ -17,7 +17,8 @@ Item {
|
||||
|
||||
Text {
|
||||
id: iconText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: IdleInhibit.enabled ? Theme.accent : Theme.text
|
||||
|
||||
@@ -26,7 +26,9 @@ Item {
|
||||
readonly property bool open: root.popouts && root.popouts.currentName === root.popoutName
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 5
|
||||
color: (hover.hovered || root.open) ? Theme.accent : Theme.text
|
||||
|
||||
@@ -10,7 +10,7 @@ Rectangle {
|
||||
// (e.g. Battery on a desktop, tray with no SNI items) must not leave a bare pill.
|
||||
visible: inner.implicitWidth > 0
|
||||
implicitWidth: inner.implicitWidth + Theme.modulePadH * 2
|
||||
implicitHeight: Theme.barHeight - Theme.moduleMarginV * 2
|
||||
implicitHeight: Theme.moduleHeight
|
||||
color: Theme.moduleBg
|
||||
radius: Theme.radius
|
||||
|
||||
|
||||
@@ -46,18 +46,20 @@ Item {
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
spacing: 6
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: root.playing ? Theme.accent : Theme.subtext0
|
||||
text: String.fromCodePoint(0xf075a) // md-music
|
||||
}
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.text
|
||||
|
||||
@@ -80,7 +80,8 @@ Item {
|
||||
|
||||
Text {
|
||||
id: iconText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: (Networking.wifiEnabled || root.wiredDev) ? Theme.text : Theme.subtext0
|
||||
|
||||
@@ -32,7 +32,8 @@ Item {
|
||||
|
||||
Text {
|
||||
id: iconText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: NightlightService.enabled ? Theme.yellow : Theme.text
|
||||
|
||||
@@ -37,7 +37,8 @@ Item {
|
||||
|
||||
Text {
|
||||
id: iconText
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: NotifService.dnd ? Theme.subtext0 : NotifService.count > 0 ? Theme.accent : Theme.text
|
||||
|
||||
@@ -28,11 +28,12 @@ Item {
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
spacing: 8
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: root.screenActive
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
@@ -40,7 +41,8 @@ Item {
|
||||
text: String.fromCodePoint(0xf1483) // md-monitor-share (screencast)
|
||||
}
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
visible: root.micActive
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
|
||||
@@ -34,11 +34,15 @@ Singleton {
|
||||
readonly property string fontFamily: "UbuntuSans Nerd Font"
|
||||
readonly property int fontSize: 13
|
||||
|
||||
// Geometry — waybar-derived.
|
||||
readonly property int barHeight: 40
|
||||
// Geometry — moduleHeight is the primary vertical unit: every bar element (module
|
||||
// pill, launcher, workspace dot) is that tall, and the bar derives its height from
|
||||
// it plus the edge margin. Sizing the pills off barHeight instead makes the modules
|
||||
// shrink whenever the bar does, since the margin is an absolute value.
|
||||
readonly property int moduleHeight: 24
|
||||
readonly property int moduleMarginV: 6
|
||||
readonly property int barHeight: moduleHeight + moduleMarginV * 2
|
||||
readonly property int radius: 4
|
||||
readonly property int modulePadH: 10
|
||||
readonly property int moduleMarginV: 8
|
||||
readonly property int spacing: 5
|
||||
|
||||
// Motion — centralized animation tokens.
|
||||
|
||||
@@ -58,18 +58,20 @@ Item {
|
||||
|
||||
Row {
|
||||
id: row
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
spacing: 3
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize + 1
|
||||
color: Theme.text
|
||||
text: String.fromCodePoint(0xf1377) // md-refresh-circle
|
||||
}
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize - 2
|
||||
color: Theme.text
|
||||
|
||||
@@ -6,6 +6,7 @@ import Quickshell
|
||||
Row {
|
||||
id: root
|
||||
property int maxWidth: 420
|
||||
height: Theme.barHeight
|
||||
spacing: 6
|
||||
|
||||
readonly property var win: Niri.activeWindow
|
||||
@@ -50,7 +51,8 @@ Row {
|
||||
}
|
||||
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
height: parent.height
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
color: Theme.subtext1
|
||||
|
||||
@@ -15,14 +15,16 @@ Row {
|
||||
delegate: Rectangle {
|
||||
id: wsItem
|
||||
required property var modelData
|
||||
width: 22
|
||||
height: 22
|
||||
width: Theme.moduleHeight
|
||||
height: Theme.moduleHeight
|
||||
anchors.verticalCenter: parent ? parent.verticalCenter : undefined
|
||||
radius: Theme.radius
|
||||
color: modelData.is_focused ? Qt.rgba(Theme.accent.r, Theme.accent.g, Theme.accent.b, 0.18) : modelData.is_active ? Qt.rgba(Theme.text.r, Theme.text.g, Theme.text.b, 0.08) : "transparent"
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
anchors.fill: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.family: Theme.fontFamily
|
||||
font.pixelSize: Theme.fontSize
|
||||
// Active (focused) dot vs default dot — matches the waybar niri/workspaces icons.
|
||||
|
||||
Reference in New Issue
Block a user