summaryrefslogtreecommitdiff
path: root/src/tagline.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 19:36:58 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit1df6bd12fafbf97062f7844a23de208add75a843 (patch)
tree78b61cfebeb12c40b679d053b457d2e28f232784 /src/tagline.zig
parent1f8de151e382755c1ce597f166ea4552e7f7e8d1 (diff)
downloadpardes-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.zig96
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));
+}
+