Normalize bar icon size and alignment
This commit is contained in:
17 files changed
+540
-116
No files matched your search
@@ -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
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
Reference in new issue
Block a user