diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 14:13:25 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | 7284b72396398213356577345151bd79e497d27c (patch) | |
| tree | 60e98eed31c4469b967f427996a8767eaaea77d8 /src | |
| parent | 0de2e7f04977b22df3f7b6ab1690eb8a6c905cd4 (diff) | |
| download | pardes-7284b72396398213356577345151bd79e497d27c.tar.gz pardes-7284b72396398213356577345151bd79e497d27c.zip | |
Paint a body with context rows once, into its layer, and join it
joinBody replaces the second paint of a file body with compact tree-sitter
context rows: the body is painted once into its body layer (more rows than
the grid has) and the grid's body rows are copies of the layer's first
ones; the rail is the grid's alone, from its region. Which rows are context
rows is still decided by a text build before the paint, and a body without
them reuses that text and paints straight onto the grid: every other body
has no row the grid cannot show, so it gets no layer and no copy.
Each part now paints only its own rows. The tag paints the tag rows of
every pointer sweep that crosses them (the select sweep in the tag's
colours, the look-hover affordance over it, a middle or right sweep over
both in its button's tint), which the body used to paint over the tag on
the grid alone; the body paints only body rows, and a select sweep begun on
the tag marks the body rows it crossed before the text is laid down, as it
did. Grid and layers agree, the snapshot goldens are byte-identical, and a
test checks the joined grid rows and caret against a plain paint of the
same body with the tag on top and at the bottom. The GUI goldens gain a
terminal pane scene.
Diffstat (limited to 'src')
| -rw-r--r-- | src/body_layer.zig | 79 | ||||
| -rw-r--r-- | src/draw.zig | 90 |
2 files changed, 127 insertions, 42 deletions
diff --git a/src/body_layer.zig b/src/body_layer.zig index e1a45058..ac87a07b 100644 --- a/src/body_layer.zig +++ b/src/body_layer.zig @@ -257,15 +257,33 @@ pub fn paintPointerSelection( } } -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 <= pane.tag_rows) return; +/// The selection colour of each pointer button's sweep: the select button's +/// and the look and exec buttons', each tinted toward its own accent. +pub fn buttonTints(th: *const colors.Theme) [3][3]u8 { + const accent2 = if (std.mem.eql(u8, &th.num, &th.str)) th.comment else th.num; + return .{ + 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)), + }; +} + +/// A pane's body, painted once. A file body with compact tree-sitter +/// context rows is painted into its body layer -- more rows than the grid +/// has, the context rows at the tagline's height -- and the grid's rows are +/// copies of that layer's first ones, at body height. Any other body has no +/// rows the grid cannot show, and is painted straight onto the grid. The +/// rail is the grid's alone. +pub fn joinBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool, rail: pardes.Region) !void { + const file = if (pane.file) |*f| f else return renderBody(p, s, arena, pane, r, id, active, rail, null); + if (!p.settings.tree_context_tag_style or pane.collapsed or r.h <= pane.tag_rows) + return renderBody(p, s, arena, pane, r, id, active, rail, null); + // Which rows are context rows is decided by the text the body shows. + const text = try panes.File.bodyText(arena, pane, file, p.settings.wrap); const viewport_rows = r.h - pane.tag_rows; - 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; + if (pane.context_rows == 0 or logical_rows <= pane.context_rows) + return renderBody(p, s, arena, pane, r, id, active, rail, text); 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); @@ -274,14 +292,14 @@ pub fn renderBodyLayer(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rec 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 layer = &s.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 + pane.tag_rows)); @memset(temporary_cells, .{}); var temporary: Surface = .{ .cols = r.w, .rows = @intCast(logical_rows + pane.tag_rows), .cells = temporary_cells }; const synthetic: Rect = .{ .x = 0, .y = 0, .w = r.w, .h = @intCast(logical_rows + pane.tag_rows) }; - try renderBody(p, &temporary, arena, pane, synthetic, id, id == p.active, null); + try renderBody(p, &temporary, arena, pane, synthetic, id, active, null, null); const first_row: usize = if (p.settings.tag_bottom) 0 else pane.tag_rows; for (0..logical_rows) |row| { const start = (row + first_row) * r.w + config.GUTTER; @@ -289,7 +307,7 @@ pub fn renderBodyLayer(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rec } layer.pane = @intCast(id); layer.serial = pane.serial; - const body = p.surface.region(.body, id).?; + const body = s.region(.body, id).?; layer.viewport = .{ .x = body.rect.x, .y = body.rect.y, .w = body.rect.w - body.clip, .h = body.rect.h }; layer.cols = width; layer.rows = @intCast(logical_rows); @@ -300,9 +318,29 @@ pub fn renderBodyLayer(p: *Pardes, arena: std.mem.Allocator, pane: *Pane, r: Rec 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 }; } + // The grid: the layer's first rows, as many as the body has, each a + // body row high; its caret where one of them has it. (pane.rows is the + // body's height after sync, and context rows are only prepared while + // the rows fit the wrap table, so the layer always has these rows.) + std.debug.assert(layer.rows >= viewport_rows); + for (0..viewport_rows) |row| { + const grid_row = s.cells[(body.rect.y + row) * s.cols ..][body.rect.x..][0..width]; + @memcpy(grid_row, layer.cells[row * width ..][0..width]); + } + if (layer.cursor) |cursor| if (cursor.y < viewport_rows) { + s.cursor = .{ .x = body.rect.x + cursor.x, .y = body.rect.y + cursor.y, .bar = cursor.bar }; + }; + const chrome = p.chromeTheme(); + const theme = p.theme(); + const pane_bg: Color = if (theme.bg) |c| .{ .rgb = c } else .default; + if (rail.rect.h > 0) { + s.fill(rail.rect.x, rail.rect.y, 1, rail.rect.h, .{ .bg = .{ .rgb = chrome.scroll_track } }); + s.fill(rail.rect.x + 1, rail.rect.y, 1, rail.rect.h, .{ .bg = pane_bg }); + s.fill(rail.rect.x, rail.rect.y + rail.thumb_y, 1, rail.thumb_h, .{ .bg = .{ .rgb = chrome.scroll_thumb } }); + } } -pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool, rail: ?pardes.Region) !void { +pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r: Rect, id: usize, active: bool, rail: ?pardes.Region, built: ?[]const u8) !void { const th = p.theme(); const chrome = p.chromeTheme(); const tx = r.x + config.GUTTER; @@ -322,8 +360,14 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane .bg = pane_bg, }; const tz_body = tracy.zone(@src(), "bodyText"); - const body = try bodyText(p, arena, pane); + const body = built orelse try bodyText(p, arena, pane); tz_body.end(); + // A select sweep begun on the tag and dragged into the body marks the + // body rows it crossed only where no text is: it is laid down before + // the rows, which paint over it. + const sweep = pane.sel[pardes.sel_slot]; + if (sweep.state != .none and (sweep.state != .done or pardes.sel_slot == 0) and @min(sweep.r0, sweep.r1) < pane.tag_rows) + paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sweep, pane.tag_rows, th.sel_bg, th.sel_fg); { // The rows themselves. `bodyText` above is only the string BUILD; // this is what writes it into the surface. @@ -349,12 +393,7 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane } 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)), - }; + const sel_btn = buttonTints(th); 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 @@ -365,7 +404,7 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane if (preview.file_word) |word| panes.File.paintWordSelection(p, s, pane, r, word.row, word.lo, word.hi, preview_bg) else if (preview.sel) |sel| - paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sel, 0, preview_bg, null); + paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sel, pane.tag_rows, preview_bg, null); } } const modal_hover = if (p.look_hover_preview) |preview| @@ -378,7 +417,7 @@ pub fn renderBody(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane // A select sweep over the tag is the tag's selection, painted there // in the tag's own selection colours. if (b == pardes.sel_slot and @min(sl.r0, sl.r1) < pane.tag_rows) continue; - paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sl, 0, sel_btn[b], th.sel_fg); + paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sl, pane.tag_rows, sel_btn[b], th.sel_fg); } // modal line selection (helix `x`): whole body rows, normal mode only if ((pane.body.mode == .normal or modal_hover) and pane.body.msel.active) { diff --git a/src/draw.zig b/src/draw.zig index b596fc22..7d91ad31 100644 --- a/src/draw.zig +++ b/src/draw.zig @@ -192,7 +192,6 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface { pane.context_row_limit = null; s.body_layers[id].rows = 0; try renderPane(p, s, arena, pane, p.rects[id], id, id == p.active); - try body_layer.renderBodyLayer(p, arena, pane, p.rects[id], id); } // Each notice once, as a tag layer, and the grid's chip a copy of it. for (s.regionList()) |region| { @@ -494,7 +493,7 @@ fn renderPane(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r: return; } - try body_layer.renderBody(p, s, arena, pane, r, id, active, rail.*); + try body_layer.joinBody(p, s, arena, pane, r, id, active, rail.*); } /// The workspace and column tags: each line a tag layer, and the grid's @@ -716,25 +715,6 @@ fn joinPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pane, r if (temporary.cursor) |cursor| if (cursor.x - TAG_TEXT_INSET < r.w - TAG_TEXT_INSET) { s.cursor = .{ .x = r.x + cursor.x, .y = region.rect.y + cursor.y, .bar = cursor.bar }; }; - // A sweep begun on the tag and dragged into the body shows on the body - // rows it crossed, which only the grid has. - const th = p.theme(); - const tx = r.x + TAG_TEXT_INSET; - const tw = r.w - TAG_TEXT_INSET; - const tag_y = region.rect.y; - const body_y = p.bodyTop(pane, r); - 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) < rows) { - const page_rgb = th.bg orelse th.tag_bg; - const preview_bg = colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg)); - body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sel, rows, preview_bg, null); - }; - } - for (pane.sel, 0..) |selection, button| { - if (selection.state == .none or (selection.state == .done and button != 0)) continue; - if (@min(selection.r0, selection.r1) >= rows) continue; - body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, rows, th.sel_bg, th.sel_fg); - } } /// A notice chip, painted once as a tag layer at the tagline's pitch, and the @@ -924,11 +904,27 @@ pub fn paintPaneTag(p: *Pardes, s: *Surface, arena: std.mem.Allocator, pane: *Pa if (col < tw) s.cursor = .{ .x = tx + @as(u16, @intCast(col)), .y = tag_y + @as(u16, @intCast(at.row)), .bar = mode == .insert }; } + // Pointer sweeps over the tag rows. A select sweep is the tag's own, in + // its selection colours; the look-hover affordance goes over it, and a + // middle or right sweep over both, tinted by its button the way the + // body tints it. for (pane.sel, 0..) |selection, button| { if (selection.state == .none or (selection.state == .done and button != 0)) continue; - if (@min(selection.r0, selection.r1) >= rows) continue; + if (button != pardes.sel_slot or @min(selection.r0, selection.r1) >= rows) continue; body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, th.sel_bg, th.sel_fg); } + 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) < rows) { + const preview_bg = colors.mix(page_rgb, colors.mix(page_rgb, th.sel_bg)); + body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, sel, 0, preview_bg, null); + }; + } + const sel_btn = body_layer.buttonTints(th); + for (pane.sel, 0..) |selection, button| { + if (selection.state == .none or (selection.state == .done and button != 0)) continue; + if (button == pardes.sel_slot or @min(selection.r0, selection.r1) >= rows) continue; + body_layer.paintPointerSelection(p, s, pane, r, tx, tw, tag_y, body_y, selection, 0, sel_btn[button], th.sel_fg); + } } test "place puts every part where the painters and layers put it, in every layout" { @@ -1036,3 +1032,53 @@ test "a wide glyph cut by a tag's or a notice's edge is joined into the grid as } } } + +test "a body with context rows is painted once, and the grid's rows are what a plain paint of it gives" { + if (comptime !@import("syntax.zig").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 line\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; + pane.file.?.tree_context = true; + pane.file.?.scroll = 8; + var arena: std.heap.ArenaAllocator = .init(std.testing.allocator); + defer arena.deinit(); + for ([_]bool{ false, true }) |bottom| { + p.settings.tag_bottom = bottom; + // A selection and the cursor on body rows, painted in the one pass. + pane.body.cur_row = 12; + pane.body.cur_col = 3; + pane.body.vsel = .{ .active = true, .row = 10, .col = 2 }; + pane.body.mode = .normal; + p.sync(); + _ = arena.reset(.retain_capacity); + const s = try p.render(arena.allocator()); + const layer = &s.body_layers[0]; + try std.testing.expect(layer.context_rows > 0); + const body = s.region(.body, 0).?; + // The plain paint the grid used to take, of the same frame. + const cells = try arena.allocator().alloc(Cell, s.cells.len); + @memset(cells, .{}); + var plain: Surface = .{ .cols = s.cols, .rows = s.rows, .cells = cells }; + const saved = .{ pane.body_rows, pane.body_visible_rows, pane.context_row_limit }; + pane.body_rows = 0; + pane.body_visible_rows = 0; + pane.context_row_limit = null; + try body_layer.renderBody(p, &plain, arena.allocator(), pane, p.rects[0], 0, true, null, null); + pane.body_rows, pane.body_visible_rows, pane.context_row_limit = saved; + for (0..body.rect.h) |row| { + const y = body.rect.y + row; + for (0..body.rect.w) |col| { + const x = body.rect.x + col; + const got = s.cells[y * s.cols + x]; + const want = plain.cells[y * s.cols + x]; + try std.testing.expect(got.visuallyEqual(&want)); + } + } + try std.testing.expectEqual(plain.cursor, s.cursor); + // The layer's own rows run past the grid's. + try std.testing.expect(layer.rows > body.rect.h); + } +} |
