Correct custom bar icon bounds
This commit is contained in:
4 files changed
+142
-3
No files matched your search
@@ -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 !== ""
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user