From fb87946e09bf33c0fe663ccba6db14a63e7cfda4 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 19:31:46 -0300 Subject: A B1 sweep held past a pane's edge scrolls the body, as acme's frselect does Dragging a selection beyond the top or bottom of a pane stopped at the last visible row. Now, while B1 is held past the body's top or bottom (at a pane that reaches the screen's bottom, its last row, since the pointer cannot leave the screen), the body scrolls a line a tick; the sweep's start keeps its place in the text, and a sweep that scrolled ends as the text's own selection. Also here: a ctrl-click is not half a double-click (lspdebug's golden stays as it was), and a test that clicks many points in a row resets the double-click between them. Co-Authored-By: Claude Opus 5.5 --- src/mouse.zig | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 2 deletions(-) (limited to 'src/mouse.zig') diff --git a/src/mouse.zig b/src/mouse.zig index 61bd30e0..790dc9d1 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -48,6 +48,14 @@ pub const Drag = union(enum) { button: Mouse.Button, chorded: bool = false, ctrl: bool = false, + /// A B1 body sweep held past the pane's top (-1) or bottom (1): the + /// body scrolls a line a tick, as acme's frselect scrolls + /// (libframe frselect.c, the `scroll` callback while the button is + /// held outside the frame). + edge: i2 = 0, + /// Where the sweep began in the text, once it has scrolled: its + /// press row may be off the screen. + anchor: ?modal.Cursor = null, /// This gesture began over a usable native PDF raster. The payload is /// zero-bit when PDF support is absent. pdf: panes.Pdf.PointerDrag = .{}, @@ -621,6 +629,17 @@ pub fn dragUpdate(p: *Pardes, mcol: u16, mrow: u16, body_hit: ?Mouse.BodyHit) vo // from the pointer. const body_y = p.bodyTop(pane, r); const body_h = r.h -| pane.tag_rows; + s.edge = if (s.button != config.select_button or pane.sel[b].r0 < tag_rows or pane.file == null or pane.hasPdf() or body_h == 0) + 0 + else if (mrow < body_y) + -1 + else if (mrow >= body_y + body_h or + // The pointer cannot leave the screen: at a bottom pane, + // its last row is the edge. + (body_y + body_h >= p.screen_h and mrow + 1 >= body_y + body_h)) + 1 + else + 0; pane.sel[b].r1 = if (pane.sel[b].r0 < tag_rows) tag_line else if (body_h == 0) @@ -778,6 +797,34 @@ fn doubleClick(p: *Pardes, id: usize, t: *panes.Text, on_tag: bool, row: i32, co t.cur_pinned = true; } +/// On each tick while a B1 body sweep is held past an edge: the body +/// scrolls a line toward it, the sweep's end stays under the pointer and +/// its start stays on its text (Drag.select.anchor). +pub fn edgeScroll(p: *Pardes) void { + if (p.drag != .select) return; + const s = &p.drag.select; + if (s.edge == 0) return; + const pane = p.panes[s.id] orelse return; + const sl = &pane.sel[sel_slot]; + if (s.anchor == null) s.anchor = sweepPoint(p, pane, sl.r0, sl.c0) orelse return; + const before = pane.scroll(); + pane.scrollBy(s.edge); + // The start's row follows its text while it is on the screen, and + // stays on the body's edge row once it is off (the anchor keeps it). + const top: i32 = pane.tag_rows; + sl.r0 = std.math.clamp(sl.r0 - (pane.scroll() - before), top, top + @as(i32, pane.rows) - 1); +} + +/// The text position under a body cell of the sweep's space. +fn sweepPoint(p: *Pardes, pane: *panes.Pane, r: i32, c: i32) ?modal.Cursor { + const visible = r - @as(i32, pane.tag_rows); + if (visible < 0) return null; + const w = pane.wrapAt(visible); + if (w.line < 0) return null; + const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, c - (if (pane.file != null) @as(i32, panes.File.gutterWidth(pane)) else 0)); + return .{ .row = @intCast(w.line), .col = @intCast(@max(0, col)) }; +} + fn clockMs() i64 { if (comptime !pardes.hosted) return 0; var ts: std.c.timespec = undefined; @@ -873,7 +920,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { } return; } - if (dragged) edit.capturePointerSelection(p, pane, sel_slot) catch {}; + if (dragged and s.anchor == null) edit.capturePointerSelection(p, pane, sel_slot) catch {}; if (dragged and pane.body.mode == .tty) mirrorTtySelection(p, pane); const body_vis = sl.r1 - @as(i32, pane.tag_rows); if (body_vis >= 0 and pane.body.mode != .tty) { @@ -895,7 +942,15 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { pane.body.select = false; pane.body.restore_cursor = false; pane.body.sticky_col = -1; - if (!dragged) doubleClick(p, s.id, &pane.body, false, sl.r1, sl.c1); + // A ctrl-click is a definition jump, not half a double-click. + if (!dragged and !s.ctrl) doubleClick(p, s.id, &pane.body, false, sl.r1, sl.c1); + if (s.ctrl) p.last_click = null; + // A sweep that scrolled has its start off the screen: it + // is kept as the text's own selection, start to end. + if (dragged) if (s.anchor) |a| { + pane.sel[sel_slot].state = .none; + pane.body.vsel = .{ .active = true, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true }; + }; if (s.ctrl) p.lspRequest(s.id, .definition, ""); } } else { -- cgit v1.3