From 15ddd805905f785f4dde463d4d6b91e1d2c41e26 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Tue, 29 Sep 2026 00:17:45 -0300 Subject: A pane's last row of text is no resize handle; the grip is, and the GUI's rule A press on a pane's last row began a seam drag, so the row could not be clicked or swept like the rest of the text (rsearch dragged the handle out of the way first). Now no row of text is a handle: in the GUI a press on the 2 px rule between two panes drags their seam, and anywhere the grip does, as acme's button moves a window -- dragged up or down its own column it moves the pane's top, either pane down to its tag alone (acme's coldragwin), dragged down inside itself too. The hover hint over a last row is gone; column edges resize as they did. Scripts that dragged a seam drag a grip instead and match their goldens; six goldens lost the hint or moved a cursor to the clicked text, re-recorded by name. Co-Authored-By: Claude Opus 5.5 --- src/mouse.zig | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 8 deletions(-) (limited to 'src/mouse.zig') diff --git a/src/mouse.zig b/src/mouse.zig index 36786612..1ca53017 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -97,13 +97,50 @@ fn cornerFixture(gpa: std.mem.Allocator) !*Pardes { return p; } +test "a pane's last row of text is no resize handle; the rule over the next pane is" { + if (platform == .web) return; + const p = try cornerFixture(std.testing.allocator); + defer p.deinit(); + const seam = seamRowOf(p, 1, 0); + const inside = p.col_x[1] + p.col_w[1] / 2; + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = inside, .row = seam } }); + try std.testing.expect(p.drag != .border_h); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = inside, .row = seam } }); + try std.testing.expect(nudgeRightSeam(p, 2) == seam + 2); +} + +test "a pane's grip dragged up or down its own column moves its top, down to a tag alone above" { + if (platform == .web) return; + const p = try cornerFixture(std.testing.allocator); + defer p.deinit(); + const upper = p.col_panes[1][0]; + const lower = p.col_panes[1][1]; + const grip_x = p.rects[lower].x; + // Up, past the pane above's first row: it keeps one row, its tag. + const top = p.rects[upper].y; + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = grip_x, .row = p.rects[lower].y } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = grip_x, .row = top } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = grip_x, .row = top } }); + p.sync(); + try std.testing.expectEqual(@as(u16, 1), p.rects[upper].h); + try std.testing.expectEqual(lower, p.col_panes[1][1]); + // Down, inside itself: its top follows, the pane above taking the rows. + const to = p.rects[lower].y + 6; + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = grip_x, .row = p.rects[lower].y } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = grip_x, .row = to } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = grip_x, .row = to } }); + p.sync(); + try std.testing.expectEqual(to, p.rects[lower].y); +} + fn nudgeRightSeam(p: *Pardes, delta: i32) u16 { const from = seamRowOf(p, 1, 0); const inside = p.col_x[1] + p.col_w[1] / 2; const to: u16 = @intCast(@as(i32, from) + delta); - p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = inside, .row = from } }); - p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = inside, .row = to } }); - p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = inside, .row = to } }); + // On the rule a pixel frontend draws over the lower pane's first row. + p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = inside, .row = from + 1, .rule_above = true } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = inside, .row = to + 1 } }); + p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = inside, .row = to + 1 } }); p.sync(); return seamRowOf(p, 1, 0); } @@ -437,13 +474,17 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { return; } } - for (0..p.ncol) |cc| { + // Two panes are resized on the rule between them, never on a + // row of text; with no rule, by the grip, as acme moves a + // window by its button. + if (m.rule_above and mrow > 0) for (0..p.ncol) |cc| { if (mcol < p.col_x[cc] or mcol >= p.col_x[cc] + p.col_w[cc]) continue; - if (seamIdxAt(p, cc, mrow)) |k| { - p.drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = mrow } }; + const seam = if (p.settings.tag_bottom) mrow else mrow - 1; + if (seamIdxAt(p, cc, seam)) |k| { + p.drag = .{ .border_h = .{ .col = cc, .top_idx = k, .cur_y = seam } }; return; } - } + }; const id = hovered orelse return; const r = p.rects[id]; const pane = p.panes[id] orelse return; @@ -597,7 +638,9 @@ pub fn dragUpdate(p: *Pardes, mcol: u16, mrow: u16, body_hit: ?Mouse.BodyHit) vo if (k + 1 < p.col_n[cc]) { const a = p.rects[p.col_panes[cc][k]]; const b = p.rects[p.col_panes[cc][k + 1]]; - d.cur_y = clampBorderRow(a.y, a.h, b.h, mrow, p.settings.tag_bottom); + // Held by the rule over the lower pane: the seam is the row above. + const seam = if (p.settings.tag_bottom) mrow else mrow -| 1; + d.cur_y = clampBorderRow(a.y, a.h, b.h, seam, p.settings.tag_bottom); } else d.cur_y = mrow; }, .column_move => |*d| d.cur_x = mcol, -- cgit v1.3