Files
omarchy/test/shell.d/bar-icon-geometry-test.sh
T

344 lines
14 KiB
Bash
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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('<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)
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"