summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/body_layer.zig79
-rw-r--r--src/draw.zig90
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);
+ }
+}