#!/bin/bash set -euo pipefail source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh" require_compositor "bar icon geometry test" if ! command -v quickshell >/dev/null 2>&1; then skip "quickshell not installed; skipping bar icon geometry test" exit 0 fi test_tmp=$(mktemp -d) 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 import Quickshell import qs.Commons 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(''.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) return false } if (icon.opticalSize !== Style.bar.iconCanvas) { fail(name + " optical canvas is " + icon.opticalSize) 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 } 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 property bool vertical: false property int barSize: Style.bar.sizeHorizontal property string fontFamily: Style.font.family property color barForeground: "white" property color urgent: "red" property bool foregroundAnimationEnabled: false function registerClickTarget(target) {} function unregisterClickTarget(target) {} function hideTooltip(target) {} function showTooltip(target, text) {} } QtObject { id: verticalBar property bool vertical: true property int barSize: Style.bar.sizeVertical property string fontFamily: Style.font.family property color barForeground: "white" property color urgent: "red" property bool foregroundAnimationEnabled: false function registerClickTarget(target) {} function unregisterClickTarget(target) {} function hideTooltip(target) {} function showTooltip(target, text) {} } BarIconButton { id: bluetooth; bar: testBar; text: "󰂯" } BarIconButton { id: network; bar: testBar; text: "󰖩" } BarIconButton { id: audio; bar: testBar; text: "󰖁" } 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 iconComponent: Component { Rectangle { width: 12; height: 12 } } } BarIconButton { id: verticalIcon; bar: verticalBar; text: "\uf021" } BarIconButton { id: compactStatusIcon; bar: testBar; text: "\uf021"; slotSize: Style.bar.statusSlot } BarIconButton { id: compactVerticalStatusIcon; bar: verticalBar; text: "\uf021"; slotSize: Style.bar.statusSlot } Row { id: horizontalIndicatorPair BarIndicator { id: horizontalIndicator; bar: testBar; active: true; activeText: "󰅶" } BarIndicator { bar: testBar; active: true; activeText: "󰔎" } } Column { id: verticalIndicatorPair BarIndicator { id: verticalIndicator; bar: verticalBar; active: true; activeText: "󰅶" } BarIndicator { bar: verticalBar; active: true; activeText: "󰔎" } } } QML output=$(timeout 15 env \ QML2_IMPORT_PATH="$ROOT/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \ QML_IMPORT_PATH="$ROOT/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \ quickshell -p "$test_tmp" --no-color 2>&1) || { printf '%s\n' "$output" >&2 fail "bar icon geometry fixture exits cleanly" } if ! grep -q 'RESULT pass' <<<"$output"; then printf '%s\n' "$output" >&2 fail "bar icons share visual size and center geometry" fi pass "bar icons share visual size and center geometry"