From 23f430573efe4f6ef55b0180aa0d742ff4eca1d9 Mon Sep 17 00:00:00 2001 From: Gabriel Schneider Date: Mon, 28 Sep 2026 00:01:57 -0300 Subject: Make a pane's tag a Text edited with the body's own keys The tag was a fixed buffer with its own one-line editor, a cursor measured across the path, and a hijack of the body's mode while it was typed into. It is now the pane's second Text: its own characters after the computed prefix (path, dirty marker, PDF page), which is never stored in it, so a rename or a dirty marker never moves its cursor. Normal and insert mode are the body's, undo included; `:` moves the keyboard between body and tag, the tag remembers its cursor and starts on Save, and executing a word from it hands the keyboard back. The mouse still sees the prefix: a sweep selects across it and a click on the path drafts a new name. Co-Authored-By: Claude Opus 5.5 --- src/mouse.zig | 82 ++++++++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 58 insertions(+), 24 deletions(-) (limited to 'src/mouse.zig') diff --git a/src/mouse.zig b/src/mouse.zig index 5f77634b..ae047c58 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -39,7 +39,6 @@ pub const Drag = union(enum) { /// `grab`: how far into the grip it was pressed, so the column's left /// edge lands at `cur_x - grab`. A release still on the grip moves nothing. column_move: struct { serial: u32, cur_x: u16, grab: u16 = 0 }, - tag: struct { id: usize }, /// chorded: a 1-2/1-3 cut/paste chord fired during this left drag — /// the drag's own release is then inert select: struct { @@ -55,7 +54,7 @@ pub const Drag = union(enum) { fn button(drag: Drag) ?Mouse.Button { return switch (drag) { .none => null, - .border_v, .border_h, .move, .column_move, .tag => config.select_button, + .border_v, .border_h, .move, .column_move => config.select_button, .select => |selection| selection.button, }; } @@ -228,7 +227,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { const previous_tag_hit = p.pointer_tag_hit; p.pointer_tag_hit = if (m.tag_hit) |point| p.reprojectTagHit(point) else null; if (m.tag_hit != null and p.pointer_tag_hit == null and m.kind == .press) return; - if (m.tag_hit == null and (m.kind == .drag or m.kind == .release) and (p.drag == .tag or p.drag == .select or p.header_drag)) { + if (m.tag_hit == null and (m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag)) { if (previous_tag_hit) |previous| { var continued = previous; continued.pixel_x = (@as(f32, @floatFromInt(raw_col)) + 0.5) * @as(f32, @floatFromInt(previous.metrics.body_w)); @@ -236,7 +235,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { p.pointer_tag_hit = continued; } } - if ((m.kind == .drag or m.kind == .release) and (p.drag == .tag or p.drag == .select or p.header_drag)) { + if ((m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag)) { if (previous_tag_hit) |previous| if (p.pointer_tag_hit) |*current| { current.kind = previous.kind; current.id = previous.id; @@ -303,7 +302,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { (m.button == config.select_button or m.button == config.exec_button or m.button == config.look_button)) { if (m.button == config.select_button) if (p.panes[p.active]) |pane| { - if (pane.tag_edit) Pardes.exitTagEdit(pane); + pane.focus = .body; pane.closePrompt(); }; var column: ?usize = null; @@ -439,7 +438,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { const r = p.rects[id]; const pane = p.panes[id] orelse return; if (id != p.active) if (p.panes[p.active]) |previous| { - if (previous.tag_edit) Pardes.exitTagEdit(previous); + previous.focus = .body; previous.closePrompt(); }; const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y; @@ -448,20 +447,29 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { // gutter scrollbar: a left click scrolls UP to that row p.active = id; pane.scrollBy(-(@as(i32, mrow) - @as(i32, body_y))); - } else if (mrow >= tag_y and mrow < tag_y + BOX_H) { - if (mcol < r.x + TAG_TEXT_INSET) return; - p.active = id; - pane.closePrompt(); - p.enterTagEdit(pane, if (tagColumn(p, .pane, id, p.pointer_tag_hit, false)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, r.x + TAG_TEXT_INSET)); - pane.tag_anchor = pane.tag_col; - p.drag = .{ .tag = .{ .id = id } }; } else { + const on_tag = mrow >= tag_y and mrow < tag_y + BOX_H; + if (on_tag and mcol < r.x + TAG_TEXT_INSET) return; p.active = id; - if (pane.tag_edit) Pardes.exitTagEdit(pane); pane.closePrompt(); const pointed = edit.pointerTextSelection(p, id, mcol, mrow, m.body_hit, p.pointer_tag_hit) orelse return; pane.clearPointerSelection(sel_slot); pane.sel[sel_slot] = pointed.sel; + // A clicked tag is typed straight into, from where it was + // clicked; `:` is the door to its normal mode. + pane.focus = if (on_tag) .tag else .body; + if (on_tag) { + // a count typed before the click was meant for the body + pane.body.normal.clear(); + pane.tag.mode = .insert; + pane.tag.normal.clear(); + pane.tag.vsel.active = false; + pane.tag.msel.active = false; + pane.tag.nsel = 0; + pane.tag.cur_pinned = true; + pane.tag.cur_row = 0; + pane.tag.cur_col = @intCast(p.tagOffsetAt(pane, @intCast(@max(0, pointed.sel.c0))) orelse 0); + } p.drag = .{ .select = .{ .id = id, .button = config.select_button, @@ -472,7 +480,7 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { config.select_button, mcol, mrow, - false, + pointed.on_tag, ), } }; } @@ -617,13 +625,6 @@ pub fn dragUpdate(p: *Pardes, mcol: u16, mrow: u16, body_hit: ?Mouse.BodyHit) vo panes.Pdf.pointerUpdate(&s.pdf, p, pane, mcol, mrow); } }, - .tag => |d| { - const pane = p.panes[d.id] orelse return; - const c = if (tagColumn(p, .pane, d.id, p.pointer_tag_hit, true)) |value| @as(i32, value) else @as(i32, mcol) - @as(i32, p.rects[d.id].x + TAG_TEXT_INSET); - const text = p.tagText(p.scratch.allocator(), pane) catch return; - pane.tag_col = @intCast(@min(text.len, panes.File.rawAtDisplay(text, @as(usize, @intCast(@max(0, c))) + pane.tag_scroll))); - pane.tag_sel = pane.tag_col != pane.tag_anchor; - }, .none => {}, } } @@ -816,6 +817,16 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { const sl = pane.sel[sel_slot]; const dragged = sl.c0 != sl.c1 or sl.r0 != sl.r1; if (!dragged) pane.sel[sel_slot].state = .none; + if (sl.r0 < BOX_H) { + // On the tag the caret goes where the button came up. A + // click on the path, if it can be renamed, drafts a name. + const at = p.tagOffsetAt(pane, @intCast(@max(0, sl.c1))); + if (at) |col| { + pane.tag.cur_row = 0; + pane.tag.cur_col = @intCast(col); + } else if (!dragged) p.draftName(pane, @intCast(@max(0, sl.c1))); + return; + } if (dragged) edit.capturePointerSelection(p, pane, sel_slot) catch {}; if (dragged and pane.body.mode == .tty) mirrorTtySelection(p, pane); const body_vis = sl.r1 - @as(i32, BOX_H); @@ -869,7 +880,7 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { } const txt = operand.text; const cmd = if (s.button == config.look_button) config.look_cmd else config.exec_cmd; - if (clk.r0 < BOX_H and s.button == config.exec_button and pane.tag_name_len != null) { + if (clk.r0 < BOX_H and s.button == config.exec_button and pane.prompt == .name) { const prefix_width = panes.File.displayWidth(p.tagPrefix(pane) catch return); const on_name = @as(usize, @intCast(@max(0, clk.c0))) < prefix_width; if (!p.commitTagName(pane)) return; @@ -895,7 +906,6 @@ fn dragRelease(p: *Pardes, button: Mouse.Button) void { }); } }, - .tag => {}, // dragUpdate already left the tag cursor + selection set .none => {}, } } @@ -913,6 +923,30 @@ fn chordCutPaste(p: *Pardes, cut: bool) void { if (cut and pv.selection_text.len > 0) edit.setYank(p, pv.selection_text); return; }; + if (@min(pane.sel[sel_slot].r0, pane.sel[sel_slot].r1) < BOX_H) { + // A sweep over the tag cuts from and pastes into the tag's own text; + // the part of it over the prefix is not the tag's to cut. + const t = &pane.tag; + if (!s.chorded) { + s.chorded = true; + const sl = pane.sel[sel_slot]; + pane.sel[sel_slot].state = .none; + const from = p.tagOffsetAt(pane, @intCast(@max(0, @min(sl.c0, sl.c1)))) orelse 0; + const to = p.tagOffsetAt(pane, @intCast(@max(0, @max(sl.c0, sl.c1)))) orelse 0; + t.cur_row = 0; + t.cur_col = @intCast(to); + t.cur_pinned = true; + t.vsel = .{ .active = from != to, .row = 0, .col = @intCast(from), .explicit = false }; + t.msel.active = false; + t.nsel = 0; + } + if (cut) { + if (t.vsel.active) edit.normalDelete(p, t, true); + } else if (t.vsel.active) { + edit.normalReplaceYank(p, t); + } else edit.pasteText(p, t, p.yank orelse return, true, 1); + return; + } if (pane.body.mode == .tty) { if (!s.chorded) { s.chorded = true; -- cgit v1.3