summaryrefslogtreecommitdiff
path: root/src/draw.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-28 14:59:21 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:15 -0300
commit360b72ade0acfa655a3740b638a0cc8e23238a05 (patch)
tree4bc9464da2cdcfd6fe636979f0b457221ec92c3d /src/draw.zig
parentd88e381793c6505bf3f2af43705bd2fa32f55ef6 (diff)
downloadpardes-360b72ade0acfa655a3740b638a0cc8e23238a05.tar.gz
pardes-360b72ade0acfa655a3740b638a0cc8e23238a05.zip
The GUI draws in tiers from the regions; pane chrome moves with its pane
Stage 8 of docs/render-pipeline.md. The frame is drawn in groups: tier 0, one per track in paint order (tiers 2 and 3), tier 4 (notices, then guides and the debug box), tier 5 (bar cursors), each cells, images, decor. Decor (rules, rails, thumbs, grip marks, spines, the workspace and column rules, the bottom band, notice rules, bar cursors) is whole-pixel rects through the new decor.frag over ui.vert, so it carries its track's transition and clip. A closing pane's chrome comes from Surface.previous_regions. Deleted: taglineBaseRgb, topbarPaneBorderHeight, bottomTaglinePresent, frameChromeBg, cellBackgroundIs and the rail inference, paneGripCell's scan, transient_on, PaintPlan. One cover map (coverFrame) is marked from the layers and regions once a frame. New regions column, guide and debug; Surface.chrome is the palette, carried in wire v8 (not shipped yet), so an attached GUI draws the same chrome (test). A per-instance clip replaces the vertical transition's scissor. Goldens: 01-12 byte-identical. 13-17 differ only past the grid: the image pass left its scissor at the grid's size, cutting rule ends, rail feet and the bottom band in the leftover pixels whenever a picture was on screen. 16-debug now shows the debug box, which a context-row layer had hidden. 18-mid-transition is new: virtual clock (PARDES_TEST_CLOCK in the GUI), PanelSlide Newcol with the picture, frame 6 of 12. Also: the GUI sleeps when idle instead of polling every 16 ms, and a minimized or occluded window sleeps through animation. Tracy 'gui frame build' at 200x60: 992/1015 us median before, 989/987 us after. Shared files touched: pardes.zig (one export), detached/client.zig, detached/server.zig, detached/wire.zig (all additive), gui.zig. Not touched: Messages.zig, mouse.zig, tagline.zig, colors.zig, tty.zig, host_io.zig, dump.zig, exec.zig, panes.zig.
Diffstat (limited to 'src/draw.zig')
-rw-r--r--src/draw.zig63
1 files changed, 45 insertions, 18 deletions
diff --git a/src/draw.zig b/src/draw.zig
index 7cfe7554..4242a6e7 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -47,14 +47,7 @@ const NOTICE_LAYER_BASE = pardes.NOTICE_LAYER_BASE;
/// painters below and every shell read these rectangles.
pub fn place(p: *Pardes, s: *Surface) void {
s.nregions = 0;
- const add = struct {
- fn region(surface: *Surface, r: Region) void {
- std.debug.assert(surface.nregions < surface.regions.len);
- surface.regions[surface.nregions] = r;
- surface.nregions += 1;
- }
- }.region;
- add(s, .{ .kind = .page, .rect = .{ .x = 0, .y = 0, .w = s.cols, .h = s.rows } });
+ s.addRegion(.{ .kind = .page, .rect = .{ .x = 0, .y = 0, .w = s.cols, .h = s.rows } });
for (&p.panes, 0..) |*slot, id| {
const pane = slot.* orelse continue;
Messages.collectNotices(p, pane, p.rects[id], id);
@@ -64,12 +57,12 @@ pub fn place(p: *Pardes, s: *Surface) void {
const active = id == p.active;
const tag_y = p.tagTop(pane, r);
const body_y = p.bodyTop(pane, r);
- add(s, .{ .kind = .grip, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x, .y = tag_y, .w = config.GUTTER, .h = pane.tag_rows } });
- add(s, .{ .kind = .tag, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x + config.GUTTER, .y = tag_y, .w = r.w - config.GUTTER, .h = pane.tag_rows } });
+ s.addRegion(.{ .kind = .grip, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x, .y = tag_y, .w = config.GUTTER, .h = pane.tag_rows } });
+ s.addRegion(.{ .kind = .tag, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x + config.GUTTER, .y = tag_y, .w = r.w - config.GUTTER, .h = pane.tag_rows } });
// A folded pane is its tag rows and nothing else.
if (pane.collapsed) continue;
const body_h = r.h -| pane.tag_rows;
- add(s, .{ .kind = .body, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x + config.GUTTER, .y = body_y, .w = r.w - config.GUTTER, .h = body_h } });
+ s.addRegion(.{ .kind = .body, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x + config.GUTTER, .y = body_y, .w = r.w - config.GUTTER, .h = body_h } });
var rail: Region = .{ .kind = .rail, .owner = owner, .serial = pane.serial, .active = active, .rect = .{ .x = r.x, .y = body_y, .w = config.GUTTER, .h = body_h } };
// An image's rail has no thumb; a native PDF's is measured as its
// pages are placed (Pdf.draw); every other pane's is here.
@@ -93,7 +86,7 @@ pub fn place(p: *Pardes, s: *Surface) void {
rail.thumb_y = @intCast(@min(pos, track_h));
rail.thumb_h = @intCast(@min(len, track_h - rail.thumb_y));
}
- add(s, rail);
+ s.addRegion(rail);
// Each notice a chip at the right edge, one to a row in the order
// collectNotices chose, as wide as its own message and no wider, so
// the text beneath the rest of the row keeps reading as the file.
@@ -106,7 +99,7 @@ pub fn place(p: *Pardes, s: *Surface) void {
const chip = Messages.noticeCols(p, text, tw);
if (chip == 0) continue;
pane.notices.left[i] = tx + tw - chip;
- add(s, .{ .kind = .notice, .owner = owner, .serial = pane.serial, .active = active, .notice = @intCast(i), .rect = .{ .x = tx + tw - chip, .y = body_y + @as(u16, pane.notices.row[i]), .w = chip, .h = BOX_H } });
+ s.addRegion(.{ .kind = .notice, .owner = owner, .serial = pane.serial, .active = active, .notice = @intCast(i), .rect = .{ .x = tx + tw - chip, .y = body_y + @as(u16, pane.notices.row[i]), .w = chip, .h = BOX_H } });
}
}
const column_rows = p.columnBarHeight();
@@ -114,11 +107,16 @@ pub fn place(p: *Pardes, s: *Surface) void {
if (p.col_w[c] == 0) continue;
const active = if (layout.findPane(p, p.active)) |found| found.col == c else false;
const inset = @min(TAG_TEXT_INSET, p.col_w[c]);
- add(s, .{ .kind = .column_grip, .owner = @intCast(c), .serial = p.col_serial[c], .active = active, .rect = .{ .x = p.col_x[c], .y = p.topBarHeight(), .w = @min(config.GUTTER, p.col_w[c]), .h = column_rows } });
- add(s, .{ .kind = .column_tag, .owner = @intCast(c), .serial = p.col_serial[c], .active = active, .rect = .{ .x = p.col_x[c] + inset, .y = p.topBarHeight(), .w = p.col_w[c] - inset, .h = column_rows } });
+ s.addRegion(.{ .kind = .column_grip, .owner = @intCast(c), .serial = p.col_serial[c], .active = active, .rect = .{ .x = p.col_x[c], .y = p.topBarHeight(), .w = @min(config.GUTTER, p.col_w[c]), .h = column_rows } });
+ s.addRegion(.{ .kind = .column_tag, .owner = @intCast(c), .serial = p.col_serial[c], .active = active, .rect = .{ .x = p.col_x[c] + inset, .y = p.topBarHeight(), .w = p.col_w[c] - inset, .h = column_rows } });
};
+ const active_col = if (layout.findPane(p, p.active)) |found| found.col else null;
+ for (0..p.ncol) |c| {
+ if (p.col_w[c] == 0) continue;
+ s.addRegion(.{ .kind = .column, .owner = @intCast(c), .serial = p.col_serial[c], .active = active_col == c, .rect = .{ .x = p.col_x[c], .y = p.topBarHeight(), .w = p.col_w[c], .h = s.rows -| p.topBarHeight() } });
+ }
if (p.settings.workspace_tag)
- add(s, .{ .kind = .workspace_tag, .rect = .{ .x = 0, .y = 0, .w = s.cols, .h = p.topBarHeight() } });
+ s.addRegion(.{ .kind = .workspace_tag, .rect = .{ .x = 0, .y = 0, .w = s.cols, .h = p.topBarHeight() } });
// A column carried to the window's right edge shows where it lands as a
// rail in the last cell: the layers under it stop a cell short.
if (p.drag == .column_move) rail: {
@@ -176,8 +174,20 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
s.previous_cells = &.{};
s.previous_body_layers = &.{};
s.previous_tag_layers = &.{};
+ s.previous_regions = &.{};
s.cell_diffs = &.{};
const chrome = p.chromeTheme();
+ s.chrome = .{
+ .page = p.theme().bg,
+ .fg = p.theme().fg,
+ .border = chrome.border,
+ .topbar_rule = config.gui_topbar_pane_border_rgb orelse chrome.border,
+ .tag_bg = chrome.tag_bg,
+ .tag_focus_bg = if (p.settings.focus_tint) chrome.tag_active_bg else chrome.tag_bg,
+ .tag_fg = chrome.tag_fg,
+ .scroll_track = chrome.scroll_track,
+ .scroll_thumb = chrome.scroll_thumb,
+ };
place(p, s);
// Separators have their own role; a scrollbar need not frame the page.
const whole = s.regions[0].rect;
@@ -203,16 +213,20 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
if (d.cur_x < s.cols) {
var row: u16 = p.topBarHeight();
while (row < s.rows) : (row += 1) s.overlayDash(d.cur_x, row, "╎");
+ s.addRegion(.{ .kind = .guide, .rect = .{ .x = d.cur_x, .y = p.topBarHeight(), .w = 1, .h = s.rows -| p.topBarHeight() } });
}
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 first = 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;
+ var col = first;
while (col <= end and col < s.cols) : (col += 1) s.overlayDash(col, d.cur_y, "╌");
+ if (col > first) s.addRegion(.{ .kind = .guide, .rect = .{ .x = first, .y = d.cur_y, .w = col - first, .h = 1 } });
};
},
.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, "╌");
+ s.addRegion(.{ .kind = .guide, .rect = .{ .x = p.col_x[d.col], .y = d.cur_y, .w = p.col_w[d.col], .h = 1 } });
},
.column_move => |d| rail: {
const source = layout.columnBySerial(p, d.serial) orelse break :rail;
@@ -232,13 +246,20 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
// 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();
+ // A guide region for each run of it the tags do not break.
+ var run: u16 = row;
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;
+ if (edge == 0 and s.at(x, row).style.font_role == .tagline) {
+ if (row > run) s.addRegion(.{ .kind = .guide, .rect = .{ .x = x, .y = run, .w = 1, .h = row - run } });
+ run = row + 1;
+ continue;
+ }
s.overlayDash(x, row, "╎");
s.at(x, row).style.fg = .{ .rgb = chrome.column_box };
}
+ if (row > run) s.addRegion(.{ .kind = .guide, .rect = .{ .x = x, .y = run, .w = 1, .h = row - run } });
},
.move => |d| if (d.cur_x < s.cols) {
if (layout.movePlacement(p, d.id, d.cur_x, d.cur_y)) |placement| {
@@ -247,6 +268,8 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
s.set(col, placement.row, "╌", .{ .fg = .{ .rgb = chrome.lineno } });
}
s.set(d.cur_x, placement.row, "▌", .{ .fg = .{ .rgb = chrome.lineno } });
+ s.addRegion(.{ .kind = .guide, .rect = .{ .x = p.col_x[placement.preview_col], .y = placement.row, .w = p.col_w[placement.preview_col], .h = 1 } });
+ s.addRegion(.{ .kind = .guide, .rect = .{ .x = d.cur_x, .y = placement.row, .w = 1, .h = 1 } });
}
},
.none, .select => if (p.pointer_inside) {
@@ -256,6 +279,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
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, "╎");
+ s.addRegion(.{ .kind = .guide, .rect = .{ .x = p.hover_col, .y = p.topBarHeight(), .w = 1, .h = s.rows -| p.topBarHeight() } });
}
}
for (0..p.ncol) |cc| {
@@ -264,6 +288,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
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, "╌");
+ s.addRegion(.{ .kind = .guide, .rect = .{ .x = p.col_x[cc], .y = p.hover_row, .w = p.col_w[cc], .h = 1 } });
}
}
for (0..p.ncol -| 1) |cn| {
@@ -272,6 +297,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
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, "╌");
+ s.addRegion(.{ .kind = .guide, .rect = .{ .x = p.col_x[cn + 1], .y = p.hover_row, .w = p.col_w[cn + 1], .h = 1 } });
}
},
}
@@ -309,6 +335,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
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) {
+ s.addRegion(.{ .kind = .debug, .rect = .{ .x = ox, .y = p.topBarHeight(), .w = ow, .h = oh } });
// vaxis single_rounded border ring, default style
s.set(ox, p.topBarHeight(), "╭", .{});
s.set(ox + ow - 1, p.topBarHeight(), "╮", .{});