diff --git a/config/dot/quickshell/config/PreferenceSchema.qml b/config/dot/quickshell/config/PreferenceSchema.qml index 5f828d0..e92e226 100644 --- a/config/dot/quickshell/config/PreferenceSchema.qml +++ b/config/dot/quickshell/config/PreferenceSchema.qml @@ -91,6 +91,25 @@ Singleton { label: "Automatically hide the Dock", detail: "Reveal it at the bottom edge when a workspace is occupied" }, + { + key: "dockPosition", type: "enum", def: "bottom", group: "dock", + label: "Position", + detail: "Which edge the Dock lives on", + options: [ + { value: "bottom", label: "Bottom" }, + { value: "left", label: "Left" }, + { value: "right", label: "Right" } + ] + }, + { + // A "json" value: the screen names the Dock appears on. Empty means + // every screen, which is both the sensible default and the right + // answer for the common single-monitor case -- storing a list of + // names there would go stale the moment a display is unplugged. + key: "dockScreens", type: "json", def: [], group: "dock", + label: "Screens", + detail: "Which displays show the Dock" + }, { key: "dockIconSize", type: "int", def: 48, min: 32, max: 80, step: 4, unit: "px", group: "dock", diff --git a/config/dot/quickshell/config/Settings.qml b/config/dot/quickshell/config/Settings.qml index 99185df..391eada 100644 --- a/config/dot/quickshell/config/Settings.qml +++ b/config/dot/quickshell/config/Settings.qml @@ -63,6 +63,11 @@ Singleton { // ── Dock ──────────────────────────────────────────────────────────────── // Pinned apps, in order, taken from the GNOME dash favorites. + readonly property string dockPosition: DesktopPreferences.get("dockPosition") + readonly property var dockScreens: { + const stored = DesktopPreferences.get("dockScreens"); + return Array.isArray(stored) ? stored : []; + } readonly property var dockPinned: DesktopPreferences.get("dockPinned") // Dash-to-Dock was set to intellihide against all windows: the dock hides diff --git a/config/dot/quickshell/modules/dock/Dock.qml b/config/dot/quickshell/modules/dock/Dock.qml index 1e16bb7..c51dd46 100644 --- a/config/dot/quickshell/modules/dock/Dock.qml +++ b/config/dot/quickshell/modules/dock/Dock.qml @@ -22,10 +22,34 @@ PanelWindow { property var modelData: null screen: root.modelData + // Which edge this dock lives on, and everything that follows from it. The + // bottom case is unchanged in every particular: same anchors, same + // geometry, same slide -- so a machine that never touches the setting sees + // exactly the dock it had. + readonly property string position: Settings.dockPosition + readonly property bool vertical: root.position === "left" || root.position === "right" + + // Only on the screens asked for. An empty list means all of them, which is + // what a single-monitor machine wants and what an unplugged display should + // not be able to change. + readonly property bool onThisScreen: { + const wanted = Settings.dockScreens; + if (!wanted || wanted.length === 0) + return true; + return wanted.indexOf(String(root.screen?.name ?? "")) >= 0; + } + + visible: root.onThisScreen + + // A dock spans the edge it lives on, which means anchoring BOTH ends of + // that edge: bottom+left+right across the screen, or top+bottom plus one + // side down it. Anchoring only one end leaves the surface free to collapse + // to its implicit size on that axis -- a side dock came out one pixel tall. anchors { + top: root.vertical bottom: true - left: true - right: true + left: root.position !== "right" + right: root.position !== "left" } color: "transparent" @@ -42,10 +66,13 @@ PanelWindow { // ── Geometry ──────────────────────────────────────────────────────────── // Height = room for the tooltip above the bar + the bar + the gap under it. readonly property int revealStripHeight: 3 - readonly property int bottomMargin: Theme.barGap + readonly property int edgeMargin: Theme.barGap readonly property int tooltipSpace: 34 - implicitHeight: tooltipSpace + body.implicitHeight + bottomMargin + // Only the axis the dock is thin on gets an implicit size; the other is + // spanned by the anchors above. Setting both would fight them. + implicitHeight: root.vertical ? 0 : tooltipSpace + body.implicitHeight + edgeMargin + implicitWidth: root.vertical ? tooltipSpace + body.implicitWidth + edgeMargin : 0 // ── Intellihide ───────────────────────────────────────────────────────── // This instance's own monitor, the same lookup Workspaces.qml uses to @@ -140,22 +167,71 @@ PanelWindow { id: pointer } + // Revealed: the dock plus everything between it and the edge, so + // crossing the gap does not count as leaving. Hidden: a sliver along + // the edge the dock lives on, which is the only thing that can bring + // it back -- every other click passes through to the window beneath. Item { id: maskItem - x: root.revealed ? body.x : 0 - y: root.revealed ? body.y : surface.height - root.revealStripHeight - width: root.revealed ? body.width : surface.width - height: root.revealed ? surface.height - body.y : root.revealStripHeight + + x: { + if (!root.revealed) + return root.position === "right" ? surface.width - root.revealStripHeight : 0; + return root.position === "right" ? body.x : 0; + } + y: { + if (!root.revealed) + return root.vertical ? 0 : surface.height - root.revealStripHeight; + return root.vertical ? body.y : body.y; + } + width: { + if (!root.revealed) + return root.vertical ? root.revealStripHeight : surface.width; + return root.vertical + ? (root.position === "right" ? surface.width - body.x : body.x + body.width) + : body.width; + } + height: { + if (!root.revealed) + return root.vertical ? surface.height : root.revealStripHeight; + return root.vertical ? body.height : surface.height - body.y; + } } DockBody { id: body - anchors.horizontalCenter: parent.horizontalCenter - // Slides off the bottom edge when hidden. - y: root.revealed ? root.tooltipSpace : surface.height + vertical: root.vertical + leftSide: root.position === "left" + + // Both axes are computed rather than anchored. Anchoring the centre + // on one axis and binding a position on the other looks tidier and + // is a conflict: an anchored centre owns that coordinate, so the + // binding beside it is fighting for the same value. + // + // Centred on the long axis; on the short one it sits a tooltip's + // width in from the edge when revealed, and off-screen when not. + x: { + if (!root.vertical) + return (surface.width - width) / 2; + if (root.position === "left") + return root.revealed ? root.tooltipSpace : -width; + return root.revealed ? surface.width - width - root.tooltipSpace : surface.width; + } + y: { + if (root.vertical) + return (surface.height - height) / 2; + return root.revealed ? root.tooltipSpace : surface.height; + } opacity: root.revealed ? 1 : 0 + Behavior on x { + NumberAnimation { + duration: root.revealed ? Theme.durDockReveal : Theme.durNormal + easing.type: root.revealed ? Easing.OutQuint : Easing.InCubic + } + } + // Asymmetric on purpose. Revealing is a response to something the // user just did, so it has to feel immediate — any delay there // reads as lag. Hiding is not a response to anything, so it can diff --git a/config/dot/quickshell/modules/dock/DockBody.qml b/config/dot/quickshell/modules/dock/DockBody.qml index 940ee6f..5aa097f 100644 --- a/config/dot/quickshell/modules/dock/DockBody.qml +++ b/config/dot/quickshell/modules/dock/DockBody.qml @@ -111,30 +111,51 @@ Rectangle { inset: parent.radius } - implicitWidth: row.implicitWidth + Theme.dockPadding * 2 - implicitHeight: row.implicitHeight + Theme.dockPadding * 2 + implicitWidth: strip.implicitWidth + Theme.dockPadding * 2 + implicitHeight: strip.implicitHeight + Theme.dockPadding * 2 // The item the tooltip is currently describing, or null. property Item hoveredItem: null - Row { - id: row + // Set by the Dock. A side dock runs the same strip down the screen instead + // of across it. + property bool vertical: false + + // Which way a tooltip points on a side dock: away from the screen edge, so + // it never opens off-screen. + property bool leftSide: true + + // Explicit rather than left to Grid's wrapping. This is always one line, so + // saying how many cells it holds is both simpler to read and immune to + // Grid's default column count quietly wrapping a long dock. + readonly property int cellCount: 2 + (root.items ? root.items.length : 0) + + // A Grid rather than a Row so one declaration serves both orientations. + // Row and Column would each need their own children, and the cross-axis + // anchors that centre items in a Row (verticalCenter) are the wrong axis in + // a Column -- Grid centres through its own alignment properties instead, + // which is the same result without the anchors positioners disallow. + Grid { + id: strip anchors.centerIn: parent spacing: Theme.dockGap + rows: root.vertical ? root.cellCount : 1 + columns: root.vertical ? 1 : root.cellCount + horizontalItemAlignment: Grid.AlignHCenter + verticalItemAlignment: Grid.AlignVCenter ShowAppsButton { id: showApps - anchors.verticalCenter: parent.verticalCenter onEntered: root.hoveredItem = showApps onExited: if (root.hoveredItem === showApps) root.hoveredItem = null } - // Separator between the launcher and the apps, as in GNOME's dash. + // Separator between the launcher and the apps, as in GNOME's dash. It + // turns with the dock: a hairline across a column, down a row. Rectangle { - anchors.verticalCenter: parent.verticalCenter - width: 1 - height: Theme.dockIconSize * 0.7 + width: root.vertical ? Theme.dockIconSize * 0.7 : 1 + height: root.vertical ? 1 : Theme.dockIconSize * 0.7 border.width: 0 color: Theme.alpha(Theme.fg, 0.14) } @@ -146,7 +167,6 @@ Rectangle { id: dockItem required property var modelData app: modelData - anchors.verticalCenter: parent.verticalCenter onEntered: root.hoveredItem = dockItem onExited: if (root.hoveredItem === dockItem) root.hoveredItem = null @@ -172,16 +192,26 @@ Rectangle { } } - // Centered on the hovered item, clamped inside the dock. Reads the item's - // x directly (rather than mapToItem) so the binding re-evaluates when - // the row reflows. + // Centred on the hovered item along the dock's own axis, and placed just + // outside the edge it lives on. Reads the item's position directly + // (rather than mapToItem) so the binding re-evaluates when the strip + // reflows. x: { if (!root.hoveredItem) return 0; - const center = row.x + root.hoveredItem.x + root.hoveredItem.width / 2; - return Math.max(4, Math.min(root.width - width - 4, center - width / 2)); + if (root.vertical) + return root.leftSide ? root.width + 8 : -width - 8; + const centre = strip.x + root.hoveredItem.x + root.hoveredItem.width / 2; + return Math.max(4, Math.min(root.width - width - 4, centre - width / 2)); + } + y: { + if (!root.hoveredItem) + return -height - 8; + if (!root.vertical) + return -height - 8; + const centre = strip.y + root.hoveredItem.y + root.hoveredItem.height / 2; + return Math.max(4, Math.min(root.height - height - 4, centre - height / 2)); } - y: -height - 8 width: tipLabel.implicitWidth + Theme.popoverPadding * 2 height: tipLabel.implicitHeight + 8 diff --git a/config/dot/quickshell/modules/settings/DesktopPage.qml b/config/dot/quickshell/modules/settings/DesktopPage.qml index 771531c..1a111c3 100644 --- a/config/dot/quickshell/modules/settings/DesktopPage.qml +++ b/config/dot/quickshell/modules/settings/DesktopPage.qml @@ -8,18 +8,96 @@ // them. import QtQuick +import Quickshell import qs.config import qs.services SettingsPage { id: root + // Turning the last screen off would leave no dock anywhere and no obvious + // way back, so the final one cannot be removed -- it collapses to "every + // screen" instead, which is the same thing on one display and recoverable + // on several. + function toggleDockScreen(name: string): void { + const all = Quickshell.screens.map(screen => String(screen.name)); + const current = Settings.dockScreens.length === 0 + ? all.slice() + : Settings.dockScreens.map(String); + const at = current.indexOf(name); + let next = current.slice(); + if (at >= 0) + next.splice(at, 1); + else + next.push(name); + if (next.length === 0 || next.length === all.length) + next = []; + DesktopPreferences.set("dockScreens", next); + } + title: "Desktop & Dock" lede: "Keep the shell instant, spatial, and out of your way." SettingsCard { title: "Dock" + ChoiceRow { setting: "dockPosition" } + + // One row per connected screen. Nothing selected means every screen, + // which is stated rather than left as an empty list somebody has to + // interpret -- and it is what a single-monitor machine should do + // without being configured at all. + SettingRow { + label: "Screens" + detail: Settings.dockScreens.length === 0 + ? "On every display" + : "On " + Settings.dockScreens.length + " of " + + Quickshell.screens.length + " displays" + visible: Quickshell.screens.length > 1 + controlWidth: 260 + + Row { + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + spacing: 7 + + Repeater { + model: Quickshell.screens + + delegate: Rectangle { + id: screenPill + + required property var modelData + + readonly property string screenName: String(screenPill.modelData.name ?? "") + // An empty list means all, so every pill reads as on. + readonly property bool on: Settings.dockScreens.length === 0 + || Settings.dockScreens.indexOf(screenPill.screenName) >= 0 + + width: pillLabel.implicitWidth + 20 + height: 28 + radius: 8 + color: screenPill.on ? Theme.alpha(Theme.accent, 0.22) + : Theme.alpha(Theme.fg, 0.06) + border.width: screenPill.on ? 1 : 0 + border.color: Theme.alpha(Theme.accent, 0.5) + + Text { + id: pillLabel + anchors.centerIn: parent + text: screenPill.screenName + color: screenPill.on ? Theme.fg : Theme.fgDim + font.family: Theme.fontFamily + font.pixelSize: Theme.fontSizeSmall + } + + HoverHandler { cursorShape: Qt.PointingHandCursor } + TapHandler { onTapped: root.toggleDockScreen(screenPill.screenName) } + } + } + } + } + ToggleRow { setting: "dockAutohide" } SliderRow { setting: "dockRevealDelayMs"; zeroLabel: "Instant" } SliderRow { setting: "dockHideDelayMs"; zeroLabel: "Instant" } diff --git a/config/dot/quickshell/modules/settings/DockPinsEditor.qml b/config/dot/quickshell/modules/settings/DockPinsEditor.qml index 1b160f6..e101044 100644 --- a/config/dot/quickshell/modules/settings/DockPinsEditor.qml +++ b/config/dot/quickshell/modules/settings/DockPinsEditor.qml @@ -45,6 +45,50 @@ Column { DesktopPreferences.set("dockPinned", next); } + // ── Dragging ──────────────────────────────────────────────────────────── + // + // By a grip rather than the whole row. The objection this file used to + // record -- that dragging inside a Flickable inside a scrolling page is + // hard to get right and fails in a way that reads as breakage -- is real, + // and the answer is preventStealing on the grip: the Flickable cannot take + // a gesture that started there, so a vertical drag reorders instead of + // scrolling the page out from under it. The arrow buttons stay, because + // they are the keyboard-reachable path and a grip is not. + // + // The order is held here while the drag runs and written once on release. + // Committing on every slot crossed would rewrite settings.json a dozen + // times for one gesture. + property int draggingIndex: -1 + property var workingOrder: [] + + readonly property var displayed: root.draggingIndex >= 0 ? root.workingOrder : root.pinned + + function beginDrag(index: int): void { + root.workingOrder = root.pinned.slice(); + root.draggingIndex = index; + } + + function dragTo(target: int): void { + if (root.draggingIndex < 0 || target === root.draggingIndex) + return; + if (target < 0 || target >= root.workingOrder.length) + return; + const next = root.workingOrder.slice(); + const moved = next.splice(root.draggingIndex, 1)[0]; + next.splice(target, 0, moved); + root.workingOrder = next; + root.draggingIndex = target; + } + + function endDrag(): void { + if (root.draggingIndex < 0) + return; + const next = root.workingOrder.slice(); + root.draggingIndex = -1; + root.workingOrder = []; + root.commit(next); + } + function move(from: int, to: int): void { if (to < 0 || to >= root.pinned.length) return; @@ -67,7 +111,7 @@ Column { } Repeater { - model: root.pinned + model: root.displayed SettingRow { id: pin @@ -87,11 +131,60 @@ Column { divider: pin.index < root.pinned.length - 1 controlWidth: 132 + // Lifted while dragging so the row being moved is the one that + // looks moved. + z: root.draggingIndex === pin.index ? 2 : 0 + opacity: root.draggingIndex === pin.index ? 0.85 : 1 + Row { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter spacing: 4 + // The grip. preventStealing is the whole reason this works + // inside a scrolling page: without it the Flickable claims the + // vertical gesture and the row never moves. + Item { + width: 26 + height: 26 + anchors.verticalCenter: parent.verticalCenter + + Text { + anchors.centerIn: parent + text: "\u2261" + color: root.draggingIndex === pin.index ? Theme.accent : Theme.fgMuted + font.family: Theme.fontFamily + font.pixelSize: 15 + } + + MouseArea { + id: grip + anchors.fill: parent + preventStealing: true + cursorShape: Qt.SizeVerCursor + + property real pressY: 0 + + onPressed: mouse => { + grip.pressY = mouse.y; + root.beginDrag(pin.index); + } + onPositionChanged: mouse => { + if (root.draggingIndex < 0 || pin.height <= 0) + return; + // How many whole rows the pointer has travelled from + // where it started. Rounded, so the swap happens as + // the grip passes the midpoint of the next row. + const travelled = (mouse.y - grip.pressY); + const slots = Math.round(travelled / pin.height); + if (slots !== 0) + root.dragTo(root.draggingIndex + slots); + } + onReleased: root.endDrag() + onCanceled: root.endDrag() + } + } + SettingsButton { text: "↑" enabled: pin.index > 0 diff --git a/config/local/share/vicinae/scripts/settings-desktop.sh b/config/local/share/vicinae/scripts/settings-desktop.sh index b9e02e3..4e16c2e 100755 --- a/config/local/share/vicinae/scripts/settings-desktop.sh +++ b/config/local/share/vicinae/scripts/settings-desktop.sh @@ -5,6 +5,6 @@ # @vicinae.mode silent # @vicinae.icon ../../icons/hicolor/scalable/apps/panama-settings.svg # @vicinae.description Open Desktop & Dock in Settings. -# @vicinae.keywords ["settings", "automatically hide the dock", "icon size", "reveal delay", "hide delay", "focus modes", "focus session length", "resize by dragging the border", "border grab area", "show the resize cursor", "snap distance between windows", "snap distance to screen edges", "snapping respects gaps"] +# @vicinae.keywords ["settings", "automatically hide the dock", "position", "screens", "icon size", "reveal delay", "hide delay", "focus modes", "focus session length", "resize by dragging the border", "border grab area", "show the resize cursor", "snap distance between windows"] exec "$HOME/.config/quickshell/scripts/panama-action" settings-page desktop diff --git a/docs/settings.md b/docs/settings.md index 6f25b07..1f1ebd6 100644 --- a/docs/settings.md +++ b/docs/settings.md @@ -4,7 +4,7 @@ Do not edit this file. Run `quickshell/scripts/panama-settings-docs` after changing the schema; a contract fails when this copy is stale. -132 settings across 27 groups. 67 of them are applied to the compositor and confirmed by reading the value back. +133 settings across 27 groups. 67 of them are applied to the compositor and confirmed by reading the value back. ## accessibility @@ -66,6 +66,7 @@ Found on **Desktop & Dock**. | Setting | Default | What it does | |---|---|---| | **Automatically hide the Dock**
`dockAutohide` | true | Reveal it at the bottom edge when a workspace is occupied | +| **Position**
`dockPosition` | bottom | Which edge the Dock lives on Choices: Bottom, Left, Right. | | **Icon size**
`dockIconSize` | 48 px | How large the Dock's application icons are drawn. Range 32–80. | | **Reveal delay**
`dockRevealDelayMs` | 0 ms | Zero reveals the Dock the instant the pointer reaches the edge. Range 0–1000. | | **Hide delay**
`dockHideDelayMs` | 250 ms | Prevents flicker when crossing between icons. Range 0–2000. | diff --git a/tests/quickshell/dock-position-contract.sh b/tests/quickshell/dock-position-contract.sh new file mode 100755 index 0000000..dd9247c --- /dev/null +++ b/tests/quickshell/dock-position-contract.sh @@ -0,0 +1,155 @@ +#!/usr/bin/env bash + +# The Dock on an edge other than the bottom, and on chosen screens. +# +# The rules: +# +# 1. A dock spans the edge it lives on, which means anchoring BOTH ends of +# that edge. Anchoring one leaves the surface free to collapse to its +# implicit size on that axis -- the first side dock written here came out +# one pixel tall, looked like nothing had rendered, and passed every static +# check. So the geometry is measured, not read. +# 2. Bottom is unchanged. Somebody who never touches the setting must get the +# dock they already had, byte for byte in behaviour. +# 3. Only one axis gets an implicit size. Setting both fights the anchors. +# 4. An empty screen list means every screen. A list of names goes stale the +# moment a display is unplugged, so "all" must not be spelled as one. +# 5. Reordering pins never loses or duplicates an entry. +# 6. The drag grip sets preventStealing. Without it the settings page's own +# Flickable claims the vertical gesture and the row never moves -- which is +# the exact objection this feature was refused over for a long time. +# +# The geometry checks launch isolated shells against a temporary config. The +# real settings are read to build them and never written. + +set -uo pipefail + +repo_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +shell_dir="$repo_dir/config/dot/quickshell" +dock="$shell_dir/modules/dock/Dock.qml" +body="$shell_dir/modules/dock/DockBody.qml" +editor="$shell_dir/modules/settings/DockPinsEditor.qml" + +fail() { + printf 'dock position contract: %s\n' "$1" >&2 + exit 1 +} + +for path in "$dock" "$body" "$editor"; do + [[ -r "$path" ]] || fail "missing $path" +done + +work="$(mktemp -d)" +trap 'rm -rf "$work" "$shell_dir/dock-position-probe.qml"' EXIT + +# ── 3, 4, 6. What can be read ─────────────────────────────────────────────── + +grep -q 'implicitHeight: root.vertical ? 0 :' "$dock" \ + || fail 'the dock sets an implicit height on both orientations, which fights the anchors' +grep -q 'implicitWidth: root.vertical ? ' "$dock" \ + || fail 'the dock has no implicit width for a side position' + +grep -q 'wanted.length === 0' "$dock" \ + || fail 'an empty screen list is not treated as every screen' + +grep -q 'preventStealing: true' "$editor" \ + || fail 'the drag grip does not set preventStealing, so the page will scroll instead of reordering' +# The arrows are the keyboard-reachable path and predate the grip. A grip is not +# a replacement for them. +grep -q 'text: "↑"' "$editor" \ + || fail 'the move-up button was removed, leaving no keyboard-reachable reorder' + +# ── 5. Reordering keeps every entry, exactly once ─────────────────────────── + +python3 - <<'PY' || fail 'reordering loses or duplicates a pinned application' +def drag_to(working, dragging, target): + if dragging < 0 or target == dragging: + return working, dragging + if target < 0 or target >= len(working): + return working, dragging + nxt = working[:] + nxt.insert(target, nxt.pop(dragging)) + return nxt, target + +base = ["a", "b", "c", "d", "e"] +for start, targets in [(0, [1, 2, 3, 4]), (4, [3, 2, 1, 0]), (2, [3]), (2, [1]), + (0, [3, 3, 3]), (1, [-1]), (3, [99])]: + working, dragging = base[:], start + for target in targets: + working, dragging = drag_to(working, dragging, target) + if sorted(working) != sorted(base) or len(working) != len(base): + raise SystemExit(f'starting at {start} through {targets} produced {working}') +PY + +# ── 1, 2. Geometry, measured ──────────────────────────────────────────────── + +cat >"$shell_dir/dock-position-probe.qml" <<'QML' +import Quickshell +import QtQuick +import qs.modules.dock + +ShellRoot { + Dock { id: probe } + Timer { + interval: 1200; running: true + onTriggered: { + console.warn("DOCKGEOM " + probe.position + + " vertical=" + probe.vertical + + " w=" + Math.round(probe.width) + + " h=" + Math.round(probe.height)); + Qt.quit(); + } + } +} +QML + +settings_source="${XDG_CONFIG_HOME:-$HOME/.config}/panama/settings.json" +[[ -r "$settings_source" ]] || fail 'no settings to build a probe configuration from' + +measure() { + local position="$1" + python3 - "$settings_source" "$work/panama/settings.json" "$position" <<'PY' +import json, pathlib, sys +data = json.loads(pathlib.Path(sys.argv[1]).read_text()) +data["dockPosition"] = sys.argv[3] +out = pathlib.Path(sys.argv[2]) +out.parent.mkdir(parents=True, exist_ok=True) +out.write_text(json.dumps(data)) +PY + ( cd "$shell_dir" && XDG_CONFIG_HOME="$work" timeout 40 qs -p ./dock-position-probe.qml 2>&1 ) \ + | grep -o 'DOCKGEOM .*' | head -1 +} + +bottom="$(measure bottom)" +[[ -n "$bottom" ]] || fail 'the bottom dock produced no geometry at all' +left="$(measure left)" +[[ -n "$left" ]] || fail 'the left dock produced no geometry at all' + +python3 - "$bottom" "$left" <<'PY' || fail 'a dock does not span the edge it lives on' +import re, sys + +def read(line): + m = re.search(r'DOCKGEOM (\w+) vertical=(\w+) w=(\d+) h=(\d+)', line) + if not m: + raise SystemExit(f'unreadable probe output: {line!r}') + return m.group(1), m.group(2) == 'true', int(m.group(3)), int(m.group(4)) + +_, bottom_vertical, bottom_w, bottom_h = read(sys.argv[1]) +_, left_vertical, left_w, left_h = read(sys.argv[2]) + +if bottom_vertical: + raise SystemExit('the bottom dock reports itself as vertical') +if not left_vertical: + raise SystemExit('the left dock does not report itself as vertical') + +# The bug this exists for: a side dock anchored at one end only collapses to a +# sliver on the axis it should span. +if left_h <= bottom_h: + raise SystemExit(f'the left dock is {left_h}px tall and does not span the screen') +if bottom_w <= left_w: + raise SystemExit(f'the bottom dock is {bottom_w}px wide and does not span the screen') +if left_w >= bottom_w or bottom_h >= left_h: + raise SystemExit('the two orientations are not thin on opposite axes') +PY + +printf 'dock position contract: ok\n'