diff --git a/shell/Commons/BorderGeometry.js b/shell/Commons/BorderGeometry.js index 38b372cc..96ba838a 100644 --- a/shell/Commons/BorderGeometry.js +++ b/shell/Commons/BorderGeometry.js @@ -206,21 +206,33 @@ function ringPath(w, h, radius, widths) { blrx: radius, blry: radius, }) - var ix = widths.left || 0 - var iy = widths.top || 0 - var iw = w - ix - (widths.right || 0) - var ih = h - iy - (widths.bottom || 0) + // Shape's OddEven fill can collapse to the outer fill when the inner + // cutout touches the outer path on one or more zero-width sides. Keep the + // cutout strictly inside the outer path with a subpixel inset so one-sided + // borders (for example selected-border-width = "0 0 0 4") render as a + // strip instead of painting the whole row. + var epsilon = 0.001 + var left = Math.max(0, widths.left || 0) + var top = Math.max(0, widths.top || 0) + var right = Math.max(0, widths.right || 0) + var bottom = Math.max(0, widths.bottom || 0) + var ix = Math.max(left, epsilon) + var iy = Math.max(top, epsilon) + var ir = Math.max(right, epsilon) + var ib = Math.max(bottom, epsilon) + var iw = w - ix - ir + var ih = h - iy - ib if (iw <= 0 || ih <= 0) return outer var inner = roundedRectPath(ix, iy, iw, ih, { - tlrx: Math.max(0, radius - (widths.left || 0)), - tlry: Math.max(0, radius - (widths.top || 0)), - trrx: Math.max(0, radius - (widths.right || 0)), - trry: Math.max(0, radius - (widths.top || 0)), - brrx: Math.max(0, radius - (widths.right || 0)), - brry: Math.max(0, radius - (widths.bottom || 0)), - blrx: Math.max(0, radius - (widths.left || 0)), - blry: Math.max(0, radius - (widths.bottom || 0)), + tlrx: Math.max(0, radius - left), + tlry: Math.max(0, radius - top), + trrx: Math.max(0, radius - right), + trry: Math.max(0, radius - top), + brrx: Math.max(0, radius - right), + brry: Math.max(0, radius - bottom), + blrx: Math.max(0, radius - left), + blry: Math.max(0, radius - bottom), }) return outer + " " + inner diff --git a/shell/plugins/launcher/Launcher.qml b/shell/plugins/launcher/Launcher.qml index 91cc0806..401648b5 100644 --- a/shell/plugins/launcher/Launcher.qml +++ b/shell/plugins/launcher/Launcher.qml @@ -43,6 +43,8 @@ Item { property color selectedText: Color.launcher.selectedText property color selectedBorder: Color.launcher.selectedBorder property var selectedBorderSpec: Border.surfaceSpec("launcher", "selected-border", selectedBorder, 0) + readonly property real rowReservedBorderLeft: Border.left(selectedBorderSpec) + readonly property real rowReservedBorderRight: Border.right(selectedBorderSpec) property string fontFamily: Style.font.menuFamily property int cardWidth: 644 @@ -495,7 +497,7 @@ Item { Item { id: iconSlot anchors.left: parent.left - anchors.leftMargin: row.borderLeft + 14 + anchors.leftMargin: root.rowReservedBorderLeft + 14 anchors.verticalCenter: parent.verticalCenter width: root.iconSlotWidth height: parent.height @@ -525,7 +527,7 @@ Item { anchors.left: iconSlot.right anchors.leftMargin: 14 anchors.right: parent.right - anchors.rightMargin: row.borderRight + 14 + anchors.rightMargin: root.rowReservedBorderRight + 14 anchors.verticalCenter: parent.verticalCenter text: row.name color: row.hasCursor ? root.selectedText : root.foreground diff --git a/shell/plugins/menu/Menu.qml b/shell/plugins/menu/Menu.qml index 54cbdf60..b77a445c 100644 --- a/shell/plugins/menu/Menu.qml +++ b/shell/plugins/menu/Menu.qml @@ -84,6 +84,8 @@ Item { property color selectedText: Color.menu.selectedText property color selectedBorder: Color.menu.selectedBorder property var selectedBorderSpec: Border.surfaceSpec("menu", "selected-border", selectedBorder, 0) + readonly property real rowReservedBorderLeft: Border.left(selectedBorderSpec) + readonly property real rowReservedBorderRight: Border.right(selectedBorderSpec) readonly property int cornerRadius: Style.cornerRadius property int contentMargin: Style.spacing.panelPadding property int headerHeight: Math.max(Style.space(34), Style.font.title + Style.spacing.controlPaddingY * 2) @@ -933,7 +935,7 @@ Item { radius: Math.min(root.cornerRadius, Style.space(4)) color: root.selectedBackground anchors.left: parent.left - anchors.leftMargin: row.borderLeft + Style.space(8) + anchors.leftMargin: root.rowReservedBorderLeft + Style.space(8) anchors.verticalCenter: parent.verticalCenter } @@ -948,14 +950,14 @@ Item { horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter anchors.left: parent.left - anchors.leftMargin: row.borderLeft + Style.space(8) + anchors.leftMargin: root.rowReservedBorderLeft + Style.space(8) y: contentColumn.y + labelText.y + (labelText.height - height) / 2 } Column { id: contentColumn anchors.left: row.hasIcon ? iconText.right : parent.left - anchors.leftMargin: row.hasIcon ? Style.space(6) : row.borderLeft + Style.space(18) + anchors.leftMargin: row.hasIcon ? Style.space(6) : root.rowReservedBorderLeft + Style.space(18) anchors.right: trail.left anchors.rightMargin: Style.space(6) anchors.verticalCenter: parent.verticalCenter @@ -988,7 +990,7 @@ Item { id: trail width: Style.space(14) anchors.right: parent.right - anchors.rightMargin: row.borderRight + Style.space(8) + anchors.rightMargin: root.rowReservedBorderRight + Style.space(8) y: contentColumn.y + labelText.y + (labelText.height - height) / 2 spacing: 0 diff --git a/test/shell.d/border-geometry-test.sh b/test/shell.d/border-geometry-test.sh index 65718dd8..9c1d70ed 100755 --- a/test/shell.d/border-geometry-test.sh +++ b/test/shell.d/border-geometry-test.sh @@ -38,6 +38,10 @@ const pathData = geometry.ringPath(100, 50, 10, { top: 4, right: 2, bottom: 8, l assert(pathData.includes('M 10 0'), 'border geometry emits outer rounded path') assert(pathData.includes('M 10 4'), 'border geometry emits inset inner rounded path') +const oneSidedPath = geometry.ringPath(100, 50, 10, { top: 0, right: 0, bottom: 0, left: 4 }) +assert(oneSidedPath.includes('M 10 0.001'), 'border geometry keeps zero-width top inside outer path') +assert(oneSidedPath.includes('99.999 10.001'), 'border geometry keeps zero-width right inside outer path') + const endpoints = geometry.gradientEndpoints(100, 50, 0) assertEqual(Math.round(endpoints.x1), 0, 'border geometry 0deg starts at left edge') assertEqual(Math.round(endpoints.x2), 100, 'border geometry 0deg ends at right edge') diff --git a/test/shell.d/row-border-stability-test.sh b/test/shell.d/row-border-stability-test.sh new file mode 100755 index 00000000..35df4c9c --- /dev/null +++ b/test/shell.d/row-border-stability-test.sh @@ -0,0 +1,31 @@ +#!/bin/bash +source "$(dirname "$0")/base-test.sh" + +run_node_test <<'JS' +const fs = require('fs') + +const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8') +const launcherQml = fs.readFileSync(path.join(root, 'shell/plugins/launcher/Launcher.qml'), 'utf8') + +assert( + /rowReservedBorderLeft:\s*Border\.left\(selectedBorderSpec\)/.test(menuQml) + && /rowReservedBorderRight:\s*Border\.right\(selectedBorderSpec\)/.test(menuQml), + 'Menu rows reserve selected border insets' +) + +assert( + !/anchors\.(left|right)Margin:[^\n]*\brow\.border(Left|Right)\b/.test(menuQml), + 'Menu row content does not depend on current selected border state' +) + +assert( + /rowReservedBorderLeft:\s*Border\.left\(selectedBorderSpec\)/.test(launcherQml) + && /rowReservedBorderRight:\s*Border\.right\(selectedBorderSpec\)/.test(launcherQml), + 'Launcher rows reserve selected border insets' +) + +assert( + !/anchors\.(left|right)Margin:[^\n]*\brow\.border(Left|Right)\b/.test(launcherQml), + 'Launcher row content does not depend on current selected border state' +) +JS