summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig237
1 files changed, 143 insertions, 94 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 60c41f50..72a282c2 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -249,8 +249,13 @@ pub const TagLayer = @import("tag_layer.zig").Layer;
pub const TagHit = @import("tag_layer.zig").Hit;
pub const TagKind = @import("tag_layer.zig").Kind;
/// Panes, then columns, then the workspace bar, then each pane's notice bands.
-pub const MAX_TAG_LAYERS = MAX_PANES + MAX_COLS + 1 + MAX_PANES * Pane.Notices.max;
+/// A tag shows at most this many lines; past them it is cut, like a line
+/// too long for its width.
+pub const MAX_TAG_ROWS = 8;
+pub const MAX_TAG_LAYERS = MAX_PANES + MAX_COLS + 1 + MAX_PANES * Pane.Notices.max + MAX_PANES * (MAX_TAG_ROWS - 1);
pub const NOTICE_LAYER_BASE = MAX_PANES + MAX_COLS + 1;
+/// Where the layers for a pane tag's second and later lines start.
+pub const TAG_LINE_LAYER_BASE = NOTICE_LAYER_BASE + MAX_PANES * Pane.Notices.max;
pub const PDF_PAGE_GAP_PX = panes.Pdf.page_gap_px;
pub const MAX_COLS = 6;
const column_weight_unit = layout.column_weight_unit;
@@ -285,16 +290,17 @@ pub fn taglineOriginCol(p: *const Pardes, col: u16, row: u16, track: ?layout.Tra
}
if (track) |active| {
const box = active.contentBox();
- const tag_y = if (p.settings.tag_bottom) box.y + box.h - @as(f32, @floatFromInt(BOX_H)) else box.y;
+ const rows: f32 = @floatFromInt(if (active.pane < MAX_PANES) if (p.panes[active.pane]) |pane| pane.tag_rows else BOX_H else BOX_H);
+ const tag_y = if (p.settings.tag_bottom) box.y + box.h - rows else box.y;
if (@as(f32, @floatFromInt(row)) >= tag_y and
- @as(f32, @floatFromInt(row)) < tag_y + @as(f32, @floatFromInt(BOX_H)))
+ @as(f32, @floatFromInt(row)) < tag_y + rows)
return box.x;
}
for (p.panes, 0..) |slot, id| {
- if (slot == null) continue;
+ const pane = slot orelse continue;
const r = p.rects[id];
- const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
- if (row == tag_y and col >= r.x and col < r.x + r.w) return @floatFromInt(r.x);
+ const tag_y = p.tagTop(pane, r);
+ if (row >= tag_y and row < tag_y + pane.tag_rows and col >= r.x and col < r.x + r.w) return @floatFromInt(r.x);
}
return @floatFromInt(col);
}
@@ -352,10 +358,10 @@ pub fn gridColAt(p: ?*const Pardes, x: f32, row: u16, body_w: f32, tagline_w: f3
}
};
if (p) |core| for (core.panes, 0..) |slot, id| {
- if (slot == null) continue;
+ const pane = slot orelse continue;
const r = core.rects[id];
- const tag_y = if (core.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
- if (row != tag_y or r.w == 0) continue;
+ const tag_y = core.tagTop(pane, r);
+ if (row < tag_y or row >= tag_y + pane.tag_rows or r.w == 0) continue;
const left = @as(f32, @floatFromInt(r.x)) * body;
const right = @as(f32, @floatFromInt(r.x + r.w)) * body;
if (x < left or x >= right) continue;
@@ -813,7 +819,7 @@ test "TreeContext tag style preserves message rows and its opt-out survives rest
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(p.bodyTop(r), band.viewport.y);
+ try std.testing.expectEqual(p.bodyTop(pane, 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.
@@ -829,7 +835,7 @@ test "TreeContext tag style preserves message rows and its opt-out survives rest
_ = 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(r)).style.font_role);
+ try std.testing.expectEqual(FontRole.body, p.surface.at(r.x + config.GUTTER, p.bodyTop(pane, r)).style.font_role);
try dump.dumpState(p);
const restored = try dump.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
defer restored.deinit();
@@ -4915,15 +4921,14 @@ pub const Pardes = struct {
pane.focus = .tag;
}
- /// Where display column `col` of the tagline (tag scroll included) lands
- /// in the tag's own text, or null when it is on the prefix.
- pub fn tagOffsetAt(p: *Pardes, pane: *Pane, col: usize) ?usize {
- const prefix = p.tagPrefix(pane) catch return null;
- const width = panes.File.displayWidth(prefix);
+ /// Where display column `col` of tag line `row` (tag scroll included)
+ /// lands in the tag's own text, or null when it is on the prefix, which
+ /// only the first line has.
+ pub fn tagOffsetAt(p: *Pardes, pane: *Pane, row: usize, col: usize) ?modal.Cursor {
+ const width = if (row == 0) panes.File.displayWidth(p.tagPrefix(pane) catch return null) else 0;
if (col < width) return null;
- const tail = curTail(pane);
- const line = tail[0 .. std.mem.indexOfScalar(u8, tail, '\n') orelse tail.len];
- return modal.graphemeStart(line, panes.File.rawAtDisplay(line, col - width));
+ const line = modal.lineSlice(curTail(pane), row);
+ return .{ .row = row, .col = modal.graphemeStart(line, panes.File.rawAtDisplay(line, col - width)) };
}
/// Start a name draft for a file that can take one, its caret at display
@@ -5822,7 +5827,7 @@ pub const Pardes = struct {
}
// A sweep over the tag is the tag's while it has the keyboard.
const swept = pane.sel[sel_slot];
- if (pane.focus == .body and swept.state == .done and @min(swept.r0, swept.r1) < BOX_H) {
+ if (pane.focus == .body and swept.state == .done and @min(swept.r0, swept.r1) < pane.tag_rows) {
pane.clearPointerSelection(sel_slot);
pane.sel[sel_slot].state = .none;
}
@@ -5836,9 +5841,14 @@ pub const Pardes = struct {
state.reply_len = 0;
};
const r = p.rects[id];
+ // The tag takes a row per line, leaving the body one row if it
+ // had any; a folded pane shows its first line only.
+ const lines = std.mem.count(u8, curTail(pane), "\n") + 1;
+ pane.tag_rows = @intCast(@min(lines, MAX_TAG_ROWS, @max(BOX_H, r.h -| 1)));
+ if (pane.collapsed) pane.tag_rows = BOX_H;
const cols = @max(1, r.w -| config.GUTTER);
- const rows = @max(1, r.h -| BOX_H); // the tag steals the top row
- if ((cols != pane.cols or rows != pane.rows) and r.h > BOX_H) {
+ const rows = @max(1, r.h -| pane.tag_rows); // the tag steals the top rows
+ if ((cols != pane.cols or rows != pane.rows) and r.h > pane.tag_rows) {
// doc panes have no pty/emulator grid to reflow; just record
// the size so bodyText renders the right number of rows
if (pane.isTerminal()) {
@@ -5905,7 +5915,8 @@ pub const Pardes = struct {
if (pane.serial != layer.serial) return null;
}
if (layer.kind == .column and (layer.id >= p.ncol or p.col_serial[layer.id] != layer.serial)) return null;
- return layer.hitAt(tag_hit.pixel_x, tag_hit.pixel_y, @floatFromInt(tag_hit.metrics.body_w), @floatFromInt(tag_hit.metrics.body_h), @floatFromInt(tag_hit.metrics.tagline_w), @floatFromInt(tag_hit.metrics.tagline_h));
+ // A taller tag has a layer per line; the point is on one of them.
+ return layer.hitAt(tag_hit.pixel_x, tag_hit.pixel_y, @floatFromInt(tag_hit.metrics.body_w), @floatFromInt(tag_hit.metrics.body_h), @floatFromInt(tag_hit.metrics.tagline_w), @floatFromInt(tag_hit.metrics.tagline_h)) orelse continue;
}
return null;
}
@@ -5913,8 +5924,13 @@ pub const Pardes = struct {
/// 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;
+ pub fn bodyTop(p: *const Pardes, pane: *const Pane, r: Rect) u16 {
+ return if (p.settings.tag_bottom) r.y else r.y + pane.tag_rows;
+ }
+
+ /// The tag's first row: the pane's top, or its last rows with Tagbottom.
+ pub fn tagTop(p: *const Pardes, pane: *const Pane, r: Rect) u16 {
+ return if (p.settings.tag_bottom) r.y + r.h -| pane.tag_rows else r.y;
}
fn tagCapacity(p: *const Pardes, physical_width: u16) u16 {
@@ -6005,7 +6021,7 @@ pub const Pardes = struct {
// 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);
+ const first = p.bodyTop(pane, r);
var leader_buf: [16]u8 = undefined;
for (pane.notices.slice(), 0..) |kind, i| {
const row = first + @as(u16, pane.notices.row[i]);
@@ -6385,26 +6401,41 @@ pub const Pardes = struct {
const r = p.rects[id];
if (r.w <= TAG_TEXT_INSET or r.h == 0) continue;
const tag = try p.tagText(arena, pane);
- const columns: u16 = @intCast(@min(std.math.maxInt(u16) - TAG_TEXT_INSET, @max(p.tagCapacity(r.w - TAG_TEXT_INSET), panes.File.displayWidth(tag) + 1)));
- const layer = &p.surface.tag_layers[id];
- if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns);
- const storage = try arena.alloc(Cell, @as(usize, columns) + TAG_TEXT_INSET);
+ const rows = pane.tag_rows;
+ // As wide as the widest line it shows, and never narrower than
+ // its viewport.
+ var widest: usize = 0;
+ var split = std.mem.splitScalar(u8, tag, '\n');
+ for (0..rows) |_| widest = @max(widest, panes.File.displayWidth(split.next() orelse ""));
+ const columns: u16 = @intCast(@min(std.math.maxInt(u16) - TAG_TEXT_INSET, @max(p.tagCapacity(r.w - TAG_TEXT_INSET), widest + 1)));
+ const pitch = @as(usize, columns) + TAG_TEXT_INSET;
+ const storage = try arena.alloc(Cell, pitch * rows);
@memset(storage, .{});
- var temporary: Surface = .{ .cols = columns + TAG_TEXT_INSET, .rows = 1, .cells = storage };
+ var temporary: Surface = .{ .cols = columns + TAG_TEXT_INSET, .rows = rows, .cells = storage };
std.mem.swap(Surface, &p.surface, &temporary);
- p.paintPaneTag(arena, pane, .{ .x = 0, .y = 0, .w = columns + TAG_TEXT_INSET, .h = 1 }, id, id == p.active, p.tagCapacity(r.w - TAG_TEXT_INSET)) catch |err| {
+ p.paintPaneTag(arena, pane, .{ .x = 0, .y = 0, .w = columns + TAG_TEXT_INSET, .h = rows }, id, id == p.active, p.tagCapacity(r.w - TAG_TEXT_INSET)) catch |err| {
std.mem.swap(Surface, &p.surface, &temporary);
return err;
};
std.mem.swap(Surface, &p.surface, &temporary);
- @memcpy(layer.cells, storage[TAG_TEXT_INSET..]);
- layer.kind = .pane;
- layer.id = @intCast(id);
- layer.serial = pane.serial;
- layer.viewport = .{ .x = r.x + TAG_TEXT_INSET, .y = if (p.settings.tag_bottom) r.y + r.h - BOX_H else r.y, .w = r.w - TAG_TEXT_INSET, .h = BOX_H };
- layer.cols = columns;
- layer.bg = if (id == p.active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
- layer.cursor = if (!p.header_focus and temporary.cursor != null) .{ .col = temporary.cursor.?.x - TAG_TEXT_INSET, .bar = temporary.cursor.?.bar } else null;
+ // A layer to a line: the first in the pane's own slot, the rest
+ // in the slots kept for the lines of taller tags.
+ for (0..rows) |line| {
+ const layer = &p.surface.tag_layers[if (line == 0) id else TAG_LINE_LAYER_BASE + id * (MAX_TAG_ROWS - 1) + line - 1];
+ if (layer.cells.len != columns) layer.cells = try p.gpa.realloc(layer.cells, columns);
+ @memcpy(layer.cells, storage[line * pitch + TAG_TEXT_INSET ..][0..columns]);
+ layer.kind = .pane;
+ layer.id = @intCast(id);
+ layer.serial = pane.serial;
+ layer.line = @intCast(line);
+ layer.viewport = .{ .x = r.x + TAG_TEXT_INSET, .y = p.tagTop(pane, r) + @as(u16, @intCast(line)), .w = r.w - TAG_TEXT_INSET, .h = BOX_H };
+ layer.cols = columns;
+ layer.bg = if (id == p.active and p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg;
+ layer.cursor = if (p.header_focus) null else if (temporary.cursor) |cursor|
+ (if (cursor.y == line) .{ .col = cursor.x - TAG_TEXT_INSET, .bar = cursor.bar } else null)
+ else
+ null;
+ }
}
// With the workspace tag row handed to native chrome there is no
// workspace layer to render; the menu bar carries its commands.
@@ -6422,8 +6453,8 @@ pub const Pardes = struct {
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 = p.bodyTop(r);
+ if (r.w <= TAG_TEXT_INSET or r.h <= pane.tag_rows) continue;
+ const first = p.bodyTop(pane, r);
var leader_buf: [16]u8 = undefined;
for (pane.notices.slice(), 0..) |kind, i| {
const text = Messages.noticeText(p, pane, i, &leader_buf);
@@ -6556,8 +6587,8 @@ pub const Pardes = struct {
const chrome = p.chromeTheme();
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(r);
+ const tag_y = p.tagTop(pane, r);
+ const body_y = p.bodyTop(pane, r);
const pane_bg: Color = if (th.bg) |c| .{ .rgb = c } else .default;
// text area resets to terminal-default cells (vaxis clear semantics);
@@ -6579,7 +6610,7 @@ pub const Pardes = struct {
// Grips keep physical grid dimensions, independently of tag text.
.font_role = .tagline,
};
- s.fill(r.x, tag_y, config.GUTTER, BOX_H, box_style);
+ s.fill(r.x, tag_y, config.GUTTER, pane.tag_rows, box_style);
s.set(r.x, tag_y, switch (pane.body.mode) {
.normal => config.tag_normal,
.insert => config.tag_insert,
@@ -6600,14 +6631,13 @@ pub const Pardes = struct {
// GUI's image pass, kitty's z-order), so a chip over it would be
// painted out. Text gets the overlay; a picture gives up the rows.
const shown = pane.notices.span();
- const image_h = (r.h -| BOX_H) -| shown;
+ const image_h = (r.h -| pane.tag_rows) -| shown;
panes.Image.draw(p, iv, @intCast(id), pane.serial, tx, body_y + shown, 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
// past the notice bands so the gutter has no notch in it
- const rail_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H;
- s.fill(r.x, rail_y, 1, r.h -| BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
- s.fill(r.x + 1, rail_y, 1, r.h -| BOX_H, .{ .bg = pane_bg });
+ s.fill(r.x, body_y, 1, r.h -| pane.tag_rows, .{ .bg = .{ .rgb = chrome.scroll_track } });
+ s.fill(r.x + 1, body_y, 1, r.h -| pane.tag_rows, .{ .bg = pane_bg });
return;
}
@@ -6620,8 +6650,9 @@ pub const Pardes = struct {
const chrome = p.chromeTheme();
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(r);
+ const tag_y = p.tagTop(pane, r);
+ const body_y = p.bodyTop(pane, r);
+ const rows = pane.tag_rows;
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;
@@ -6629,44 +6660,53 @@ pub const Pardes = struct {
const tag_style: CellStyle = .{ .fg = .{ .rgb = tag_fg }, .bg = .{ .rgb = tag_bg }, .font_role = .tagline };
// Padding belongs to the same font row but keeps its default ink: a
// role must not manufacture a foreground color for blank cells.
- s.fill(r.x + config.GUTTER, tag_y, @min(TAG_GAP, r.w -| config.GUTTER), BOX_H, .{ .bg = .{ .rgb = tag_bg }, .font_role = .tagline });
- s.fill(tx, tag_y, tw, BOX_H, .{
+ s.fill(r.x + config.GUTTER, tag_y, @min(TAG_GAP, r.w -| config.GUTTER), rows, .{ .bg = .{ .rgb = tag_bg }, .font_role = .tagline });
+ s.fill(tx, tag_y, tw, rows, .{
.bg = .{ .rgb = tag_bg },
.font_role = .tagline,
});
const whole = try p.tagText(arena, pane);
const tail = curTail(pane);
const prefix_len = whole.len - tail.len;
- // The tagline shows the tag's first line: its prefix and the first
- // line of its own text.
- const tag = whole[0 .. std.mem.indexOfScalar(u8, whole, '\n') orelse whole.len];
- // The keyboard's caret on it, as a byte offset: in its own text when
- // the tag has the keyboard, in the path while a name is drafted.
- const caret_at: ?usize = if (!active)
+ // A row per line: the first carries the prefix, the rest are the
+ // tag's own text alone.
+ var lines: [MAX_TAG_ROWS][]const u8 = @splat("");
+ var split = std.mem.splitScalar(u8, whole, '\n');
+ for (lines[0..rows]) |*line| line.* = split.next() orelse "";
+ // The keyboard's caret, as a row and a byte offset in that row's
+ // line: in the tag's own text when the tag has the keyboard, in the
+ // path while a name is drafted.
+ const Caret = struct { row: usize, at: usize };
+ const caret: ?Caret = if (!active)
null
else if (pane.prompt == .name)
- @min(tag.len, @as(usize, @intCast(@max(0, pane.input.cur_col))))
- else if (pane.focus == .tag and pane.tag.cur_row == 0)
- @min(tag.len, prefix_len + @as(usize, @intCast(@max(0, pane.tag.cur_col))))
- else
- null;
- if (caret_at) |at| {
- const caret = panes.File.rawDisplayCol(tag, at);
+ .{ .row = 0, .at = @min(lines[0].len, @as(usize, @intCast(@max(0, pane.input.cur_col)))) }
+ else if (pane.focus == .tag and pane.tag.cur_row < rows) caret: {
+ const row: usize = @intCast(@max(0, pane.tag.cur_row));
+ const lead: usize = if (row == 0) prefix_len else 0;
+ break :caret .{ .row = row, .at = @min(lines[row].len, lead + @as(usize, @intCast(@max(0, pane.tag.cur_col)))) };
+ } else null;
+ if (caret) |at| {
+ const line = lines[at.row];
+ const col = panes.File.rawDisplayCol(line, at.at);
var scroll: usize = pane.tag_scroll;
- if (caret < scroll) scroll = caret;
- if (caret >= scroll + capacity) scroll = caret - capacity + 1;
- var raw = panes.File.rawAtDisplay(tag, scroll);
- scroll = panes.File.rawDisplayCol(tag, raw);
- if (caret >= scroll + capacity) {
- raw = modal.nextGrapheme(tag, raw);
- scroll = panes.File.rawDisplayCol(tag, raw);
+ if (col < scroll) scroll = col;
+ if (col >= scroll + capacity) scroll = col - capacity + 1;
+ var raw = panes.File.rawAtDisplay(line, scroll);
+ scroll = panes.File.rawDisplayCol(line, raw);
+ if (col >= scroll + capacity) {
+ raw = modal.nextGrapheme(line, raw);
+ scroll = panes.File.rawDisplayCol(line, raw);
}
pane.tag_scroll = @intCast(scroll);
} else if (pane.focus != .tag) pane.tag_scroll = 0;
- const tag_start = panes.File.rawAtDisplay(tag, pane.tag_scroll);
- _ = s.print(tx, tag_y, tw, tag[tag_start..], tag_style);
+ for (lines[0..rows], 0..) |line, row| {
+ const start = panes.File.rawAtDisplay(line, pane.tag_scroll);
+ _ = s.print(tx, tag_y + @as(u16, @intCast(row)), tw, line[start..], tag_style);
+ }
+ const tag = lines[0];
if (tagTintRange(pane, tag, prefix_len)) |name| {
- const lo = @max(name.lo, tag_start);
+ const lo = @max(name.lo, panes.File.rawAtDisplay(tag, pane.tag_scroll));
const hi = @min(name.hi, tag.len);
if (lo < hi) {
var col = panes.File.rawDisplayCol(tag, lo) -| pane.tag_scroll;
@@ -6677,41 +6717,50 @@ pub const Pardes = struct {
}
}
if (p.look_hover_preview) |preview| {
- if (preview.pane == id and preview.serial == pane.serial) if (preview.sel) |sel| if (@min(sel.r0, sel.r1) < BOX_H) {
+ if (preview.pane == id and preview.serial == pane.serial) if (preview.sel) |sel| if (@min(sel.r0, sel.r1) < rows) {
const preview_bg = colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg));
body_layer.paintPointerSelection(p, pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
};
}
- // The tag's own selections, while it has the keyboard. A range of one
- // grapheme is the block cursor, not a selection.
+ // The tag's own selections, while it has the keyboard, a line at a
+ // time. A range of one grapheme is the block cursor, not a selection.
if (pane.focus == .tag) {
var ranges: [Text.max_selections]modal.Selection = undefined;
const got = pane.tag.ranges(tail, 0, &ranges);
- const shown = tag.len - prefix_len;
for (ranges[0..got.n]) |range| {
const lo = @min(range.anchor, range.head);
const hi = @max(range.anchor, range.head);
- if (modal.nextGrapheme(tail, lo) >= hi or lo >= shown) continue;
- var col = panes.File.rawDisplayCol(tag, prefix_len + lo) -| pane.tag_scroll;
- const end = panes.File.rawDisplayCol(tag, prefix_len + @min(hi, shown));
- while (col + pane.tag_scroll < end and col < tw) : (col += 1) {
- const cell = s.at(tx + @as(u16, @intCast(col)), tag_y);
- cell.default = false;
- cell.style.bg = .{ .rgb = th.sel_bg };
- cell.style.fg = .{ .rgb = th.sel_fg };
+ if (modal.nextGrapheme(tail, lo) >= hi) continue;
+ const first = modal.positionAt(tail, lo);
+ const last = modal.positionAt(tail, hi);
+ var row = first.row;
+ while (row <= last.row and row < rows) : (row += 1) {
+ const line = lines[row];
+ const lead: usize = if (row == 0) prefix_len else 0;
+ const from = lead + if (row == first.row) first.col else 0;
+ const to = @min(line.len, lead + if (row == last.row) last.col else modal.lineSlice(tail, row).len);
+ var col = panes.File.rawDisplayCol(line, from) -| pane.tag_scroll;
+ const end = panes.File.rawDisplayCol(line, to);
+ const y = tag_y + @as(u16, @intCast(row));
+ while (col + pane.tag_scroll < end and col < tw) : (col += 1) {
+ const cell = s.at(tx + @as(u16, @intCast(col)), y);
+ cell.default = false;
+ cell.style.bg = .{ .rgb = th.sel_bg };
+ cell.style.fg = .{ .rgb = th.sel_fg };
+ }
}
}
}
// the caret: bar while typing, block in normal mode (same rule as a body)
- if (caret_at) |at| {
- const col = panes.File.rawDisplayCol(tag, at) -| pane.tag_scroll;
+ if (caret) |at| {
+ const col = panes.File.rawDisplayCol(lines[at.row], at.at) -| pane.tag_scroll;
const mode = if (pane.prompt == .name) pane.input.mode else pane.tag.mode;
- if (col < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(col)), .y = tag_y, .bar = mode == .insert };
+ if (col < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(col)), .y = tag_y + @as(u16, @intCast(at.row)), .bar = mode == .insert };
}
for (pane.sel, 0..) |selection, button| {
if (selection.state == .none or (selection.state == .done and button != 0)) continue;
- if (@min(selection.r0, selection.r1) >= BOX_H) continue;
+ if (@min(selection.r0, selection.r1) >= rows) continue;
body_layer.paintPointerSelection(p, pane, r, tx, tw, tag_y, body_y, selection, th.sel_bg, th.sel_fg);
}
}
@@ -6945,7 +6994,7 @@ test "a prompt chip is a band with its caret, still on the grid, and only the ch
_ = try p.render(frame.allocator());
const r = p.rects[id];
const cursor = p.surface.cursor orelse return error.MissingPromptCursor;
- const row = p.bodyTop(r);
+ const row = p.bodyTop(pane, 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);
@@ -7010,7 +7059,7 @@ test "a prompt is typed on its own line in the notice band and leaves the tag ro
try std.testing.expectEqualStrings("t", band.cells[caret.col - 1].grapheme());
try std.testing.expectEqualStrings("w", band.cells[caret.col].grapheme());
const grid = p.surface.cursor orelse return error.MissingPromptCursor;
- try std.testing.expectEqual(p.bodyTop(p.rects[0]), grid.y);
+ try std.testing.expectEqual(p.bodyTop(pane, p.rects[0]), grid.y);
// Submitting searches for what was typed and gives the prompt its line back.
p.update(.{ .key = .{ .cp = Key.enter } });