Normalize bar icon size and alignment

This commit is contained in:
Bjarne Oeverli committed 2026-10-11 21:39:17 +02:00
1 parent e71a816e90
commit 201de1c0d6
17 files changed
+540 -116

No files matched your search

+4 -1
View File
@@ -353,6 +353,7 @@ QtObject {
readonly property int iconSlot: root.barToken("icon-slot", 27)
readonly property int iconCanvas: root.barToken("icon-canvas", 16)
readonly property int iconFont: root.barToken("icon-font", 13)
readonly property int iconSize: Math.min(iconCanvas, root.barToken("icon-size", 12))
readonly property int statusSlot: root.barToken("status-slot", 21)
}
@@ -424,7 +425,9 @@ QtObject {
} else if (section === "bar") {
if (key === "scale-with-font") {
nextBarScaleWithFont = boolToken(raw, nextBarScaleWithFont)
} else if (key === "size-horizontal" || key === "size-vertical" || key === "notch-height") {
} else if (key === "size-horizontal" || key === "size-vertical" || key === "notch-height"
|| key === "icon-slot" || key === "icon-canvas" || key === "icon-font"
|| key === "icon-size" || key === "status-slot") {
var b = parseInt(raw, 10)
if (isFinite(b)) barOut[key] = b
}
+66 -28
View File
@@ -8,52 +8,90 @@ WidgetButton {
property Component iconComponent: null
property real slotSize: Style.bar.iconSlot
property real opticalSize: Style.bar.iconCanvas
property real iconSize: Math.min(Style.bar.iconSize, opticalSize)
readonly property real visualIconSize: Style.bar.iconSize
readonly property real canvasSize: Math.max(visualIconSize, opticalSize)
property string labelText: ""
property bool debugOpticalBounds: Quickshell.env("OMARCHY_DEBUG_BAR_ICONS") === "1"
// Measured against the slot, which the open-panel underline centers on.
readonly property real opticalCenterErrorX: glyph.visible ? opticalCanvas.x + glyph.paintedCenterX - root.width / 2 : 0
readonly property real glyphPaintedWidth: glyph.visible ? glyph.tightWidth : 0
// A label shifts the icon within the centered group.
readonly property real opticalCenterErrorX:
content.x + opticalCanvas.x + (glyph.visible ? glyph.paintedCenterX : customIcon.width / 2 + customIcon.centerErrorX) - root.width / 2
- (label.visible ? (label.implicitWidth + Style.space(4)) / 2 : 0)
readonly property real opticalCenterErrorY:
content.y + (glyph.visible ? glyph.paintedCenterY : customIcon.height / 2 + customIcon.centerErrorY) - root.height / 2
readonly property real glyphPaintedWidth: glyph.visible ? glyph.paintedWidth : 0
readonly property real glyphPaintedHeight: glyph.visible ? glyph.paintedHeight : 0
readonly property real glyphBaselineY: glyph.visible ? glyph.baselineY : 0
readonly property int glyphFontSize: glyph.visible ? glyph.renderedFontSize : 0
readonly property real componentPaintedWidth: customIcon.visible ? customIcon.paintedWidth : 0
readonly property real componentPaintedHeight: customIcon.visible ? customIcon.paintedHeight : 0
labelVisible: false
hasVisualContent: text !== "" || iconComponent !== null
hasVisualContent: text !== "" || iconComponent !== null || labelText !== ""
fontSize: Style.bar.iconFont
fixedWidth: vertical ? -1 : slotSize
fixedHeight: vertical ? slotSize : -1
Item {
id: opticalCanvas
// Placed exactly, not anchored: centerIn snaps an even canvas in an odd slot
// to a whole pixel, pulling every glyph off the open-panel underline.
id: content
// Exact coordinates preserve the center of odd-width slots.
x: (root.width - width) / 2
y: (root.height - height) / 2
width: root.opticalSize
height: root.opticalSize
width: root.canvasSize + (label.visible ? label.implicitWidth + Style.space(4) : 0)
height: root.canvasSize
OpticalGlyph {
id: glyph
anchors.fill: parent
visible: root.iconComponent === null
text: root.text
fontFamily: root.fontFamily
fontSize: root.fontSize
Text {
id: label
visible: root.labelText !== ""
textFormat: Text.PlainText
text: root.labelText
font.family: root.fontFamily
font.pixelSize: root.fontSize
color: root.active && root.useActiveColor ? root.activeColor : root.foreground
rotation: root.textRotation
debugBounds: root.debugOpticalBounds
y: (parent.height - height) / 2
+ (labelMetrics.descent - labelMetrics.ascent + labelMetrics.tightBoundingRect("0").height) / 2
renderType: Text.NativeRendering
}
Loader {
anchors.fill: parent
visible: root.iconComponent !== null
sourceComponent: root.iconComponent
FontMetrics {
id: labelMetrics
font: label.font
}
Rectangle {
visible: root.debugOpticalBounds && root.iconComponent !== null
anchors.fill: parent
color: "transparent"
border.width: 1
border.color: "#4488ff"
Item {
id: opticalCanvas
x: label.visible ? label.implicitWidth + Style.space(4) : 0
width: root.canvasSize
height: root.canvasSize
OpticalGlyph {
id: glyph
anchors.fill: parent
visible: root.iconComponent === null
text: root.text
fontFamily: root.fontFamily
fontSize: root.fontSize
paintedSize: root.visualIconSize
color: root.active && root.useActiveColor ? root.activeColor : root.foreground
rotation: root.textRotation
debugBounds: root.debugOpticalBounds
}
BarIconContent {
id: customIcon
anchors.fill: parent
visible: root.iconComponent !== null
sourceComponent: root.iconComponent
sourceSize: root.opticalSize
}
Rectangle {
visible: root.debugOpticalBounds && root.iconComponent !== null
anchors.fill: parent
color: "transparent"
border.width: 1
border.color: "#4488ff"
}
}
}
+38
View File
@@ -0,0 +1,38 @@
import QtQuick
import qs.Commons
Item {
id: root
property Component sourceComponent: null
property real sourceSize: Style.bar.iconCanvas
readonly property rect sourceBounds: bounds.mappedBounds
readonly property real contentScale: sourceBounds.width > 0 && sourceBounds.height > 0
? Style.bar.iconSize / Math.max(sourceBounds.width, sourceBounds.height) : 1
readonly property real paintedWidth: sourceBounds.width * contentScale
readonly property real paintedHeight: sourceBounds.height * contentScale
readonly property real centerErrorX: content.x + (sourceBounds.x + sourceBounds.width / 2) * contentScale - width / 2
readonly property real centerErrorY: content.y + (sourceBounds.y + sourceBounds.height / 2) * contentScale - height / 2
Loader {
id: content
sourceComponent: root.sourceComponent
onLoaded: {
// Keep declared dimensions. Empty wrapper items receive a source canvas.
if (!(item instanceof Image)) {
if (item.width <= 0) item.width = Qt.binding(function() { return root.sourceSize })
if (item.height <= 0) item.height = Qt.binding(function() { return root.sourceSize })
}
}
transformOrigin: Item.TopLeft
scale: root.contentScale
x: root.width / 2 - (root.sourceBounds.x + root.sourceBounds.width / 2) * scale
y: root.height / 2 - (root.sourceBounds.y + root.sourceBounds.height / 2) * scale
}
IconContentBounds {
id: bounds
sourceItem: content.item
boundsParent: content
}
}
-1
View File
@@ -46,7 +46,6 @@ BarIconButton {
useActiveColor: false
maintainIndicatorReveal: indicatorBlock === "inactive"
revealHost: indicatorHost
fontSize: Style.font.caption
horizontalMargin: 5
verticalPadding: 5
fixedWidth: vertical ? -1 : Style.bar.statusSlot
+115
View File
@@ -0,0 +1,115 @@
import QtQuick
import QtQuick.Shapes
Item {
id: root
property Item sourceItem: null
property Item boundsParent: null
property rect mappedBounds: Qt.rect(0, 0, 0, 0)
visible: false
readonly property bool textItem: sourceItem instanceof Text
readonly property bool imageItem: sourceItem instanceof Image
readonly property bool boxItem: sourceItem instanceof Rectangle
|| sourceItem instanceof Shape || sourceItem instanceof Canvas || sourceItem instanceof ShaderEffect
function unionBounds(a, b) {
if (b.width <= 0 || b.height <= 0) return a
if (a.width <= 0 || a.height <= 0) return b
var x = Math.min(a.x, b.x)
var y = Math.min(a.y, b.y)
return Qt.rect(x, y, Math.max(a.x + a.width, b.x + b.width) - x,
Math.max(a.y + a.height, b.y + b.height) - y)
}
readonly property rect paintedBounds: {
var item = sourceItem
var bounds = Qt.rect(0, 0, 0, 0)
if (!item || !item.visible || item.opacity <= 0) return bounds
if (textItem) {
var ink = textMetrics.tightBoundingRect
bounds = Qt.rect(ink.x, item.baselineOffset + ink.y, ink.width, ink.height)
} else if (imageItem) {
var w = item.paintedWidth
var h = item.paintedHeight
var x = item.horizontalAlignment === Image.AlignLeft ? 0
: (item.horizontalAlignment === Image.AlignRight ? item.width - w : (item.width - w) / 2)
var y = item.verticalAlignment === Image.AlignTop ? 0
: (item.verticalAlignment === Image.AlignBottom ? item.height - h : (item.height - h) / 2)
if (item.fillMode === Image.PreserveAspectFit) {
bounds = Qt.rect(x, y, w, h)
} else if (item.fillMode === Image.Pad) {
var left = Math.max(0, x)
var top = Math.max(0, y)
bounds = Qt.rect(left, top, Math.max(0, Math.min(item.width, x + w) - left),
Math.max(0, Math.min(item.height, y + h) - top))
} else {
bounds = Qt.rect(0, 0, item.width, item.height)
}
} else if (boxItem) {
bounds = Qt.rect(0, 0, item.width, item.height)
}
for (var i = 0; i < childrenBounds.count; i++) {
var child = childrenBounds.itemAt(i)
if (child) bounds = unionBounds(bounds, child.mappedBounds)
}
if (item.clip && bounds.width > 0 && bounds.height > 0) {
var left = Math.max(0, bounds.x)
var top = Math.max(0, bounds.y)
bounds = Qt.rect(left, top,
Math.max(0, Math.min(item.width, bounds.x + bounds.width) - left),
Math.max(0, Math.min(item.height, bounds.y + bounds.height) - top))
}
return bounds
}
function updateMappedBounds() {
var b = paintedBounds
mappedBounds = sourceItem && boundsParent && b.width > 0 && b.height > 0
? sourceItem.mapToItem(boundsParent, b.x, b.y, b.width, b.height)
: b
}
function configureSource() {
if (sourceItem instanceof Text) sourceItem.renderType = Text.CurveRendering
updateMappedBounds()
}
onPaintedBoundsChanged: updateMappedBounds()
onSourceItemChanged: configureSource()
onBoundsParentChanged: updateMappedBounds()
Component.onCompleted: updateMappedBounds()
TextMetrics {
id: textMetrics
font: root.textItem ? root.sourceItem.font : Qt.font({ pixelSize: 12 })
text: root.textItem ? root.sourceItem.text : ""
}
Connections {
target: root.sourceItem
function onXChanged() { root.updateMappedBounds() }
function onYChanged() { root.updateMappedBounds() }
function onWidthChanged() { root.updateMappedBounds() }
function onHeightChanged() { root.updateMappedBounds() }
function onScaleChanged() { root.updateMappedBounds() }
function onRotationChanged() { root.updateMappedBounds() }
function onTransformOriginChanged() { root.updateMappedBounds() }
}
Repeater {
id: childrenBounds
model: root.sourceItem ? root.sourceItem.children : []
delegate: Loader {
id: childLoader
required property var modelData
readonly property rect mappedBounds: item ? item.mappedBounds : Qt.rect(0, 0, 0, 0)
source: Qt.resolvedUrl("IconContentBounds.qml")
onLoaded: {
item.sourceItem = Qt.binding(function() { return childLoader.modelData })
item.boundsParent = Qt.binding(function() { return root.sourceItem })
}
}
}
}
+18 -9
View File
@@ -8,14 +8,22 @@ Item {
property string text: ""
property string fontFamily: Style.font.family
property real fontSize: Style.font.body
property real paintedSize: 0
property color color: Commons.Color.foreground
property bool debugBounds: false
readonly property int renderedFontSize: Math.max(1, Math.round(fontSize))
readonly property real tightWidth: Math.max(1, glyphMetrics.tightBoundingRect.width)
readonly property real tightHeight: Math.max(1, glyphMetrics.tightBoundingRect.height)
readonly property real paintedScale: paintedSize > 0
? Math.min(paintedSize, width, height) / Math.max(tightWidth, tightHeight)
: 1
readonly property real paintedWidth: tightWidth * paintedScale
readonly property real paintedHeight: tightHeight * paintedScale
readonly property real horizontalCorrection: glyph.implicitWidth / 2 - (glyphMetrics.tightBoundingRect.x + tightWidth / 2)
readonly property real paintedCenterX: glyph.x + glyphMetrics.tightBoundingRect.x + tightWidth / 2
readonly property real baselineY: glyph.y + glyph.baselineOffset
readonly property real paintedCenterX: glyph.x + (glyphMetrics.tightBoundingRect.x + tightWidth / 2) * paintedScale
readonly property real paintedCenterY: glyph.y + (glyph.baselineOffset + glyphMetrics.tightBoundingRect.y + tightHeight / 2) * paintedScale
readonly property real baselineY: glyph.y + glyph.baselineOffset * paintedScale
TextMetrics {
id: glyphMetrics
@@ -27,17 +35,18 @@ Item {
Text {
id: glyph
textFormat: Text.PlainText
// Keep the shared line box and baseline intact. Correcting only the
// horizontal painted bounds avoids per-glyph vertical drift.
// The correction is fractional, so x is set exactly: centerIn would snap it
// to a whole pixel and undo it. y still snaps, keeping the baseline crisp.
x: (root.width - width) / 2 + root.horizontalCorrection
y: Math.round((root.height - height) / 2)
// Bar icons fit their painted bounds. Text labels retain their line box.
x: root.width / 2 - (glyphMetrics.tightBoundingRect.x + root.tightWidth / 2) * root.paintedScale
y: root.paintedSize > 0
? root.height / 2 - (baselineOffset + glyphMetrics.tightBoundingRect.y + root.tightHeight / 2) * root.paintedScale
: Math.round((root.height - height) / 2)
text: root.text
color: root.color
font.family: root.fontFamily
font.pixelSize: root.renderedFontSize
renderType: Text.NativeRendering
transformOrigin: Item.TopLeft
scale: root.paintedScale
renderType: root.paintedSize > 0 ? Text.CurveRendering : Text.NativeRendering
}
Rectangle {
+42 -3
View File
@@ -65,11 +65,31 @@ Item {
// Width of the painted label, for bar chrome that wants to line up with the
// text rather than with the slot it sits in. Zero on icon-only buttons.
readonly property real labelWidth: label.visible ? label.implicitWidth : 0
readonly property bool iconLabel: labelVisible && isIconGlyph(text)
readonly property real iconPaintedWidth: iconLabel && iconLoader.item ? iconLoader.item.paintedWidth : 0
readonly property real iconPaintedHeight: iconLabel && iconLoader.item ? iconLoader.item.paintedHeight : 0
readonly property real iconCenterErrorX: iconLabel && iconLoader.item
? iconLoader.x + iconLoader.item.paintedCenterX - width / 2 : 0
readonly property real iconCenterErrorY: iconLabel && iconLoader.item
? iconLoader.y + iconLoader.item.paintedCenterY - height / 2 : 0
function isIconGlyph(value) {
var text = String(value || "")
if (!text) return false
var code = text.codePointAt(0)
if (text.length !== (code > 0xffff ? 2 : 1)) return false
return (code >= 0xe000 && code <= 0xf8ff)
|| (code >= 0xf0000 && code <= 0xffffd)
|| (code >= 0x100000 && code <= 0x10fffd)
|| (code >= 0x2190 && code <= 0x2bff)
}
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)
implicitWidth: fixedWidth > 0 ? fixedWidth
: (vertical ? barSize : (iconLabel ? Style.bar.iconSlot : Math.max(12, label.implicitWidth + scaledHorizontalMargin * 2)))
implicitHeight: fixedHeight > 0 ? fixedHeight
: (vertical ? (iconLabel ? Style.bar.iconSlot : Math.max(12, label.implicitHeight + scaledVerticalPadding * 2)) : barSize)
Behavior on opacity {
NumberAnimation { duration: Style.duration(140); easing.type: Easing.OutCubic }
@@ -78,7 +98,7 @@ Item {
Text {
id: label
textFormat: Text.PlainText
visible: root.labelVisible
visible: root.labelVisible && !root.iconLabel
anchors.centerIn: parent
// Centering the line box puts the figures above the bar's middle, since
// the box keeps room for descenders. Center the figure height instead, so
@@ -106,6 +126,25 @@ Item {
font: label.font
}
Loader {
id: iconLoader
active: root.iconLabel
width: Style.bar.iconCanvas
height: Style.bar.iconCanvas
x: (root.width - width) / 2
y: (root.height - height) / 2
sourceComponent: Component {
OpticalGlyph {
text: root.text
fontFamily: root.fontFamily
fontSize: root.fontSize
paintedSize: Style.bar.iconSize
color: root.active && root.useActiveColor ? root.activeColor : root.foreground
rotation: root.textRotation
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
+2
View File
@@ -2,6 +2,7 @@ module qs.Ui
BarIndicator 1.0 BarIndicator.qml
BarIconButton 1.0 BarIconButton.qml
BarIconContent 1.0 BarIconContent.qml
BarWidget 1.0 BarWidget.qml
BackgroundMedia 1.0 BackgroundMedia.qml
BorderOverlay 1.0 BorderOverlay.qml
@@ -11,6 +12,7 @@ ButtonGroup 1.0 ButtonGroup.qml
ConfirmDialog 1.0 ConfirmDialog.qml
CursorSurface 1.0 CursorSurface.qml
Dropdown 1.0 Dropdown.qml
IconContentBounds 1.0 IconContentBounds.qml
KeyboardPanel 1.0 KeyboardPanel.qml
MultiSelect 1.0 MultiSelect.qml
NumberField 1.0 NumberField.qml
+10
View File
@@ -78,6 +78,16 @@ The `omarchy.indicators` widget loads individual bar indicators from `indicators
All widgets work in `top`, `bottom`, `left`, and `right` positions. Popups anchor on the side opposite the bar edge, sliding into the workspace. Vertical bars use 28px width; widgets that show text fall back to compact icon-only forms (e.g. `media` hides its scrolling label).
## Icon geometry
Use `BarIconButton` from `qs.Ui` for bar icons. It fits the visible glyph bounds to `Style.bar.iconSize` and centers them on both axes. It preserves each glyph's aspect ratio. Set `labelText` for an adjacent text label, such as the battery percentage.
The `[bar]` section of a theme's `shell.toml` supplies the shared dimensions. `icon-size` sets the visual target, with a default of 12 logical pixels. `icon-canvas` sets the canvas, with a default of 16 logical pixels. `icon-slot` and `status-slot` set the button extent. These dimensions follow the font scale unless `scale-with-font` is false.
Custom `iconComponent` content also fits the shared target. Its source dimensions can be 15px or another size. The bar measures its glyph, image, or vector bounds and applies one scale without changing its aspect ratio. A source `iconSize` override does not change the rendered target. Image padding remains part of an application's tray asset.
`WidgetButton` also uses the shared renderer for a single icon glyph. This covers custom command widgets and existing plugins that use `WidgetButton` with a different `fontSize`. Text labels retain their text size. Custom widgets must use `WidgetButton` or `BarIconButton` for this icon contract.
## Custom user modules
The schema accepts arbitrary module ids that you provide. Set `type` to `command` for shell-driven output or `qml` for a custom QML widget. Both still go under `bar.layout.<section>` in `shell.json`.
@@ -58,7 +58,6 @@ BarWidget {
bar: root.bar
text: "\uf021"
slotSize: Style.bar.statusSlot
fontSize: Style.font.caption
tooltipText: "Pending Omarchy Updates"
onPressed: root.runUpdate()
}
+4 -3
View File
@@ -810,9 +810,10 @@ BarWidget {
}
TrayIcon {
anchors.centerIn: parent
width: Style.space(12)
height: Style.space(12)
x: (parent.width - width) / 2
y: (parent.height - height) / 2
width: Style.bar.iconSize
height: Style.bar.iconSize
icon: trayItemRoot.modelData.icon
}
+1 -3
View File
@@ -8,14 +8,12 @@ BarWidget {
implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight
WidgetButton {
BarIconButton {
id: button
anchors.fill: parent
bar: root.bar
text: "\ue900"
fontFamily: "omarchy"
centerFigures: false
horizontalMargin: 7.5
onPressed: function(button) {
if (!root.bar) return
if (button === Qt.RightButton) root.bar.run("xdg-terminal-exec")
+5 -3
View File
@@ -7,11 +7,13 @@ Item {
id: root
property real iconSize: Style.font.icon
readonly property real aspectRatio: 1.18
property real artworkScale: 0.95
property color color: Commons.Color.foreground
width: iconSize * 1.18
width: iconSize * aspectRatio
height: iconSize
implicitWidth: iconSize * 1.18
implicitWidth: iconSize * aspectRatio
implicitHeight: iconSize
Shape {
@@ -19,7 +21,7 @@ Item {
antialiasing: true
layer.enabled: true
layer.samples: 4
scale: 0.95
scale: root.artworkScale
Tile { cx: root.width * 0.25; cy: root.height * 0.188 }
Tile { cx: root.width * 0.75; cy: root.height * 0.188 }
+4 -2
View File
@@ -175,8 +175,10 @@ Panel {
iconComponent: Component {
Item {
DropboxIcon {
anchors.centerIn: parent
iconSize: Style.space(12)
x: (parent.width - width) / 2
y: (parent.height - height) / 2
iconSize: button.iconSize / aspectRatio
artworkScale: 1
color: root.barIconColor
opacity: dropbox.active ? 1.0 : 0.6
}
+2 -3
View File
@@ -278,9 +278,8 @@ Panel {
id: button
anchors.fill: parent
bar: root.bar
text: root.showPercentage && !vertical
? Math.round(root.batteryFraction * 100) + "% " + root.batteryIcon()
: root.batteryIcon()
text: root.batteryIcon()
labelText: root.showPercentage && !vertical ? Math.round(root.batteryFraction * 100) + "%" : ""
slotSize: Style.bar.iconSlot * (root.showPercentage && !vertical ? 2 : 1)
tooltipText: ""
onPressed: function(b) {
+3 -2
View File
@@ -385,8 +385,9 @@ Panel {
iconComponent: Component {
Item {
TailscaleIcon {
anchors.centerIn: parent
iconSize: Style.space(11)
x: (parent.width - width) / 2
y: (parent.height - height) / 2
iconSize: button.iconSize
color: root.barIconColor
badgeColor: root.urgent
crossed: !tailscale.active && !tailscale.needsLogin
+226 -57
View File
@@ -16,6 +16,8 @@ trap 'rm -rf "$test_tmp"' EXIT
ln -s "$ROOT/shell/Ui" "$test_tmp/Ui"
ln -s "$ROOT/shell/Commons" "$test_tmp/Commons"
ln -s "$ROOT/shell/plugins/panels/dropbox/DropboxIcon.qml" "$test_tmp/DropboxIcon.qml"
ln -s "$ROOT/shell/plugins/panels/tailscale/TailscaleIcon.qml" "$test_tmp/TailscaleIcon.qml"
cat >"$test_tmp/shell.qml" <<'QML'
import QtQuick
@@ -25,12 +27,30 @@ import qs.Ui
ShellRoot {
id: root
property real customWidth: 15
property bool completed: false
property bool testsStarted: false
property int customRenderType: -1
readonly property bool imageReady: customImage.componentPaintedWidth > 0 && croppedImage.componentPaintedWidth > 0
readonly property string imageSource: "data:image/svg+xml;base64," + Qt.btoa('<svg xmlns="http://www.w3.org/2000/svg" width="15" height="9"><rect width="15" height="9" fill="white"/></svg>'.split("").map(function(c) { return c.charCodeAt(0) }))
function fail(message) {
console.log("RESULT fail " + message)
Qt.quit()
}
function checkPaintedBounds(icon, name) {
if (Math.abs(icon.opticalCenterErrorX) > 0.05 || Math.abs(icon.opticalCenterErrorY) > 0.05) {
fail(name + " painted bounds are off center: " + icon.opticalCenterErrorX + ", " + icon.opticalCenterErrorY)
return false
}
if (Math.abs(Math.max(icon.glyphPaintedWidth, icon.glyphPaintedHeight) - Style.bar.iconSize) > 0.05) {
fail(name + " painted size is " + icon.glyphPaintedWidth + " × " + icon.glyphPaintedHeight)
return false
}
return true
}
function checkIcon(icon, name) {
if (icon.implicitWidth !== Style.bar.iconSlot) {
fail(name + " slot width is " + icon.implicitWidth)
@@ -40,68 +60,144 @@ ShellRoot {
fail(name + " optical canvas is " + icon.opticalSize)
return false
}
// The open-panel underline centers on the slot, so the glyph must too.
if (Math.abs(icon.opticalCenterErrorX) > 0.05) {
fail(name + " painted bounds are off the slot's center by " + icon.opticalCenterErrorX)
return false
}
if (icon.glyphFontSize !== Style.bar.iconFont) {
fail(name + " font size is " + icon.glyphFontSize)
return false
}
return checkPaintedBounds(icon, name)
}
function checkGeometry() {
if (!checkIcon(bluetooth, "bluetooth")) return false
if (!checkIcon(network, "network")) return false
if (!checkIcon(audio, "audio")) return false
if (!checkIcon(monitor, "monitor")) return false
if (!checkIcon(power, "power")) return false
if (!checkIcon(elsewhen, "elsewhen")) return false
if (!checkIcon(menu, "menu")) return false
var sourceRatio = sourceMetrics.tightBoundingRect.width / sourceMetrics.tightBoundingRect.height
if (Math.abs(bluetooth.glyphPaintedWidth / bluetooth.glyphPaintedHeight - sourceRatio) > 0.01) {
fail("the glyph's aspect ratio changes")
return false
}
if (!checkPaintedBounds(smallFont, "small source font")) return false
if (!checkPaintedBounds(largeFont, "large source font")) return false
if (!checkPaintedBounds(customSize, "15px size override")) return false
if (!checkPaintedBounds(smallCanvas, "small source canvas")) return false
if (!legacyIcon.iconLabel
|| Math.abs(Math.max(legacyIcon.iconPaintedWidth, legacyIcon.iconPaintedHeight) - Style.bar.iconSize) > 0.05
|| Math.abs(legacyIcon.iconCenterErrorX) > 0.05 || Math.abs(legacyIcon.iconCenterErrorY) > 0.05) {
fail("a custom WidgetButton does not fit: " + legacyIcon.iconLabel + " "
+ legacyIcon.iconPaintedWidth + " × " + legacyIcon.iconPaintedHeight + " "
+ legacyIcon.iconCenterErrorX + ", " + legacyIcon.iconCenterErrorY)
return false
}
if (textLabel.iconLabel || textLabel.fontSize !== 15) {
fail("icon normalization changes an ordinary text label")
return false
}
if (customRenderType !== Text.CurveRendering) {
fail("custom glyphs do not use the scalable renderer")
return false
}
var customIcons = [customBox, nestedBox, customGlyph, rotatedBox, customImage, croppedImage]
for (var i = 0; i < customIcons.length; i++) {
var custom = customIcons[i]
if (Math.abs(Math.max(custom.componentPaintedWidth, custom.componentPaintedHeight) - Style.bar.iconSize) > 0.05
|| Math.abs(custom.opticalCenterErrorX) > 0.05 || Math.abs(custom.opticalCenterErrorY) > 0.05) {
fail("custom component " + i + " does not fit: " + custom.componentPaintedWidth + " × "
+ custom.componentPaintedHeight + " " + custom.opticalCenterErrorX + ", " + custom.opticalCenterErrorY)
return false
}
}
if (Math.abs(customBox.componentPaintedWidth / customBox.componentPaintedHeight - root.customWidth / 9) > 0.01
|| Math.abs(rotatedBox.componentPaintedWidth / rotatedBox.componentPaintedHeight - 9 / 15) > 0.01
|| Math.abs(customImage.componentPaintedWidth / customImage.componentPaintedHeight - 15 / 9) > 0.01
|| Math.abs(croppedImage.componentPaintedWidth / croppedImage.componentPaintedHeight - 1) > 0.01) {
fail("a custom component's aspect ratio changes")
return false
}
if (!checkPaintedBounds(labelledPower, "battery percentage")) return false
if (Math.abs(labelledPower.glyphPaintedWidth - power.glyphPaintedWidth) > 0.05
|| Math.abs(labelledPower.glyphPaintedHeight - power.glyphPaintedHeight) > 0.05) {
fail("the battery percentage changes the icon's size")
return false
}
if (vector.implicitWidth !== Style.bar.iconSlot || vector.opticalSize !== Style.bar.iconCanvas) {
fail("vector icon does not share glyph geometry")
return false
}
if (verticalIcon.implicitWidth !== Style.bar.sizeVertical || verticalIcon.implicitHeight !== Style.bar.iconSlot) {
fail("vertical icon does not use the shared slot")
return false
}
if (!checkPaintedBounds(verticalIcon, "vertical icon")) return false
if (!checkPaintedBounds(horizontalIndicator, "horizontal indicator")) return false
if (!checkPaintedBounds(verticalIndicator, "vertical indicator")) return false
if (verticalIndicator.implicitWidth !== Style.bar.sizeVertical || verticalIndicator.implicitHeight >= Style.bar.iconSlot) {
fail("vertical indicator does not retain compact spacing")
return false
}
if (horizontalIndicator.implicitWidth >= Style.bar.iconSlot) {
fail("horizontal indicator does not retain compact spacing")
return false
}
if (horizontalIndicatorPair.implicitWidth >= Style.bar.iconSlot * 2
|| verticalIndicatorPair.implicitHeight >= Style.bar.iconSlot * 2) {
fail("indicator groups do not retain compact internal spacing")
return false
}
if (compactStatusIcon.implicitWidth !== Style.bar.statusSlot
|| compactVerticalStatusIcon.implicitHeight !== Style.bar.statusSlot) {
fail("compact status icons do not use the shared status slot")
return false
}
if (!checkPaintedBounds(compactStatusIcon, "compact status icon")) return false
if (!checkPaintedBounds(compactVerticalStatusIcon, "compact vertical status icon")) return false
if (!checkPaintedBounds(rotatedIcon, "rotated icon")) return false
if (Math.abs(Math.max(dropbox.width, dropbox.height) - Style.bar.iconSize) > 0.05
|| Math.abs(Math.max(tailscale.width, tailscale.height) - Style.bar.iconSize) > 0.05) {
fail("vector icons do not use the shared visual size")
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 (!checkIcon(elsewhen, "elsewhen")) return
var baseline = bluetooth.glyphBaselineY
if (network.glyphBaselineY !== baseline || audio.glyphBaselineY !== baseline
|| monitor.glyphBaselineY !== baseline || power.glyphBaselineY !== baseline) {
fail("glyph baselines do not match")
return
}
if (vector.implicitWidth !== Style.bar.iconSlot || vector.opticalSize !== Style.bar.iconCanvas) {
fail("vector icon does not share glyph geometry")
return
}
if (verticalIcon.implicitWidth !== Style.bar.sizeVertical || verticalIcon.implicitHeight !== Style.bar.iconSlot) {
fail("vertical icon does not use the shared slot")
return
}
if (verticalIndicator.implicitWidth !== Style.bar.sizeVertical || verticalIndicator.implicitHeight >= Style.bar.iconSlot) {
fail("vertical indicator does not retain compact spacing")
return
}
if (verticalIndicator.glyphFontSize !== Style.font.caption) {
fail("indicator does not use the secondary icon scale")
return
}
if (Math.abs(verticalIndicator.opticalCenterErrorX) > 0.05) {
fail("vertical indicator is not optically centered")
return
}
if (horizontalIndicator.implicitWidth >= Style.bar.iconSlot) {
fail("horizontal indicator does not retain compact spacing")
return
}
if (horizontalIndicatorPair.implicitWidth >= Style.bar.iconSlot * 2
|| verticalIndicatorPair.implicitHeight >= Style.bar.iconSlot * 2) {
fail("indicator groups do not retain compact internal spacing")
return
}
if (compactStatusIcon.implicitWidth !== Style.bar.statusSlot
|| compactVerticalStatusIcon.implicitHeight !== Style.bar.statusSlot) {
fail("compact status icons do not use the shared status slot")
return
}
console.log("RESULT pass")
Qt.quit()
})
function runGeometryTests() {
if (testsStarted) return
testsStarted = true
if (!checkGeometry()) return
var labelBaseline = plainLabel.baselineY
root.customWidth = 23
Style.applyShellValues({ "font.base-size": "18", "bar.icon-size": "14", "bar.icon-canvas": "19" })
Qt.callLater(function() {
if (!checkGeometry()) return
if (Style.bar.iconSize !== 21 || Style.bar.iconCanvas !== 29) {
fail("theme icon dimensions do not follow the font scale")
return
}
if (plainLabel.baselineY !== labelBaseline) {
fail("bar icon settings change a text label's baseline")
return
}
Style.applyShellValues({ "font.base-size": "18", "bar.scale-with-font": "false", "bar.icon-size": "20" })
Qt.callLater(function() {
if (!checkGeometry()) return
if (Style.bar.iconSize !== Style.bar.iconCanvas) {
fail("oversized icons do not fit their canvas")
return
}
console.log("RESULT pass")
Qt.quit()
})
})
}
onImageReadyChanged: if (imageReady && completed) Qt.callLater(runGeometryTests)
Component.onCompleted: {
completed = true
if (imageReady) Qt.callLater(runGeometryTests)
}
QtObject {
id: testBar
@@ -137,6 +233,79 @@ ShellRoot {
BarIconButton { id: monitor; bar: testBar; text: "󰍹" }
BarIconButton { id: power; bar: testBar; text: "󰁹" }
BarIconButton { id: elsewhen; bar: testBar; text: "󰇧" }
TextMetrics {
id: sourceMetrics
font.family: testBar.fontFamily
font.pixelSize: Style.bar.iconFont
text: bluetooth.text
}
BarIconButton { id: menu; bar: testBar; text: "\ue900"; fontFamily: "omarchy" }
BarIconButton { id: smallFont; bar: testBar; text: "\uf053"; fontSize: 8 }
BarIconButton { id: largeFont; bar: testBar; text: "󰍹"; fontSize: 24 }
BarIconButton { id: customSize; bar: testBar; text: "󰍹"; iconSize: 15 }
BarIconButton { id: smallCanvas; bar: testBar; text: "󰍹"; opticalSize: 8 }
WidgetButton { id: legacyIcon; bar: testBar; text: "󰍹"; fontSize: 15 }
WidgetButton { id: textLabel; bar: testBar; text: "GPU"; fontSize: 15 }
BarIconButton {
id: customBox
bar: testBar
iconSize: 15
iconComponent: Component { Rectangle { width: root.customWidth; height: 9; color: "white" } }
}
BarIconButton {
id: nestedBox
bar: testBar
iconComponent: Component {
Item {
Rectangle { x: 1; y: 5; width: 15; height: 9; color: "white" }
}
}
}
BarIconButton {
id: customGlyph
bar: testBar
iconComponent: Component {
Text {
textFormat: Text.PlainText
text: "\ue900"
font.family: "omarchy"
font.pixelSize: 15
onRenderTypeChanged: root.customRenderType = renderType
}
}
}
BarIconButton {
id: rotatedBox
bar: testBar
iconComponent: Component { Rectangle { width: 15; height: 9; rotation: 90; color: "white" } }
}
BarIconButton {
id: customImage
bar: testBar
iconComponent: Component {
Image {
fillMode: Image.PreserveAspectFit
source: root.imageSource
}
}
}
BarIconButton {
id: croppedImage
bar: testBar
iconComponent: Component {
Image {
width: 15
height: 15
fillMode: Image.PreserveAspectCrop
source: root.imageSource
}
}
}
BarIconButton { id: labelledPower; bar: testBar; text: "󰁹"; labelText: "100%"; slotSize: Style.bar.iconSlot * 2 }
BarIconButton { id: rotatedIcon; bar: verticalBar; text: "\uf053"; textRotation: 90 }
OpticalGlyph { id: plainLabel; width: 30; height: 20; text: "12"; fontSize: 12 }
DropboxIcon { id: dropbox; iconSize: Style.bar.iconSize / aspectRatio; artworkScale: 1 }
TailscaleIcon { id: tailscale; iconSize: Style.bar.iconSize }
BarIconButton {
id: vector
bar: testBar
@@ -168,7 +337,7 @@ output=$(timeout 15 env \
if ! grep -q 'RESULT pass' <<<"$output"; then
printf '%s\n' "$output" >&2
fail "bar icons share slot and baseline geometry"
fail "bar icons share visual size and center geometry"
fi
pass "bar icons share slot and baseline geometry"
pass "bar icons share visual size and center geometry"