a73x

a14ea705

fix: a click finds its pane by column, not just row

a73x   2026-08-25 09:10

Commit message
fix: a click finds its pane by column, not just row

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

src/wallview.zig
Old New
@@ -484,16 +484,18 @@ const Tile = struct {
484 } 484 }
485 }; 485 };
486 486
487 /// Which tile's rectangle a zero-based terminal row falls in, or null 487 /// Which tile's rectangle a zero-based terminal (row, col) falls in, or
488 /// when none does. The keyboard routes a click to the tile it lands in so 488 /// null when none does. A beside layout gives every tile the same rows, so
489 /// focus can follow it; the drag itself is the focused tile's Core's. 489 /// the column is what separates them; a click on a rail column hits no
490 /// Under `paint_mu` because `Tile.rect` is written under it. 490 /// tile and focus stays put. Under `paint_mu` because `Tile.rect` is
491 fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16) ?usize { 491 /// written under it.
492 fn rectHit(tiles: []Tile, present: []const bool, shared: *Shared, row: u16, col: u16) ?usize {
492 shared.paint_mu.lock(); 493 shared.paint_mu.lock();
493 defer shared.paint_mu.unlock(); 494 defer shared.paint_mu.unlock();
494 for (tiles, present) |*t, p| { 495 for (tiles, present) |*t, p| {
495 if (!p) continue; 496 if (!p) continue;
496 if (row >= t.rect.top and row < t.rect.top + t.rect.rows) return t.idx; 497 if (row >= t.rect.top and row < t.rect.top + t.rect.rows and
498 col >= t.rect.left and col < t.rect.left + t.rect.cols) return t.idx;
497 } 499 }
498 return null; 500 return null;
499 } 501 }
@@ -2794,7 +2796,7 @@ pub fn run(alloc: std.mem.Allocator, resolved: []const Resolved, entry: Entry) !
2794 var seg_start: usize = 0; 2796 var seg_start: usize = 0;
2795 for (report.events) |ev| { 2797 for (report.events) |ev| {
2796 if (ev.kind != .press) continue; 2798 if (ev.kind != .press) continue;
2797 if (rectHit(tiles[0..live], present[0..live], &shared, ev.row)) |hit| { 2799 if (rectHit(tiles[0..live], present[0..live], &shared, ev.row, ev.col)) |hit| {
2798 if (hit != shared.sel) { 2800 if (hit != shared.sel) {
2799 if (ev.at > seg_start) 2801 if (ev.at > seg_start)
2800 sendKeys(&tiles[shared.sel], cmd.forward[seg_start..ev.at]); 2802 sendKeys(&tiles[shared.sel], cmd.forward[seg_start..ev.at]);
@@ -3655,7 +3657,7 @@ test "bare keys reach the focused tile; the prefix does not" {
3655 try std.testing.expectEqual(interact.PrefixFilter.Action.next_session, b.action); 3657 try std.testing.expectEqual(interact.PrefixFilter.Action.next_session, b.action);
3656 } 3658 }
3657 3659
3658 test "focus follows a click: rectHit picks the tile under a row" { 3660 test "focus follows a click: rectHit picks the tile under a row and column" {
3659 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false }; 3661 var shared = Shared{ .out_fd = -1, .size = .{ .cols = 80, .rows = 24 }, .is_tty = false };
3660 shared.label_rows = 1; 3662 shared.label_rows = 1;
3661 var tiles: [2]Tile = undefined; 3663 var tiles: [2]Tile = undefined;
@@ -3677,9 +3679,18 @@ test "focus follows a click: rectHit picks the tile under a row" {
3677 }; 3679 };
3678 const present = [_]bool{ true, true }; 3680 const present = [_]bool{ true, true };
3679 // A click in row 3 hits tile 0. 3681 // A click in row 3 hits tile 0.
3680 try std.testing.expectEqual(@as(?usize, 0), rectHit(&tiles, &present, &shared, 3)); 3682 try std.testing.expectEqual(@as(?usize, 0), rectHit(&tiles, &present, &shared, 3, 0));
3681 // A click in row 15 hits tile 1. 3683 // A click in row 15 hits tile 1.
3682 try std.testing.expectEqual(@as(?usize, 1), rectHit(&tiles, &present, &shared, 15)); 3684 try std.testing.expectEqual(@as(?usize, 1), rectHit(&tiles, &present, &shared, 15, 0));
3685
3686 // A beside pair: same rows, different columns. A click in the left
3687 // half hits tile 0, the right half hits tile 1, and a rail column
3688 // between them hits neither.
3689 tiles[0].rect = .{ .top = 0, .left = 0, .rows = 24, .cols = 39 };
3690 tiles[1].rect = .{ .top = 0, .left = 41, .rows = 24, .cols = 39 };
3691 try std.testing.expectEqual(@as(?usize, 0), rectHit(&tiles, &present, &shared, 5, 10));
3692 try std.testing.expectEqual(@as(?usize, 1), rectHit(&tiles, &present, &shared, 5, 50));
3693 try std.testing.expectEqual(@as(?usize, null), rectHit(&tiles, &present, &shared, 5, 40));
3683 } 3694 }
3684 3695
3685 // A transport that swallows every frame: a Core's selection request goes 3696 // A transport that swallows every frame: a Core's selection request goes
test/e2e.sh
Old New
@@ -1606,6 +1606,7 @@ cleanup() {
1606 "$OUT.wmcap2" "$OUT.wmcap2.err" "$OUT.wmpc2" "$OUT.wmfa" "$OUT.wmfb" \ 1606 "$OUT.wmcap2" "$OUT.wmcap2.err" "$OUT.wmpc2" "$OUT.wmfa" "$OUT.wmfb" \
1607 "$OUT.wmcap3" "$OUT.wmcap3.err" "$OUT.wmpc3" \ 1607 "$OUT.wmcap3" "$OUT.wmcap3.err" "$OUT.wmpc3" \
1608 "$OUT.wmcap4" "$OUT.wmcap4.err" "$OUT.wmpc4" \ 1608 "$OUT.wmcap4" "$OUT.wmcap4.err" "$OUT.wmpc4" \
1609 "$OUT.wmcap5" "$OUT.wmcap5.err" "$OUT.wmpc5" \
1609 "$OUT.selcap" "$OUT.selcap.err" "$OUT.selpc" \ 1610 "$OUT.selcap" "$OUT.selcap.err" "$OUT.selpc" \
1610 "$OUT.wmstop" 1611 "$OUT.wmstop"
1611 # The convergence files a FAILING assert_converged leaves behind 1612 # The convergence files a FAILING assert_converged leaves behind
@@ -7104,6 +7105,39 @@ grep -q "wm-four" "$OUT.wmfb" && {
7104 cat "$OUT.wmfb"; exit 1; } 7105 cat "$OUT.wmfb"; exit 1; }
7105 ok "a click in a tile's rect focuses it, from wherever the focus was" 7106 ok "a click in a tile's rect focuses it, from wherever the focus was"
7106 7107
7108 # A beside layout gives every tile the same rows, so a click that tests
7109 # only the row hits the first present tile every time. A click in the
7110 # RIGHT pane's content — column, not row — is what proves the column
7111 # test in rectHit earns its keep: focus starts on a (the left pane) and
7112 # the click must move it to b (the right pane).
7113 set +e
7114 timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wmcap5" --err "$OUT.wmcap5.err" -- \
7115 "$MUX" wall "--sock $SOCK50#a" "--sock $SOCK50#b" > "$OUT.wmpc5" 2>&1 <<'EOF'
7116 expect wmb-pin 20000
7117 settle 700 20000
7118 send \x1b[<0;70;9M\x1b[<0;70;9m
7119 settle 700 20000
7120 send printf 'wm-%s\\n' five\n
7121 expect wm-five 15000
7122 settle 400 15000
7123 send \x1cd
7124 waitexit 10000
7125 EOF
7126 RC=$?
7127 set -e
7128 [ "$RC" -eq 0 ] || {
7129 echo "e2e FAIL: wall click: beside leg exited $RC (did the column click focus b?):"
7130 cat "$OUT.wmpc5"; exit 1; }
7131 timeout 20 "$MUXA" capture --sock "$SOCK50" --session a > "$OUT.wmfa" 2>&1
7132 timeout 20 "$MUXA" capture --sock "$SOCK50" --session b > "$OUT.wmfb" 2>&1
7133 grep -q "wm-five" "$OUT.wmfb" || {
7134 echo "e2e FAIL: wall click: a click in the right pane did not focus it:"
7135 cat "$OUT.wmfb"; exit 1; }
7136 grep -q "wm-five" "$OUT.wmfa" && {
7137 echo "e2e FAIL: wall click: the focus stayed on the left pane:"
7138 cat "$OUT.wmfa"; exit 1; }
7139 ok "a click in a beside pane finds it by column, not just row"
7140
7107 # ---- a drag copies, and the copy leaves as OSC 52 ---------------------- 7141 # ---- a drag copies, and the copy leaves as OSC 52 ----------------------
7108 # 7142 #
7109 # The first end-to-end proof that this feature does what it was asked for. 7143 # The first end-to-end proof that this feature does what it was asked for.
@@ -8256,8 +8290,8 @@ D62PID=""
8256 rm -rf "$LPDSTATE" 8290 rm -rf "$LPDSTATE"
8257 ok "a corrupted sidecar degrades silently to the default layout" 8291 ok "a corrupted sidecar degrades silently to the default layout"
8258 8292
8259 [ "$OK_COUNT" = "71" ] || { 8293 [ "$OK_COUNT" = "72" ] || {
8260 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 71 —" 8294 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 72 —"
8261 echo " a scenario was added (update the pin) or silently lost" 8295 echo " a scenario was added (update the pin) or silently lost"
8262 exit 1 8296 exit 1
8263 } 8297 }