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 iconSlot: root.barToken("icon-slot", 27)
readonly property int iconCanvas: root.barToken("icon-canvas", 16) readonly property int iconCanvas: root.barToken("icon-canvas", 16)
readonly property int iconFont: root.barToken("icon-font", 13) 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) readonly property int statusSlot: root.barToken("status-slot", 21)
} }
@@ -424,7 +425,9 @@ QtObject {
} else if (section === "bar") { } else if (section === "bar") {
if (key === "scale-with-font") { if (key === "scale-with-font") {
nextBarScaleWithFont = boolToken(raw, nextBarScaleWithFont) 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) var b = parseInt(raw, 10)
if (isFinite(b)) barOut[key] = b if (isFinite(b)) barOut[key] = b
} }
+66 -28
View File
@@ -8,52 +8,90 @@ WidgetButton {
property Component iconComponent: null property Component iconComponent: null
property real slotSize: Style.bar.iconSlot property real slotSize: Style.bar.iconSlot
property real opticalSize: Style.bar.iconCanvas 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" property bool debugOpticalBounds: Quickshell.env("OMARCHY_DEBUG_BAR_ICONS") === "1"
// Measured against the slot, which the open-panel underline centers on. // A label shifts the icon within the centered group.
readonly property real opticalCenterErrorX: glyph.visible ? opticalCanvas.x + glyph.paintedCenterX - root.width / 2 : 0 readonly property real opticalCenterErrorX:
readonly property real glyphPaintedWidth: glyph.visible ? glyph.tightWidth : 0 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 real glyphBaselineY: glyph.visible ? glyph.baselineY : 0
readonly property int glyphFontSize: glyph.visible ? glyph.renderedFontSize : 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 labelVisible: false
hasVisualContent: text !== "" || iconComponent !== null hasVisualContent: text !== "" || iconComponent !== null || labelText !== ""
fontSize: Style.bar.iconFont fontSize: Style.bar.iconFont
fixedWidth: vertical ? -1 : slotSize fixedWidth: vertical ? -1 : slotSize
fixedHeight: vertical ? slotSize : -1 fixedHeight: vertical ? slotSize : -1
Item { Item {
id: opticalCanvas id: content
// Placed exactly, not anchored: centerIn snaps an even canvas in an odd slot // Exact coordinates preserve the center of odd-width slots.
// to a whole pixel, pulling every glyph off the open-panel underline.
x: (root.width - width) / 2 x: (root.width - width) / 2
y: (root.height - height) / 2 y: (root.height - height) / 2
width: root.opticalSize width: root.canvasSize + (label.visible ? label.implicitWidth + Style.space(4) : 0)
height: root.opticalSize height: root.canvasSize
OpticalGlyph { Text {
id: glyph id: label
anchors.fill: parent visible: root.labelText !== ""
visible: root.iconComponent === null textFormat: Text.PlainText
text: root.text text: root.labelText
fontFamily: root.fontFamily font.family: root.fontFamily
fontSize: root.fontSize font.pixelSize: root.fontSize
color: root.active && root.useActiveColor ? root.activeColor : root.foreground color: root.active && root.useActiveColor ? root.activeColor : root.foreground
rotation: root.textRotation y: (parent.height - height) / 2
debugBounds: root.debugOpticalBounds + (labelMetrics.descent - labelMetrics.ascent + labelMetrics.tightBoundingRect("0").height) / 2
renderType: Text.NativeRendering
} }
Loader { FontMetrics {
anchors.fill: parent id: labelMetrics
visible: root.iconComponent !== null font: label.font
sourceComponent: root.iconComponent
} }
Rectangle { Item {
visible: root.debugOpticalBounds && root.iconComponent !== null id: opticalCanvas
anchors.fill: parent x: label.visible ? label.implicitWidth + Style.space(4) : 0
color: "transparent" width: root.canvasSize
border.width: 1 height: root.canvasSize
border.color: "#4488ff"
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 useActiveColor: false
maintainIndicatorReveal: indicatorBlock === "inactive" maintainIndicatorReveal: indicatorBlock === "inactive"
revealHost: indicatorHost revealHost: indicatorHost
fontSize: Style.font.caption
horizontalMargin: 5 horizontalMargin: 5
verticalPadding: 5 verticalPadding: 5
fixedWidth: vertical ? -1 : Style.bar.statusSlot 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 text: ""
property string fontFamily: Style.font.family property string fontFamily: Style.font.family
property real fontSize: Style.font.body property real fontSize: Style.font.body
property real paintedSize: 0
property color color: Commons.Color.foreground property color color: Commons.Color.foreground
property bool debugBounds: false property bool debugBounds: false
readonly property int renderedFontSize: Math.max(1, Math.round(fontSize)) readonly property int renderedFontSize: Math.max(1, Math.round(fontSize))
readonly property real tightWidth: Math.max(1, glyphMetrics.tightBoundingRect.width) 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 horizontalCorrection: glyph.implicitWidth / 2 - (glyphMetrics.tightBoundingRect.x + tightWidth / 2)
readonly property real paintedCenterX: glyph.x + glyphMetrics.tightBoundingRect.x + tightWidth / 2 readonly property real paintedCenterX: glyph.x + (glyphMetrics.tightBoundingRect.x + tightWidth / 2) * paintedScale
readonly property real baselineY: glyph.y + glyph.baselineOffset readonly property real paintedCenterY: glyph.y + (glyph.baselineOffset + glyphMetrics.tightBoundingRect.y + tightHeight / 2) * paintedScale
readonly property real baselineY: glyph.y + glyph.baselineOffset * paintedScale
TextMetrics { TextMetrics {
id: glyphMetrics id: glyphMetrics
@@ -27,17 +35,18 @@ Item {
Text { Text {
id: glyph id: glyph
textFormat: Text.PlainText textFormat: Text.PlainText
// Keep the shared line box and baseline intact. Correcting only the // Bar icons fit their painted bounds. Text labels retain their line box.
// horizontal painted bounds avoids per-glyph vertical drift. x: root.width / 2 - (glyphMetrics.tightBoundingRect.x + root.tightWidth / 2) * root.paintedScale
// The correction is fractional, so x is set exactly: centerIn would snap it y: root.paintedSize > 0
// to a whole pixel and undo it. y still snaps, keeping the baseline crisp. ? root.height / 2 - (baselineOffset + glyphMetrics.tightBoundingRect.y + root.tightHeight / 2) * root.paintedScale
x: (root.width - width) / 2 + root.horizontalCorrection : Math.round((root.height - height) / 2)
y: Math.round((root.height - height) / 2)
text: root.text text: root.text
color: root.color color: root.color
font.family: root.fontFamily font.family: root.fontFamily
font.pixelSize: root.renderedFontSize font.pixelSize: root.renderedFontSize
renderType: Text.NativeRendering transformOrigin: Item.TopLeft
scale: root.paintedScale
renderType: root.paintedSize > 0 ? Text.CurveRendering : Text.NativeRendering
} }
Rectangle { 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 // 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. // 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 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 visible: hasVisualContent || keepSpace
opacity: !hasVisualContent || concealed ? 0 : (dimmed ? 0.45 : 1) opacity: !hasVisualContent || concealed ? 0 : (dimmed ? 0.45 : 1)
implicitWidth: fixedWidth > 0 ? fixedWidth : (vertical ? barSize : Math.max(12, label.implicitWidth + scaledHorizontalMargin * 2)) implicitWidth: fixedWidth > 0 ? fixedWidth
implicitHeight: fixedHeight > 0 ? fixedHeight : (vertical ? Math.max(12, label.implicitHeight + scaledVerticalPadding * 2) : barSize) : (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 { Behavior on opacity {
NumberAnimation { duration: Style.duration(140); easing.type: Easing.OutCubic } NumberAnimation { duration: Style.duration(140); easing.type: Easing.OutCubic }
@@ -78,7 +98,7 @@ Item {
Text { Text {
id: label id: label
textFormat: Text.PlainText textFormat: Text.PlainText
visible: root.labelVisible visible: root.labelVisible && !root.iconLabel
anchors.centerIn: parent anchors.centerIn: parent
// Centering the line box puts the figures above the bar's middle, since // 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 // the box keeps room for descenders. Center the figure height instead, so
@@ -106,6 +126,25 @@ Item {
font: label.font 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 { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
+2
View File
@@ -2,6 +2,7 @@ module qs.Ui
BarIndicator 1.0 BarIndicator.qml BarIndicator 1.0 BarIndicator.qml
BarIconButton 1.0 BarIconButton.qml BarIconButton 1.0 BarIconButton.qml
BarIconContent 1.0 BarIconContent.qml
BarWidget 1.0 BarWidget.qml BarWidget 1.0 BarWidget.qml
BackgroundMedia 1.0 BackgroundMedia.qml BackgroundMedia 1.0 BackgroundMedia.qml
BorderOverlay 1.0 BorderOverlay.qml BorderOverlay 1.0 BorderOverlay.qml
@@ -11,6 +12,7 @@ ButtonGroup 1.0 ButtonGroup.qml
ConfirmDialog 1.0 ConfirmDialog.qml ConfirmDialog 1.0 ConfirmDialog.qml
CursorSurface 1.0 CursorSurface.qml CursorSurface 1.0 CursorSurface.qml
Dropdown 1.0 Dropdown.qml Dropdown 1.0 Dropdown.qml
IconContentBounds 1.0 IconContentBounds.qml
KeyboardPanel 1.0 KeyboardPanel.qml KeyboardPanel 1.0 KeyboardPanel.qml
MultiSelect 1.0 MultiSelect.qml MultiSelect 1.0 MultiSelect.qml
NumberField 1.0 NumberField.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). 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 ## 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`. 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 bar: root.bar
text: "\uf021" text: "\uf021"
slotSize: Style.bar.statusSlot slotSize: Style.bar.statusSlot
fontSize: Style.font.caption
tooltipText: "Pending Omarchy Updates" tooltipText: "Pending Omarchy Updates"
onPressed: root.runUpdate() onPressed: root.runUpdate()
} }
+4 -3
View File
@@ -810,9 +810,10 @@ BarWidget {
} }
TrayIcon { TrayIcon {
anchors.centerIn: parent x: (parent.width - width) / 2
width: Style.space(12) y: (parent.height - height) / 2
height: Style.space(12) width: Style.bar.iconSize
height: Style.bar.iconSize
icon: trayItemRoot.modelData.icon icon: trayItemRoot.modelData.icon
} }
+1 -3
View File
@@ -8,14 +8,12 @@ BarWidget {
implicitWidth: button.implicitWidth implicitWidth: button.implicitWidth
implicitHeight: button.implicitHeight implicitHeight: button.implicitHeight
WidgetButton { BarIconButton {
id: button id: button
anchors.fill: parent anchors.fill: parent
bar: root.bar bar: root.bar
text: "\ue900" text: "\ue900"
fontFamily: "omarchy" fontFamily: "omarchy"
centerFigures: false
horizontalMargin: 7.5
onPressed: function(button) { onPressed: function(button) {
if (!root.bar) return if (!root.bar) return
if (button === Qt.RightButton) root.bar.run("xdg-terminal-exec") if (button === Qt.RightButton) root.bar.run("xdg-terminal-exec")
+5 -3
View File
@@ -7,11 +7,13 @@ Item {
id: root id: root
property real iconSize: Style.font.icon property real iconSize: Style.font.icon
readonly property real aspectRatio: 1.18
property real artworkScale: 0.95
property color color: Commons.Color.foreground property color color: Commons.Color.foreground
width: iconSize * 1.18 width: iconSize * aspectRatio
height: iconSize height: iconSize
implicitWidth: iconSize * 1.18 implicitWidth: iconSize * aspectRatio
implicitHeight: iconSize implicitHeight: iconSize
Shape { Shape {
@@ -19,7 +21,7 @@ Item {
antialiasing: true antialiasing: true
layer.enabled: true layer.enabled: true
layer.samples: 4 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.25; cy: root.height * 0.188 }
Tile { cx: root.width * 0.75; 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 { iconComponent: Component {
Item { Item {
DropboxIcon { DropboxIcon {
anchors.centerIn: parent x: (parent.width - width) / 2
iconSize: Style.space(12) y: (parent.height - height) / 2
iconSize: button.iconSize / aspectRatio
artworkScale: 1
color: root.barIconColor color: root.barIconColor
opacity: dropbox.active ? 1.0 : 0.6 opacity: dropbox.active ? 1.0 : 0.6
} }
+2 -3
View File
@@ -278,9 +278,8 @@ Panel {
id: button id: button
anchors.fill: parent anchors.fill: parent
bar: root.bar bar: root.bar
text: root.showPercentage && !vertical text: root.batteryIcon()
? Math.round(root.batteryFraction * 100) + "% " + root.batteryIcon() labelText: root.showPercentage && !vertical ? Math.round(root.batteryFraction * 100) + "%" : ""
: root.batteryIcon()
slotSize: Style.bar.iconSlot * (root.showPercentage && !vertical ? 2 : 1) slotSize: Style.bar.iconSlot * (root.showPercentage && !vertical ? 2 : 1)
tooltipText: "" tooltipText: ""
onPressed: function(b) { onPressed: function(b) {
+3 -2
View File
@@ -385,8 +385,9 @@ Panel {
iconComponent: Component { iconComponent: Component {
Item { Item {
TailscaleIcon { TailscaleIcon {
anchors.centerIn: parent x: (parent.width - width) / 2
iconSize: Style.space(11) y: (parent.height - height) / 2
iconSize: button.iconSize
color: root.barIconColor color: root.barIconColor
badgeColor: root.urgent badgeColor: root.urgent
crossed: !tailscale.active && !tailscale.needsLogin 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/Ui" "$test_tmp/Ui"
ln -s "$ROOT/shell/Commons" "$test_tmp/Commons" 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' cat >"$test_tmp/shell.qml" <<'QML'
import QtQuick import QtQuick
@@ -25,12 +27,30 @@ import qs.Ui
ShellRoot { ShellRoot {
id: root 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) { function fail(message) {
console.log("RESULT fail " + message) console.log("RESULT fail " + message)
Qt.quit() 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) { function checkIcon(icon, name) {
if (icon.implicitWidth !== Style.bar.iconSlot) { if (icon.implicitWidth !== Style.bar.iconSlot) {
fail(name + " slot width is " + icon.implicitWidth) fail(name + " slot width is " + icon.implicitWidth)
@@ -40,68 +60,144 @@ ShellRoot {
fail(name + " optical canvas is " + icon.opticalSize) fail(name + " optical canvas is " + icon.opticalSize)
return false 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) { if (icon.glyphFontSize !== Style.bar.iconFont) {
fail(name + " font size is " + icon.glyphFontSize) fail(name + " font size is " + icon.glyphFontSize)
return false 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 return true
} }
Component.onCompleted: Qt.callLater(function() { function runGeometryTests() {
if (!checkIcon(bluetooth, "bluetooth")) return if (testsStarted) return
if (!checkIcon(network, "network")) return testsStarted = true
if (!checkIcon(audio, "audio")) return if (!checkGeometry()) return
if (!checkIcon(monitor, "monitor")) return var labelBaseline = plainLabel.baselineY
if (!checkIcon(power, "power")) return root.customWidth = 23
if (!checkIcon(elsewhen, "elsewhen")) return Style.applyShellValues({ "font.base-size": "18", "bar.icon-size": "14", "bar.icon-canvas": "19" })
var baseline = bluetooth.glyphBaselineY Qt.callLater(function() {
if (network.glyphBaselineY !== baseline || audio.glyphBaselineY !== baseline if (!checkGeometry()) return
|| monitor.glyphBaselineY !== baseline || power.glyphBaselineY !== baseline) { if (Style.bar.iconSize !== 21 || Style.bar.iconCanvas !== 29) {
fail("glyph baselines do not match") fail("theme icon dimensions do not follow the font scale")
return return
} }
if (vector.implicitWidth !== Style.bar.iconSlot || vector.opticalSize !== Style.bar.iconCanvas) { if (plainLabel.baselineY !== labelBaseline) {
fail("vector icon does not share glyph geometry") fail("bar icon settings change a text label's baseline")
return return
} }
if (verticalIcon.implicitWidth !== Style.bar.sizeVertical || verticalIcon.implicitHeight !== Style.bar.iconSlot) { Style.applyShellValues({ "font.base-size": "18", "bar.scale-with-font": "false", "bar.icon-size": "20" })
fail("vertical icon does not use the shared slot") Qt.callLater(function() {
return if (!checkGeometry()) return
} if (Style.bar.iconSize !== Style.bar.iconCanvas) {
if (verticalIndicator.implicitWidth !== Style.bar.sizeVertical || verticalIndicator.implicitHeight >= Style.bar.iconSlot) { fail("oversized icons do not fit their canvas")
fail("vertical indicator does not retain compact spacing") return
return }
} console.log("RESULT pass")
if (verticalIndicator.glyphFontSize !== Style.font.caption) { Qt.quit()
fail("indicator does not use the secondary icon scale") })
return })
} }
if (Math.abs(verticalIndicator.opticalCenterErrorX) > 0.05) {
fail("vertical indicator is not optically centered") onImageReadyChanged: if (imageReady && completed) Qt.callLater(runGeometryTests)
return Component.onCompleted: {
} completed = true
if (horizontalIndicator.implicitWidth >= Style.bar.iconSlot) { if (imageReady) Qt.callLater(runGeometryTests)
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()
})
QtObject { QtObject {
id: testBar id: testBar
@@ -137,6 +233,79 @@ ShellRoot {
BarIconButton { id: monitor; bar: testBar; text: "󰍹" } BarIconButton { id: monitor; bar: testBar; text: "󰍹" }
BarIconButton { id: power; bar: testBar; text: "󰁹" } BarIconButton { id: power; bar: testBar; text: "󰁹" }
BarIconButton { id: elsewhen; 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 { BarIconButton {
id: vector id: vector
bar: testBar bar: testBar
@@ -168,7 +337,7 @@ output=$(timeout 15 env \
if ! grep -q 'RESULT pass' <<<"$output"; then if ! grep -q 'RESULT pass' <<<"$output"; then
printf '%s\n' "$output" >&2 printf '%s\n' "$output" >&2
fail "bar icons share slot and baseline geometry" fail "bar icons share visual size and center geometry"
fi fi
pass "bar icons share slot and baseline geometry" pass "bar icons share visual size and center geometry"