summaryrefslogtreecommitdiff
path: root/src/mouse.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/mouse.zig')
-rw-r--r--src/mouse.zig126
1 files changed, 115 insertions, 11 deletions
diff --git a/src/mouse.zig b/src/mouse.zig
index 22eb3270..36786612 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 = .{},
@@ -337,6 +345,9 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
wordAtCol(bar, at);
const word = p.scratch.allocator().dupe(u8, selected) catch return;
tagline.exitHeader(p);
+ // A column tag's command acts on that column, empty or not.
+ p.exec_column = column;
+ defer p.exec_column = null;
if (word.len > 0) {
if (m.button == config.exec_button) {
const named = std.meta.stringToEnum(Builtin, word);
@@ -450,6 +461,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void {
const on_tag = mrow >= tag_y and mrow < tag_y + pane.tag_rows;
if (on_tag and mcol < r.x + TAG_TEXT_INSET) return;
p.active = id;
+ p.noteTyped(); // acme's button 1 sets activecol (acme.c:659)
pane.closePrompt();
const pointed = edit.pointerTextSelection(p, id, mcol, mrow, m.body_hit, p.pointer_tag_hit) orelse return;
pane.clearPointerSelection(sel_slot);
@@ -621,6 +633,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)
@@ -666,11 +689,12 @@ fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand:
const pane = p.panes[id] orelse return .{};
if (on_tag) {
const tag = tagline.tagText(p, p.scratch.allocator(), pane) catch return .{};
+ const shown = tagline.shownTag(p, p.scratch.allocator(), pane) catch return .{};
const sel = operand.expanded orelse operand.preview orelse return .{};
const lead = if (sel.r0 < sel.r1 or (sel.r0 == sel.r1 and sel.c0 <= sel.c1)) sel else Pane.Sel{ .r0 = sel.r1, .c0 = sel.c1 };
const row: usize = @as(usize, @intCast(@max(0, lead.r0))) + pane.tag_top;
- const start = modal.lineStartOffset(tag, row);
- const at = start + panes.File.rawAtDisplay(modal.lineSlice(tag, row), @intCast(@max(0, lead.c0)));
+ const start = modal.lineStartOffset(shown.text, row);
+ const at = shown.toReal(start + panes.File.rawAtDisplay(modal.lineSlice(shown.text, row), @intCast(@max(0, lead.c0))));
const q0: u32 = @intCast(@min(at, tag.len));
return .{ .q0 = q0, .q1 = @intCast(@min(q0 + text.len, tag.len)) };
}
@@ -691,12 +715,13 @@ fn gestureRange(p: *Pardes, id: usize, text: []const u8, on_tag: bool, operand:
const start: ?modal.Cursor = if (operand.file_word) |w|
.{ .row = @intCast(@max(0, w.row)), .col = @intCast(@max(0, w.lo)) }
else if (operand.expanded orelse operand.preview) |sel| lead: {
- // A selection's leading corner in reading order, converted the way
+ // A selection's start in reading order, converted the way
// `pointerOperand` converts the click itself.
- const top = @min(sel.r0, sel.r1) - @as(i32, pane.tag_rows);
+ const lead = edit.sweepEnds(sel)[0];
+ const top = lead[0] - @as(i32, pane.tag_rows);
if (top < 0) break :lead null;
const w = pane.wrapAt(top);
- const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, @min(sel.c0, sel.c1) - panes.File.gutterWidth(pane));
+ const col = edit.paneByteAtDisplay(p, &pane.body, w.line, w.at, lead[1] - panes.File.gutterWidth(pane));
break :lead .{
.row = @intCast(@max(0, w.line)),
.col = @intCast(@max(0, col)),
@@ -730,10 +755,11 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void {
const rows = pane.pointerSelection(gesture) orelse return;
const text = pane.rawPointerText(gesture) orelse return;
if (rows.len == 0) return;
- const endpoint = rows[if (gesture.r1 >= gesture.r0) rows.len - 1 else 0];
+ const forward = gesture.r1 > gesture.r0 or (gesture.r1 == gesture.r0 and gesture.c1 >= gesture.c0);
+ const endpoint = rows[if (forward) rows.len - 1 else 0];
const body = body_layer.bodyText(p, p.scratch.allocator(), pane) catch return;
const line = modal.lineSlice(body, @intCast(@max(0, endpoint.row - panes.Terminal.gridOffset(pane))));
- const byte = if (gesture.c1 >= gesture.c0) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo;
+ const byte = if (forward) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo;
pane.body.cur_row = pane.surfRow(endpoint.row);
var shown = std.mem.trimEnd(u8, line[@min(endpoint.prompt_bytes, line.len)..], " \t");
if (pane.ovl) |overlay| {
@@ -753,11 +779,70 @@ fn mirrorTtySelection(p: *Pardes, pane: *Pane) void {
edit.setYank(p, text);
}
+/// acme's double-click (text.c:881 textselect, plan9port text.c:1407
+/// textdoubleclick): a second plain B1 click at the same cell within half a
+/// second selects what edit.doubleClickRange says, from the caret the click
+/// left. A plain click still puts the caret on its cell.
+fn doubleClick(p: *Pardes, id: usize, t: *panes.Text, on_tag: bool, row: i32, col: i32) void {
+ const now = clockMs();
+ const prev = p.last_click;
+ p.last_click = .{ .id = id, .on_tag = on_tag, .row = row, .col = col, .ms = now };
+ const c = prev orelse return;
+ if (c.id != id or c.on_tag != on_tag or c.row != row or c.col != col or now - c.ms > 500) return;
+ p.last_click = null;
+ const text = edit.flatSurface(p, t) catch return;
+ const q = modal.offsetAt(text, .{ .row = @intCast(@max(0, t.cur_row)), .col = @intCast(@max(0, t.cur_col)) });
+ const r = edit.doubleClickRange(text, q);
+ if (r[1] <= r[0]) return;
+ const a = modal.positionAt(text, r[0]);
+ const h = modal.positionAt(text, modal.prevGrapheme(text, r[1]));
+ t.vsel = .{ .active = true, .row = @intCast(a.row), .col = @intCast(a.col), .explicit = true };
+ t.cur_row = @intCast(h.row);
+ t.cur_col = @intCast(h.col);
+ 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;
+ if (std.c.clock_gettime(.MONOTONIC, &ts) != 0) return 0;
+ return @as(i64, ts.sec) * 1000 + @divTrunc(@as(i64, ts.nsec), 1_000_000);
+}
+
fn dragRelease(p: *Pardes, button: Mouse.Button) void {
// A different button's release belongs to a chord or another host
// stream; only the button which began this gesture may end it.
if (p.drag.button() != button) return;
defer p.drag = .none;
+ defer p.chord_hold = false;
switch (p.drag) {
.border_v => |d| {
const c = d.left_col;
@@ -775,13 +860,17 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
layout.applyRowSplit(p, d.col, d.top_idx, d.cur_y),
.column_move => |d| if (layout.columnBySerial(p, d.serial)) |column| switch (layout.columnDrop(p, column, d.cur_x, d.grab)) {
.stay => {},
- .reorder => |target| layout.reorderColumn(p, column, target),
+ .reorder => |target| {
+ layout.reorderColumn(p, column, target);
+ p.setActiveColumn(target); // a grip drag sets activecol (acme.c:640)
+ },
.edge => |x| layout.setColumnPairWidth(p, column - 1, x - p.col_x[column - 1]),
},
.move => |d| {
layout.movePane(p, d.id, d.cur_x, d.cur_y);
- if (p.panes[d.id]) |mp| p.stillborn_joiner = mp.serial;
- // a file moved into the left column evicts a lone unused shell
+ if (layout.findPane(p, d.id)) |f| p.setActiveColumn(f.col);
+ // BootShell replace: a file moved into the left column closes a
+ // lone unused shell there
if (p.panes[d.id]) |mt| if (mt.file != null) {
if (layout.findPane(p, d.id)) |f| if (f.col == 0)
exec.evictLonePristineTty(p, 0, d.id);
@@ -836,10 +925,11 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
} else if (tagline.tagOffsetAt(p, pane, row, col)) |cursor| {
pane.tag.cur_row = @intCast(cursor.row);
pane.tag.cur_col = @intCast(cursor.col);
+ if (!dragged) doubleClick(p, s.id, &pane.tag, true, sl.r1, sl.c1);
}
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) {
@@ -861,6 +951,15 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void {
pane.body.select = false;
pane.body.restore_cursor = false;
pane.body.sticky_col = -1;
+ // 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 {
@@ -993,6 +1092,11 @@ fn chordCutPaste(p: *Pardes, cut: bool) void {
}
if (!s.chorded) {
s.chorded = true;
+ // Everything done while B1 stays down is one undo step, as acme
+ // marks the file once per B1 hold (text.c:881 textselect, `state`
+ // and its one filemark): B1-B2 then B1-B3 is a copy.
+ edit.pushUndo(p, &pane.body);
+ p.chord_hold = true;
const sl = pane.sel[sel_slot];
pane.sel[sel_slot].state = .none;
const pfx: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0;