summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig298
1 files changed, 159 insertions, 139 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index c271a16f..0ac16162 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1168,29 +1168,30 @@ test "TreeContext tag style preserves message rows and its opt-out survives rest
pane.msg[0] = '!';
pane.msg_len = 1;
_ = try p.render(p.scratch.allocator());
- try std.testing.expectEqual(full_height - 1, p.surface.body_layers[0].viewport.h);
+ // An overlay takes nothing: the message is a chip OVER the body's first
+ // row, so the viewport keeps every row it had.
+ try std.testing.expectEqual(full_height, p.surface.body_layers[0].viewport.h);
const r = p.rects[0];
- // The message is a tagline band at the top of the body now, not a body row
- // at the bottom: same reservation, different carrier.
const band = for (p.surface.tagLayers()) |*layer| {
if (layer.cols != 0 and layer.kind == .notice) break layer;
} else return error.NoNoticeBand;
- try std.testing.expectEqual(r.y + BOX_H, band.viewport.y);
- // Right aligned: the text sits in the last cell the viewport actually
- // shows, which is a column short of the layer's cursor slack.
- try std.testing.expectEqualStrings("!", band.cells[p.tagCapacity(band.viewport.w) - 1].grapheme());
- // ...and the body starts below it rather than under it.
- try std.testing.expectEqual(band.viewport.y + 1, p.surface.body_layers[0].viewport.y);
+ try std.testing.expectEqual(p.bodyTop(r), band.viewport.y);
+ try std.testing.expectEqual(p.surface.body_layers[0].viewport.y, band.viewport.y);
+ // Sized to its message, flush with the pane's right edge, and the text
+ // right aligned a cell short of that edge.
+ try std.testing.expectEqual(r.x + r.w, band.viewport.x + band.viewport.w);
+ try std.testing.expect(band.viewport.w < r.w - config.GUTTER);
+ try std.testing.expectEqualStrings("!", band.cells[p.tagCapacity(band.viewport.w) - 2].grapheme());
pane.msg_len = 0;
p.leader_on = true;
_ = try p.render(p.scratch.allocator());
- try std.testing.expectEqual(full_height - 1, p.surface.body_layers[0].viewport.h);
+ try std.testing.expectEqual(full_height, p.surface.body_layers[0].viewport.h);
p.leader_on = false;
try std.testing.expect(p.executeBuiltinLine(0, "TreeContextTagStyle"));
_ = try p.render(p.scratch.allocator());
try std.testing.expectEqual(@as(u16, 0), p.surface.body_layers[0].rows);
try std.testing.expectEqual(@as(u16, 0), pane.body_rows);
- try std.testing.expectEqual(FontRole.body, p.surface.at(r.x + config.GUTTER, p.bodyTop(pane, r)).style.font_role);
+ try std.testing.expectEqual(FontRole.body, p.surface.at(r.x + config.GUTTER, p.bodyTop(r)).style.font_role);
try p.dumpState();
const restored = try Pardes.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
defer restored.deinit();
@@ -6282,6 +6283,33 @@ pub const Pardes = struct {
return p;
}
+ /// `SPC` plus the chord keys typed so far, spaced out the way the help
+ /// index spells a leader path.
+ fn leaderText(p: *const Pardes, buf: *[16]u8) []const u8 {
+ buf.* = @splat(' ');
+ @memcpy(buf[0..3], "SPC");
+ var at: usize = 3;
+ for (p.leader_keys[0..p.leader_n]) |ch| {
+ if (at + 2 > buf.len) break;
+ buf[at + 1] = ch;
+ at += 2;
+ }
+ return buf[0..at];
+ }
+
+ /// How wide a notice chip is, in grid columns: its own text plus a blank
+ /// cell either side. A notice is an OVERLAY over the body's top rows, not
+ /// a row taken from them, so it is sized to its message and never to the
+ /// pane. Counted in GRID columns rather than scaled into the tagline face
+ /// it is drawn in, because the canonical grid is what a terminal client
+ /// draws and a chip narrower than its own text there would clip it; the
+ /// tagline face simply leaves a little more room inside the chip.
+ fn noticeCols(p: *const Pardes, text: []const u8, limit: u16) u16 {
+ _ = p;
+ if (limit == 0) return 0;
+ return @intCast(std.math.clamp(panes.File.displayWidth(text) + 2, 1, @as(usize, limit)));
+ }
+
/// Prints `text` flush with the right edge of the band, and answers the
/// column it started at so a cursor can follow it.
fn printRight(s: *Surface, x: u16, row: u16, w: u16, text: []const u8, style: CellStyle) u16 {
@@ -11264,13 +11292,14 @@ pub const Pardes = struct {
const r = p.rects[id];
if (col < r.x + config.GUTTER or col >= r.x + r.w or row < r.y or row >= r.y + r.h) return null;
const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
- const body_y = p.bodyTop(p.panes[id].?, r);
+ const body_y = p.bodyTop(r);
const on_tag = row >= tag_y and row < tag_y + BOX_H;
if (on_tag and col < r.x + TAG_TEXT_INSET) return null;
- // A notice band is chrome. Without this it maps to Sel row 0 or less,
- // which is the TAG row -- so a click on a leader band would expand a
- // word out of the tag text and run it as a builtin.
- if (!on_tag and row >= body_y -| p.panes[id].?.body_offset and row < body_y) return null;
+ // A notice chip is chrome: it sits OVER the body's top rows, so a
+ // click inside one belongs to nobody. The rest of those rows is
+ // ordinary body text and still answers normally.
+ if (!on_tag and row >= body_y and row - body_y < p.panes[id].?.notices.len and
+ p.panes[id].?.notices.covers(row - body_y, col)) return null;
if (!on_tag) if (body_hit) |supplied| {
const mapped_hit = p.bodyHitForPane(id, supplied, false) orelse return null;
const c = @as(i32, mapped_hit.col) - config.GUTTER;
@@ -11657,7 +11686,7 @@ pub const Pardes = struct {
if (previous.tag_edit) exitTagEdit(previous);
};
const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
- const body_y = p.bodyTop(pane, r);
+ const body_y = p.bodyTop(r);
if (mcol < r.x + config.GUTTER) {
// gutter scrollbar: a left click scrolls UP to that row
p.active = id;
@@ -11721,7 +11750,7 @@ pub const Pardes = struct {
// the tag row and the body's first, exactly as the select
// arm above derives them
const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
- const body_y = p.bodyTop(pane, r);
+ const body_y = p.bodyTop(r);
const on_tag = mrow >= tag_y and mrow < tag_y + BOX_H;
if (mcol < r.x + config.GUTTER and !on_tag) {
const local = @as(i32, mrow) - @as(i32, body_y);
@@ -11818,8 +11847,8 @@ pub const Pardes = struct {
// measure from the body's top, notice bands included, or a
// selection made while a message is up lands rows away
// from the pointer.
- const body_y = p.bodyTop(pane, r);
- const body_h = (r.h -| BOX_H) -| pane.body_offset;
+ const body_y = p.bodyTop(r);
+ const body_h = r.h -| BOX_H;
pane.sel[b].r1 = if (pane.sel[b].r0 < BOX_H or body_h == 0)
0
else
@@ -13370,12 +13399,11 @@ pub const Pardes = struct {
return null;
}
- /// The pane's first body row: under the tag when it is on top, and under
- /// whatever notice bands `collectNotices` stacked above the body. Every
- /// painter and hit test derives the body from here so the bands cannot
- /// come adrift from the text they sit over.
- pub fn bodyTop(p: *const Pardes, pane: *const Pane, r: Rect) u16 {
- return (if (p.settings.tag_bottom) r.y else r.y + BOX_H) + pane.body_offset;
+ /// The pane's first body row: under the tag when it is on top, over it
+ /// when it is at the bottom. Notice bands do NOT move it -- they are an
+ /// overlay, drawn over the top rows rather than taking them.
+ pub fn bodyTop(p: *const Pardes, r: Rect) u16 {
+ return if (p.settings.tag_bottom) r.y else r.y + BOX_H;
}
fn tagCapacity(p: *const Pardes, physical_width: u16) u16 {
@@ -13461,12 +13489,13 @@ pub const Pardes = struct {
.bg = .{ .rgb = chrome.tag_bg },
.font_role = .tagline,
};
- // Sticky at the TOP of the body, where the tree-sitter context
- // headers sit, one line to a row in the order collectNotices chose.
- // Text is right aligned: the left edge is where the body's own text
- // begins, and a notice that shared it would read as a line of the
- // file rather than as chrome over it.
- const first = if (p.settings.tag_bottom) r.y else r.y + BOX_H;
+ // An OVERLAY over the body's top rows, at the right edge, one
+ // chip to a row in the order collectNotices chose -- not a row
+ // taken out of the body the way a tree-sitter context header is.
+ // The chip is as wide as its own message and no wider, so the text
+ // beneath the rest of the row keeps reading as the file it is.
+ const first = p.bodyTop(r);
+ var leader_buf: [16]u8 = undefined;
for (pane.notices.slice(), 0..) |kind, i| {
const row = first + @as(u16, @intCast(i));
// Every notice is painted on the canonical grid as well as
@@ -13474,39 +13503,34 @@ pub const Pardes = struct {
// draws, and a band it cannot see is a message it never gets.
// The GUI skips grid cells a tag layer covers, so nothing is
// drawn twice there.
- s.fill(tx, row, tw, 1, .{ .bg = msg_style.bg, .font_role = .tagline });
- switch (kind) {
- .message => _ = printRight(s, tx, row, tw, pane.msg[0..pane.msg_len], msg_style),
- .leader => {
- var ibuf: [16]u8 = @splat(' ');
- @memcpy(ibuf[0..3], "SPC");
- var iw: usize = 3;
- for (p.leader_keys[0..p.leader_n]) |ch| {
- ibuf[iw + 1] = ch;
- iw += 2;
- }
- _ = printRight(s, tx, row, tw, ibuf[0..iw], msg_style);
- },
- .prompt => {
+ const text = switch (kind) {
+ .message => pane.msg[0..pane.msg_len],
+ .leader => p.leaderText(&leader_buf),
+ .prompt => blk: {
const at = pane.promptAt() orelse continue;
- const text = pane.tagSlice()[@min(at, pane.tag_tail_len)..];
- // One column short of the edge: the caret sits AFTER
- // the last character the moment a prompt opens, and
- // flush right would put it at r.x + r.w -- off the
- // pane, off the grid, and past what the detached wire
- // will encode, which drops every frame while it is up.
- const left = printRight(s, tx, row, tw -| 1, text, msg_style);
- if (id != p.active) continue;
- const prompt0 = (p.tagPrefix(pane) catch continue).len + at;
- const col = @as(usize, pane.tag_col);
- if (col >= prompt0) {
- // The cursor follows the text to wherever it landed.
- const prompt_col = panes.File.displayWidth(text[0..@min(col - prompt0, text.len)]);
- if (prompt_col < tw)
- s.cursor = .{ .x = left + @as(u16, @intCast(prompt_col)), .y = row, .bar = pane.mode == .insert };
- }
+ break :blk pane.tagSlice()[@min(at, pane.tag_tail_len)..];
},
- }
+ };
+ const chip = p.noticeCols(text, tw);
+ if (chip == 0) continue;
+ const cx = tx + tw - chip;
+ pane.notices.left[i] = cx;
+ s.fill(cx, row, chip, 1, .{ .bg = msg_style.bg, .font_role = .tagline });
+ // One column short of the chip's right edge: a prompt's caret
+ // sits AFTER its last character the moment it opens, and flush
+ // right would put it past the pane, off the grid, and past what
+ // the detached wire will encode -- which drops every frame for
+ // as long as the prompt is up.
+ const left = printRight(s, cx, row, chip -| 1, text, msg_style);
+ if (kind != .prompt or id != p.active) continue;
+ const at = pane.promptAt() orelse continue;
+ const prompt0 = (p.tagPrefix(pane) catch continue).len + at;
+ const col = @as(usize, pane.tag_col);
+ if (col < prompt0) continue;
+ // The cursor follows the text to wherever it landed.
+ const prompt_col = panes.File.displayWidth(text[0..@min(col - prompt0, text.len)]);
+ if (left + prompt_col < tx + tw)
+ s.cursor = .{ .x = left + @as(u16, @intCast(prompt_col)), .y = row, .bar = pane.mode == .insert };
}
}
@@ -13829,11 +13853,11 @@ pub const Pardes = struct {
const raw = pane.mode == .tty;
const tx = r.x + config.GUTTER;
const width = r.w -| config.GUTTER;
- const body_y = p.bodyTop(pane, r);
+ const body_y = p.bodyTop(r);
var lines = std.mem.splitScalar(u8, body, '\n');
var visible: i32 = 0;
while (lines.next()) |line| : (visible += 1) {
- if (visible >= (r.h -| BOX_H) -| pane.body_offset) break;
+ if (visible >= r.h -| BOX_H) break;
const source_row = if (raw) panes.Terminal.gridOffset(pane) + visible else pane.wrapAt(visible).line;
for (rows) |row| {
const target = if (row.raw_terminal)
@@ -13871,10 +13895,10 @@ pub const Pardes = struct {
const terminal_lines = if (pane.file == null) panes.Terminal.cursorLines(p, pane) catch return else &.{};
const tx = r.x + config.GUTTER;
const width = r.w -| config.GUTTER;
- const body_y = p.bodyTop(pane, r);
+ const body_y = p.bodyTop(r);
const prefix: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0;
var visible: i32 = 0;
- while (visible < @as(i32, (r.h -| BOX_H) -| pane.body_offset)) : (visible += 1) {
+ while (visible < @as(i32, r.h -| BOX_H)) : (visible += 1) {
const source = pane.wrapAt(visible);
const line = pointerSourceLine(pane, terminal_lines, source.line);
const start = @min(line.len, @as(usize, @intCast(@max(0, source.at))));
@@ -13919,11 +13943,6 @@ pub const Pardes = struct {
while (row < r.h) : (row += 1) {
if (@as(i32, row) < r0 or @as(i32, row) > r1) continue;
const sy = if (row < BOX_H) tag_y else body_y + row - BOX_H;
- // The body sits `body_offset` rows lower than it used to, so the
- // last rows of Sel space now fall past the pane. Bounding the loop
- // instead would break the caller that paints a tag into a
- // one-row surface of its own, where the offset means nothing.
- if (row >= BOX_H and sy >= r.y + r.h) break;
// File line numbers occupy a gutter only in the body. The tag is
// row zero in Sel space and starts at its real first text cell.
const scroll: i32 = if (row < BOX_H) pane.tag_scroll else 0;
@@ -13972,48 +13991,45 @@ pub const Pardes = struct {
// workspace layer to render; the menu bar carries its commands.
if (p.settings.workspace_tag)
try p.renderHeaderLayer(arena, MAX_PANES + MAX_COLS, .workspace, 0, .{ .x = 0, .y = 0, .w = p.screen_w, .h = TOPBAR_H }, p.topbar(&text_buf), chrome.tag_bg, chrome.tag_fg);
- // Each notice is its own tagline-shaped band at the top of the body.
+ // Each notice is its own tagline-shaped chip over the top of the body.
// Going through the same layer machinery the pane and column tags use
// is what gives it the tagline's height, its small-font metrics, the
// band offset and the border -- none of which a body-grid row can have
- // just by wearing a tagline font.
+ // just by wearing a tagline font. The geometry is the one the grid
+ // pass already chose and recorded, so the two cannot disagree and the
+ // GUI's "skip the cells a tag layer covers" leaves no stub behind.
for (p.panes, 0..) |slot, id| {
const pane = slot orelse continue;
const r = p.rects[id];
if (r.w <= TAG_TEXT_INSET or r.h <= BOX_H) continue;
- const first = if (p.settings.tag_bottom) r.y else r.y + BOX_H;
- var band: u16 = 0;
- for (pane.notices.slice()) |kind| {
+ const first = p.bodyTop(r);
+ var leader_buf: [16]u8 = undefined;
+ for (pane.notices.slice(), 0..) |kind, i| {
const text = switch (kind) {
.message => pane.msg[0..pane.msg_len],
- .leader => blk: {
- var ibuf: [16]u8 = @splat(' ');
- @memcpy(ibuf[0..3], "SPC");
- var iw: usize = 3;
- for (p.leader_keys[0..p.leader_n]) |ch| {
- ibuf[iw + 1] = ch;
- iw += 2;
- }
- break :blk try arena.dupe(u8, ibuf[0..iw]);
- },
+ .leader => p.leaderText(&leader_buf),
// A prompt is an editing line with a cursor in it, so it
// stays on the grid where the cursor can sit on a cell.
.prompt => continue,
};
if (text.len == 0) continue;
- const room = p.tagCapacity(r.w -| TAG_TEXT_INSET);
+ const cx = pane.notices.left[i];
+ if (cx == std.math.maxInt(u16) or cx >= r.x + r.w) continue;
+ const chip = r.x + r.w - cx;
+ // Right aligned inside the chip, a blank cell short of its
+ // edge: the same one the grid pass leaves for a prompt caret.
+ const room = p.tagCapacity(chip) -| 1;
const shown = panes.File.displayWidth(text);
const pad = room -| shown;
const line = try arena.alloc(u8, pad + text.len);
@memset(line[0..pad], ' ');
@memcpy(line[pad..], text);
- try p.renderHeaderLayer(arena, NOTICE_LAYER_BASE + id * Pane.Notices.max + band, .notice, @intCast(id), .{
- .x = r.x + TAG_TEXT_INSET,
- .y = first + band,
- .w = r.w -| TAG_TEXT_INSET,
+ try p.renderHeaderLayer(arena, NOTICE_LAYER_BASE + id * Pane.Notices.max + i, .notice, @intCast(id), .{
+ .x = cx,
+ .y = first + @as(u16, @intCast(i)),
+ .w = chip,
.h = BOX_H,
}, line, chrome.tag_bg, chrome.tag_fg);
- band += 1;
}
}
if (p.columnBarHeight() > 0) for (0..p.ncol) |column| {
@@ -14087,7 +14103,6 @@ pub const Pardes = struct {
/// them instead of letting the last one win the row.
fn collectNotices(p: *Pardes, pane: *Pane, r: Rect, id: usize) void {
pane.notices.len = 0;
- pane.body_offset = 0;
if (r.w <= config.GUTTER or r.h <= BOX_H) return;
if (pane.msg_len > 0) pane.notices.push(.message);
if (id == p.active and p.leader_on) pane.notices.push(.leader);
@@ -14105,13 +14120,12 @@ pub const Pardes = struct {
for (0..room) |i| pane.notices.kinds[i] = pane.notices.kinds[i + drop];
pane.notices.len = room;
}
- pane.body_offset = pane.notices.len;
}
fn renderBodyLayer(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize) !void {
if (!p.settings.tree_context_tag_style or pane.context_rows == 0 or pane.file == null or pane.collapsed) return;
if (r.w <= config.GUTTER or r.h <= BOX_H) return;
- const viewport_rows = r.h - BOX_H -| @as(u16, pane.notices.len);
+ const viewport_rows = r.h - BOX_H;
if (viewport_rows == 0) return;
// The canonical fixed grid remains available to terminal clients. The
// separate body layer reserves enough rows for any compact font size.
@@ -14132,9 +14146,6 @@ pub const Pardes = struct {
@memset(temporary_cells, .{});
var temporary: Surface = .{ .cols = r.w, .rows = @intCast(logical_rows + BOX_H), .cells = temporary_cells };
std.mem.swap(Surface, &p.surface, &temporary);
- // This surface carries no notice bands, so the body starts at its top.
- pane.body_offset = 0;
- defer pane.body_offset = pane.notices.len;
const synthetic: Rect = .{ .x = 0, .y = 0, .w = r.w, .h = @intCast(logical_rows + BOX_H) };
p.renderBody(arena, pane, synthetic, id, id == p.active, false) catch |err| {
std.mem.swap(Surface, &p.surface, &temporary);
@@ -14148,11 +14159,7 @@ pub const Pardes = struct {
}
layer.pane = @intCast(id);
layer.serial = pane.serial;
- // Below the notice bands, which are stacked at the TOP of the body
- // now: the rows they took came off this viewport, so its origin has to
- // move with them or the first context header renders under a band.
- const body_top = (if (p.settings.tag_bottom) r.y else r.y + BOX_H) + @as(u16, pane.notices.len);
- layer.viewport = .{ .x = r.x + config.GUTTER, .y = body_top, .w = width, .h = viewport_rows };
+ layer.viewport = .{ .x = r.x + config.GUTTER, .y = p.bodyTop(r), .w = width, .h = viewport_rows };
layer.cols = width;
layer.rows = @intCast(logical_rows);
layer.context_rows = pane.context_rows;
@@ -14174,7 +14181,7 @@ pub const Pardes = struct {
const tx = r.x + config.GUTTER; // text area (tag + body), right of the gutter
const tw = r.w - config.GUTTER;
const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
- const body_y = p.bodyTop(pane, r);
+ const body_y = p.bodyTop(r);
const pane_bg: Color = if (th.bg) |c| .{ .rgb = c } else .default;
// text area resets to terminal-default cells (vaxis clear semantics);
@@ -14214,7 +14221,7 @@ pub const Pardes = struct {
if (pane.hasPdf() and panes.Pdf.draw(p, pane, r, id, tx, tw)) return;
if (pane.image) |*iv| {
- const image_h = (r.h -| BOX_H) -| pane.body_offset;
+ const image_h = r.h -| BOX_H;
panes.Image.draw(p, iv, @intCast(id), pane.serial, tx, body_y, tw, image_h);
// thumbless, but the same one column as the real scrollbar below —
// that is the whole point of drawing it, and like that one it runs
@@ -14235,7 +14242,7 @@ pub const Pardes = struct {
const tx = r.x + @min(TAG_TEXT_INSET, r.w);
const tw = r.w -| TAG_TEXT_INSET;
const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
- const body_y = p.bodyTop(pane, r);
+ const body_y = p.bodyTop(r);
const page_rgb = th.bg orelse th.tag_bg;
// Focus is visible along the tag without changing its live text grid.
const tag_bg = if (active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
@@ -14322,12 +14329,12 @@ pub const Pardes = struct {
// Below the notice bands: they are stacked at the top of the body the
// way tree-sitter context rows are, so the body starts under them
// rather than being painted over by them.
- const body_y = p.bodyTop(pane, r);
+ const body_y = p.bodyTop(r);
const pane_bg: Color = if (th.bg) |c| .{ .rgb = c } else .default;
const page_rgb = th.bg orelse th.tag_bg;
// body: emulator rows, prompt rows hidden outside tty mode
- const body_h = (r.h -| BOX_H) -| pane.body_offset;
- const body_bottom = r.h -| pane.body_offset;
+ const body_h = r.h -| BOX_H;
+ const body_bottom = r.h;
const body_style: CellStyle = .{
.fg = if (th.fg) |c| .{ .rgb = c } else .default,
.bg = pane_bg,
@@ -14820,7 +14827,7 @@ test "a builtin announces itself on the message row, and Verbose silences it" {
try std.testing.expectEqual(@as(u16, 0), pane.msg_len);
}
-test "notices stack downward from the top of the body, which starts below them" {
+test "notices stack over the body's top rows without moving them" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
@@ -14831,44 +14838,45 @@ test "notices stack downward from the top of the body, which starts below them"
defer frame.deinit();
const r = p.rects[id];
- const plain = p.bodyTop(pane, r);
- try std.testing.expectEqual(@as(u16, 0), pane.body_offset);
+ const top = p.bodyTop(r);
- // A message and a leader prefix at once: two bands, stacked in the order
+ // A message and a leader prefix at once: two chips, in the order
// collectNotices chose, each one row tall and each one row further down.
try std.testing.expect(p.executeBuiltinLine(id, "Msg first"));
p.leader_on = true;
p.leader_n = 0;
_ = frame.reset(.retain_capacity);
_ = try p.render(frame.allocator());
- try std.testing.expectEqual(@as(u16, 2), pane.body_offset);
- try std.testing.expectEqual(plain + 2, p.bodyTop(pane, r));
+ try std.testing.expectEqual(@as(u8, 2), pane.notices.len);
+ // An overlay, not a reservation: the body still starts where it did.
+ try std.testing.expectEqual(top, p.bodyTop(r));
- var rows: [Pane.Notices.max]u16 = @splat(0);
- var found: usize = 0;
+ var seen: usize = 0;
for (p.surface.tagLayers()) |*layer| {
if (layer.cols == 0 or layer.kind != .notice) continue;
try std.testing.expectEqual(@as(u16, 1), layer.viewport.h);
- rows[found] = layer.viewport.y;
- found += 1;
+ // Sized to its own message, right up against the pane's right edge --
+ // not the width of the pane.
+ try std.testing.expectEqual(r.x + r.w, layer.viewport.x + layer.viewport.w);
+ try std.testing.expect(layer.viewport.w < r.w - config.GUTTER);
+ try std.testing.expect(layer.viewport.y == top or layer.viewport.y == top + 1);
+ try std.testing.expectEqual(pane.notices.left[layer.viewport.y - top], layer.viewport.x);
+ seen += 1;
}
- try std.testing.expectEqual(@as(usize, 2), found);
- std.mem.sort(u16, rows[0..2], {}, std.sort.asc(u16));
- try std.testing.expectEqual(plain, rows[0]);
- try std.testing.expectEqual(plain + 1, rows[1]);
+ try std.testing.expectEqual(@as(usize, 2), seen);
+ // "first" is longer than "SPC", so its chip is wider.
+ try std.testing.expect(pane.notices.left[0] < pane.notices.left[1]);
- // Gone with the notices: no stale band, and the body back at the top.
+ // Gone with the notices: no stale chip left behind.
p.leader_on = false;
pane.msg_len = 0;
_ = frame.reset(.retain_capacity);
_ = try p.render(frame.allocator());
- try std.testing.expectEqual(@as(u16, 0), pane.body_offset);
- try std.testing.expectEqual(plain, p.bodyTop(pane, r));
for (p.surface.tagLayers()) |*layer|
try std.testing.expect(layer.cols == 0 or layer.kind != .notice);
}
-test "a prompt band keeps its caret on the grid, and a band is not a click target" {
+test "a prompt chip keeps its caret on the grid, and only the chip swallows a click" {
if (platform == .web) return;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 80, .rows = 24 });
defer p.deinit();
@@ -14879,24 +14887,36 @@ test "a prompt band keeps its caret on the grid, and a band is not a click targe
defer frame.deinit();
// The caret sits AFTER the prompt text the moment the prompt opens, and
- // the band is flush with the pane's right edge -- so this is the frame
- // that used to put it one column past the pane, off the grid and past
- // what the detached wire will encode.
+ // the chip ends at the pane's right edge -- so this is the frame that
+ // used to put it one column past the pane, off the grid and past what the
+ // detached wire will encode.
p.startPrompt(pane, .{ .search = "/" });
_ = try p.render(frame.allocator());
const r = p.rects[id];
const cursor = p.surface.cursor orelse return error.MissingPromptCursor;
- try std.testing.expect(pane.body_offset > 0);
- try std.testing.expectEqual(p.bodyTop(pane, r) - 1, cursor.y);
+ const row = p.bodyTop(r);
+ try std.testing.expectEqual(@as(u8, 1), pane.notices.len);
+ try std.testing.expectEqual(row, cursor.y);
try std.testing.expect(cursor.x < r.x + r.w);
try std.testing.expect(cursor.x < p.surface.cols);
- // The band is chrome. Mapped as body it would land on Sel row 0, which is
- // the TAG row, so a click on it would run a word out of the tag as a
- // builtin.
- const band_row = p.bodyTop(pane, r) - 1;
- p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = r.x + r.w - 4, .row = band_row } });
- p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = r.x + r.w - 4, .row = band_row } });
+ // The chip covers only its own columns. Left of it is ordinary body, and
+ // a sweep there still selects.
+ const left = pane.notices.left[0];
+ try std.testing.expect(left > r.x + config.GUTTER + 2);
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = left - 3, .row = row } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = left - 1, .row = row } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = left - 1, .row = row } });
+ try std.testing.expect(pane.sel[0].state != .none);
+ try std.testing.expect(pane.sel[0].r0 >= BOX_H); // the body, not the tag
+
+ // Inside it is chrome: mapped as body it would land on Sel row 0, which is
+ // the TAG row, so a sweep there would highlight the tagline instead.
+ pane.clearPointerSelection(0);
+ pane.sel[0] = .{};
+ p.update(.{ .mouse = .{ .button = .left, .kind = .press, .col = r.x + r.w - 3, .row = row } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .drag, .col = r.x + r.w - 2, .row = row } });
+ p.update(.{ .mouse = .{ .button = .left, .kind = .release, .col = r.x + r.w - 2, .row = row } });
for (pane.sel) |selection| try std.testing.expect(selection.state == .none);
}