summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/body_layer.zig433
-rw-r--r--src/fs.zig1
-rw-r--r--src/pardes.zig434
3 files changed, 445 insertions, 423 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);
+}
diff --git a/src/fs.zig b/src/fs.zig
index 5817b3a4..4d8a5188 100644
--- a/src/fs.zig
+++ b/src/fs.zig
@@ -39,6 +39,7 @@ const source_files = [_]Source{
.{ .path = "src/surface.zig", .contents = @embedFile("surface.zig") },
.{ .path = "src/colors.zig", .contents = @embedFile("colors.zig") },
.{ .path = "src/Messages.zig", .contents = @embedFile("Messages.zig") },
+ .{ .path = "src/body_layer.zig", .contents = @embedFile("body_layer.zig") },
.{ .path = "src/File.zig", .contents = @embedFile("File.zig") },
.{ .path = "src/Output.zig", .contents = @embedFile("Output.zig") },
.{ .path = "src/Mini.zig", .contents = @embedFile("Mini.zig") },
diff --git a/src/pardes.zig b/src/pardes.zig
index c171c669..fe96a221 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -11,6 +11,7 @@ pub const syntax = @import("syntax.zig");
pub const locations_config = @import("locations_config.zig");
const tracy = @import("tracy.zig");
pub const panes = @import("panes.zig");
+pub const body_layer = @import("body_layer.zig");
pub const Messages = @import("Messages.zig");
pub const colors = @import("colors.zig");
pub const Theme = colors.Theme;
@@ -6390,10 +6391,10 @@ pub const Pardes = struct {
/// tag + '\n' + body: the full selectable pane text (row 0 = the tag)
fn paneText(p: *Pardes, pane: *Pane) ![]u8 {
const arena = p.scratch.allocator();
- return std.fmt.allocPrint(arena, "{s}\n{s}", .{ try p.tagText(arena, pane), try p.bodyText(arena, pane) });
+ return std.fmt.allocPrint(arena, "{s}\n{s}", .{ try p.tagText(arena, pane), try body_layer.bodyText(p, arena, pane) });
}
- fn pointerSourceLine(pane: *Pane, terminal_lines: []const []const u8, row: i32) []const u8 {
+ pub fn pointerSourceLine(pane: *Pane, terminal_lines: []const []const u8, row: i32) []const u8 {
if (row < 0) return "";
if (pane.file != null) return panes.File.sourceLine(pane, row);
const index: usize = @intCast(row);
@@ -6408,7 +6409,7 @@ pub const Pardes = struct {
pane.clearPointerSelection(slot);
// Refresh the source map before translating the rendered rectangle.
const raw = pane.isTerminal() and pane.mode == .tty;
- const body = try p.bodyText(p.scratch.allocator(), pane);
+ const body = try body_layer.bodyText(p, p.scratch.allocator(), pane);
const terminal_lines = if (pane.file == null and !raw) try panes.Terminal.cursorLines(p, pane) else &.{};
const first = @max(0, @min(gesture.r0, gesture.r1) - BOX_H);
const logical_rows = if (pane.body_rows > 0) pane.body_rows else pane.rows;
@@ -6638,7 +6639,7 @@ pub const Pardes = struct {
const wrapped = pane.wrapAt(visible);
const raw = pane.isTerminal() and pane.mode == .tty and clicked.r0 >= BOX_H;
const row = if (raw) panes.Terminal.gridOffset(pane) + visible else wrapped.line;
- const raw_line = if (raw) modal.lineSlice(p.bodyText(p.scratch.allocator(), pane) catch "", @intCast(@max(0, visible))) else "";
+ const raw_line = if (raw) modal.lineSlice(body_layer.bodyText(p, p.scratch.allocator(), pane) catch "", @intCast(@max(0, visible))) else "";
const col = if (raw)
@as(i32, @intCast(panes.File.rawAtDisplay(raw_line, @intCast(@max(0, clicked.c0)))))
else if (clicked.r0 >= BOX_H)
@@ -7045,7 +7046,7 @@ pub const Pardes = struct {
p.startPrompt(pane, .{ .search = if (split) config.split_marker else config.select_marker });
}
- fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } {
+ pub fn selRegexArmed(pane: *Pane) ?struct { pat: []const u8, split: bool } {
const prompt_at = switch (pane.prompt) {
.search => |at| at,
else => return null,
@@ -8576,7 +8577,7 @@ pub const Pardes = struct {
const Bounds = struct { lo_row: i32, lo_col: i32, hi_row: i32, hi_col: i32 };
/// a range's two cells, normalized to document order
- fn cellBounds(s: Pane.SelRange) Bounds {
+ pub fn cellBounds(s: Pane.SelRange) Bounds {
if (s.row < s.arow or (s.row == s.arow and s.col < s.acol))
return .{ .lo_row = s.row, .lo_col = s.col, .hi_row = s.arow, .hi_col = s.acol };
return .{ .lo_row = s.arow, .lo_col = s.acol, .hi_row = s.row, .hi_col = s.col };
@@ -10182,7 +10183,7 @@ pub const Pardes = struct {
const text = pane.rawPointerText(gesture) orelse return;
if (rows.len == 0) return;
const endpoint = rows[if (gesture.r1 >= gesture.r0) rows.len - 1 else 0];
- const body = p.bodyText(p.scratch.allocator(), pane) catch return;
+ const body = body_layer.bodyText(p, p.scratch.allocator(), pane) catch return;
const line = modal.lineSlice(body, @intCast(@max(0, endpoint.row - panes.Terminal.gridOffset(pane))));
const byte = if (gesture.c1 >= gesture.c0) modal.prevGrapheme(line, @min(endpoint.hi, line.len)) else endpoint.lo;
pane.cur_row = pane.surfRow(endpoint.row);
@@ -11085,7 +11086,7 @@ pub const Pardes = struct {
const pane = slot orelse continue;
slot_to_pane[id] = panes_len;
const tag = try p.tagText(arena, pane);
- const body = try p.bodyText(arena, pane);
+ const body = try body_layer.bodyText(p, arena, pane);
const scroll: usize = @intCast(@max(0, pane.scroll()));
var dp: dump.Pane = if (pane.file) |*f| try panes.File.dumpPane(
arena,
@@ -11700,7 +11701,7 @@ pub const Pardes = struct {
s.body_layers[id].rows = 0;
Messages.collectNotices(p, pane, p.rects[id], id);
try p.renderPane(arena, pane, p.rects[id], id, id == p.active);
- try p.renderBodyLayer(arena, pane, p.rects[id], id);
+ try body_layer.renderBodyLayer(p, arena, pane, p.rects[id], id);
}
for (&p.panes, 0..) |*slot, id| {
const pane = slot.* orelse continue;
@@ -12101,116 +12102,6 @@ pub const Pardes = struct {
return !cell.default and !(cell.len == 1 and cell.text[0] == ' ');
}
- fn paintTerminalSelection(p: *Pardes, pane: *Pane, r: Rect, rows: []const Pane.PointerRow, bg: [3]u8, fg: ?[3]u8) void {
- const body = p.bodyText(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 p.paintTerminalSelection(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 = 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 };
- }
- }
- }
- }
-
- 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 p.paintSourceSelection(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 };
- }
- }
- }
-
fn renderTagLayers(p: *Pardes, arena: std.mem.Allocator) !void {
const chrome = p.chromeTheme();
for (p.panes, 0..) |slot, id| {
@@ -12373,55 +12264,6 @@ pub const Pardes = struct {
}
}
- 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) };
- p.renderBody(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 };
- }
- }
-
fn renderPane(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool) !void {
const tz = tracy.zone(@src(), "renderPane");
defer tz.end();
@@ -12487,7 +12329,7 @@ pub const Pardes = struct {
return;
}
- try p.renderBody(arena, pane, r, id, active, true);
+ try body_layer.renderBody(p, arena, pane, r, id, active, true);
}
fn paintPaneTag(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool, capacity: u16) !void {
@@ -12545,7 +12387,7 @@ 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) {
const preview_bg = colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg));
- p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
+ body_layer.paintPointerSelection(p, pane, r, tx, tw, tag_y, body_y, sel, preview_bg, null);
};
}
if (pane.tag_edit and pane.tag_sel) {
@@ -12570,238 +12412,7 @@ pub const Pardes = struct {
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;
- p.paintPointerSelection(pane, r, tx, tw, tag_y, body_y, selection, th.sel_bg, th.sel_fg);
- }
- }
-
- 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 p.bodyText(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|
- p.paintPointerSelection(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;
- p.paintPointerSelection(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 = 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 = 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 } });
- }
+ body_layer.paintPointerSelection(p, pane, r, tx, tw, tag_y, body_y, selection, th.sel_bg, th.sel_fg);
}
}
@@ -12821,25 +12432,6 @@ pub const Pardes = struct {
s.nimages += 1;
return true;
}
-
- 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);
- }
};
test "Esc alternates between two panes of the SAME kind" {