diff --git a/config/dot/quickshell/modules/dock/Dock.qml b/config/dot/quickshell/modules/dock/Dock.qml index c51dd46..d476b29 100644 --- a/config/dot/quickshell/modules/dock/Dock.qml +++ b/config/dot/quickshell/modules/dock/Dock.qml @@ -177,12 +177,21 @@ PanelWindow { x: { if (!root.revealed) return root.position === "right" ? surface.width - root.revealStripHeight : 0; + // A horizontal dock is centred on its edge, so the region has + // to start where the body starts -- starting at 0 puts it over + // the left of the screen while the pointer that summoned the + // dock is in the middle, and the hover drops on the very frame + // the dock arrives. A vertical dock reaches from the body out + // to its own edge, which is x 0 on the left and the body on + // the right. + if (!root.vertical) + return body.x; 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; + return body.y; } width: { if (!root.revealed) diff --git a/tests/quickshell/dock-position-contract.sh b/tests/quickshell/dock-position-contract.sh index dd9247c..172ee39 100755 --- a/tests/quickshell/dock-position-contract.sh +++ b/tests/quickshell/dock-position-contract.sh @@ -18,6 +18,11 @@ # 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. +# 7. The point that reveals the dock is still inside the input region once the +# dock has revealed. The region follows the body, and a bottom dock is +# centred while its reveal strip spans the whole edge -- so a region that +# forgets the body's own offset lands somewhere the pointer is not, hover +# drops on the frame the dock arrives, and it hides under a still cursor. # # The geometry checks launch isolated shells against a temporary config. The # real settings are read to build them and never written. @@ -81,7 +86,7 @@ for start, targets in [(0, [1, 2, 3, 4]), (4, [3, 2, 1, 0]), (2, [3]), (2, [1]), raise SystemExit(f'starting at {start} through {targets} produced {working}') PY -# ── 1, 2. Geometry, measured ──────────────────────────────────────────────── +# ── 1, 2, 7. Geometry, measured ──────────────────────────────────────────────── cat >"$shell_dir/dock-position-probe.qml" <<'QML' import Quickshell @@ -90,6 +95,19 @@ import qs.modules.dock ShellRoot { Dock { id: probe } + + // The input region is measured in both states, because the bug it exists + // for lives in the transition between them: a region that stops covering + // the pointer the moment the dock arrives takes the hover away with it. + function report(state) { + const m = probe.mask.item; + console.warn("MASKGEOM " + probe.position + " " + state + + " surfaceW=" + Math.round(probe.width) + + " surfaceH=" + Math.round(probe.height) + + " x=" + Math.round(m.x) + " y=" + Math.round(m.y) + + " w=" + Math.round(m.width) + " h=" + Math.round(m.height)); + } + Timer { interval: 1200; running: true onTriggered: { @@ -97,6 +115,26 @@ ShellRoot { + " vertical=" + probe.vertical + " w=" + Math.round(probe.width) + " h=" + Math.round(probe.height)); + probe.revealed = false; + hidden.start(); + } + } + // Long enough for the slide to finish; the region follows the body, so + // measuring mid-animation measures nothing in particular. + Timer { + id: hidden + interval: 400 + onTriggered: { + report("hidden"); + probe.revealed = true; + shown.start(); + } + } + Timer { + id: shown + interval: 400 + onTriggered: { + report("revealed"); Qt.quit(); } } @@ -117,15 +155,20 @@ 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 + | grep -oE '(DOCKGEOM|MASKGEOM) .*' } 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' +right="$(measure right)" +[[ -n "$right" ]] || fail 'the right dock produced no geometry at all' -python3 - "$bottom" "$left" <<'PY' || fail 'a dock does not span the edge it lives on' +# The span checks read the window; the region checks read every line. +first_dock() { grep -o 'DOCKGEOM .*' <<<"$1" | head -1; } + +python3 - "$(first_dock "$bottom")" "$(first_dock "$left")" <<'PY' || fail 'a dock does not span the edge it lives on' import re, sys def read(line): @@ -152,4 +195,44 @@ if left_w >= bottom_w or bottom_h >= left_h: raise SystemExit('the two orientations are not thin on opposite axes') PY +# ── 7. The pointer that reveals the dock is still inside the region ───────── + +python3 - "$bottom" "$left" "$right" <<'REGION' || fail 'revealing the dock moves the input region off the pointer that revealed it' +import re, sys + +FIELDS = re.compile( + r'MASKGEOM (\w+) (\w+) surfaceW=(\d+) surfaceH=(\d+) ' + r'x=(-?\d+) y=(-?\d+) w=(-?\d+) h=(-?\d+)') + +def regions(blob): + found = {} + for line in blob.splitlines(): + m = FIELDS.search(line) + if m: + found[m.group(2)] = (m.group(1),) + tuple(int(g) for g in m.groups()[2:]) + return found + +# Where a hand actually goes to summon the dock: the middle of the edge it +# lives on, a pixel in from that edge. +def aim(position, surface_w, surface_h): + if position == "left": + return 1, surface_h / 2 + if position == "right": + return surface_w - 1, surface_h / 2 + return surface_w / 2, surface_h - 1 + +for blob in sys.argv[1:]: + found = regions(blob) + for state in ("hidden", "revealed"): + if state not in found: + raise SystemExit(f'the probe reported no {state} input region') + position, surface_w, surface_h, x, y, w, h = found[state] + px, py = aim(position, surface_w, surface_h) + if not (x <= px <= x + w and y <= py <= y + h): + raise SystemExit( + f'the {position} dock {state} takes input over x {x}..{x + w}, ' + f'y {y}..{y + h}, which does not contain the pointer at ' + f'({px:.0f}, {py:.0f})') +REGION + printf 'dock position contract: ok\n'