diff options
| -rw-r--r-- | src/body_layer.zig | 433 | ||||
| -rw-r--r-- | src/fs.zig | 1 | ||||
| -rw-r--r-- | src/pardes.zig | 434 |
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); +} @@ -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" { |
