summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig473
1 files changed, 28 insertions, 445 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 6e78380a..bfd8a4f6 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -1,5 +1,7 @@
const std = @import("std");
pub const layout = @import("layout.zig");
+pub const animation = @import("animation.zig");
+pub const Presentation = @import("Presentation.zig");
const uucode = @import("uucode");
const vaxis = @import("vaxis");
pub const modal = @import("modal.zig");
@@ -16,6 +18,7 @@ pub const mouse = @import("mouse.zig");
pub const normal = @import("normal.zig");
pub const edit = @import("edit.zig");
pub const body_layer = @import("body_layer.zig");
+pub const draw = @import("draw.zig");
pub const Messages = @import("Messages.zig");
pub const Pipe = @import("Pipe.zig");
pub const colors = @import("colors.zig");
@@ -284,7 +287,7 @@ pub fn topbarPaneBorderPixels(cell_h: u32, tagline_h: u32) u32 {
return @min(@as(u32, config.gui_topbar_pane_border_px), spare * 2);
}
-pub fn taglineOriginCol(p: *const Pardes, col: u16, row: u16, track: ?layout.Track) f32 {
+pub fn taglineOriginCol(p: *const Pardes, col: u16, row: u16, track: ?animation.Track) f32 {
if (row < p.topBarHeight()) return 0;
if (row < p.topBarHeight() + p.columnBarHeight()) {
for (p.col_x[0..p.ncol], p.col_w[0..p.ncol]) |x, w|
@@ -315,7 +318,7 @@ pub fn taglineOriginColForFrame(p: *const Pardes, col: u16, row: u16) f32 {
}
test "paint order is moving, then opening, then closing tombstones on top" {
- const Track = layout.Track;
+ const Track = animation.Track;
// Deliberately interleaved on the way in: the phases are what order the
// output, not the slot they happened to occupy.
const live = [_]?Track{
@@ -329,7 +332,7 @@ test "paint order is moving, then opening, then closing tombstones on top" {
.{ .serial = 16, .pane = 2, .phase = .closing, .effect = .vertical },
};
var out: [8]Track = undefined;
- const len = layout.paintOrder(&live, &closing, &out);
+ const len = animation.paintOrder(&live, &closing, &out);
var serials: [8]u32 = undefined;
for (out[0..len], 0..) |track, i| serials[i] = track.serial;
@@ -338,7 +341,7 @@ test "paint order is moving, then opening, then closing tombstones on top" {
// A host's array is fixed-size and the core's is not its business: writing
// past it would be a buffer overrun in whichever shell had the smaller one.
var tight: [2]Track = undefined;
- try std.testing.expectEqual(@as(usize, 2), layout.paintOrder(&live, &closing, &tight));
+ try std.testing.expectEqual(@as(usize, 2), animation.paintOrder(&live, &closing, &tight));
try std.testing.expectEqual(@as(u32, 12), tight[0].serial);
try std.testing.expectEqual(@as(u32, 15), tight[1].serial);
}
@@ -458,6 +461,7 @@ test {
_ = @import("colors.zig");
_ = @import("surface.zig");
_ = @import("body_layer.zig");
+ _ = @import("draw.zig");
_ = @import("tag_layer.zig");
}
@@ -2223,7 +2227,7 @@ test "Font keeps requested and acknowledged faces as separate plain state" {
try std.testing.expectEqualStrings(installed[0].path, request);
try std.testing.expect(p.settings.font.pending);
try std.testing.expect(p.takeFontRequest() == null);
- const fake_track: layout.Track = .{
+ const fake_track: animation.Track = .{
.serial = p.panes[p.active].?.serial,
.pane = @intCast(p.active),
.effect = .slide,
@@ -2329,7 +2333,7 @@ test "restored sessions animate layout changes after bootstrap" {
source.dump_out.?,
);
defer restored.deinit();
- try std.testing.expectEqual(layout.Transition.slide, restored.settings.panel_transition);
+ try std.testing.expectEqual(animation.Transition.slide, restored.settings.panel_transition);
try std.testing.expect(!restored.animationActive());
var frame: std.heap.ArenaAllocator = .init(std.testing.allocator);
@@ -3284,8 +3288,8 @@ test "layout commits publish finite tracks only for changed panes" {
try std.testing.expect(p.presentation.tracks[1] == null);
try std.testing.expect(p.presentation.tracks[0] != null);
try std.testing.expect(p.presentation.tracks[2] != null);
- try std.testing.expectEqual(layout.Phase.moving, p.presentation.tracks[0].?.phase);
- try std.testing.expectEqual(layout.Phase.opening, p.presentation.tracks[2].?.phase);
+ try std.testing.expectEqual(animation.Phase.moving, p.presentation.tracks[0].?.phase);
+ try std.testing.expectEqual(animation.Phase.opening, p.presentation.tracks[2].?.phase);
try std.testing.expectEqual(untouched, p.rects[1]);
try std.testing.expect(p.animationActive());
@@ -3294,7 +3298,7 @@ test "layout commits publish finite tracks only for changed panes" {
const surface = try p.render(frame.allocator());
try std.testing.expectEqual(@as(usize, 2), surface.panelTracks().len);
- for (0..layout.Transition.slide.frames()) |_| p.update(.tick);
+ for (0..animation.Transition.slide.frames()) |_| p.update(.tick);
try std.testing.expect(!p.animationActive());
_ = frame.reset(.retain_capacity);
try std.testing.expectEqual(@as(usize, 0), (try p.render(frame.allocator())).panelTracks().len);
@@ -3363,7 +3367,7 @@ test "vertical close samples only a canonical baseline containing that pane" {
_ = try p.newShell(1, "");
try std.testing.expect(layout.splitColumn(p, 0, 1, false));
p.sync();
- for (0..layout.Transition.vertical.frames()) |_| p.update(.tick);
+ for (0..animation.Transition.vertical.frames()) |_| p.update(.tick);
_ = frame.reset(.retain_capacity);
const canonical = try p.render(frame.allocator());
try std.testing.expectEqual(@as(usize, 0), canonical.panelTracks().len);
@@ -3452,7 +3456,7 @@ test "canonical fallback and transition toggle retire unpresented tracks" {
p.acknowledgePanelPresentation(moving.panelTracks());
try std.testing.expect(moving.panelTracks().len > 0);
exec.applySettingBuiltin(p, config.Runtime.find("PanelZoom").?, null);
- try std.testing.expectEqual(layout.Transition.zoom, p.settings.panel_transition);
+ try std.testing.expectEqual(animation.Transition.zoom, p.settings.panel_transition);
try std.testing.expect(p.presentation.pending);
try std.testing.expect(p.presentation.pointer(p.screen_w, p.screen_h, 10, 4) == null);
for (p.presentation.tracks) |track| try std.testing.expect(track == null);
@@ -3506,11 +3510,11 @@ test "back-to-back layout commits retarget from the last presented boxes" {
const target_c = p.presentation.tracks[0] orelse return error.MissingSecondRetarget;
try std.testing.expect(target_c.from.eql(shown_a));
try std.testing.expect(target_c.to.eql(layout.panelBox(p.rects[0])));
- try std.testing.expectEqual(layout.Phase.moving, target_c.phase);
+ try std.testing.expectEqual(animation.Phase.moving, target_c.phase);
_ = frame.reset(.retain_capacity);
const latest = try p.render(frame.allocator());
- var submitted: ?layout.Track = null;
+ var submitted: ?animation.Track = null;
for (latest.panelTracks()) |track| if (track.pane == 0) {
submitted = track;
break;
@@ -3533,13 +3537,13 @@ test "an unpresented opening pane remains in the opening paint phase" {
try std.testing.expect(layout.splitColumn(p, 0, 1, false));
p.sync();
const first = p.presentation.tracks[1] orelse return error.MissingOpeningTrack;
- try std.testing.expectEqual(layout.Phase.opening, first.phase);
+ try std.testing.expectEqual(animation.Phase.opening, first.phase);
_ = try p.newShell(2, "");
try std.testing.expect(layout.splitColumn(p, 1, 2, true));
p.sync();
const retargeted = p.presentation.tracks[1] orelse return error.MissingOpeningRetarget;
- try std.testing.expectEqual(layout.Phase.opening, retargeted.phase);
+ try std.testing.expectEqual(animation.Phase.opening, retargeted.phase);
try std.testing.expect(retargeted.from.eql(first.from));
}
@@ -3577,7 +3581,7 @@ test "core composes ASCII bytes while visual-only cells pass through" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
- const presented = try p.composeAsciiTransitions(arena.allocator(), &surface);
+ const presented = try draw.composeAsciiTransitions(p, arena.allocator(), &surface);
try std.testing.expect(presented != &surface);
try std.testing.expectEqualStrings("C", presented.at(0, 0).grapheme());
try std.testing.expectEqualStrings("x", presented.at(1, 0).grapheme());
@@ -3612,7 +3616,7 @@ test "core composes character motion out of the new grid, not a fade" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
- const presented = try p.composeAsciiTransitions(arena.allocator(), &surface);
+ const presented = try draw.composeAsciiTransitions(p, arena.allocator(), &surface);
try std.testing.expect(presented != &surface);
var seen: [8]u8 = undefined;
for (&seen, 0..) |*byte, col| byte.* = presented.at(@intCast(col), 0).text[0];
@@ -3620,8 +3624,8 @@ test "core composes character motion out of the new grid, not a fade" {
try std.testing.expectEqualStrings("a", surface.at(0, 0).grapheme());
// The last active sample is the untouched canonical grid, with no copy.
- surface.panel_tracks[0].frame = layout.Transition.edges.frames() - 1;
- try std.testing.expect(try p.composeAsciiTransitions(arena.allocator(), &surface) == &surface);
+ surface.panel_tracks[0].frame = animation.Transition.edges.frames() - 1;
+ try std.testing.expect(try draw.composeAsciiTransitions(p, arena.allocator(), &surface) == &surface);
}
test "repeated non-dyadic column splits preserve every unrelated boundary" {
@@ -3775,7 +3779,7 @@ pub const Pardes = struct {
rects: [MAX_PANES]Rect = @splat(.{}),
col_x: [MAX_COLS]u16 = undefined,
col_w: [MAX_COLS]u16 = undefined,
- presentation: layout.Presentation = .{},
+ presentation: Presentation = .{},
active: usize = 0,
jumps: [MAX_JUMPS]Loc = undefined,
@@ -4643,7 +4647,7 @@ pub const Pardes = struct {
pub fn pump(p: *Pardes, h: Host) !void {
p.host = h;
const v = h.vtable;
- if (v.wait_input) |f| f(h.ctx, if (p.animationActive()) layout.Animation.frame_ms else 0);
+ if (v.wait_input) |f| f(h.ctx, if (p.animationActive()) animation.Animation.frame_ms else 0);
while (p.nextQueued()) |ev| p.update(ev);
while (p.nextEffect()) |e| p.perform(e);
turn.settle();
@@ -5404,7 +5408,7 @@ pub const Pardes = struct {
// ---- dumb undo/redo: whole-state snapshots, one per edit op ----
- pub fn acknowledgePanelPresentation(p: *Pardes, tracks: []const layout.Track) void {
+ pub fn acknowledgePanelPresentation(p: *Pardes, tracks: []const animation.Track) void {
p.presentation.acknowledge(p, tracks);
if (p.pointer_inside and p.drag != .none) {
if (p.presentation.pointer(p.screen_w, p.screen_h, p.pointer_raw_col, p.pointer_raw_row)) |mapped| {
@@ -5779,430 +5783,9 @@ pub const Pardes = struct {
return null;
}
- // ---- render: build the canonical surface ----
+ // ---- render: build the canonical surface (draw.zig) ----
- pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
- panes.File.refreshHighlights(p);
- const s = &p.surface;
- const ncells = @as(usize, p.screen_w) * p.screen_h;
- if (s.cells.len != ncells) {
- const cells = try p.gpa.alloc(Cell, ncells);
- p.gpa.free(s.cells);
- s.cells = cells;
- }
- s.cols = p.screen_w;
- s.rows = p.screen_h;
- s.cursor = null;
- s.pointer_shape = .arrow;
- if (comptime pdf_enabled) if (p.pdf_hover_preview) |preview| {
- if (preview.probe.link != null and preview.pane < p.panes.len) {
- if (p.panes[preview.pane]) |pane| {
- if (pane.serial == preview.serial and panes.Pdf.paneNativeReady(p, pane))
- s.pointer_shape = .link;
- }
- }
- };
- // A look-hover preview means the pointed word is a real Look or Exec
- // operand: the same plumbing a click would act on. Hosts that model
- // the pointer per-text (the macOS I-beam) read this as "target" and
- // afford it the arrow instead.
- if (p.look_hover_preview != null) s.pointer_shape = .target;
- // Stale hover bits must not survive into a frame whose pointer has
- // moved on: the ordinary content paint does not clear fields it
- // never set.
- for (s.cells) |*c| c.hover = false;
- for (&s.body_layers) |*layer| layer.rows = 0;
- for (&s.tag_layers) |*layer| layer.cols = 0;
- s.nimages = 0;
- s.npanel_tracks = 0;
- s.previous_cells = &.{};
- s.previous_body_layers = &.{};
- s.previous_tag_layers = &.{};
- s.cell_diffs = &.{};
- const chrome = p.chromeTheme();
- const th = p.theme();
- // Separators have their own role; a scrollbar need not frame the page.
- s.fill(0, 0, s.cols, s.rows, .{ .bg = .{ .rgb = chrome.border } });
-
- for (&p.panes, 0..) |*slot, id| {
- const pane = slot.* orelse continue;
- pane.body_rows = 0;
- pane.body_visible_rows = 0;
- pane.context_row_limit = null;
- 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 body_layer.renderBodyLayer(p, arena, pane, p.rects[id], id);
- }
- for (&p.panes, 0..) |*slot, id| {
- const pane = slot.* orelse continue;
- if (pane.notices.len == 0) continue;
- const r = p.rects[id];
- const tx = r.x + config.GUTTER;
- const tw = r.w - config.GUTTER;
- // Painted the way the sticky context headers above them are: the
- // tagline font and the tagline's own colours, so a notice reads as
- // a band belonging to the pane rather than as a line of body text
- // that happens to sit at the bottom of it.
- const msg_style: CellStyle = .{
- .fg = .{ .rgb = chrome.tag_fg },
- .bg = .{ .rgb = chrome.tag_bg },
- .font_role = .tagline,
- };
- // An OVERLAY over the body's top rows, at the right edge, one
- // chip to a row in the order collectNotices chose -- not a row
- // taken out of the body the way a tree-sitter context header is.
- // The chip is as wide as its own message and no wider, so the text
- // beneath the rest of the row keeps reading as the file it is.
- const first = p.bodyTop(pane, r);
- var leader_buf: [16]u8 = undefined;
- for (pane.notices.slice(), 0..) |kind, i| {
- const row = first + @as(u16, pane.notices.row[i]);
- // Every notice is painted on the canonical grid as well as
- // emitted as a tag layer: the grid is what a terminal client
- // draws, and a band it cannot see is a message it never gets.
- // The GUI skips grid cells a tag layer covers, so nothing is
- // drawn twice there.
- const text = Messages.noticeText(p, pane, i, &leader_buf);
- const chip = Messages.noticeCols(p, text, tw);
- if (chip == 0) continue;
- const cx = tx + tw - chip;
- pane.notices.left[i] = cx;
- // A message coming or going blends toward the page it sits
- // on; a grid cannot move it by less than a row.
- var style = msg_style;
- if (Messages.noticeLife(pane, i)) |life| {
- // No page colour to fade to (a terminal's own default):
- // the text dissolves into its chip instead.
- const alpha = Messages.messageMotion(p, life).alpha;
- const page = th.bg orelse chrome.tag_bg;
- style.fg = .{ .rgb = Messages.blendRgb(chrome.tag_fg, page, alpha) };
- style.bg = .{ .rgb = Messages.blendRgb(chrome.tag_bg, page, alpha) };
- }
- s.fill(cx, row, chip, 1, .{ .bg = style.bg, .font_role = .tagline });
- // One column short of the chip's right edge: a prompt's caret
- // sits AFTER its last character the moment it opens, and flush
- // right would put it past the pane, off the grid, and past what
- // the detached wire will encode -- which drops every frame for
- // as long as the prompt is up.
- const printed = Messages.printRight(s, cx, row, chip -| 1, text, style);
- if (kind != .prompt or id != p.active or pane.prompt == .del_side) continue;
- // The cursor follows the text to wherever it landed; a caret
- // in the part a narrow band dropped has nowhere to be.
- const prompt_col = panes.File.displayWidth(text[0..@min(@as(usize, @intCast(@max(0, pane.input.cur_col))), text.len)]);
- if (prompt_col < printed.dropped) continue;
- const caret = printed.left + (prompt_col - printed.dropped);
- if (caret < tx + tw)
- s.cursor = .{ .x = @intCast(caret), .y = row, .bar = pane.input.mode == .insert };
- }
- }
-
- try body_layer.paintHeaders(p);
- try body_layer.renderTagLayers(p, arena);
-
- switch (p.drag) {
- .border_v => |d| {
- if (d.cur_x < s.cols) {
- var row: u16 = p.topBarHeight();
- while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎");
- }
- if (d.corner) |k| if (d.cur_y < s.rows) {
- var col = if (k.col == d.left_col) p.col_x[k.col] else d.cur_x +| 1;
- const end = if (k.col == d.left_col) d.cur_x else p.col_x[k.col] +| p.col_w[k.col] -| 1;
- while (col <= end and col < s.cols) : (col += 1) s.overlayDash(col, d.cur_y, "╌");
- };
- },
- .border_h => |d| if (d.cur_y < s.rows) {
- var col = p.col_x[d.col];
- while (col < p.col_x[d.col] + p.col_w[d.col]) : (col += 1) s.overlayDash(col, d.cur_y, "╌");
- },
- .column_move => |d| rail: {
- const source = layout.columnBySerial(p, d.serial) orelse break :rail;
- // Where the column's left edge would land, reordered or not.
- const edge: u16 = switch (layout.columnDrop(p, source, d.cur_x, d.grab)) {
- .stay => break :rail,
- .reorder => |target| if (target < source) p.col_x[target] else p.col_x[target] + p.col_w[target],
- .edge => |x| x,
- };
- // Leave the outside insertion rail visible above compact
- // text layers. This clips presentation only, never reflows.
- if (edge == s.cols) {
- for (&s.body_layers) |*layer| if (layer.rows > 0 and layer.viewport.x + layer.viewport.w == s.cols) {
- layer.viewport.w -|= 1;
- };
- for (&s.tag_layers) |*layer| if (layer.cols > 0 and layer.viewport.x + layer.viewport.w == s.cols and layer.viewport.y >= p.topBarHeight()) {
- layer.viewport.w -|= 1;
- };
- }
- // The rail runs in the seam cell left of the edge, where a
- // border drag's does: the edge cell itself is a gutter, which
- // the GUI paints as a scroll rail that swallowed the dashes.
- // At the window's left edge there is no seam, so it runs just
- // past that scroll rail instead. It takes the lit grip's
- // color: this is where the grabbed column lands.
- const x = @min(if (edge == 0) config.GUTTER - 1 else edge - 1, s.cols -| 1);
- var row: u16 = p.topBarHeight();
- while (row < s.rows) : (row += 1) {
- // Past the left edge's scroll rail the rail would cut
- // through column 0's grip and pane boxes: leave them whole.
- if (edge == 0 and s.at(x, row).style.font_role == .tagline) continue;
- s.overlayDash(x, row, "╎");
- s.at(x, row).style.fg = .{ .rgb = chrome.column_box };
- }
- },
- .move => |d| if (d.cur_x < s.cols) {
- if (layout.movePlacement(p, d.id, d.cur_x, d.cur_y)) |placement| {
- var col: u16 = p.col_x[placement.preview_col];
- while (col < p.col_x[placement.preview_col] + p.col_w[placement.preview_col]) : (col += 1) {
- s.set(col, placement.row, "╌", .{ .fg = .{ .rgb = chrome.lineno } });
- }
- s.set(d.cur_x, placement.row, "▌", .{ .fg = .{ .rgb = chrome.lineno } });
- }
- },
- .none, .select => if (p.pointer_inside) {
- // hover hints on a pane's own trailing edge
- var c: usize = 0;
- while (c + 1 < p.ncol) : (c += 1) {
- if (p.hover_col == p.col_x[c] + p.col_w[c] -| 1) {
- var row: u16 = p.topBarHeight();
- while (row < s.rows) : (row += 1) s.overlayDash(p.hover_col, row, "╎");
- }
- }
- for (0..p.ncol) |cc| {
- if (p.hover_col < p.col_x[cc] or p.hover_col >= p.col_x[cc] + p.col_w[cc]) continue;
- // the same seam the h-handle hit test picks (see there)
- if (mouse.seamIdxAt(p, cc, p.hover_row) != null) {
- var col = p.col_x[cc];
- while (col < p.col_x[cc] + p.col_w[cc]) : (col += 1) s.overlayDash(col, p.hover_row, "╌");
- }
- }
- for (0..p.ncol -| 1) |cn| {
- if (p.hover_col != p.col_x[cn] + p.col_w[cn] -| 1) continue;
- if (mouse.seamIdxAt(p, cn, p.hover_row) != null) continue;
- if (mouse.seamIdxAt(p, cn + 1, p.hover_row) == null) continue;
- var col = p.col_x[cn + 1];
- while (col < p.col_x[cn + 1] + p.col_w[cn + 1]) : (col += 1) s.overlayDash(col, p.hover_row, "╌");
- }
- },
- }
-
- // debug overlay: a small stats box drawn last, top-right ("Debug")
- if (p.settings.debug) if (p.panes[p.active]) |at| {
- var nlive: usize = 0;
- for (p.panes) |slot| {
- if (slot != null) nlive += 1;
- }
- var sb_off: usize = undefined;
- var sb_total: usize = undefined;
- if (at.file) |*f| {
- sb_off = f.scroll;
- sb_total = panes.File.nlines(p.gpa, f);
- } else if (at.pdfPage()) |page| {
- sb_off = page;
- sb_total = if (comptime pdf_enabled) at.pdf.?.page_count else 0;
- } else {
- const sb = panes.Terminal.scrollbar(at);
- sb_off = sb.offset;
- sb_total = sb.total;
- }
- const effective_font = p.settings.font.effective_name.get();
- const font_name = if (effective_font.len == 0) "default" else effective_font;
- var ov_buf: [256]u8 = undefined;
- const text = std.fmt.bufPrint(
- &ov_buf,
- "pardes resizes={d}\ntheme {s}\nfont {s}\nterms {d} cols {d}\nactive #{d} {d}x{d}\nscroll {d}/{d}\npinch {d:6.4}\ntscroll {d:7.4}",
- .{ p.resize_count, p.theme().name, font_name[0..@min(font_name.len, 64)], nlive, p.ncol, p.active, at.cols, at.rows, sb_off, sb_total, p.ov_pinch_scale, p.ov_touch_scroll_delta },
- ) catch "";
- // wide enough for the longest theme name compiled in, which is the
- // only row here whose width is not ours to choose
- const ow: u16 = 38;
- const oh: u16 = 10;
- const ox: u16 = if (s.cols > ow) s.cols - ow else 0;
- if (ox + ow <= s.cols and p.topBarHeight() + oh <= s.rows) {
- // vaxis single_rounded border ring, default style
- s.set(ox, p.topBarHeight(), "╭", .{});
- s.set(ox + ow - 1, p.topBarHeight(), "╮", .{});
- s.set(ox, p.topBarHeight() + oh - 1, "╰", .{});
- s.set(ox + ow - 1, p.topBarHeight() + oh - 1, "╯", .{});
- var bx = ox + 1;
- while (bx < ox + ow - 1) : (bx += 1) {
- s.set(bx, p.topBarHeight(), "─", .{});
- s.set(bx, p.topBarHeight() + oh - 1, "─", .{});
- }
- var by = p.topBarHeight() + 1;
- while (by < p.topBarHeight() + oh - 1) : (by += 1) {
- s.set(ox, by, "│", .{});
- s.set(ox + ow - 1, by, "│", .{});
- }
- s.fill(ox + 1, p.topBarHeight() + 1, ow - 2, oh - 2, .{ .bg = .{ .rgb = chrome.scroll_track } });
- var lines = std.mem.splitScalar(u8, text, '\n');
- var ly: u16 = p.topBarHeight() + 1;
- while (lines.next()) |line| : (ly += 1) {
- if (ly >= p.topBarHeight() + oh - 1) break;
- _ = s.print(ox + 1, ly, ow - 2, line, .{ .fg = .{ .rgb = chrome.tag_fg } });
- }
- }
- };
- try p.presentation.submit(p, s);
- return p.composeAsciiTransitions(arena, s);
- }
-
- fn composeAsciiTransitions(p: *Pardes, arena: std.mem.Allocator, canonical: *Surface) !*Surface {
- _ = p;
- if (!canonical.hasPanelDiff()) return canonical;
- var presented: ?*Surface = null;
- for (canonical.panelTracks()) |track| {
- if (!track.effect.composedByCore() or track.phase == .closing) continue;
- const area = layout.CellArea.of(track.to);
- const col_end = @min(canonical.cols, area.x0 +| area.cols);
- const row_end = @min(canonical.rows, area.y0 +| area.rows);
- var row: u16 = area.y0;
- while (row < row_end) : (row += 1) {
- var col: u16 = area.x0;
- while (col < col_end) : (col += 1) {
- const index = @as(usize, row) * canonical.cols + col;
- const composed = composedCell(canonical, track, area, col, row, index) orelse
- continue;
- if (composed.visuallyEqual(&canonical.cells[index])) continue;
- if (presented == null) {
- const next = try arena.create(Surface);
- next.* = canonical.*;
- next.cells = try arena.dupe(Cell, canonical.cells);
- presented = next;
- }
- presented.?.cells[index] = composed;
- }
- }
- }
- return presented orelse canonical;
- }
-
- fn composedCell(
- canonical: *const Surface,
- track: layout.Track,
- area: layout.CellArea,
- col: u16,
- row: u16,
- index: usize,
- ) ?Cell {
- if (track.effect == .ascii) {
- const diff = switch (canonical.cell_diffs[index]) {
- .ascii => |diff| diff,
- .unchanged, .visual => return null,
- };
- const byte = diff.byteAt(track.frame);
- if (byte == diff.to) return null;
- if (track.frame == 0) return canonical.previous_cells[index];
- return withByte(canonical.cells[index], byte);
- }
- return switch (layout.charSource(track, col - area.x0, row - area.y0, area)) {
- .old => canonical.previous_cells[index],
- .byte => |byte| withByte(canonical.cells[index], byte),
- // Churn belongs on the glyph, not on the pane's empty space, and it
- // borrows whichever side of the diff actually has one.
- .churn => |byte| blk: {
- const new_cell = &canonical.cells[index];
- if (hasGlyph(new_cell)) break :blk withByte(new_cell.*, byte);
- const old_cell = &canonical.previous_cells[index];
- if (hasGlyph(old_cell)) break :blk withByte(old_cell.*, byte);
- break :blk old_cell.*;
- },
- .at => |offset| blk: {
- const source_col = @as(i32, col) + offset.cols;
- const source_row = @as(i32, row) + offset.rows;
- // A glyph whose source lies outside the pane has not arrived
- // yet: the frozen old cell is the only truthful thing under it.
- if (source_col < area.x0 or source_row < area.y0 or
- source_col >= @as(i32, area.x0) + area.cols or
- source_row >= @as(i32, area.y0) + area.rows or
- source_col >= canonical.cols or source_row >= canonical.rows)
- break :blk canonical.previous_cells[index];
- const source = @as(usize, @intCast(source_row)) * canonical.cols +
- @as(usize, @intCast(source_col));
- break :blk canonical.cells[source];
- },
- };
- }
-
- fn withByte(cell: Cell, byte: u8) Cell {
- var out = cell;
- out.text[0] = byte;
- out.len = 1;
- out.default = false;
- return out;
- }
-
- /// Whether a cell shows a glyph a character effect can work on. An unpainted
- /// cell and a space are both blank space, whatever style they carry.
- fn hasGlyph(cell: *const Cell) bool {
- return !cell.default and !(cell.len == 1 and cell.text[0] == ' ');
- }
-
- 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();
- if (r.w <= config.GUTTER or r.h == 0) return;
- const s = &p.surface;
- const th = p.theme();
- const chrome = p.chromeTheme();
- const tx = r.x + config.GUTTER; // text area (tag + body), right of the gutter
- const tw = r.w - config.GUTTER;
- const tag_y = p.tagTop(pane, r);
- const body_y = p.bodyTop(pane, r);
- const pane_bg: Color = if (th.bg) |c| .{ .rgb = c } else .default;
-
- // text area resets to terminal-default cells (vaxis clear semantics);
- // light themes paint the page over it.
- {
- // Two full passes over every cell in the pane, every frame.
- const tz_clear = tracy.zone(@src(), "paneClear");
- defer tz_clear.end();
- s.clearRect(tx, r.y, tw, r.h);
- if (th.bg) |bg| s.fill(tx, r.y, tw, r.h, .{ .bg = .{ .rgb = bg } });
- }
-
- const box_bg = if (active) chrome.box else chrome.box_dim;
- const box_lum = (@as(u16, box_bg[0]) * 3 + @as(u16, box_bg[1]) * 6 + @as(u16, box_bg[2])) / 10;
- const box_ink: [3]u8 = if (box_lum > 140) .{ 0x00, 0x00, 0x00 } else .{ 0xff, 0xff, 0xff };
- const box_style: CellStyle = .{
- .fg = .{ .rgb = box_ink },
- .bg = .{ .rgb = box_bg },
- // Grips keep physical grid dimensions, independently of tag text.
- .font_role = .tagline,
- };
- s.fill(r.x, tag_y, config.GUTTER, pane.tag_rows, box_style);
- s.set(r.x, tag_y, switch (pane.body.mode) {
- .normal => config.tag_normal,
- .insert => config.tag_insert,
- .tty => config.tag_tty,
- }, box_style);
-
- try body_layer.paintPaneTag(p, arena, pane, r, id, active, p.tagCapacity(r.w -| TAG_TEXT_INSET));
-
- // Folded panes retain their body state, but neither body pixels nor
- // its cursor belong outside the one-row tag rectangle.
- if (pane.collapsed) return;
-
- if (comptime pdf_enabled)
- if (pane.hasPdf() and panes.Pdf.draw(p, pane, r, id, tx, tw)) return;
-
- if (pane.image) |*iv| {
- // Below the notice chips: a picture is drawn after the cells (the
- // GUI's image pass, kitty's z-order), so a chip over it would be
- // painted out. Text gets the overlay; a picture gives up the rows.
- const shown = pane.notices.span();
- const image_h = (r.h -| pane.tag_rows) -| shown;
- panes.Image.draw(p, iv, @intCast(id), pane.serial, tx, body_y + shown, tw, image_h);
- // thumbless, but the same one column as the real scrollbar below —
- // that is the whole point of drawing it, and like that one it runs
- // past the notice bands so the gutter has no notch in it
- s.fill(r.x, body_y, 1, r.h -| pane.tag_rows, .{ .bg = .{ .rgb = chrome.scroll_track } });
- s.fill(r.x + 1, body_y, 1, r.h -| pane.tag_rows, .{ .bg = pane_bg });
- return;
- }
-
- try body_layer.renderBody(p, arena, pane, r, id, active, true);
- }
+ pub const render = draw.render;
pub fn appendImagePlace(p: *Pardes, place: ImagePlace) bool {
const s = &p.surface;