feat(quickshell): bar height 36, invert geometry coupling, unify centering
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:
2026-07-28 12:03:32 +02:00
parent 30c9b55be4
commit fe1fa43a4d
18 changed files with 69 additions and 29 deletions
+7 -1
View File
@@ -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
+6
View File
@@ -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
+7 -3
View File
@@ -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.