diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-29 22:21:56 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:17 -0300 |
| commit | dfa5bd4d413504a6afff414962c4de70ab093af0 (patch) | |
| tree | 1985488bde887799bc14d4b65b9557c6e8910545 /src | |
| parent | 714da820136ac89d6a79ed0141e7325829ab8204 (diff) | |
| download | pardes-dfa5bd4d413504a6afff414962c4de70ab093af0.tar.gz pardes-dfa5bd4d413504a6afff414962c4de70ab093af0.zip | |
A middle or right sweep over a column's or the workspace's tag runs or looks for what it swept, as a pane tag's does
Both headers acted on the press: a middle or right press ran, or looked
for, the word under it (or the header's selection when it was on that) at
once, so a drag had nothing left to do. A sweep of `echo SWEPT` in a column
tag ran a bare `echo`. They have done so since the headers were drawn apart
from the panes; it is no regression, and the -l/Restore allocator fix
(ompukopz) does not touch it.
Now a press on a header holds a HeaderSweep (the column by its serial, or
the workspace), which the drag extends, held to the header's row and text
as a pane tag's sweep is. It is painted in its button's tint meanwhile, and
the release runs what it swept in that header's context; a release that has
not moved runs the word or selection a click always did. The other of
middle and right cancels it (acme 2-3/3-2); the select button is inert, as
a header's command already takes the held selection as its argument.
Running on the release, a builtin from a header is announced as one from a
pane tag is: the press used to set the announcement and the release clear it
at once. find, restore, theme, topbarexec, ttyhelp, tutor and windowops
re-recorded for that badge alone.
headersweep.snap sweeps the column and workspace tags with the middle
button and a word part with the right one, and cancels a sweep;
headersweep-restored.snap sweeps both after Dump and Restore.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
| -rw-r--r-- | src/draw.zig | 26 | ||||
| -rw-r--r-- | src/mouse.zig | 161 | ||||
| -rw-r--r-- | src/pardes.zig | 2 |
3 files changed, 153 insertions, 36 deletions
diff --git a/src/draw.zig b/src/draw.zig index ad24153f..0ea0b35e 100644 --- a/src/draw.zig +++ b/src/draw.zig @@ -1109,8 +1109,9 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer, // The word under the pointer, a Look or Exec target: on a header even // while it is edited, on a notice, and on the grid's copy of either // (joined from this); not on a column a carried column's pointer is - // aiming at a place over. - const hovered = p.pointer_inside and !(kind == .column and p.drag == .column_move); + // aiming at a place over; nor under a held middle or right sweep, + // which paints what it sweeps instead. + const hovered = p.pointer_inside and !(kind == .column and p.drag == .column_move) and p.header_sweep == null; if (hovered) { const tag_hit = p.pointer_tag_hit; const col: ?usize = if (tag_hit) |pointer| @@ -1141,6 +1142,27 @@ fn renderHeaderLayer(p: *Pardes, arena: std.mem.Allocator, layer: *pardes.Layer, const asking = if (p.panes[p.active]) |active| active.prompt != .none else false; if (!asking and t.cur_row == line + p.header_top and caret < columns) layer.cursor = .{ .x = @intCast(caret), .y = line, .bar = t.mode == .insert }; } + // A middle or right sweep held over this header, tinted by its + // button as a pane tag's is. + if (p.header_sweep) |sweep| if (sweep.column(p)) |column| { + const mine = switch (kind) { + .workspace => column == null, + .column => column == id, + .pane, .notice, .body => false, + }; + const swept = sweep.span(text.len); + const start = modal.lineStartOffset(text, index + top); + const end = start + shown.len; + if (mine and swept.lo < swept.hi and swept.hi > start and swept.lo < end) { + const button = @intFromEnum(sweep.button); + var col = panes.File.rawDisplayCol(shown, @max(swept.lo, start) - start) -| scroll; + const stop = panes.File.rawDisplayCol(shown, @min(swept.hi, end) - start) -| scroll; + while (col < stop and col < columns) : (col += 1) { + surface.at(@intCast(col), 0).style.bg = .{ .rgb = body_layer.buttonTints(p.theme())[button] }; + surface.at(@intCast(col), 0).style.fg = .{ .rgb = body_layer.buttonInks(p.theme())[button] }; + } + } + } else |_| {}; } } diff --git a/src/mouse.zig b/src/mouse.zig index cb0378de..2280020d 100644 --- a/src/mouse.zig +++ b/src/mouse.zig @@ -70,6 +70,84 @@ pub const Drag = union(enum) { } }; +/// A middle or right sweep over a column's tag or the workspace's, held as +/// a pane tag's is: painted in its button's tint while the button is down, +/// and run (or looked for) in that tag's context when it comes up. `anchor` +/// and `cur` are byte offsets into the header's text. +pub const HeaderSweep = struct { + /// The column's serial; null for the workspace's tag. + serial: ?u32, + button: Mouse.Button, + anchor: usize, + cur: usize, + + /// The column index the sweep is over, or null for the workspace's + /// tag; `error.Gone` once its column has been closed. + pub fn column(s: HeaderSweep, p: *const Pardes) error{Gone}!?usize { + const serial = s.serial orelse return null; + return layout.columnBySerial(p, serial) orelse error.Gone; + } + + /// The swept bytes, empty for a click that has not moved. + pub fn span(s: HeaderSweep, len: usize) struct { lo: usize, hi: usize } { + return .{ .lo = @min(s.anchor, s.cur, len), .hi = @min(@max(s.anchor, s.cur), len) }; + } +}; + +/// What a pointer at (mcol, mrow) is over in a column's tag, or the +/// workspace's for null: the header's text, the line, and the byte offset +/// in that text. `clamp` holds a held sweep to the header's rows and text, +/// as a pane tag's sweep stays on its tag. +fn headerPoint(p: *Pardes, column: ?usize, mcol: u16, mrow: u16, clamp: bool) ?struct { t: *panes.Text, text: []const u8, line: u16, at: usize } { + const t = if (column) |c| &p.col_tags[c] else &p.global_tag; + const x = if (column) |c| p.col_x[c] + TAG_TEXT_INSET else 0; + const focused = p.header_focus and p.header_column == column; + const text = edit.flatSurface(p, t) catch return null; + const base = if (column != null) p.topBarHeight() else 0; + const rows = if (column != null) p.columnBarHeight() else p.topBarHeight(); + const row: u16 = if (clamp) @min(mrow -| base, rows -| 1) else mrow -| base; + const top: usize = if (focused) p.header_top else 0; + const lines = modal.lineCount(text); + const line: u16 = if (clamp) @intCast(@min(row + top, lines -| 1)) else @intCast(row + top); + if (line > 0 and line >= lines) return null; + const bar = modal.lineSlice(text, line); + const scroll = if (focused) p.header_scroll else 0; + const kind: Layer.Kind = if (column != null) .column else .workspace; + const at = @min(bar.len, panes.File.rawAtDisplay(bar, (Layer.columnAt(p, kind, column orelse 0, p.pointer_tag_hit, clamp, row) orelse (mcol -| x)) + scroll)); + return .{ .t = t, .text = text, .line = line, .at = modal.lineStartOffset(text, line) + at }; +} + +/// A header sweep's button came up: a click runs the word under it, or the +/// header's selection when it is on that; a sweep runs what it swept. +fn headerSweepRun(p: *Pardes, sweep: HeaderSweep) void { + const column = sweep.column(p) catch return; + const t = if (column) |c| &p.col_tags[c] else &p.global_tag; + const text = edit.flatSurface(p, t) catch return; + const swept = sweep.span(text.len); + const selected = if (swept.lo < swept.hi) text[swept.lo..swept.hi] else click: { + const line = std.mem.count(u8, text[0..swept.lo], "\n"); + const start = modal.lineStartOffset(text, line); + const bar = modal.lineSlice(text, line); + const at = swept.lo - start; + const focused = p.header_focus and p.header_column == column; + const on = if (focused) tagline.headerSelection(t, text, line) else null; + break :click if (on) |sp| (if (at >= sp.lo and at < sp.hi) bar[sp.lo..sp.hi] else wordAtCol(bar, at)) else 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; + p.exec_header = true; + defer p.exec_header = false; + if (word.len == 0) return; + if (sweep.button == config.exec_button) { + const named = std.meta.stringToEnum(Builtin, word); + const held = if (named) |b| (if (builtins.registry.takesArg(b)) exec.heldSelection(p, p.active) else null) else exec.heldSelection(p, p.active); + _ = exec.execute(p, p.active, exec.withArg(p, word, held)); + } else exec.runBuiltin(p, config.look_cmd, p.active, "", word); +} + fn clampBorderCol(lx: u16, lw: u16, rw: u16, mcol: u16) u16 { const lo = lx + config.MINW; const hi = lx +| lw +| rw -| config.MINW; @@ -255,7 +333,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 == .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 or p.header_sweep != null)) { 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)); @@ -263,7 +341,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 == .select or p.header_drag)) { + if ((m.kind == .drag or m.kind == .release) and (p.drag == .select or p.header_drag or p.header_sweep != null)) { if (previous_tag_hit) |previous| if (p.pointer_tag_hit) |*current| { current.kind = previous.kind; current.id = previous.id; @@ -289,7 +367,13 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { p.raw_hover_intent = false; const mapped = p.presentation.pointer(p.screen_w, p.screen_h, raw_col, raw_row) orelse { look.cancelLookHover(p); - if (m.kind == .release) dragRelease(p, m.button); + if (m.kind == .release) { + if (p.header_sweep) |sweep| if (sweep.button == m.button) { + p.header_sweep = null; + return headerSweepRun(p, sweep); + }; + dragRelease(p, m.button); + } return; }; const mcol = mapped.col; @@ -309,6 +393,32 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { p.chord_arg = null; } } + if (p.header_sweep) |*sweep| { + if (m.kind == .press and switch (m.button) { + .left, .middle, .right => true, + else => false, + }) { + // acme 2-3 / 3-2: the other of execute and look cancels the + // sweep. The select button is inert: a header's command already + // takes the held selection as its argument, which is what a 2-1 + // chord would give it. + if (m.button != config.select_button) p.header_sweep = null; + return; + } + if (m.button == sweep.button and (m.kind == .drag or m.kind == .release)) { + const column = sweep.column(p) catch { + p.header_sweep = null; + return; + }; + if (headerPoint(p, column, mcol, mrow, true)) |point| sweep.cur = point.at; + if (m.kind == .release) { + const done = sweep.*; + p.header_sweep = null; + headerSweepRun(p, done); + } + return; + } + } if (mrow >= p.topBarHeight() and mrow < p.topBarHeight() + p.columnBarHeight() and m.kind == .press) { for (0..p.ncol) |column| { if (mcol < p.col_x[column] or mcol >= p.col_x[column] + @min(config.GUTTER, p.col_w[column])) continue; @@ -360,43 +470,26 @@ pub fn handleMouse(p: *Pardes, m: Mouse) void { }; if (column == null or mcol < x) return; } - const t = if (column) |c| &p.col_tags[c] else &p.global_tag; - const focused = p.header_focus and p.header_column == column; - const text = edit.flatSurface(p, t) catch return; - const row: u16 = @intCast(mrow - if (column != null) p.topBarHeight() else 0); - const line: u16 = @intCast(row + if (focused) p.header_top else 0); - if (line > 0 and line >= modal.lineCount(text)) return; - const bar = modal.lineSlice(text, line); - const scroll = if (focused) p.header_scroll else 0; - const at = @min(bar.len, panes.File.rawAtDisplay(bar, (Layer.columnAt(p, if (column != null) .column else .workspace, column orelse 0, p.pointer_tag_hit, p.header_drag, row) orelse (mcol -| x)) + scroll)); + const point = headerPoint(p, column, mcol, mrow, false) orelse return; if (m.button == config.select_button) { + const t = point.t; + const at = point.at - modal.lineStartOffset(point.text, point.line); // A clicked header is typed straight into, as a clicked tag is. - if (!focused) tagline.enterHeader(p, column); + if (!(p.header_focus and p.header_column == column)) tagline.enterHeader(p, column); t.mode = .insert; - t.cur_row = line; + t.cur_row = point.line; t.cur_col = @intCast(at); - t.vsel = .{ .active = false, .row = line, .col = @intCast(at) }; + t.vsel = .{ .active = false, .row = point.line, .col = @intCast(at) }; p.header_drag = true; } else { - const span = if (focused) tagline.headerSelection(t, text, line) else null; - const selected = if (span) |sp| - (if (at >= sp.lo and at < sp.hi) bar[sp.lo..sp.hi] else wordAtCol(bar, at)) - else - 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; - p.exec_header = true; - defer p.exec_header = false; - if (word.len > 0) { - if (m.button == config.exec_button) { - const named = std.meta.stringToEnum(Builtin, word); - const held = if (named) |b| (if (builtins.registry.takesArg(b)) exec.heldSelection(p, p.active) else null) else exec.heldSelection(p, p.active); - _ = exec.execute(p, p.active, exec.withArg(p, word, held)); - } else exec.runBuiltin(p, config.look_cmd, p.active, "", word); - } + // Held, as a pane tag's sweep is: it runs when the button + // comes up (headerSweepRun). + p.header_sweep = .{ + .serial = if (column) |c| p.col_serial[c] else null, + .button = m.button, + .anchor = point.at, + .cur = point.at, + }; } return; } diff --git a/src/pardes.zig b/src/pardes.zig index 3ecc80ae..18794975 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -4474,6 +4474,8 @@ pub const Pardes = struct { header_column: ?usize = null, /// A mouse sweep is extending the focused header's selection. header_drag: bool = false, + /// A middle or right sweep held over a column's or the workspace's tag. + header_sweep: ?mouse.HeaderSweep = null, header_scroll: usize = 0, /// The focused header's first line on screen, which follows its caret /// when it has more lines than rows. |
