summaryrefslogtreecommitdiff
path: root/src/body_layer.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-27 22:05:19 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:14 -0300
commit08942ef68b10d4ce478bd3c62d306f2a633d51cb (patch)
tree5e47334fcfb2301fe3545bf69ad73ac9967c4cd6 /src/body_layer.zig
parentfc2bc13e9f45099d4e2fe086e2a52aab7de8ad5c (diff)
downloadpardes-08942ef68b10d4ce478bd3c62d306f2a633d51cb.tar.gz
pardes-08942ef68b10d4ce478bd3c62d306f2a633d51cb.zip
Move body drawing out of pardes.zig into body_layer.zig
Pure move, no behaviour change: renderBody, renderBodyLayer, paintPointerSelection, paintSourceSelection, paintTerminalSelection and bodyText go verbatim to the end of body_layer.zig, which already held the body's layer geometry and hit-testing, so the pane body as the surface shows it is one file. The file doc now says so. The methods become free functions taking `p: *Pardes`; their 16 call sites (render and renderPane, the tag painter, selection and dump code) change from `p.renderBody(..)` to `body_layer.renderBody(p, ..)`. pardes.zig gains `pub const body_layer = @import("body_layer.zig");`; pointerSourceLine, selRegexArmed and cellBounds become pub because the moved code calls them. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/body_layer.zig')
-rw-r--r--src/body_layer.zig433
1 files changed, 431 insertions, 2 deletions
diff --git a/src/body_layer.zig b/src/body_layer.zig
index 14fb91ec..d7ddbfb5 100644
--- a/src/body_layer.zig
+++ b/src/body_layer.zig
@@ -1,9 +1,27 @@
-//! A file body's logical rows can outnumber its physical grid rows. Hosts
-//! place the compact declaration rows first and clip the remaining body rows.
+//! A pane body as the surface shows it. A file body's logical rows can
+//! outnumber its physical grid rows: hosts place the compact declaration rows
+//! first and clip the remaining body rows. After the layer types come the
+//! functions that draw a body into the surface: its text, colours, selections
+//! and cursor.
const std = @import("std");
const pardes = @import("pardes.zig");
const config = @import("config.zig");
const limits = @import("memory.zig").limits;
+const layout = @import("layout.zig");
+const modal = @import("modal.zig");
+const tracy = @import("tracy.zig");
+const panes = @import("panes.zig");
+const colors = @import("colors.zig");
+const Color = @import("surface.zig").Color;
+const CellStyle = @import("surface.zig").CellStyle;
+const Cell = @import("surface.zig").Cell;
+const Surface = @import("surface.zig").Surface;
+const pdf_enabled = panes.Pdf.enabled;
+const Pane = panes.Pane;
+const BOX_H = pardes.BOX_H;
+const TAG_TEXT_INSET = pardes.TAG_TEXT_INSET;
+const Rect = layout.Rect;
+const Pardes = pardes.Pardes;
pub const Metrics = struct {
body_w: u16 = 1,
@@ -107,3 +125,414 @@ test "compact context borders separate source gaps and preserve multiline header
try std.testing.expect(!layer.hasContextBorderAfter(4));
try std.testing.expect(!layer.context_separators.isSet(4));
}
+
+// ---- drawing a pane body into the surface ----
+
+fn paintTerminalSelection(p: *Pardes, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void {
+ const body = bodyText(p, p.scratch.allocator(), pane) catch return;
+ const raw = pane.mode == .tty;
+ const tx = r.x + config.GUTTER;
+ const width = r.w -| config.GUTTER;
+ 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) 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)
+ (if (raw) row.row else pane.surfRow(row.row))
+ else
+ (if (raw) pane.gridRow(row.row) else row.row);
+ if (target != source_row) continue;
+ var lo = row.lo;
+ var hi = row.hi;
+ if (row.raw_terminal and !raw) {
+ lo -|= row.prompt_bytes;
+ hi -|= row.prompt_bytes;
+ } else if (!row.raw_terminal and raw) {
+ const prefix = panes.Terminal.promptPrefixBytes(pane, source_row, line);
+ lo += prefix;
+ hi += prefix;
+ }
+ lo = modal.graphemeStart(line, @min(lo, line.len));
+ hi = @min(hi, line.len);
+ if (lo >= hi) continue;
+ var col = panes.File.rawDisplayCol(line, lo);
+ const end = panes.File.rawDisplayCol(line, hi);
+ while (col < end and col < width) : (col += 1) {
+ const cell = p.surface.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible)));
+ cell.default = false;
+ cell.style.bg = .{ .rgb = bg };
+ if (fg) |ink| cell.style.fg = .{ .rgb = ink };
+ }
+ }
+ }
+}
+
+fn paintSourceSelection(p: *Pardes, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void {
+ if (pane.isTerminal()) return paintTerminalSelection(p, pane, r, rows, bg, fg);
+ 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(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)) : (visible += 1) {
+ const source = pane.wrapAt(visible);
+ const line = Pardes.pointerSourceLine(pane, terminal_lines, source.line);
+ const start = @min(line.len, @as(usize, @intCast(@max(0, source.at))));
+ const next = pane.wrapAt(visible + 1);
+ const end = if (next.line == source.line) @min(line.len, @as(usize, @intCast(@max(0, next.at)))) else line.len;
+ for (rows) |row| {
+ if (row.row != source.line) continue;
+ const lo = @max(start, row.lo);
+ const hi = @min(end, row.hi);
+ if (lo >= hi) continue;
+ var col = prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], lo - start)));
+ const last = prefix + @as(i32, @intCast(panes.File.rawDisplayCol(line[start..], hi - start)));
+ while (col < last and col < width) : (col += 1) {
+ const cell = p.surface.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(visible)));
+ cell.default = false;
+ cell.style.bg = .{ .rgb = bg };
+ if (fg) |ink| cell.style.fg = .{ .rgb = ink };
+ }
+ }
+ }
+}
+
+pub fn paintPointerSelection(
+ p: *Pardes,
+ pane: *Pane,
+ r: Rect,
+ tx: u16,
+ tw: u16,
+ tag_y: u16,
+ body_y: u16,
+ sl: Pane.Sel,
+ bg: [3]u8,
+ fg: ?[3]u8,
+) void {
+ if (pane.pointerSelection(sl)) |rows| return paintSourceSelection(p, pane, r, rows, bg, fg);
+ const s = &p.surface;
+ const r0 = @min(sl.r0, sl.r1);
+ const r1 = @max(sl.r0, sl.r1);
+ const c0 = @max(0, @min(sl.c0, sl.c1));
+ const c1 = @max(0, @max(sl.c0, sl.c1));
+ var row: u16 = 0;
+ 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;
+ // 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;
+ var col: i32 = if (pane.file != null and row >= BOX_H) @max(c0, @as(i32, panes.File.gutterWidth(pane))) else @max(0, c0 - scroll);
+ const row_x = if (row < BOX_H) r.x + @min(TAG_TEXT_INSET, r.w) else tx;
+ const row_width = if (row < BOX_H) r.w -| TAG_TEXT_INSET else tw;
+ while (col <= c1 - scroll and col < row_width) : (col += 1) {
+ const cell = s.at(row_x + @as(u16, @intCast(col)), sy);
+ cell.default = false;
+ cell.style.bg = .{ .rgb = bg };
+ if (fg) |ink| cell.style.fg = .{ .rgb = ink };
+ }
+ }
+}
+
+pub 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;
+ 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.
+ const logical_rows = @min(@as(usize, pane.rows) + pane.context_rows, pane.wrap_line.len);
+ if (logical_rows <= pane.context_rows) return;
+ pane.body_rows = @intCast(logical_rows);
+ const bh: usize = @max(1, p.row_metrics.body_h);
+ const th: usize = std.math.clamp(p.row_metrics.tagline_h, 1, bh);
+ const body_pixels = @as(usize, viewport_rows) * bh;
+ pane.context_row_limit = @intCast(@min(pane.context_rows, (body_pixels -| bh) / th));
+ const context_pixels = @as(usize, pane.context_row_limit.?) * th;
+ pane.body_visible_rows = @intCast(@min(logical_rows, pane.context_row_limit.? + @max(1, (body_pixels -| context_pixels) / bh)));
+ const width = r.w - config.GUTTER;
+ const layer = &p.surface.body_layers[id];
+ const count = logical_rows * width;
+ if (layer.cells.len != count) layer.cells = try p.gpa.realloc(layer.cells, count);
+ const temporary_cells = try arena.alloc(Cell, @as(usize, r.w) * (logical_rows + BOX_H));
+ @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);
+ const synthetic: Rect = .{ .x = 0, .y = 0, .w = r.w, .h = @intCast(logical_rows + BOX_H) };
+ renderBody(p, arena, pane, synthetic, id, id == p.active, false) catch |err| {
+ std.mem.swap(Surface, &p.surface, &temporary);
+ return err;
+ };
+ std.mem.swap(Surface, &p.surface, &temporary);
+ const first_row: usize = if (p.settings.tag_bottom) 0 else BOX_H;
+ for (0..logical_rows) |row| {
+ const start = (row + first_row) * r.w + config.GUTTER;
+ @memcpy(layer.cells[row * width ..][0..width], temporary_cells[start..][0..width]);
+ }
+ layer.pane = @intCast(id);
+ layer.serial = pane.serial;
+ 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;
+ layer.setContextLines(pane.wrap_line[0..pane.context_rows]);
+ layer.cursor = null;
+ if (temporary.cursor) |cursor| {
+ if (cursor.x >= config.GUTTER and cursor.y >= first_row)
+ layer.cursor = .{ .x = cursor.x - config.GUTTER, .y = @intCast(cursor.y - first_row), .bar = cursor.bar };
+ }
+}
+
+pub fn renderBody(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool, scrollbar: bool) !void {
+ const s = &p.surface;
+ const th = p.theme();
+ const chrome = p.chromeTheme();
+ const tx = r.x + config.GUTTER;
+ const tw = r.w - config.GUTTER;
+ const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
+ // 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(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;
+ const body_bottom = r.h;
+ const body_style: CellStyle = .{
+ .fg = if (th.fg) |c| .{ .rgb = c } else .default,
+ .bg = pane_bg,
+ };
+ const tz_body = tracy.zone(@src(), "bodyText");
+ const body = try bodyText(p, arena, pane);
+ tz_body.end();
+ {
+ // The rows themselves. `bodyText` above is only the string BUILD;
+ // this is what writes it into the surface.
+ const tz_rows = tracy.zone(@src(), "paneBodyRows");
+ defer tz_rows.end();
+ var it = std.mem.splitScalar(u8, body, '\n');
+ var i: u16 = 0;
+ while (it.next()) |line| : (i += 1) {
+ if (i >= body_h) break;
+ _ = s.print(tx, body_y + i, tw, line, body_style);
+ }
+ }
+
+ const tz_color = tracy.zone(@src(), "paneRecolor");
+ // File layout is independent of whether its producer requests colors.
+ if (pane.file) |*f| {
+ panes.File.drawGutter(p, pane, r, tx, tw, body_h, active);
+ if (p.settings.colors and pane.colorAlgo() != .none)
+ panes.File.recolorSyntax(p, pane, f, r, tx, tw, body_h);
+ panes.File.drawWrapMarkers(p, pane, r, tx, tw, body_h, pane_bg);
+ } else if (pane.isTerminal() and p.settings.colors) {
+ panes.Terminal.recolorAnsi(p, pane, r, tx, tw, body_h, body);
+ }
+ tz_color.end();
+
+ const accent2 = if (std.mem.eql(u8, &th.num, &th.str)) th.comment else th.num;
+ const sel_btn = [3][3]u8{
+ colors.mix(th.sel_bg, colors.mix(th.sel_bg, th.kw)),
+ colors.mix(th.sel_bg, colors.mix(th.sel_bg, th.str)),
+ colors.mix(th.sel_bg, colors.mix(th.sel_bg, accent2)),
+ };
+ if (p.look_hover_preview) |preview| {
+ if (preview.pane == id and preview.serial == pane.serial) {
+ // Preserve body ink, then let every real user selection below
+ // win over this quiet affordance. mark_hover carries the
+ // cells this paints out to the hosts as the affordance rect —
+ // macOS composes its glass overlay over exactly these.
+ const preview_bg = colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg));
+ if (preview.file_word) |word|
+ panes.File.paintWordSelection(p, pane, r, word.row, word.lo, word.hi, preview_bg)
+ else if (preview.sel) |sel|
+ paintPointerSelection(p, pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
+ }
+ }
+ const modal_hover = if (p.look_hover_preview) |preview|
+ preview.pane == id and preview.serial == pane.serial and preview.sel == null and preview.file_word == null
+ else
+ false;
+ for (pane.sel, 0..) |sl, b| {
+ if (sl.state == .none) continue;
+ if (sl.state == .done and b != 0) continue;
+ paintPointerSelection(p, pane, r, tx, tw, tag_y, body_y, sl, sel_btn[b], th.sel_fg);
+ }
+ // modal line selection (helix `x`): whole body rows, normal mode only
+ if ((pane.mode == .normal or modal_hover) and pane.msel.active) {
+ const hover_only = modal_hover and pane.mode != .normal;
+ const modal_bg = if (hover_only)
+ colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg))
+ else
+ th.sel_bg;
+ const lo = @min(pane.msel.r0, pane.msel.r1);
+ const hi = @max(pane.msel.r0, pane.msel.r1);
+ var row: u16 = BOX_H; // never paint the tag row
+ while (row < body_bottom) : (row += 1) {
+ const ar = pane.wrapAt(@as(i32, row) - @as(i32, BOX_H)).line;
+ if (ar < lo or ar > hi) continue;
+ var col: u16 = if (pane.file != null) panes.File.gutterWidth(pane) else 0;
+ while (col < tw) : (col += 1) {
+ const cell = s.at(tx + col, body_y + row - BOX_H);
+ cell.default = false;
+ cell.style.bg = .{ .rgb = modal_bg };
+ if (!hover_only) cell.style.fg = .{ .rgb = th.sel_fg };
+ }
+ }
+ }
+ const preview = Pardes.selRegexArmed(pane) != null;
+ const show_prim = (pane.mode == .normal and pane.vsel.active) or preview or
+ (modal_hover and pane.vsel.active);
+ const show_extra = pane.mode != .tty and pane.nsel > 0;
+ if (show_prim or show_extra) {
+ const vpfx: i32 = if (pane.file != null) panes.File.gutterWidth(pane) else 0;
+ var si: usize = 0;
+ while (si <= pane.nsel) : (si += 1) {
+ const primary = si == pane.nsel;
+ if (if (primary) !show_prim else !show_extra) continue;
+ const sr = if (primary) Pane.SelRange{
+ .row = pane.cur_row,
+ .col = pane.cur_col,
+ .arow = if (pane.vsel.active) pane.vsel.row else pane.cur_row,
+ .acol = if (pane.vsel.active) pane.vsel.col else pane.cur_col,
+ } else pane.sels[si];
+ const bnd = Pardes.cellBounds(sr);
+ const hover_only = primary and modal_hover and pane.mode != .normal and !preview;
+ const bg = if (hover_only)
+ colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg))
+ else if (primary)
+ th.sel_bg
+ else
+ colors.mix(th.sel_bg, page_rgb);
+ var vr: i32 = 0;
+ while (vr + @as(i32, BOX_H) < @as(i32, body_bottom)) : (vr += 1) {
+ const w = pane.wrapAt(vr);
+ if (w.line < bnd.lo_row or w.line > bnd.hi_row) continue;
+ const visible_line = modal.lineSlice(body, @intCast(vr));
+ const cstart: i32 = if (w.line == bnd.lo_row)
+ (if (pane.file != null)
+ panes.File.displayOffset(pane, w.line, w.at, bnd.lo_col)
+ else
+ panes.File.lineDisplayOffset(visible_line, @intCast(@max(0, w.at)), @intCast(@max(0, bnd.lo_col)))) + vpfx
+ else
+ vpfx;
+ const cend: i32 = if (w.line == bnd.hi_row)
+ (if (pane.file != null)
+ panes.File.displayEndOffset(pane, w.line, w.at, bnd.hi_col)
+ else
+ panes.File.lineDisplayEndOffset(visible_line, @intCast(@max(0, w.at)), @intCast(@max(0, bnd.hi_col)))) + vpfx
+ else
+ @as(i32, tw) - 1;
+ var col: i32 = @max(cstart, vpfx);
+ while (col <= cend and col < tw) : (col += 1) {
+ const cell = s.at(tx + @as(u16, @intCast(col)), body_y + @as(u16, @intCast(vr)));
+ cell.default = false;
+ cell.style.bg = .{ .rgb = bg };
+ if (!hover_only) cell.style.fg = .{ .rgb = th.sel_fg };
+ }
+ }
+ if (primary and !preview and !hover_only) continue; // the hardware cursor IS the primary's
+ if (hover_only) continue; // quiet preview preserves the source ink
+ const cw = pane.wrapRow(sr.row, sr.col);
+ const crow = cw.row + @as(i32, BOX_H);
+ const ccol = (if (pane.file != null)
+ panes.File.displayOffset(pane, sr.row, cw.at, sr.col)
+ else
+ panes.File.lineDisplayOffset(
+ modal.lineSlice(body, @intCast(@max(0, cw.row))),
+ @intCast(@max(0, cw.at)),
+ @intCast(@max(0, sr.col)),
+ )) + vpfx;
+ if (crow >= BOX_H and crow < @as(i32, body_bottom) and ccol >= vpfx and ccol < tw) {
+ const cell = s.at(tx + @as(u16, @intCast(ccol)), body_y + @as(u16, @intCast(crow - BOX_H)));
+ cell.default = false;
+ cell.style.bg = .{ .rgb = th.sel_fg };
+ cell.style.fg = .{ .rgb = th.sel_bg };
+ }
+ }
+ }
+
+ // cursor: tracks the shell cursor until pinned by a click or a key
+ // (the tag cursor above wins while the tag is focused)
+ if (active and !pane.tag_edit) {
+ if (pane.mode != .tty) {
+ const cur = panes.Terminal.gridCursor(pane);
+ const goff = panes.Terminal.gridOffset(pane);
+ const crow = if (pane.cur_pinned) pane.cur_row else pane.surfRow(@as(i32, @intCast(cur.y)) + goff);
+ const ccol = if (pane.cur_pinned) pane.cur_col else @as(i32, @intCast(cur.x));
+ const cwp = pane.wrapRow(crow, ccol);
+ const prow = cwp.row + @as(i32, BOX_H);
+ // Files store source-byte columns; the Surface stores display
+ // cells, so account for every expanded tab before the cursor.
+ const cx = if (pane.file != null)
+ @as(i32, panes.File.gutterWidth(pane)) + panes.File.displayOffset(pane, crow, cwp.at, ccol)
+ else if (pane.cur_pinned)
+ panes.File.lineDisplayOffset(
+ modal.lineSlice(body, @intCast(@max(0, cwp.row))),
+ @intCast(@max(0, cwp.at)),
+ @intCast(@max(0, ccol)),
+ )
+ else
+ ccol;
+ if (prow >= BOX_H and cx >= 0 and prow < body_bottom and cx < tw)
+ s.cursor = .{ .x = tx + @as(u16, @intCast(cx)), .y = body_y + @as(u16, @intCast(prow - BOX_H)), .bar = pane.mode == .insert };
+ } else if (panes.Terminal.visibleCursor(pane)) |cur| {
+ if (cur.y + BOX_H < body_bottom and cur.x < tw)
+ s.cursor = .{ .x = tx + cur.x, .y = body_y + cur.y };
+ }
+ }
+
+ if (scrollbar and body_h > 0) {
+ // The rail covers the notice bands too: nothing else paints those
+ // two gutter columns, so a gap there keeps the frame-wide border
+ // fill and reads as a notch cut out of the scrollbar.
+ const rail_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H;
+ const rail_h = r.h -| BOX_H;
+ s.fill(r.x, rail_y, 1, rail_h, .{ .bg = .{ .rgb = chrome.scroll_track } });
+ s.fill(r.x + 1, rail_y, 1, rail_h, .{ .bg = pane_bg });
+ const sb: struct { total: usize, offset: usize, len: usize } = if (pane.file) |*f| .{
+ .total = panes.File.nlines(p.gpa, f),
+ .offset = f.scroll,
+ .len = pane.rows,
+ } else if (pane.pdfPage()) |page| .{
+ .total = if (comptime pdf_enabled) pane.pdf.?.page_count else 1,
+ .offset = page,
+ .len = 1,
+ } else blk: {
+ const gsb = panes.Terminal.scrollbar(pane);
+ break :blk .{ .total = gsb.total, .offset = gsb.offset, .len = gsb.len };
+ };
+ const track_h: usize = body_h;
+ const total = if (sb.total == 0) 1 else sb.total;
+ const len = @max(1, (track_h * @max(1, sb.len)) / total);
+ const pos = (track_h * sb.offset) / total;
+ var sy: usize = pos;
+ while (sy < track_h and sy < pos + len) : (sy += 1) {
+ s.fill(r.x, body_y + @as(u16, @intCast(sy)), 1, 1, .{ .bg = .{ .rgb = chrome.scroll_thumb } });
+ }
+ }
+}
+
+pub fn bodyText(p: *Pardes, arena: std.mem.Allocator, pane: *Pane) ![]const u8 {
+ if (pane.image != null) {
+ const buf = try arena.alloc(u8, pane.rows -| 1);
+ @memset(buf, '\n');
+ return buf;
+ }
+ if (pane.hasPdf()) {
+ if (comptime pdf_enabled) return panes.Pdf.visibleText(
+ &pane.pdf.?,
+ p.pdf_gpa,
+ arena,
+ pane.rows -| BOX_H,
+ );
+ unreachable;
+ }
+ if (pane.file) |*f| return panes.File.bodyText(arena, pane, f, p.settings.wrap);
+ return panes.Terminal.bodyText(arena, pane);
+}