summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 14:46:38 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1 (patch)
tree642150be8703f1d9a0766ff18516f06bfdc8801a /src/pardes.zig
parent379f4fe55c7c9c7c8cc7b97dc921c83e2c24698b (diff)
downloadpardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.tar.gz
pardes-40c19fd4f52d20df94c2dfa2d68102b89bd9d3f1.zip
Add optional compact tagline styling for source context
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig272
1 files changed, 250 insertions, 22 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 3965c200..b6d6ed8f 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -69,6 +69,8 @@ pub const pdf_raster_policy: PdfRasterPolicy = switch (platform) {
pub const Pane = panes.Pane;
pub const MAX_PANES = 16;
+pub const BodyLayer = @import("body_layer.zig").Layer;
+pub const RowMetrics = @import("body_layer.zig").Metrics;
pub const PDF_PAGE_GAP_PX = panes.Pdf.page_gap_px;
pub const MAX_COLS = 6;
const column_weight_unit = layout.column_weight_unit;
@@ -253,7 +255,9 @@ pub const Loc = struct {
col: u32,
};
-test {}
+test {
+ _ = @import("body_layer.zig");
+}
const pane_tail = " " ++ config.pane_builtins_str;
const pdf_pane_tail = " " ++ config.pdf_pane_builtins_str;
@@ -1118,6 +1122,45 @@ test "LocationsConfig command reports partial updates and survives restore" {
try std.testing.expectEqual(expected, restored.locations_config);
}
+test "TreeContext tag style preserves message rows and its opt-out survives restore" {
+ if (comptime !syntax.enabled) return error.SkipZigTest;
+ const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 60, .rows = 16 });
+ defer p.deinit();
+ const pane = try p.setTestFile("pub const Box = struct {\n pub fn run() void {\n" ++ (" // body\n" ** 30) ++ " }\n};\n");
+ p.gpa.free(pane.file.?.path);
+ pane.file.?.path = try p.gpa.dupe(u8, "/test.zig");
+ if (!panes.File.supportsContext(pane)) return error.SkipZigTest;
+ p.update(.{ .resize = .{ .cols = 60, .rows = 16, .row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 6, .tagline_h = 12 } } });
+ pane.file.?.tree_context = true;
+ pane.file.?.scroll = 8;
+ pane.msg_len = 0;
+ _ = try p.render(p.scratch.allocator());
+ try std.testing.expect(p.settings.tree_context_tag_style);
+ const full_height = p.surface.body_layers[0].viewport.h;
+ try std.testing.expect(p.surface.body_layers[0].rows > full_height);
+ try std.testing.expectEqual(FontRole.tagline, p.surface.body_layers[0].cells[0].style.font_role);
+ pane.msg[0] = '!';
+ pane.msg_len = 1;
+ _ = try p.render(p.scratch.allocator());
+ try std.testing.expectEqual(full_height - 1, p.surface.body_layers[0].viewport.h);
+ const r = p.rects[0];
+ try std.testing.expectEqualStrings("!", p.surface.at(r.x + config.GUTTER, r.y + r.h - 1).grapheme());
+ pane.msg_len = 0;
+ p.leader_on = true;
+ _ = try p.render(p.scratch.allocator());
+ try std.testing.expectEqual(full_height - 1, p.surface.body_layers[0].viewport.h);
+ p.leader_on = false;
+ try std.testing.expect(p.executeBuiltinLine(0, "TreeContextTagStyle"));
+ _ = try p.render(p.scratch.allocator());
+ try std.testing.expectEqual(@as(u16, 0), p.surface.body_layers[0].rows);
+ try std.testing.expectEqual(@as(u16, 0), pane.body_rows);
+ try std.testing.expectEqual(FontRole.body, p.surface.at(r.x + config.GUTTER, r.y + BOX_H).style.font_role);
+ try p.dumpState();
+ const restored = try Pardes.initFromDump(p.gpa, .{ .tty_only = true }, p.dump_out.?);
+ defer restored.deinit();
+ try std.testing.expect(!restored.settings.tree_context_tag_style);
+}
+
test "TreeContext command and custom tag survive a workspace round trip" {
if (comptime !syntax.enabled) return error.SkipZigTest;
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true });
@@ -3496,6 +3539,7 @@ test "pixel attachment fit and pan do not invalidate cached pixels" {
pub const Surface = struct {
pub const PointerShape = enum(u8) { arrow, link };
pointer_shape: PointerShape = .arrow,
+ body_layers: [MAX_PANES]BodyLayer = @splat(.{}),
cols: u16 = 0,
rows: u16 = 0,
cells: []Cell = &.{},
@@ -3506,8 +3550,13 @@ pub const Surface = struct {
panel_tracks: [MAX_PANES * 2]layout.Track = undefined,
npanel_tracks: usize = 0,
previous_cells: []const Cell = &.{},
+ previous_body_layers: []const BodyLayer = &.{},
cell_diffs: []const PanelCellDiff = &.{},
+ pub fn bodyLayers(s: *const Surface) []const BodyLayer {
+ return &s.body_layers;
+ }
+
pub fn panelTracks(s: *const Surface) []const layout.Track {
return s.panel_tracks[0..s.npanel_tracks];
}
@@ -4388,6 +4437,16 @@ test "normal header Enter executes its command rather than searching for it" {
}
pub const Mouse = struct {
+ /// The host has inverted a compact body layer. Physical col/row still
+ /// route pane chrome; these coordinates address the pane's logical text.
+ pub const BodyHit = struct {
+ serial: u32,
+ row: u16,
+ col: u16,
+ pixel_x: f32,
+ pixel_y: f32,
+ metrics: RowMetrics,
+ };
// sel indexing relies on left/middle/right being 0/1/2
pub const Button = enum { left, middle, right, wheel_up, wheel_down, wheel_left, wheel_right, none, back, forward };
pub const Kind = enum { press, release, motion, drag };
@@ -4396,6 +4455,7 @@ pub const Mouse = struct {
col: u16,
row: u16,
ctrl: bool = false,
+ body_hit: ?BodyHit = null,
};
pub const Key = struct {
@@ -4487,6 +4547,7 @@ pub const Event = union(enum) {
cols: u16,
rows: u16,
cell_pixels: CellPixels = .{},
+ row_metrics: ?RowMetrics = null,
},
output: struct { pane: u8, bytes: []const u8 },
eof: struct { pane: u8 },
@@ -4577,6 +4638,7 @@ const LookHoverWait = struct {
pane: usize,
serial: u32,
frames: u16 = 0,
+ body_hit: ?Mouse.BodyHit = null,
};
const LookHoverPreview = struct {
@@ -4586,6 +4648,7 @@ const LookHoverPreview = struct {
serial: u32,
sel: ?Pane.Sel,
file_word: ?FileWordSpan = null,
+ body_hit: ?Mouse.BodyHit = null,
};
const FileWordSpan = struct {
@@ -5732,6 +5795,7 @@ pub const Pardes = struct {
screen_w: u16,
screen_h: u16,
cell_pixels: CellPixels = .{},
+ row_metrics: RowMetrics = .{},
resize_count: usize = 0,
panes: [MAX_PANES]?*Pane = @splat(null),
@@ -5779,6 +5843,7 @@ pub const Pardes = struct {
hover_row: u16 = 0,
pointer_raw_col: u16 = 0,
pointer_raw_row: u16 = 0,
+ pointer_body_hit: ?Mouse.BodyHit = null,
raw_hover_intent: bool = false,
pointer_inside: bool = false,
look_hover_wait: ?LookHoverWait = null,
@@ -6009,6 +6074,7 @@ pub const Pardes = struct {
p.scratch.deinit();
p.frame_arena.deinit();
gpa.free(p.surface.cells);
+ for (&p.surface.body_layers) |*layer| gpa.free(layer.cells);
p.presentation.deinit(gpa);
if (p.surface.images.len > 0) gpa.free(p.surface.images);
gpa.destroy(p);
@@ -6871,6 +6937,7 @@ pub const Pardes = struct {
switch (ev) {
.resize => |sz| {
p.presentation.snap_once = true;
+ if (sz.row_metrics) |metrics| p.row_metrics = metrics;
if (comptime pdf_enabled) {
var before: [MAX_PANES]?panes.Pdf.Viewport = @splat(null);
for (p.panes, 0..) |slot, id| {
@@ -6933,7 +7000,10 @@ pub const Pardes = struct {
.fs_req => |r| p.emit(.{ .fs_reply = filesystem.handle(p, r) }),
.pinch => |scale| p.ov_pinch_scale = scale,
.touch_scroll => |delta| p.ov_touch_scroll_delta = delta,
- .pointer_leave => p.pointer_inside = false,
+ .pointer_leave => {
+ p.pointer_inside = false;
+ p.pointer_body_hit = null;
+ },
.tick => {
p.chrome_animation.advance();
p.presentation.advance();
@@ -7594,7 +7664,8 @@ pub const Pardes = struct {
const body = try p.bodyText(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 last = @min(@as(i32, pane.rows) - 1, @max(gesture.r0, gesture.r1) - BOX_H);
+ const logical_rows = if (pane.body_rows > 0) pane.body_rows else pane.rows;
+ const last = @min(@as(i32, logical_rows) - 1, @max(gesture.r0, gesture.r1) - BOX_H);
if (first > last) return;
const rows = try p.gpa.alloc(Pane.PointerRow, @intCast(last - first + 1));
errdefer p.gpa.free(rows);
@@ -10812,13 +10883,43 @@ pub const Pardes = struct {
on_tag: bool,
};
- fn pointerTextSelection(p: *const Pardes, id: usize, col: u16, row: u16) ?PointerTextSelection {
+ fn bodyHitForPane(p: *const Pardes, id: usize, supplied: Mouse.BodyHit, clamp: bool) ?Mouse.BodyHit {
+ const pane = p.panes[id] orelse return null;
+ if (pane.file == null or (!clamp and supplied.serial != pane.serial)) return null;
+ if (!std.math.isFinite(supplied.pixel_x) or !std.math.isFinite(supplied.pixel_y) or
+ supplied.metrics.body_w == 0 or supplied.metrics.body_h == 0 or
+ supplied.metrics.tagline_w == 0 or supplied.metrics.tagline_h == 0) return null;
+ var mapped_hit = supplied;
+ if (clamp) {
+ mapped_hit.serial = pane.serial;
+ const layer = for (&p.surface.body_layers) |*layer| {
+ if (layer.rows > 0 and layer.serial == pane.serial) break layer;
+ } else return null;
+ const bw: f32 = @floatFromInt(mapped_hit.metrics.body_w);
+ const bh: f32 = @floatFromInt(mapped_hit.metrics.body_h);
+ const x0 = @as(f32, @floatFromInt(layer.viewport.x)) * bw;
+ const y0 = @as(f32, @floatFromInt(layer.viewport.y)) * bh;
+ const x1 = x0 + @as(f32, @floatFromInt(layer.viewport.w)) * bw;
+ const y1 = y0 + @as(f32, @floatFromInt(layer.viewport.h)) * bh;
+ mapped_hit.pixel_x = std.math.clamp(mapped_hit.pixel_x, x0, @max(x0, x1 - 0.001));
+ mapped_hit.pixel_y = std.math.clamp(mapped_hit.pixel_y, y0, @max(y0, y1 - 0.001));
+ }
+ return p.reprojectBodyHit(mapped_hit);
+ }
+
+ fn pointerTextSelection(p: *const Pardes, id: usize, col: u16, row: u16, body_hit: ?Mouse.BodyHit) ?PointerTextSelection {
if (p.panes[id] == null) return null;
const r = p.rects[id];
if (col < r.x + config.GUTTER or col >= r.x + r.w or row < r.y or row >= r.y + r.h) return null;
const tag_y = if (p.settings.tag_bottom) r.y + r.h -| BOX_H else r.y;
const body_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H;
const on_tag = row >= tag_y and row < tag_y + BOX_H;
+ if (!on_tag) if (body_hit) |supplied| {
+ const mapped_hit = p.bodyHitForPane(id, supplied, false) orelse return null;
+ const c = @as(i32, mapped_hit.col) - config.GUTTER;
+ const v = @as(i32, mapped_hit.row) + BOX_H;
+ return .{ .sel = .{ .state = .dragging, .c0 = c, .c1 = c, .r0 = v, .r1 = v }, .on_tag = false };
+ };
const c: i32 = @as(i32, col) - @as(i32, r.x + config.GUTTER) +
(if (on_tag) @as(i32, p.panes[id].?.tag_scroll) else 0);
const v: i32 = if (on_tag) 0 else @as(i32, row) - @as(i32, body_y) + @as(i32, BOX_H);
@@ -10848,22 +10949,28 @@ pub const Pardes = struct {
return null;
}
- fn noteLookHover(p: *Pardes, col: u16, row: u16) void {
+ fn sameBodyCell(a: ?Mouse.BodyHit, b: ?Mouse.BodyHit) bool {
+ const first = a orelse return b == null;
+ const second = b orelse return false;
+ return first.serial == second.serial and first.row == second.row and first.col == second.col;
+ }
+
+ fn noteLookHover(p: *Pardes, col: u16, row: u16, body_hit: ?Mouse.BodyHit) void {
const delay = config.look_preview_delay_frames orelse return;
_ = delay;
const id = p.paneAt(col, row) orelse return p.cancelLookHover();
const pane = p.panes[id] orelse return p.cancelLookHover();
- _ = p.pointerTextSelection(id, col, row) orelse return p.cancelLookHover();
+ _ = p.pointerTextSelection(id, col, row, body_hit) orelse return p.cancelLookHover();
if (comptime pdf_enabled) if (p.pdf_hover_preview) |shown|
if (shown.col == col and shown.row == row and shown.pane == id and shown.serial == pane.serial) return;
if (p.look_hover_preview) |shown|
- if (shown.col == col and shown.row == row and shown.pane == id and shown.serial == pane.serial) return;
+ if (shown.col == col and shown.row == row and shown.pane == id and shown.serial == pane.serial and sameBodyCell(shown.body_hit, body_hit)) return;
if (p.look_hover_wait) |waiting|
- if (waiting.col == col and waiting.row == row and waiting.pane == id and waiting.serial == pane.serial) return;
+ if (waiting.col == col and waiting.row == row and waiting.pane == id and waiting.serial == pane.serial and sameBodyCell(waiting.body_hit, body_hit)) return;
p.cancelLookHover();
- p.look_hover_wait = .{ .col = col, .row = row, .pane = id, .serial = pane.serial };
+ p.look_hover_wait = .{ .col = col, .row = row, .pane = id, .serial = pane.serial, .body_hit = body_hit };
}
fn refreshLookHoverFromRaw(p: *Pardes) void {
@@ -10872,7 +10979,11 @@ pub const Pardes = struct {
return p.cancelLookHover();
p.hover_col = mapped.col;
p.hover_row = mapped.row;
- p.noteLookHover(mapped.col, mapped.row);
+ const body_hit: ?Mouse.BodyHit = if (p.pointer_body_hit) |mapped_hit|
+ p.bodyHitForPane(p.paneAt(mapped.col, mapped.row) orelse return p.cancelLookHover(), mapped_hit, false) orelse return p.cancelLookHover()
+ else
+ null;
+ p.noteLookHover(mapped.col, mapped.row, body_hit);
}
fn advanceLookHover(p: *Pardes) void {
@@ -10882,7 +10993,7 @@ pub const Pardes = struct {
if (waiting.frames < delay) return;
const pane = p.panes[waiting.pane] orelse return p.cancelLookHover();
if (pane.serial != waiting.serial) return p.cancelLookHover();
- const pointed = p.pointerTextSelection(waiting.pane, waiting.col, waiting.row) orelse
+ const pointed = p.pointerTextSelection(waiting.pane, waiting.col, waiting.row, waiting.body_hit) orelse
return p.cancelLookHover();
if (comptime pdf_enabled) if (!pointed.on_tag and panes.Pdf.paneNativeReady(p, pane)) {
const probe = panes.Pdf.probeAt(p, pane, waiting.col, waiting.row) orelse
@@ -10910,6 +11021,7 @@ pub const Pardes = struct {
.serial = waiting.serial,
.sel = preview,
.file_word = operand.file_word,
+ .body_hit = waiting.body_hit,
};
p.look_hover_wait = null;
}
@@ -10920,7 +11032,7 @@ pub const Pardes = struct {
if (p.presentation.pointer(p.screen_w, p.screen_h, p.pointer_raw_col, p.pointer_raw_row)) |mapped| {
p.hover_col = mapped.col;
p.hover_row = mapped.row;
- p.dragUpdate(mapped.col, mapped.row);
+ p.dragUpdate(mapped.col, mapped.row, p.pointer_body_hit);
}
}
p.refreshLookHoverFromRaw();
@@ -10946,6 +11058,18 @@ pub const Pardes = struct {
const raw_row = @min(m.row, p.screen_h -| 1);
p.pointer_raw_col = raw_col;
p.pointer_raw_row = raw_row;
+ const previous_hit = p.pointer_body_hit;
+ p.pointer_body_hit = m.body_hit;
+ // A drag can leave the compact text viewport. Preserve its metrics so
+ // the endpoint still clamps to that body's last visible logical row.
+ if (m.body_hit == null and (m.kind == .drag or m.kind == .release) and p.drag == .select) {
+ if (previous_hit) |previous| {
+ var continued = previous;
+ continued.pixel_x = (@as(f32, @floatFromInt(raw_col)) + 0.5) * @as(f32, @floatFromInt(previous.metrics.body_w));
+ continued.pixel_y = (@as(f32, @floatFromInt(raw_row)) + 0.5) * @as(f32, @floatFromInt(previous.metrics.body_h));
+ p.pointer_body_hit = continued;
+ }
+ }
p.pointer_inside = true;
if (m.button == .none and m.kind == .motion) {
p.raw_hover_intent = true;
@@ -11137,10 +11261,9 @@ pub const Pardes = struct {
} else {
p.active = id;
if (pane.tag_edit) exitTagEdit(pane);
- const sc: i32 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
- const v: i32 = @as(i32, mrow) - @as(i32, body_y) + @as(i32, BOX_H);
+ const pointed = p.pointerTextSelection(id, mcol, mrow, m.body_hit) orelse return;
pane.clearPointerSelection(sel_slot);
- pane.sel[sel_slot] = .{ .state = .dragging, .c0 = sc, .c1 = sc, .r0 = v, .r1 = v };
+ pane.sel[sel_slot] = pointed.sel;
p.drag = .{ .select = .{
.id = id,
.button = config.select_button,
@@ -11156,8 +11279,11 @@ pub const Pardes = struct {
} };
}
},
- .drag => p.dragUpdate(mcol, mrow),
- .release => p.dragRelease(config.select_button),
+ .drag => p.dragUpdate(mcol, mrow, p.pointer_body_hit),
+ .release => {
+ if (p.pointer_body_hit != null and p.drag.button() == m.button) p.dragUpdate(mcol, mrow, p.pointer_body_hit);
+ p.dragRelease(config.select_button);
+ },
.motion => {},
},
config.exec_button, config.look_button => switch (m.kind) {
@@ -11191,7 +11317,7 @@ pub const Pardes = struct {
const local = @as(i32, mrow) - @as(i32, body_y);
if (m.button == config.look_button) p.active = id;
pane.scrollBy(if (m.button == config.look_button) local else -local);
- } else if (p.pointerTextSelection(id, mcol, mrow)) |pointed| {
+ } else if (p.pointerTextSelection(id, mcol, mrow, m.body_hit)) |pointed| {
if (m.button == config.look_button) p.active = id;
pane.clearPointerSelection(@intFromEnum(m.button));
pane.sel[@intFromEnum(m.button)] = pointed.sel;
@@ -11213,8 +11339,11 @@ pub const Pardes = struct {
};
}
},
- .drag => p.dragUpdate(mcol, mrow),
- .release => p.dragRelease(m.button),
+ .drag => p.dragUpdate(mcol, mrow, p.pointer_body_hit),
+ .release => {
+ if (p.pointer_body_hit != null and p.drag.button() == m.button) p.dragUpdate(mcol, mrow, p.pointer_body_hit);
+ p.dragRelease(m.button);
+ },
.motion => {},
},
}
@@ -11228,7 +11357,7 @@ pub const Pardes = struct {
return null;
}
- fn dragUpdate(p: *Pardes, mcol: u16, mrow: u16) void {
+ fn dragUpdate(p: *Pardes, mcol: u16, mrow: u16, body_hit: ?Mouse.BodyHit) void {
switch (p.drag) {
.border_v => |*d| {
const c = d.left_col;
@@ -11261,6 +11390,12 @@ pub const Pardes = struct {
if (p.panes[s.id]) |pane| {
const r = p.rects[s.id];
const b = @intFromEnum(s.button);
+ if (pane.body_rows > 0 and pane.sel[b].r0 >= BOX_H) if (body_hit) |supplied| {
+ const mapped_hit = p.bodyHitForPane(s.id, supplied, true) orelse return;
+ pane.sel[b].c1 = @as(i32, mapped_hit.col) - config.GUTTER;
+ pane.sel[b].r1 = @as(i32, mapped_hit.row) + BOX_H;
+ return;
+ };
pane.sel[b].c1 = @as(i32, mcol) - @as(i32, r.x + config.GUTTER);
if (p.settings.tag_bottom) {
const body_h = r.h -| BOX_H;
@@ -12095,6 +12230,7 @@ pub const Pardes = struct {
pub fn applySettingBuiltin(p: *Pardes, setting: config.Runtime.Setting, arg: ?[]const u8) void {
const previous_colors = p.settings.colors;
+ const previous_context_style = p.settings.tree_context_tag_style;
const previous_transition = p.settings.panel_transition;
const previous_tagline_percent = p.settings.font.tagline_percent;
const previous_column_tags = p.settings.column_tags;
@@ -12122,6 +12258,17 @@ pub const Pardes = struct {
if (pane.file) |*file| file.syntax_dirty = true;
}
}
+ if (p.settings.tree_context_tag_style != previous_context_style) {
+ for (p.panes, 0..) |slot, id| {
+ p.surface.body_layers[id].rows = 0;
+ const pane = slot orelse continue;
+ pane.body_rows = 0;
+ pane.body_visible_rows = 0;
+ pane.context_row_limit = null;
+ pane.wrap_n = 0;
+ }
+ p.abandonPanelAnimations();
+ }
const transition_changed = setting.action == .transition and
p.settings.panel_transition != previous_transition;
const tagline_metrics_changed = setting.action == .tagline_size and
@@ -12242,6 +12389,7 @@ pub const Pardes = struct {
.topbar_custom = if (p.global_tag.custom) p.global_tag.slice("") else null,
.theme = p.theme().name,
.locations_config = p.locations_config,
+ .tree_context_tag_style = p.settings.tree_context_tag_style,
.columns = columns[0..columns_len],
.panes = dump_panes[0..panes_len],
.mounts = mounts[0..p.fs.mounts.items.len],
@@ -12294,6 +12442,7 @@ pub const Pardes = struct {
p.lsp_seq = old.lsp_seq;
p.pipe_seq = old.pipe_seq;
p.cell_pixels = old.cell_pixels;
+ p.row_metrics = old.row_metrics;
p.native_images = old.native_images;
p.fs.socket_path = old.fs.socket_path;
p.fs.tcp_address = old.fs.tcp_address;
@@ -12306,6 +12455,7 @@ pub const Pardes = struct {
defer parsed.deinit();
const st = parsed.value;
p.locations_config = st.locations_config;
+ p.settings.tree_context_tag_style = st.tree_context_tag_style;
for (st.mounts) |mount| {
var already_mounted = false;
for (p.fs.mounts.items) |existing| {
@@ -12722,6 +12872,14 @@ pub const Pardes = struct {
p.invalidateThemeDependentRasters();
}
+ pub fn reprojectBodyHit(p: *const Pardes, body_hit: Mouse.BodyHit) ?Mouse.BodyHit {
+ for (&p.surface.body_layers) |*layer| {
+ if (layer.rows == 0 or layer.serial != body_hit.serial) continue;
+ return layer.hitAt(body_hit.pixel_x, body_hit.pixel_y, @floatFromInt(body_hit.metrics.body_w), @floatFromInt(body_hit.metrics.body_h), @floatFromInt(body_hit.metrics.tagline_w), @floatFromInt(body_hit.metrics.tagline_h));
+ }
+ return null;
+ }
+
// ---- render: build the canonical surface ----
pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
@@ -12745,9 +12903,11 @@ pub const Pardes = struct {
}
}
};
+ for (&s.body_layers) |*layer| layer.rows = 0;
s.nimages = 0;
s.npanel_tracks = 0;
s.previous_cells = &.{};
+ s.previous_body_layers = &.{};
s.cell_diffs = &.{};
const chrome = p.chromeTheme();
const th = p.theme(); // the message row paints in the editor's colours
@@ -12756,7 +12916,12 @@ pub const Pardes = struct {
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;
try p.renderPane(arena, pane, p.rects[id], id, id == p.active);
+ try p.renderBodyLayer(arena, pane, p.rects[id], id);
}
for (&p.panes, 0..) |*slot, id| {
const pane = slot.* orelse continue;
@@ -13190,6 +13355,56 @@ 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 message = if (pane.promptAt()) |at| pane.tagSlice()[@min(at, pane.tag_tail_len)..].len > 0 else pane.msg_len > 0;
+ const reserve: u16 = if (message or (id == p.active and p.leader_on)) 1 else 0;
+ const viewport_rows = r.h - BOX_H -| reserve;
+ 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 = if (p.settings.tag_bottom) r.y else r.y + BOX_H, .w = width, .h = viewport_rows };
+ layer.cols = width;
+ layer.rows = @intCast(logical_rows);
+ layer.context_rows = 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();
@@ -13315,6 +13530,19 @@ pub const Pardes = struct {
return;
}
+ try p.renderBody(arena, pane, r, id, active, true);
+ }
+
+ 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;
+ const body_y = if (p.settings.tag_bottom) r.y else r.y + BOX_H;
+ 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 = if (r.h > BOX_H) r.h - BOX_H else 0;
const body_style: CellStyle = .{
@@ -13503,7 +13731,7 @@ pub const Pardes = struct {
}
}
- if (r.h > BOX_H) {
+ if (scrollbar and r.h > BOX_H) {
s.fill(r.x, body_y, 1, r.h - BOX_H, .{ .bg = .{ .rgb = chrome.scroll_track } });
s.fill(r.x + 1, body_y, 1, r.h - BOX_H, .{ .bg = pane_bg });
const sb: struct { total: usize, offset: usize, len: usize } = if (pane.file) |*f| .{