diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 19:36:58 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 1df6bd12fafbf97062f7844a23de208add75a843 (patch) | |
| tree | 78b61cfebeb12c40b679d053b457d2e28f232784 /src/tagline.zig | |
| parent | 1f8de151e382755c1ce597f166ea4552e7f7e8d1 (diff) | |
| download | pardes-1df6bd12fafbf97062f7844a23de208add75a843.tar.gz pardes-1df6bd12fafbf97062f7844a23de208add75a843.zip | |
A pane tag wraps onto rows and can collapse to one, as acme's does
acme's frame wraps a tag at its right edge and the window grows to show
every row (wind.c wintaglines); Tagup/Tagdown shrink it to one row and back.
Pane tags used to be one row, scrolled sideways to the caret. Now a tag's
lines wrap at its width (one cell kept for the caret past a full row) and it
starts expanded; insert-mode Up on the first row collapses it, Down on the
last row expands it, and Alt-Up/Alt-Down do so in either mode. A word the
wrap breaks across rows is still one word to a click and a selection.
Column and workspace tags keep scrolling sideways.
Tests read body rows relative to the body's first row; the 41 snapshot
goldens that moved were re-recorded by name after a programmatic check that
each differs only by body rows shifting under the taller tags, and twelve
scripts whose clicks targeted fixed rows now click the same text.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/tagline.zig')
| -rw-r--r-- | src/tagline.zig | 96 |
1 files changed, 85 insertions, 11 deletions
diff --git a/src/tagline.zig b/src/tagline.zig index d0d0b413..5ed65c26 100644 --- a/src/tagline.zig +++ b/src/tagline.zig @@ -206,13 +206,6 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void { t.cur_row = @intCast(cursor.row); t.cur_col = @intCast(cursor.col); t.cur_pinned = true; - // Reveal the suffix together with its commands, not merely the - // first command letter at the viewport's far right edge. - if (p.paneIdOf(pane)) |id| { - const line = whole[0 .. std.mem.indexOfScalar(u8, whole, '\n') orelse whole.len]; - const width = p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET); - pane.tag_scroll = @intCast(panes.File.displayWidth(line) + 1 -| width); - } } // The text may have changed since (a 9P write, a rename), so the // remembered cursor is pulled back inside it. It is the cursor that @@ -232,12 +225,83 @@ pub fn enterTag(p: *Pardes, pane: *Pane) void { pane.focus = .tag; } -/// Where display column `col` of the tag's shown row `row` (tag scroll -/// included) puts its cursor: the tag as shown, prefix and all. +/// A pane's tag as it is shown: its lines, each wrapped onto more rows at +/// the tag's width as acme's frame wraps a tag at its right edge (plan9port +/// acme wind.c:140, wintaglines, counts the frame's lines). `soft` are the +/// offsets in `text` of the newlines the wrapping put in. +pub const Shown = struct { + text: []u8, + soft: []const usize, + + /// The tag's own offset of a shown one. + pub fn toReal(s: Shown, off: usize) usize { + var n: usize = 0; + for (s.soft) |b| { + if (b < off) n += 1 else break; + } + return off - n; + } + + /// The shown offset of the tag's own one. + pub fn fromReal(s: Shown, real: usize) usize { + var off = real; + for (s.soft) |b| { + if (b <= off) off += 1 else break; + } + return off; + } + + pub fn rows(s: Shown) usize { + return modal.cursorLineCount(s.text); + } +}; + +/// `whole` wrapped at `width` display columns. +pub fn wrapTag(arena: std.mem.Allocator, whole: []const u8, width: usize) !Shown { + var out: std.ArrayList(u8) = .empty; + var soft: std.ArrayList(usize) = .empty; + const w = @max(1, width); + var lines = std.mem.splitScalar(u8, whole, '\n'); + var first = true; + while (lines.next()) |line| { + if (!first) try out.append(arena, '\n'); + first = false; + var rest = line; + while (panes.File.displayWidth(rest) > w) { + var cut = modal.graphemeStart(rest, panes.File.rawAtDisplay(rest, w)); + if (cut == 0) cut = modal.nextGrapheme(rest, 0); + try out.appendSlice(arena, rest[0..cut]); + try soft.append(arena, out.items.len); + try out.append(arena, '\n'); + rest = rest[cut..]; + } + try out.appendSlice(arena, rest); + } + return .{ .text = out.items, .soft = soft.items }; +} + +/// The tag's width for wrapping: its columns, one kept for the caret past +/// a full row's end. +pub fn tagWidth(p: *const Pardes, id: usize) usize { + return @max(1, @as(usize, p.tagCapacity(p.rects[id].w -| TAG_TEXT_INSET)) -| 1); +} + +pub fn shownTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) !Shown { + const whole = try tagText(p, arena, pane); + const id = p.paneIdOf(pane) orelse return .{ .text = whole, .soft = &.{} }; + return wrapTag(arena, whole, tagWidth(p, id)); +} + +/// Where display column `col` of the tag's shown row `row` puts its +/// cursor: the tag as shown, prefix and all, wrapped rows included. pub fn tagOffsetAt(p: *Pardes, pane: *Pane, row: usize, col: usize) ?modal.Cursor { + const arena = p.scratch.allocator(); + const shown = shownTag(p, arena, pane) catch return null; + const whole = tagText(p, arena, pane) catch return null; const at = row + pane.tag_top; - const line = modal.lineSlice(tagText(p, p.scratch.allocator(), pane) catch return null, at); - return .{ .row = at, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col)) }; + const line = modal.lineSlice(shown.text, at); + const off = modal.lineStartOffset(shown.text, at) + modal.graphemeStart(line, panes.File.rawAtDisplay(line, col)); + return modal.positionAt(whole, @min(shown.toReal(off), whole.len)); } /// Whether display column `col` of the tag's shown row `row` is on a name @@ -490,3 +554,13 @@ test "a column or workspace tag is one line: a newline in it is a space" { try std.testing.expectEqual(pardes.TOPBAR_H, p.topBarHeight()); } +test "a pane tag wraps a long line onto rows, and offsets map both ways" { + var arena_state = std.heap.ArenaAllocator.init(std.testing.allocator); + defer arena_state.deinit(); + const shown = try wrapTag(arena_state.allocator(), "/a/b.txt Del Snarf Undo Put\nMore", 10); + try std.testing.expectEqualStrings("/a/b.txt D\nel Snarf U\nndo Put\nMore", shown.text); + try std.testing.expectEqual(@as(usize, 4), shown.rows()); + try std.testing.expectEqual(@as(usize, 10), shown.toReal(11)); // the 'e' of Del + try std.testing.expectEqual(@as(usize, 11), shown.fromReal(10)); +} + |
