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 int glyphFontSize: glyph.visible ? glyph.renderedFontSize : 0
readonly property real componentPaintedWidth: customIcon.visible ? customIcon.paintedWidth : 0 readonly property real componentPaintedWidth: customIcon.visible ? customIcon.paintedWidth : 0
readonly property real componentPaintedHeight: customIcon.visible ? customIcon.paintedHeight : 0 readonly property real componentPaintedHeight: customIcon.visible ? customIcon.paintedHeight : 0
readonly property rect componentSourceBounds: customIcon.sourceBounds
labelVisible: false labelVisible: false
hasVisualContent: text !== "" || iconComponent !== null || labelText !== "" hasVisualContent: text !== "" || iconComponent !== null || labelText !== ""
+28 -2
View File
@@ -11,8 +11,9 @@ Item {
readonly property bool textItem: sourceItem instanceof Text readonly property bool textItem: sourceItem instanceof Text
readonly property bool imageItem: sourceItem instanceof Image readonly property bool imageItem: sourceItem instanceof Image
readonly property bool shapeItem: sourceItem instanceof Shape
readonly property bool boxItem: sourceItem instanceof Rectangle 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) { function unionBounds(a, b) {
if (b.width <= 0 || b.height <= 0) return a 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) 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: { readonly property rect paintedBounds: {
var item = sourceItem var item = sourceItem
var bounds = Qt.rect(0, 0, 0, 0) var bounds = Qt.rect(0, 0, 0, 0)
if (!item || !item.visible || item.opacity <= 0) return bounds if (!item || !item.visible || item.opacity <= 0) return bounds
if (textItem) { if (textItem) {
var ink = textMetrics.tightBoundingRect 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) { } else if (imageItem) {
var w = item.paintedWidth var w = item.paintedWidth
var h = item.paintedHeight var h = item.paintedHeight
@@ -47,6 +71,8 @@ Item {
} else { } else {
bounds = Qt.rect(0, 0, item.width, item.height) bounds = Qt.rect(0, 0, item.width, item.height)
} }
} else if (shapeItem) {
bounds = shapeBounds(item)
} else if (boxItem) { } else if (boxItem) {
bounds = Qt.rect(0, 0, item.width, item.height) 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 `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. `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
+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' cat >"$test_tmp/shell.qml" <<'QML'
import QtQuick import QtQuick
import QtQuick.Shapes
import Quickshell import Quickshell
import qs.Commons import qs.Commons
import qs.Ui import qs.Ui
@@ -31,6 +32,10 @@ ShellRoot {
property bool completed: false property bool completed: false
property bool testsStarted: false property bool testsStarted: false
property int customRenderType: -1 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 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) })) 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") fail("custom glyphs do not use the scalable renderer")
return false 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++) { for (var i = 0; i < customIcons.length; i++) {
var custom = customIcons[i] var custom = customIcons[i]
if (Math.abs(Math.max(custom.componentPaintedWidth, custom.componentPaintedHeight) - Style.bar.iconSize) > 0.05 if (Math.abs(Math.max(custom.componentPaintedWidth, custom.componentPaintedHeight) - Style.bar.iconSize) > 0.05
@@ -169,6 +201,9 @@ ShellRoot {
if (!checkGeometry()) return if (!checkGeometry()) return
var labelBaseline = plainLabel.baselineY var labelBaseline = plainLabel.baselineY
root.customWidth = 23 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" }) Style.applyShellValues({ "font.base-size": "18", "bar.icon-size": "14", "bar.icon-canvas": "19" })
Qt.callLater(function() { Qt.callLater(function() {
if (!checkGeometry()) return 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 { BarIconButton {
id: rotatedBox id: rotatedBox
bar: testBar bar: testBar