Standardize bar icon geometry

This commit is contained in:
David Heinemeier Hansson
2026-07-17 15:20:55 -07:00
parent fe48e83222
commit 6e3b69b8da
17 changed files with 272 additions and 124 deletions
+2
View File
@@ -341,6 +341,8 @@ QtObject {
readonly property QtObject bar: QtObject {
readonly property int sizeHorizontal: root.barToken("size-horizontal", 26)
readonly property int sizeVertical: root.barToken("size-vertical", 28)
readonly property int iconSlot: root.barToken("icon-slot", 27)
readonly property int iconCanvas: root.barToken("icon-canvas", 16)
}
function refresh() {
+60
View File
@@ -0,0 +1,60 @@
import QtQuick
import Quickshell
import qs.Commons
WidgetButton {
id: root
property Component iconComponent: null
property real opticalSize: Style.bar.iconCanvas
property bool debugOpticalBounds: Quickshell.env("OMARCHY_DEBUG_BAR_ICONS") === "1"
readonly property real opticalCenterErrorX: glyph.visible ? glyph.paintedCenterX - opticalCanvas.width / 2 : 0
readonly property real opticalCenterErrorY: glyph.visible ? glyph.paintedCenterY - opticalCanvas.height / 2 : 0
labelVisible: false
hasVisualContent: text !== "" || iconComponent !== null
fontSize: Style.font.body
fixedWidth: vertical ? -1 : Style.bar.iconSlot
fixedHeight: vertical ? Style.bar.iconSlot : -1
Item {
id: opticalCanvas
anchors.centerIn: parent
width: root.opticalSize
height: root.opticalSize
OpticalGlyph {
id: glyph
anchors.fill: parent
visible: root.iconComponent === null
text: root.text
fontFamily: root.fontFamily
fontSize: root.fontSize
color: root.active && root.useActiveColor ? root.activeColor : root.foreground
rotation: root.textRotation
debugBounds: root.debugOpticalBounds
}
Loader {
anchors.fill: parent
visible: root.iconComponent !== null
sourceComponent: root.iconComponent
}
Rectangle {
visible: root.debugOpticalBounds && root.iconComponent !== null
anchors.fill: parent
color: "transparent"
border.width: 1
border.color: "#4488ff"
}
}
Rectangle {
visible: root.debugOpticalBounds
anchors.fill: parent
color: "transparent"
border.width: 1
border.color: "#ff4455"
}
}
+58
View File
@@ -0,0 +1,58 @@
import QtQuick
import qs.Commons
Item {
id: root
property string text: ""
property string fontFamily: Style.font.family
property real fontSize: Style.font.body
property color color: Color.foreground
property real targetExtent: Math.min(width, height) * 0.82
property bool debugBounds: false
readonly property real baseTightWidth: Math.max(1, baseMetrics.tightBoundingRect.width)
readonly property real baseTightHeight: Math.max(1, baseMetrics.tightBoundingRect.height)
readonly property real normalizedScale: Math.min(1.5, targetExtent / Math.max(baseTightWidth, baseTightHeight))
readonly property int normalizedFontSize: Math.max(1, Math.round(fontSize * normalizedScale))
readonly property real tightWidth: Math.max(1, glyphMetrics.tightBoundingRect.width)
readonly property real tightHeight: Math.max(1, glyphMetrics.tightBoundingRect.height)
readonly property real horizontalCorrection: glyph.implicitWidth / 2 - (glyphMetrics.tightBoundingRect.x + tightWidth / 2)
readonly property real verticalCorrection: glyph.implicitHeight / 2 - (glyph.baselineOffset + glyphMetrics.tightBoundingRect.y + tightHeight / 2)
readonly property real paintedCenterX: glyph.x + glyphMetrics.tightBoundingRect.x + tightWidth / 2
readonly property real paintedCenterY: glyph.y + glyph.baselineOffset + glyphMetrics.tightBoundingRect.y + tightHeight / 2
TextMetrics {
id: baseMetrics
font.family: root.fontFamily
font.pixelSize: root.fontSize
text: root.text
}
TextMetrics {
id: glyphMetrics
font.family: root.fontFamily
font.pixelSize: root.normalizedFontSize
text: root.text
}
Text {
id: glyph
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.horizontalCorrection
anchors.verticalCenterOffset: root.verticalCorrection
text: root.text
color: root.color
font.family: root.fontFamily
font.pixelSize: root.normalizedFontSize
renderType: Text.NativeRendering
}
Rectangle {
visible: root.debugBounds
anchors.fill: parent
color: "transparent"
border.width: 1
border.color: "#4488ff"
}
}
+6 -6
View File
@@ -12,7 +12,6 @@ Item {
property color activeColor: bar ? bar.urgent : Color.urgent
property bool active: false
property real horizontalMargin: 8.5
property real rightExtraMargin: 0
property real verticalPadding: 6
property real fixedWidth: -1
property real fixedHeight: -1
@@ -24,6 +23,8 @@ Item {
property bool pressable: true
property bool useActiveColor: true
property bool maintainIndicatorReveal: false
property bool labelVisible: true
property bool hasVisualContent: text !== ""
property var revealHost: bar
property string tooltipText: ""
property var registeredBar: null
@@ -56,13 +57,12 @@ Item {
readonly property bool vertical: bar ? bar.vertical : false
readonly property int barSize: bar ? bar.barSize : Style.bar.sizeHorizontal
readonly property real scaledHorizontalMargin: Style.spaceReal(horizontalMargin)
readonly property real scaledRightExtraMargin: Style.spaceReal(rightExtraMargin)
readonly property real scaledVerticalPadding: Style.spaceReal(verticalPadding)
readonly property bool tooltipHovered: visible && interactive && !concealed && mouseArea.containsMouse
visible: text !== "" || keepSpace
opacity: text === "" || concealed ? 0 : (dimmed ? 0.45 : 1)
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + scaledHorizontalMargin * 2 + scaledRightExtraMargin))
visible: hasVisualContent || keepSpace
opacity: !hasVisualContent || concealed ? 0 : (dimmed ? 0.45 : 1)
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + scaledHorizontalMargin * 2))
implicitHeight: fixedHeight > 0 ? fixedHeight : (vertical ? Math.max(12, label.implicitHeight + scaledVerticalPadding * 2) : barSize)
Behavior on opacity {
@@ -71,8 +71,8 @@ Item {
Text {
id: label
visible: root.labelVisible
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.vertical ? 0 : -root.scaledRightExtraMargin / 2
text: root.text
color: root.active && root.useActiveColor ? root.activeColor : root.foreground
font.family: root.fontFamily
+2
View File
@@ -1,6 +1,7 @@
module qs.Ui
BarIndicator 1.0 BarIndicator.qml
BarIconButton 1.0 BarIconButton.qml
BarWidget 1.0 BarWidget.qml
BorderOverlay 1.0 BorderOverlay.qml
BorderSurface 1.0 BorderSurface.qml
@@ -12,6 +13,7 @@ Dropdown 1.0 Dropdown.qml
KeyboardPanel 1.0 KeyboardPanel.qml
MultiSelect 1.0 MultiSelect.qml
NumberField 1.0 NumberField.qml
OpticalGlyph 1.0 OpticalGlyph.qml
Panel 1.0 Panel.qml
PanelActionButton 1.0 PanelActionButton.qml
PanelController 1.0 PanelController.qml
+2 -9
View File
@@ -1297,13 +1297,6 @@ Item {
readonly property bool hovered: moduleHover.hovered
readonly property bool dragSource: root.barDragSource === slot
readonly property bool panelOpen: root.activePopout === slot.activeItem
readonly property real openIndicatorInlineOffset: {
var item = slot.activeItem
if (!item || !("openIndicatorInlineOffset" in item)) return 0
var offset = Number(item.openIndicatorInlineOffset)
return isFinite(offset) ? offset : 0
}
implicitWidth: activeItem && activeItem.visible ? (root.vertical ? root.barSize : activeItem.implicitWidth) : 0
implicitHeight: activeItem && activeItem.visible ? activeItem.implicitHeight : 0
width: implicitWidth
@@ -1377,9 +1370,9 @@ Item {
height: root.vertical ? Math.max(Style.space(10), Math.round(parent.height * 0.55)) : Style.space(2)
x: root.vertical
? (root.position === "left" ? parent.width - width - inset : inset)
: (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.width - width) / 2) : (parent.width - width) / 2 + slot.openIndicatorInlineOffset)
: Math.round((parent.width - width) / 2)
y: root.vertical
? (slot.openIndicatorInlineOffset === 0 ? Math.round((parent.height - height) / 2) : (parent.height - height) / 2 + slot.openIndicatorInlineOffset)
? Math.round((parent.height - height) / 2)
: (root.position === "top" ? parent.height - height - inset : inset)
z: 50
+1 -1
View File
@@ -34,7 +34,7 @@ BarWidget {
PwObjectTracker { objects: root.source ? [root.source] : [] }
WidgetButton {
BarIconButton {
id: button
anchors.fill: parent
bar: root.bar
+1 -2
View File
@@ -52,12 +52,11 @@ BarWidget {
onTriggered: root.refresh()
}
WidgetButton {
BarIconButton {
id: button
anchors.fill: parent
bar: root.bar
text: "\uf021"
fontSize: Style.font.caption
tooltipText: ""
onPressed: root.runUpdate()
}
+5 -9
View File
@@ -24,9 +24,9 @@ BarWidget {
readonly property var drawerItems: bucket("drawer")
readonly property var allItems: bucket("all")
readonly property int drawerCount: drawerItems.length
readonly property int trayItemExtent: Style.space(16)
readonly property int trayItemGap: Style.space(9)
readonly property int trayJoinGap: Style.space(4)
readonly property int trayItemExtent: Style.bar.iconSlot
readonly property int trayItemGap: 0
readonly property int trayJoinGap: 0
readonly property int drawerExtent: drawerCount > 0 ? drawerCount * trayItemExtent + (drawerCount - 1) * trayItemGap : 0
// Match Waybar's group/tray-expander drawer transition-duration.
readonly property int animationDuration: 600
@@ -182,15 +182,13 @@ BarWidget {
onHoveredChanged: root.expanded = hovered
}
WidgetButton {
BarIconButton {
id: expandIcon
bar: root.bar
width: implicitWidth
height: implicitHeight
x: root.drawerExtent - root.revealExtent
text: "\uf053"
horizontalMargin: 9
verticalPadding: 6
onPressed: function(button) {
if (button === Qt.RightButton) root.managePopupOpen = !root.managePopupOpen
}
@@ -266,7 +264,7 @@ BarWidget {
onHoveredChanged: root.expanded = hovered
}
WidgetButton {
BarIconButton {
id: expandIcon
bar: root.bar
width: implicitWidth
@@ -274,8 +272,6 @@ BarWidget {
y: root.drawerExtent - root.revealExtent
text: "\uf053"
textRotation: 90
horizontalMargin: 9
verticalPadding: 6
onPressed: function(button) {
if (button === Qt.RightButton) root.managePopupOpen = !root.managePopupOpen
}
+1 -4
View File
@@ -522,14 +522,11 @@ Panel {
onTriggered: root.refreshDisplayAudioModels()
}
WidgetButton {
BarIconButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.outputIcon()
fontSize: Style.font.body
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
onPressed: function(b) {
if (b === Qt.RightButton) root.toggleOutputMute()
else root.toggle()
+1 -3
View File
@@ -469,13 +469,11 @@ Panel {
})
}
WidgetButton {
BarIconButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.icon
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
onPressed: function(b) {
if (b === Qt.RightButton) root.toggleBluetooth()
else if (b === Qt.MiddleButton) root.bar.run("omarchy-launch-bluetooth")
+13 -36
View File
@@ -139,48 +139,25 @@ Panel {
function status(): string { return dropbox.statusText }
}
Item {
BarIconButton {
id: button
anchors.fill: parent
implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(27)
implicitHeight: root.bar && root.bar.vertical ? Style.space(26) : (root.bar ? root.bar.barSize : Style.space(26))
property var registeredBar: null
function triggerPress(buttonCode) {
bar: root.bar
iconComponent: Component {
Item {
DropboxIcon {
anchors.centerIn: parent
iconSize: Style.space(12)
color: root.barIconColor
opacity: dropbox.authenticated ? 1.0 : 0.6
}
}
}
onPressed: function(buttonCode) {
if (buttonCode === Qt.RightButton) dropbox.refresh()
else if (buttonCode === Qt.MiddleButton) dropbox.login()
else root.toggle()
}
function syncClickRegistration() {
if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(button)
registeredBar = root.bar
if (registeredBar && registeredBar.registerClickTarget) registeredBar.registerClickTarget(button)
}
Component.onCompleted: syncClickRegistration()
Component.onDestruction: if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(button)
Connections {
target: root
function onBarChanged() { button.syncClickRegistration() }
}
DropboxIcon {
anchors.centerIn: parent
iconSize: Style.space(12)
color: root.barIconColor
opacity: dropbox.authenticated ? 1.0 : 0.6
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(mouse) { button.triggerPress(mouse.button) }
}
}
KeyboardPanel {
+1 -5
View File
@@ -337,15 +337,11 @@ Panel {
onRunningChanged: if (!running) root.refresh()
}
WidgetButton {
BarIconButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.displays.length > 1 ? "󰍺" : "󰍹"
fontSize: Style.font.subtitle
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
rightExtraMargin: 4
onPressed: function(b) { root.toggle() }
onWheelMoved: function(delta) {
if (root.brightnessAvailable) root.setBrightness(root.brightnessPercent + (delta > 0 ? 5 : -5))
+1 -4
View File
@@ -707,14 +707,11 @@ Panel {
}
}
WidgetButton {
BarIconButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.icon
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
rightExtraMargin: 5.5
onPressed: function(b) {
if (root.opened) root.close()
+1 -3
View File
@@ -249,13 +249,11 @@ Panel {
}
}
WidgetButton {
BarIconButton {
id: button
anchors.fill: parent
bar: root.bar
text: root.batteryIcon()
fixedWidth: root.bar && root.bar.vertical ? -1 : Style.space(27)
fixedHeight: root.bar && root.bar.vertical ? Style.space(26) : -1
tooltipText: ""
onPressed: function(b) { if (root.batteryPresent) root.toggle() }
}
+15 -42
View File
@@ -12,7 +12,6 @@ Panel {
ipcTarget: "omarchy.tailscale"
manageIpc: false
readonly property real openIndicatorInlineOffset: bar && bar.vertical ? 0 : Style.spaceReal(1.5)
property string focusSection: "header"
property int headerIndex: 0
property int accountIndex: 0
@@ -360,53 +359,27 @@ Panel {
function status(): string { return tailscale.statusText }
}
Item {
BarIconButton {
id: button
anchors.fill: parent
implicitWidth: root.bar && root.bar.vertical ? root.bar.barSize : Style.space(27)
implicitHeight: root.bar && root.bar.vertical ? Style.space(26) : (root.bar ? root.bar.barSize : Style.space(26))
property var registeredBar: null
function triggerPress(buttonCode) {
bar: root.bar
iconComponent: Component {
Item {
TailscaleIcon {
anchors.centerIn: parent
iconSize: Style.space(11)
color: root.barIconColor
badgeColor: root.urgent
crossed: !tailscale.running && !tailscale.needsLogin
warning: tailscale.needsLogin
}
}
}
onPressed: function(buttonCode) {
if (buttonCode === Qt.RightButton) tailscale.toggleTailscale()
else if (buttonCode === Qt.MiddleButton) tailscale.refresh()
else root.toggle()
}
function syncClickRegistration() {
if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(button)
registeredBar = root.bar
if (registeredBar && registeredBar.registerClickTarget) registeredBar.registerClickTarget(button)
}
Component.onCompleted: syncClickRegistration()
Component.onDestruction: if (registeredBar && registeredBar.unregisterClickTarget) registeredBar.unregisterClickTarget(button)
Connections {
target: root
function onBarChanged() { button.syncClickRegistration() }
}
TailscaleIcon {
anchors.centerIn: parent
anchors.horizontalCenterOffset: root.openIndicatorInlineOffset
anchors.verticalCenterOffset: -Style.space(1)
iconSize: Style.space(12) * 0.85
color: root.barIconColor
badgeColor: root.urgent
crossed: !tailscale.running && !tailscale.needsLogin
warning: tailscale.needsLogin
}
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
hoverEnabled: true
cursorShape: Qt.PointingHandCursor
onClicked: function(mouse) { button.triggerPress(mouse.button) }
}
}
KeyboardPanel {
+102
View File
@@ -0,0 +1,102 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping bar icon geometry test"
exit 0
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
ln -s "$ROOT/shell/Ui" "$test_tmp/Ui"
ln -s "$ROOT/shell/Commons" "$test_tmp/Commons"
cat >"$test_tmp/shell.qml" <<'QML'
import QtQuick
import Quickshell
import qs.Commons
import qs.Ui
ShellRoot {
id: root
function fail(message) {
console.log("RESULT fail " + message)
Qt.quit()
}
function checkIcon(icon, name) {
if (icon.implicitWidth !== Style.bar.iconSlot) {
fail(name + " slot width is " + icon.implicitWidth)
return false
}
if (icon.opticalSize !== Style.bar.iconCanvas) {
fail(name + " optical canvas is " + icon.opticalSize)
return false
}
if (Math.abs(icon.opticalCenterErrorX) > 0.5 || Math.abs(icon.opticalCenterErrorY) > 0.5) {
fail(name + " painted bounds are over half a pixel off center by " + icon.opticalCenterErrorX + "," + icon.opticalCenterErrorY)
return false
}
return true
}
Component.onCompleted: Qt.callLater(function() {
if (!checkIcon(bluetooth, "bluetooth")) return
if (!checkIcon(network, "network")) return
if (!checkIcon(audio, "audio")) return
if (!checkIcon(monitor, "monitor")) return
if (!checkIcon(power, "power")) return
if (vector.implicitWidth !== Style.bar.iconSlot || vector.opticalSize !== Style.bar.iconCanvas) {
fail("vector icon does not share glyph geometry")
return
}
console.log("RESULT pass")
Qt.quit()
})
QtObject {
id: testBar
property bool vertical: false
property int barSize: Style.bar.sizeHorizontal
property string fontFamily: Style.font.family
property color barForeground: "white"
property color urgent: "red"
property bool foregroundAnimationEnabled: false
function registerClickTarget(target) {}
function unregisterClickTarget(target) {}
function hideTooltip(target) {}
function showTooltip(target, text) {}
}
BarIconButton { id: bluetooth; bar: testBar; text: "󰂯" }
BarIconButton { id: network; bar: testBar; text: "󰖩" }
BarIconButton { id: audio; bar: testBar; text: "󰖁" }
BarIconButton { id: monitor; bar: testBar; text: "󰍹" }
BarIconButton { id: power; bar: testBar; text: "󰁹" }
BarIconButton {
id: vector
bar: testBar
iconComponent: Component { Rectangle { width: 12; height: 12 } }
}
}
QML
output=$(timeout 15 env \
QML2_IMPORT_PATH="$ROOT/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \
QML_IMPORT_PATH="$ROOT/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \
quickshell -p "$test_tmp" --no-color 2>&1) || {
printf '%s\n' "$output" >&2
fail "bar icon geometry fixture exits cleanly"
}
if ! grep -q 'RESULT pass' <<<"$output"; then
printf '%s\n' "$output" >&2
fail "bar icons share centered optical geometry"
fi
pass "bar icons share centered optical geometry"