Correct custom bar icon bounds

This commit is contained in:
Bjarne Oeverli committed 2026-10-11 22:02:37 +02:00
1 parent 201de1c0d6
commit 6575f06d53
4 files changed
+142 -3

No files matched your search

+1
View File
@@ -25,6 +25,7 @@ WidgetButton {
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
readonly property rect componentSourceBounds: customIcon.sourceBounds
labelVisible: false
hasVisualContent: text !== "" || iconComponent !== null || labelText !== ""
+28 -2
View File
@@ -11,8 +11,9 @@ Item {
readonly property bool textItem: sourceItem instanceof Text
readonly property bool imageItem: sourceItem instanceof Image
readonly property bool shapeItem: sourceItem instanceof Shape
readonly property bool boxItem: sourceItem instanceof Rectangle
|| sourceItem instanceof Shape || sourceItem instanceof Canvas || sourceItem instanceof ShaderEffect
|| sourceItem instanceof Canvas || sourceItem instanceof ShaderEffect
function unionBounds(a, b) {
if (b.width <= 0 || b.height <= 0) return a
@@ -23,13 +24,36 @@ Item {
Math.max(a.y + a.height, b.y + b.height) - y)
}
function shapeBounds(item) {
var bounds = item.boundingRect
var sx = 1
var sy = 1
if (item.fillMode !== Shape.NoResize && item.implicitWidth > 0 && item.implicitHeight > 0) {
sx = item.width / item.implicitWidth
sy = item.height / item.implicitHeight
if (item.fillMode === Shape.PreserveAspectFit) sx = sy = Math.min(sx, sy)
else if (item.fillMode === Shape.PreserveAspectCrop) sx = sy = Math.max(sx, sy)
}
var spareWidth = item.width - item.implicitWidth * sx
var spareHeight = item.height - item.implicitHeight * sy
var x = item.horizontalAlignment === Shape.AlignHCenter ? spareWidth / 2
: (item.horizontalAlignment === Shape.AlignRight ? spareWidth : 0)
var y = item.verticalAlignment === Shape.AlignVCenter ? spareHeight / 2
: (item.verticalAlignment === Shape.AlignBottom ? spareHeight : 0)
return Qt.rect(x + bounds.x * sx, y + bounds.y * sy, bounds.width * sx, bounds.height * sy)
}
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)
var spareWidth = item.width - item.leftPadding - item.rightPadding - item.contentWidth
var textX = item.leftPadding
if (item.effectiveHorizontalAlignment === Text.AlignHCenter) textX += spareWidth / 2
else if (item.effectiveHorizontalAlignment === Text.AlignRight) textX += spareWidth
bounds = Qt.rect(textX + ink.x, item.baselineOffset + ink.y, ink.width, ink.height)
} else if (imageItem) {
var w = item.paintedWidth
var h = item.paintedHeight
@@ -47,6 +71,8 @@ Item {
} else {
bounds = Qt.rect(0, 0, item.width, item.height)
}
} else if (shapeItem) {
bounds = shapeBounds(item)
} else if (boxItem) {
bounds = Qt.rect(0, 0, item.width, item.height)
}
+2
View File
@@ -86,6 +86,8 @@ The `[bar]` section of a theme's `shell.toml` supplies the shared dimensions. `i
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.
Custom text bounds include text alignment and padding. The bar applies each shape's fill mode and alignment to `Shape.boundingRect`. Canvas and shader icons use their item bounds. Set their item size to match their artwork.
`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
+111 -1
View File
@@ -21,6 +21,7 @@ ln -s "$ROOT/shell/plugins/panels/tailscale/TailscaleIcon.qml" "$test_tmp/Tailsc
cat >"$test_tmp/shell.qml" <<'QML'
import QtQuick
import QtQuick.Shapes
import Quickshell
import qs.Commons
import qs.Ui
@@ -31,6 +32,10 @@ ShellRoot {
property bool completed: false
property bool testsStarted: false
property int customRenderType: -1
property real textBoxWidth: 32
property real alignedTextContentWidth: 0
property real pathX: 6
property real pathY: 8
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) }))
@@ -100,7 +105,34 @@ ShellRoot {
fail("custom glyphs do not use the scalable renderer")
return false
}
var customIcons = [customBox, nestedBox, customGlyph, rotatedBox, customImage, croppedImage]
var alignedBounds = centeredGlyph.componentSourceBounds
var textSpare = root.textBoxWidth - 3 - 5 - root.alignedTextContentWidth
var expectedTextX = 3 + textSpare / 2 + customTextMetrics.tightBoundingRect.x
var pathBounds = paddedShape.componentSourceBounds
if (Math.abs(alignedBounds.x - expectedTextX) > 0.05
|| Math.abs(pathBounds.x - root.pathX) > 0.05 || Math.abs(pathBounds.y - root.pathY) > 0.05
|| Math.abs(pathBounds.width - 12) > 0.05 || Math.abs(pathBounds.height - 10) > 0.05) {
fail("custom source bounds are incorrect. Text origin: " + alignedBounds.x + " instead of " + expectedTextX
+ ". Shape bounds: " + [pathBounds.x, pathBounds.y, pathBounds.width, pathBounds.height].join(", "))
return false
}
var expectedRightX = 3 + textSpare + customTextMetrics.tightBoundingRect.x
if (Math.abs(rightGlyph.componentSourceBounds.x - expectedRightX) > 0.05
|| Math.abs(mirroredGlyph.componentSourceBounds.x - expectedRightX) > 0.05) {
fail("right-aligned or mirrored custom text has an incorrect origin")
return false
}
var shapeScale = Math.min(24 / (root.pathX + 12), 36 / (root.pathY + 10))
var fittedBounds = fittedShape.componentSourceBounds
if (Math.abs(fittedBounds.x - root.pathX * shapeScale) > 0.05
|| Math.abs(fittedBounds.y - (36 - 10 * shapeScale)) > 0.05
|| Math.abs(fittedBounds.width - 12 * shapeScale) > 0.05
|| Math.abs(fittedBounds.height - 10 * shapeScale) > 0.05) {
fail("a resized shape's path bounds ignore its fill mode or alignment")
return false
}
var customIcons = [customBox, nestedBox, customGlyph, rotatedBox, customImage, croppedImage,
centeredGlyph, rightGlyph, mirroredGlyph, paddedShape, fittedShape]
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
@@ -169,6 +201,9 @@ ShellRoot {
if (!checkGeometry()) return
var labelBaseline = plainLabel.baselineY
root.customWidth = 23
root.textBoxWidth = 48
root.pathX = 2
root.pathY = 3
Style.applyShellValues({ "font.base-size": "18", "bar.icon-size": "14", "bar.icon-canvas": "19" })
Qt.callLater(function() {
if (!checkGeometry()) return
@@ -274,6 +309,81 @@ ShellRoot {
}
}
}
TextMetrics {
id: customTextMetrics
font.family: "omarchy"
font.pixelSize: 15
text: "\ue900"
}
component AlignedTextIcon: Text {
width: root.textBoxWidth
height: 28
leftPadding: 3
rightPadding: 5
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
textFormat: Text.PlainText
text: "\ue900"
font.family: "omarchy"
font.pixelSize: 15
}
BarIconButton {
id: centeredGlyph
bar: testBar
iconComponent: Component {
AlignedTextIcon {
id: alignedText
onContentWidthChanged: root.alignedTextContentWidth = alignedText.contentWidth
Component.onCompleted: root.alignedTextContentWidth = alignedText.contentWidth
}
}
}
BarIconButton {
id: rightGlyph
bar: testBar
iconComponent: Component { AlignedTextIcon { horizontalAlignment: Text.AlignRight } }
}
BarIconButton {
id: mirroredGlyph
bar: testBar
iconComponent: Component {
AlignedTextIcon {
horizontalAlignment: Text.AlignLeft
LayoutMirroring.enabled: true
}
}
}
component PaddedShapeIcon: Shape {
width: 24
height: 24
ShapePath {
strokeWidth: 0
fillColor: "white"
startX: root.pathX
startY: root.pathY
PathLine { x: root.pathX + 12; y: root.pathY }
PathLine { x: root.pathX + 12; y: root.pathY + 10 }
PathLine { x: root.pathX; y: root.pathY + 10 }
PathLine { x: root.pathX; y: root.pathY }
}
}
BarIconButton {
id: paddedShape
bar: testBar
iconComponent: Component { PaddedShapeIcon {} }
}
BarIconButton {
id: fittedShape
bar: testBar
iconComponent: Component {
PaddedShapeIcon {
height: 36
fillMode: Shape.PreserveAspectFit
horizontalAlignment: Shape.AlignHCenter
verticalAlignment: Shape.AlignBottom
}
}
}
BarIconButton {
id: rotatedBox
bar: testBar