diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-16 15:49:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:44:42 -0300 |
| commit | 1551e409c31992437cb2fa864f576d45c8433801 (patch) | |
| tree | e2fae8451f87b735a1360c7c2e383fdc40165789 /src/file_pane.zig | |
| parent | be2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff) | |
| download | pardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz pardes-1551e409c31992437cb2fa864f576d45c8433801.zip | |
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'src/file_pane.zig')
| -rw-r--r-- | src/file_pane.zig | 388 |
1 files changed, 371 insertions, 17 deletions
diff --git a/src/file_pane.zig b/src/file_pane.zig index b0921bd0..cf2b9571 100644 --- a/src/file_pane.zig +++ b/src/file_pane.zig @@ -1,4 +1,4 @@ -//! File panes: everything a Pane does BECAUSE it has `file: ?File` set — the +//! File panes: everything a Pane does BECAUSE it has `file: ?State` set — the //! disk read, the content swap undo/redo commits through, the tree-sitter //! highlight window, and the two render passes only a file has (the line //! number gutter and the syntax recolor). The rest of a file pane's behaviour @@ -8,14 +8,79 @@ const pardes = @import("pardes.zig"); const config = @import("config.zig"); const Pardes = pardes.Pardes; const Pane = pardes.Pane; -const File = pardes.File; -const FileSnap = pardes.FileSnap; const modal = @import("modal.zig"); const look = @import("look.zig"); +const output_pane = @import("output_pane.zig"); const syntax = @import("syntax.zig"); const tracy = @import("tracy.zig"); +const dump = @import("dump.zig"); const SYNTAX_CONTEXT_AFTER_ROWS: usize = 2; +const undo_max = 256; + +/// Content and primary selection at one file edit boundary. Keeping only the +/// primary avoids putting pardes.MAX_SELS ranges in every history entry. +pub const Snapshot = struct { + content: []u8, + cur_row: i32, + cur_col: i32, + vsel: pardes.CharSel, +}; + +/// A file pane's backing: owned content, its derived caches, and undo history. +pub const State = struct { + path: []u8, + content: []u8, + /// Monotonic content identity for asynchronous edits. Every content swap + /// goes through setContent, which bumps this; a pipe completion accepted + /// against another revision would overwrite intervening work. + revision: u32 = 0, + /// Revision last handed to Save. Equal means the screen matches disk. + saved_revision: u32 = 0, + /// Non-null for a generated output buffer rather than an on-disk file. + output: ?output_pane.Output = null, + scroll: usize = 0, + /// `line_starts[i]` is line i's byte offset. Empty means not built yet. + line_starts: []usize = &.{}, + /// One tree_sitter_gpa-owned syntax.Syn byte per highlighted source byte. + highlights: []u8 = &.{}, + highlight_start: usize = 0, + syntax_dirty: bool = true, + undo: [undo_max]Snapshot = undefined, + undo_len: usize = 0, + redo: [undo_max]Snapshot = undefined, + redo_len: usize = 0, +}; + +/// Serialize file-owned bytes and identity; output origin vocabulary is +/// supplied by output_pane at the core dispatch edge. +pub fn dumpPane( + arena: std.mem.Allocator, + pane: *const Pane, + file: *const State, + tag: []const u8, + body: []const u8, + scroll: usize, + origin: []const u8, + origin_arg: []const u8, +) !dump.Pane { + return .{ + .kind = .file, + .tag = tag, + .body = body, + .scroll = scroll, + .cols = pane.cols, + .rows = pane.rows, + .vweight = pane.vweight, + .file = .{ + .path = file.path, + .content = file.content, + .content_b64 = try dump.encodeBytes(arena, file.content), + .origin = origin, + .origin_arg = origin_arg, + }, + }; +} pub fn byteDisplayWidth(byte: u8) usize { return if (byte == '\t') config.tab_width else 1; @@ -39,6 +104,67 @@ pub fn byteAtDisplay(text: []const u8, display_col: usize) usize { return text.len; } +/// File cursor columns may live past EOL. Tabs expand before that boundary; +/// every virtual column after it remains one screen cell. +pub fn rawDisplayCol(line_text: []const u8, raw_col: usize) usize { + const bounded = @min(raw_col, line_text.len); + return displayWidth(line_text[0..bounded]) +| (raw_col - bounded); +} + +pub fn rawAtDisplay(line_text: []const u8, display_col: usize) usize { + const width = displayWidth(line_text); + if (display_col > width) return line_text.len +| (display_col - width); + return byteAtDisplay(line_text, display_col); +} + +pub fn sourceLine(pane: *const Pane, row: i32) []const u8 { + const f = pane.file orelse return ""; + if (row < 0) return ""; + return modal.lineSlice(f.content, @intCast(row)); +} + +pub fn displayOffset(pane: *const Pane, row: i32, from_raw: i32, to_raw: i32) i32 { + const line_text = sourceLine(pane, row); + const from: usize = @intCast(@max(0, from_raw)); + const to: usize = @intCast(@max(0, to_raw)); + const from_display = rawDisplayCol(line_text, from); + const to_display = rawDisplayCol(line_text, to); + if (to_display >= from_display) return @intCast(to_display - from_display); + return -@as(i32, @intCast(from_display - to_display)); +} + +pub fn displayEndOffset(pane: *const Pane, row: i32, from_raw: i32, at_raw: i32) i32 { + const start = displayOffset(pane, row, from_raw, at_raw); + const line_text = sourceLine(pane, row); + const at: usize = @intCast(@max(0, at_raw)); + if (at >= line_text.len) return start; + return start + @as(i32, @intCast(byteDisplayWidth(line_text[at]))) - 1; +} + +pub fn byteAtRowDisplay(pane: *const Pane, row: i32, from_raw: i32, display_col: i32) i32 { + const line_text = sourceLine(pane, row); + const from: usize = @intCast(@max(0, from_raw)); + const display: usize = @intCast(@max(0, display_col)); + if (from >= line_text.len) return @intCast(from +| display); + return @intCast(from +| rawAtDisplay(line_text[from..], display)); +} + +/// Convert between rendered pane rows (whose file body includes PREFIX_W) +/// and source-byte columns. Non-file rows are already in byte coordinates. +pub fn renderedLineByteCol(pane: *const Pane, row: i32, line_text: []const u8, display_col: usize) usize { + if (pane.file == null or row < pardes.BOX_H) return display_col; + const prefix = @min(@as(usize, config.PREFIX_W), line_text.len); + if (display_col <= prefix) return display_col; + return prefix +| rawAtDisplay(line_text[prefix..], display_col - prefix); +} + +pub fn renderedLineDisplayCol(pane: *const Pane, row: i32, line_text: []const u8, byte_col: usize) usize { + if (pane.file == null or row < pardes.BOX_H) return byte_col; + const prefix = @min(@as(usize, config.PREFIX_W), line_text.len); + if (byte_col <= prefix) return byte_col; + return prefix +| rawDisplayCol(line_text[prefix..], byte_col - prefix); +} + fn fitEnd(text: []const u8, start: usize, width: usize) usize { var end = start; var used: usize = 0; @@ -64,7 +190,7 @@ pub fn lineCount(content: []const u8) usize { /// INVALIDATION — the part that rots if nobody says it out loud. The index is /// dropped in EXACTLY ONE PLACE: setContent, immediately below, which is the /// funnel every content swap in the editor already goes through (typing, undo, -/// redo, a save's normalisation, an output buffer refilling itself). A File +/// redo, a save's normalisation, an output buffer refilling itself). A State /// built by a struct literal starts with an empty index, and empty reads as /// "not built yet" — a real index always has at least one entry, because a /// file always has at least one line. So there is one and only one way to make @@ -74,7 +200,7 @@ pub fn lineCount(content: []const u8) usize { /// swallow that and scan the old way, so OOM there is slow rather than wrong; /// callers that need the whole table say `try` and drop the keystroke, which /// is what they already did when their own arena ran out. -pub fn lineIndex(gpa: std.mem.Allocator, f: *File) ![]const usize { +pub fn lineIndex(gpa: std.mem.Allocator, f: *State) ![]const usize { if (f.line_starts.len > 0) return f.line_starts; // Exact allocation: deinitPane frees `line_starts` itself, so the stored // slice must span the complete allocation rather than spare capacity. @@ -92,18 +218,65 @@ pub fn lineIndex(gpa: std.mem.Allocator, f: *File) ![]const usize { } /// line count, O(1) once the index is warm -pub fn nlines(gpa: std.mem.Allocator, f: *File) usize { +pub fn nlines(gpa: std.mem.Allocator, f: *State) usize { const idx = lineIndex(gpa, f) catch return lineCount(f.content); return idx.len; } /// byte offset of line `row`, or content.len past the end — modal /// .lineStartOffset's contract exactly, without its walk -pub fn lineStart(gpa: std.mem.Allocator, f: *File, row: usize) usize { +pub fn lineStart(gpa: std.mem.Allocator, f: *State, row: usize) usize { const idx = lineIndex(gpa, f) catch return modal.lineStartOffset(f.content, row); return if (row >= idx.len) f.content.len else idx[row]; } +pub fn cursorLines(arena: std.mem.Allocator, pane: *Pane, f: *State) ![]const []const u8 { + const index = try lineIndex(pane.gpa, f); + const lines = try arena.alloc([]const u8, index.len); + for (index, 0..) |start, i| { + const end = if (i + 1 < index.len) index[i + 1] - 1 else f.content.len; + lines[i] = f.content[start..end]; + } + return lines; +} + +/// Use the file's line index only when `text` is its complete live content. +/// Edit-buffer fragments and other temporary text retain modal's scan path. +fn contentIndex(pane: *Pane, text: []const u8) ?[]const usize { + const f = if (pane.file) |*file| file else return null; + if (text.ptr != f.content.ptr or text.len != f.content.len) return null; + return lineIndex(pane.gpa, f) catch null; +} + +pub fn textOffset(pane: *Pane, text: []const u8, cursor: modal.Cursor) usize { + const index = contentIndex(pane, text) orelse return modal.hxOff(text, cursor); + const row = @min(cursor.row, index.len - 1); + const start = index[row]; + const end = if (row + 1 < index.len) index[row + 1] - 1 else text.len; + return @min(start + cursor.col, end); +} + +pub fn textLineStart(pane: *Pane, text: []const u8, row: usize) usize { + const index = contentIndex(pane, text) orelse return modal.lineStartOffset(text, row); + return if (row >= index.len) text.len else index[row]; +} + +pub fn textLineCount(pane: *Pane, text: []const u8) usize { + const index = contentIndex(pane, text) orelse return modal.hxLineCount(text); + return index.len; +} + +pub fn textPosition(pane: *Pane, text: []const u8, offset: usize) modal.Cursor { + const index = contentIndex(pane, text) orelse return modal.hxPos(text, offset); + const bounded = @min(offset, text.len); + const row = std.sort.upperBound(usize, index, bounded, struct { + fn cmp(key: usize, item: usize) std.math.Order { + return std.math.order(key, item); + } + }.cmp) - 1; + return .{ .row = row, .col = bounded - index[row] }; +} + pub fn open(p: *Pardes, id: usize, path: []const u8, line: usize) !*Pane { const content = try look.readFile(p.gpa, path); errdefer p.gpa.free(content); @@ -113,7 +286,6 @@ pub fn open(p: *Pardes, id: usize, path: []const u8, line: usize) !*Pane { const total = lineCount(content); const scroll: usize = if (line > 0 and line <= total) line - 1 else 0; pane.file = .{ .path = path_copy, .content = content, .scroll = scroll }; - pane.kind = .file; pane.cur_pinned = true; pane.cur_row = @intCast(scroll); // watches follow pane lifetime: this is the only place a real file is read @@ -122,9 +294,56 @@ pub fn open(p: *Pardes, id: usize, path: []const u8, line: usize) !*Pane { return pane; } +/// Rebuild a dumped file or output buffer. Byte ownership, output identity, +/// cursor projection, and file watching are all properties of this payload; +/// column registration and custom tag restoration remain core invariants. +pub fn restore(p: *Pardes, id: usize, src: dump.Pane) !*Pane { + const saved = src.file.?; + const content: []u8 = if (saved.content_b64.len > 0) + try dump.decodeBytes(p.gpa, saved.content_b64) + else + try p.gpa.dupe(u8, saved.content); + errdefer p.gpa.free(content); + const path = try p.gpa.dupe(u8, saved.path); + errdefer p.gpa.free(path); + + const output: ?output_pane.Output = if (output_pane.fromWord(saved.origin)) |origin| blk: { + var value: output_pane.Output = .{ .from = origin }; + try output_pane.setArg(&value, saved.origin_arg); + break :blk value; + } else null; + + const pane = try p.newDocPane(id); + pane.file = .{ .path = path, .content = content, .output = output, .scroll = src.scroll }; + pane.cur_pinned = true; + pane.cur_row = @intCast(src.scroll); + pane.cols = @max(1, src.cols); + pane.rows = @max(1, src.rows); + // A restored file is watched exactly like one opened from disk. Its dump + // bytes may differ from disk; the first external write reconciles them and + // leaves the restored version one undo away. Output buffers have no file. + if (output == null) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = true } }); + return pane; +} + +/// Release the complete file payload while the owning pane is still installed +/// (the slot is needed to identify a disappearing file watch). Common pane +/// overlays and the shared terminal stub remain the core's responsibility. +pub fn deinit(p: *Pardes, pane: *Pane, file: *State) void { + if (file.output == null) for (p.panes, 0..) |slot, id| { + if (slot == pane) p.emit(.{ .watch = .{ .pane = @intCast(id), .on = false } }); + }; + p.gpa.free(file.path); + p.gpa.free(file.content); + if (file.line_starts.len > 0) p.gpa.free(file.line_starts); + if (file.highlights.len > 0) p.tree_sitter_gpa.free(file.highlights); + for (file.undo[0..file.undo_len]) |snap| p.gpa.free(snap.content); + for (file.redo[0..file.redo_len]) |snap| p.gpa.free(snap.content); +} + /// The ONE content swap. Everything that edits a file pane lands here, which /// is what lets the line index above have a single invalidation point. -pub fn setContent(p: *Pardes, f: *File, new: []u8) void { +pub fn setContent(p: *Pardes, f: *State, new: []u8) void { p.gpa.free(f.content); f.content = new; f.revision +%= 1; @@ -142,7 +361,7 @@ pub fn setContent(p: *Pardes, f: *File, new: []u8) void { /// undo/redo restores the selection recorded with the snapshot (helix keeps /// selections in its history transactions), clamped: the content it was taken /// against may be shorter than the one it is being restored onto. -pub fn restoreSnap(pane: *Pane, f: *File, snap: FileSnap) void { +pub fn restoreSnap(pane: *Pane, f: *State, snap: Snapshot) void { const n = nlines(pane.gpa, f); const row: usize = @min(@as(usize, @intCast(@max(0, snap.cur_row))), n - 1); const llen = modal.lineSlice(f.content, row).len; @@ -155,6 +374,80 @@ pub fn restoreSnap(pane: *Pane, f: *File, snap: FileSnap) void { pane.ensureCursorVisible(); } +fn pushHistory(gpa: std.mem.Allocator, slots: []Snapshot, len: *usize, snap: Snapshot) void { + if (len.* == slots.len) { + gpa.free(slots[0].content); + std.mem.copyForwards(Snapshot, slots[0 .. slots.len - 1], slots[1..]); + len.* -= 1; + } + slots[len.*] = snap; + len.* += 1; +} + +pub fn pushUndo(p: *Pardes, pane: *Pane) void { + const f = if (pane.file) |*file| file else return; + if (f.undo_len > 0 and std.mem.eql(u8, f.undo[f.undo_len - 1].content, f.content)) return; + const snap: Snapshot = .{ + .content = p.gpa.dupe(u8, f.content) catch return, + .cur_row = pane.cur_row, + .cur_col = pane.cur_col, + .vsel = pane.vsel, + }; + pushHistory(p.gpa, &f.undo, &f.undo_len, snap); + for (f.redo[0..f.redo_len]) |item| p.gpa.free(item.content); + f.redo_len = 0; +} + +pub fn undo(p: *Pardes, pane: *Pane) void { + const f = if (pane.file) |*file| file else return; + if (f.undo_len == 0) return; + const current: Snapshot = .{ + .content = p.gpa.dupe(u8, f.content) catch return, + .cur_row = pane.cur_row, + .cur_col = pane.cur_col, + .vsel = pane.vsel, + }; + pushHistory(p.gpa, &f.redo, &f.redo_len, current); + f.undo_len -= 1; + const previous = f.undo[f.undo_len]; + setContent(p, f, previous.content); + restoreSnap(pane, f, previous); +} + +pub fn redo(p: *Pardes, pane: *Pane) void { + const f = if (pane.file) |*file| file else return; + if (f.redo_len == 0) return; + const current: Snapshot = .{ + .content = p.gpa.dupe(u8, f.content) catch return, + .cur_row = pane.cur_row, + .cur_col = pane.cur_col, + .vsel = pane.vsel, + }; + pushHistory(p.gpa, &f.undo, &f.undo_len, current); + f.redo_len -= 1; + const next = f.redo[f.redo_len]; + setContent(p, f, next.content); + restoreSnap(pane, f, next); +} + +/// Commit an externally rewritten file onto the same undo history as typed +/// edits. Unsaved work remains one `u` away; there is no third merge state. +pub fn changed(p: *Pardes, id: u8, bytes: []const u8) void { + const pane = p.panes[id] orelse return; + const f = if (pane.file) |*file| file else return; + if (std.mem.eql(u8, f.content, bytes)) return; + const new = p.gpa.dupe(u8, bytes) catch return; + pushUndo(p, pane); + setContent(p, f, new); + // restoreSnap only consumes cursor/selection from this synthetic snapshot. + restoreSnap(pane, f, .{ + .content = undefined, + .cur_row = pane.cur_row, + .cur_col = pane.cur_col, + .vsel = pane.vsel, + }); +} + /// re-highlight the visible window of any file whose syntax went stale /// (edit, scroll, load) — visible-range-first so big files stay snappy pub fn refreshHighlights(p: *Pardes) void { @@ -165,7 +458,7 @@ pub fn refreshHighlights(p: *Pardes) void { if (pane.file == null) continue; const f = &pane.file.?; if (!f.syntax_dirty) continue; - if (!p.colors_on) { + if (!p.settings.colors) { if (f.highlights.len > 0) p.tree_sitter_gpa.free(f.highlights); f.highlights = &.{}; f.highlight_start = 0; @@ -222,7 +515,7 @@ pub fn refreshHighlights(p: *Pardes) void { /// column it began, recorded into pane.wrap_line/wrap_col as the rows are /// built. `wrap_n` stays 0 for an unwrapped body, and that is the value the /// readers treat as "rows are lines", so the off path never consults an array. -pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File, wrap: bool) ![]const u8 { +pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *State, wrap: bool) ![]const u8 { // the pane's own content width, less the ONE column the break marker sits // in — a wrapped row that filled its last cell would have nowhere to say it // wrapped. 0 means don't wrap. A pane taller than the map refuses too, @@ -245,7 +538,7 @@ pub fn bodyText(arena: std.mem.Allocator, pane: *Pane, f: *File, wrap: bool) ![] /// Run the file-body row walk. With no destination it is the exact sizing /// pass; with one it fills that allocation and records the wrapping map. -fn fillBody(dst: ?[]u8, pane: *Pane, f: *File, width: usize, record_wrap: bool) usize { +fn fillBody(dst: ?[]u8, pane: *Pane, f: *State, width: usize, record_wrap: bool) usize { if (record_wrap) pane.wrap_n = 0; // start ON the first visible line instead of walking the file to it: this // walk was O(f.scroll) and recolorSyntax below ran the identical one again @@ -313,7 +606,7 @@ fn fillBody(dst: ?[]u8, pane: *Pane, f: *File, width: usize, record_wrap: bool) } /// line-number gutter: mute the first PREFIX_W columns. Cheap chrome, not -/// gated on colors_on; selection/cursor passes still win. The cursor row's +/// gated on settings.colors; selection/cursor passes still win. The cursor row's /// number takes the tag style (same row math as renderPane's cursor pass) so /// the eye finds the current line. pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, body_h: u16, active: bool) void { @@ -327,7 +620,7 @@ pub fn drawGutter(p: *Pardes, pane: *Pane, r: pardes.Rect, tx: u16, tw: u16, bod // gutter is naming the line you are on, and that is still one line const cur_line: i32 = if (active and !pane.tag_edit) gcrow else std.math.minInt(i32); // the body's first row, the way renderPane derives it (Tagbottom) - const body_y = if (p.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; var vr: u16 = 0; while (vr < body_h) : (vr += 1) { const row_line: i32 = if (pane.wrap_n == 0) @@ -360,7 +653,7 @@ fn synStyle(p: *Pardes, sy: syntax.Syn) ?SynStyle { /// syntax colors: recolor each content cell from its tree-sitter style byte; /// content starts after the lineno gutter -pub fn recolorSyntax(p: *Pardes, pane: *Pane, f: *File, r: pardes.Rect, tx: u16, tw: u16, body_h: u16) void { +pub fn recolorSyntax(p: *Pardes, pane: *Pane, f: *State, r: pardes.Rect, tx: u16, tw: u16, body_h: u16) void { if (f.highlights.len == 0) return; const s = &p.surface; const tz_recolor = tracy.zone(@src(), "synRecolor"); @@ -368,7 +661,7 @@ pub fn recolorSyntax(p: *Pardes, pane: *Pane, f: *File, r: pardes.Rect, tx: u16, // indexed start, same as bodyText — an empty tail simply paints nothing var flines = std.mem.splitScalar(u8, f.content[lineStart(p.gpa, f, f.scroll)..], '\n'); const total = nlines(p.gpa, f); - const body_y = if (p.tag_bottom) r.y else r.y + pardes.BOX_H; + const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; var vr: u16 = 0; while (vr < body_h) : (vr += 1) { // A colour has to land on the byte it belongs to, so this walk reads @@ -420,3 +713,64 @@ pub fn recolorSyntax(p: *Pardes, pane: *Pane, f: *File, r: pardes.Rect, tx: u16, } } } + +/// Mark every visible wrapped row which continues onto the next screen row. +/// This is file chrome: it follows syntax recoloring and precedes the shared +/// selection passes, so neither source ink nor the marker can win over a user +/// selection. +pub fn drawWrapMarkers( + p: *Pardes, + pane: *const Pane, + r: pardes.Rect, + tx: u16, + tw: u16, + body_h: u16, + pane_bg: pardes.Color, +) void { + if (tw <= config.PREFIX_W + 1) return; + const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + const marker_fg = p.chromeTheme().lineno; + var row: u16 = 0; + while (row + 1 < pane.wrap_n and row + 1 < body_h) : (row += 1) { + if (pane.wrap_line[row + 1] != pane.wrap_line[row]) continue; + p.surface.set(tx + tw - 1, body_y + row, config.wrap_marker, .{ + .fg = .{ .rgb = marker_fg }, + .bg = pane_bg, + }); + } +} + +/// Paint one logical file word through the last frame's wrap map. Unlike a +/// rectangular mouse selection, a path may cross continuation rows without +/// highlighting unrelated cells between its endpoints. +pub fn paintWordSelection( + p: *Pardes, + pane: *Pane, + r: pardes.Rect, + row: i32, + word_lo: i32, + word_hi: i32, + bg: [3]u8, +) void { + const tx = r.x + config.GUTTER; + const tw = r.w - config.GUTTER; + const body_y = if (p.settings.tag_bottom) r.y else r.y + pardes.BOX_H; + var vr: i32 = 0; + while (vr + @as(i32, pardes.BOX_H) < @as(i32, r.h)) : (vr += 1) { + const here = pane.wrapAt(vr); + if (here.line != row) continue; + var hi = word_hi; + const next = pane.wrapAt(vr + 1); + if (next.line == row) hi = @min(hi, next.at); + const lo = @max(word_lo, here.at); + if (hi <= lo) continue; + const c0 = @as(i32, config.PREFIX_W) + displayOffset(pane, row, here.at, lo); + const c1 = @as(i32, config.PREFIX_W) + displayEndOffset(pane, row, here.at, hi - 1); + var col = @max(@as(i32, config.PREFIX_W), c0); + while (col <= c1 and col < @as(i32, tw)) : (col += 1) { + const cell = p.surface.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(vr))); + cell.default = false; + cell.style.bg = .{ .rgb = bg }; + } + } +} |
