summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--build.zig1
-rw-r--r--docs/render-pipeline.md14
-rw-r--r--shaders/decor.frag.glsl24
-rw-r--r--shaders/prebuilt/decor.frag.glsl24
-rw-r--r--shaders/prebuilt/decor.frag.spvbin0 -> 2100 bytes
-rw-r--r--shaders/prebuilt/ui.frag.glsl3
-rw-r--r--shaders/prebuilt/ui.frag.spvbin4668 -> 5700 bytes
-rw-r--r--shaders/prebuilt/ui.vert.glsl6
-rw-r--r--shaders/prebuilt/ui.vert.spvbin5468 -> 5612 bytes
-rw-r--r--shaders/ui.frag.glsl3
-rw-r--r--shaders/ui.vert.glsl6
-rw-r--r--src/Presentation.zig5
-rw-r--r--src/detached/client.zig4
-rw-r--r--src/detached/server.zig1
-rw-r--r--src/detached/wire.zig41
-rw-r--r--src/draw.zig63
-rw-r--r--src/gui/gui.zig1472
-rw-r--r--src/pardes.zig1
-rw-r--r--src/surface.zig40
-rw-r--r--test/gui-goldens.txt11
-rw-r--r--test/gui_golden.py18
21 files changed, 976 insertions, 761 deletions
diff --git a/build.zig b/build.zig
index 666e792d..803f773d 100644
--- a/build.zig
+++ b/build.zig
@@ -47,6 +47,7 @@ pub const TreeSitterGrammars = enum { disabled, zig, minimal, full };
const gui_shaders = [_][]const u8{
"ui.vert", "ui.frag",
+ "decor.frag",
"overlay.vert", "overlay.frag",
"image.vert", "image.frag",
"crt.vert", "crt.frag",
diff --git a/docs/render-pipeline.md b/docs/render-pipeline.md
index a8b3f019..69645c96 100644
--- a/docs/render-pipeline.md
+++ b/docs/render-pipeline.md
@@ -810,7 +810,7 @@ stripped. No visual change until stage 9 unless stated.
| 5 | JOIN: paint tags/notices/headers once into layers, copy into grid; the grid wins where the copies disagree, and each disagreement is listed for a later decision | goldens are the oracle; wide-grapheme re-clip at the edge (§3.2) |
| 6 | body layer joined the same way (paint once, copy visible rows) | riskiest join; A/B the old double paint in a temporary test with tag_bottom on and off, then delete it |
| 7 | Layer merge (TagLayer + BodyLayer), wire v8, web accessors | after the other agent lands; touches mouse hit paths; breaks the macOS shell's layer ABI (accepted: macOS build ignored for now); done: one `Layer` (src/Layer.zig) with `rows` (0 = no layer) and a `cursor{x, y}`; a tag of N rows is ONE layer of N grid rows (`tagHit` answers the row as `line`, `bodyHit` keeps its meaning), so the per-line layer bases are gone. Wire v8 ships rows, cursor y and the region list in the one bump; v7 and v9 peers are refused in both directions (tests). web: `tag_layer_value` 11 = rows, 12 = cursor y, and app.mjs lays every row. macOS: its Zig side compiles against `Layer`, but pardes.h still sees one row per tag layer (a taller tag shows its first row there) |
-| 8 | GUI draws from regions: role tiers with track groups in tiers 2-3, page cover, hard-edged snapped decor for rules/rails/grips (pane chrome in the pane's tier), per-instance clip; delete inference functions, `transient_on` and `mark_hover` (breaks macOS glass hover; accepted, macOS ignored for now) | stage-0 PPM goldens byte-identical (possible only because hard decor is snapped and not anti-aliased); pane chrome now also shows during transitions, which is the one allowed visible delta, listed |
+| 8 | GUI draws from regions: role tiers with track groups in tiers 2-3, page cover, hard-edged snapped decor for rules/rails/grips (pane chrome in the pane's tier), per-instance clip; delete inference functions, `transient_on` and `mark_hover` (breaks macOS glass hover; accepted, macOS ignored for now) | stage-0 PPM goldens byte-identical (possible only because hard decor is snapped and not anti-aliased); pane chrome now also shows during transitions, which is the one allowed visible delta, listed Done: the frame is drawn in groups (makeGroups): tier 0, one group per track in paint order (tiers 2 and 3), tier 4 as two groups (the notices; then the guides and the debug box, which the core paints over them), tier 5 (bar cursors), each drawing cells, images, decor. Decor is every rule, rail, thumb, grip mark, spine, the workspace and column rules, the bottom band, notice rules and bar cursors: whole-pixel rects drawn by `decor.frag` over `ui.vert` as cell instances (colour in the ground, coverage in fg.r, blended like the overlay), so each carries its track's transition and clip; a closing pane's comes from the last frame's regions (`Surface.previous_regions`). Only `solid` exists yet: `decor.vert` and the other kinds (§5.1) come with the first effect that needs them. Deleted: taglineBaseRgb, topbarPaneBorderHeight, bottomTaglinePresent, frameChromeBg, cellBackgroundIs and the rail inference, paneGripCell's scan, transient_on, PaintPlan; one cover map (coverFrame: layer, grip and offset, focus, anchor, floating) is marked from layers and regions once a frame. New regions: `column` (spines, anchors, the focused column's tint), `guide`, `debug`; `Surface.chrome` is the palette, on the wire in v8 (not yet shipped, so no bump), and an attached GUI draws the same chrome (test). Per-instance clip (`CellInstance.clip_*`) replaces the vertical transition's scissor. Goldens: 01-12 byte-identical; 13-17 differ only in pixels past the grid (with a picture on screen the image pass left the scissor at the grid's size, cutting every rule end, rail foot and band that runs into the leftover pixels; they now run to the edge as in every scene without one); 16-debug shows the debug box (it was drawn from the grid under the source's context-row layer, so the GUI never showed it there); 18-mid-transition is new (virtual clock, PanelSlide Newcol with the picture, frame 6 of 12: chrome moves with its pane). Other deltas, outside the goldens: a guide over a tag or a context-row body now shows; with WindowOpacity < 100 a layer's bar cursor is ink like the grid's; an attached GUI gains the focus tint, notice rules, spines and the theme's page and caret colours. Deferrals fixed: the GUI sleeps when idle (SDL and queue events wake it; a smooth scroll, a gamepad, the test feed, a shell's kill deadline, a present to retry still poll), and a minimized or occluded window sleeps through animation. Tracy, `gui frame build`, 200x60, terminal output plus scrolling, ReleaseFast, two interleaved runs of ~180 frames: before 992/1015 µs median, after 989/987 µs |
| 9 | post chain: glslang, Shadertoy prefix, ping-pong, ShaderAnimation, redraw levels A/B (§5.5); bundled CRT without barrel; delete scene_effects/crt.zig/crt.frag | first visual change (the CRT look) |
| 10+ | `fx` bookmark: G1–G3 → feel review → G4 bundled → G5 lapis theme → P2s; tty T1–T2 → feel review → removals of audited effects (after user decision) → T3–T5 | each effect its own change, default off |
@@ -845,13 +845,21 @@ goldens; ssh/tty byte budgets; wire version bump breaks mixed-version attach.
6. Revisit at the first visual stage (from stage 5): the notice layer lost
its hover word to keep the grid's behaviour. Notice words are Look/Exec
targets, so bring the hover affordance back in BOTH the grid and the
- layer then.
+ layer then. Carried forward from stage 8 (a GUI-only stage) to the hover
+ change after it, which also retires `mark_hover`: the affordance is a
+ core paint every shell and snapshot sees.
7. Revisit at stage 8 (from stage 6): a body without context rows has no
layer (paint once, straight onto the grid). If the GUI is to read every
body from a layer, give every body one then and measure the copy. The
context-row path also still builds the body's text twice (a pre-pass
with body_rows 0 decides the context rows, renderBody builds it again at
- the layer's rows); the paint is single. Measure both there.
+ the layer's rows); the paint is single. Measure both there. Decided at
+ stage 8, measured (Tracy, 200x60, TreeContext on a scrolled source,
+ ReleaseFast): the pre-pass text build is 5 µs median and the layer's
+ copies 10 µs per context-row body per frame. Kept as they are: nothing in
+ the GUI reads a layerless body from anything but the grid, which is
+ exact, so a layer for every body would be ~10 µs a body a frame for no
+ pixel.
## 15. Questions for the user
diff --git a/shaders/decor.frag.glsl b/shaders/decor.frag.glsl
new file mode 100644
index 00000000..4c2bd6d8
--- /dev/null
+++ b/shaders/decor.frag.glsl
@@ -0,0 +1,24 @@
+#version 450
+
+// Decor: a flat, hard-edged fill (a rule, a rail, a grip mark, a bar cursor)
+// that ui.vert.glsl places like a cell, its track's transition included. Its
+// colour rides in the cell's ground (a_bg) and its coverage in a_fg.r. The
+// pipeline blends it as the overlay pipeline does (backgroundLayerBlend), so
+// a fading rule shows through, and the result is that pipeline's exactly.
+// Ink (a cursor) is drawn with the opacity uniform at 1.
+
+layout(std140, set = 3, binding = 0) uniform UiUniforms {
+ float background_opacity;
+} u_ui;
+
+layout(location = 1) in vec3 v_fg;
+layout(location = 2) in vec3 v_bg;
+layout(location = 5) flat in vec4 v_clip;
+
+layout(location = 0) out vec4 o_col;
+
+void main() {
+ if (v_clip.z >= v_clip.x && (gl_FragCoord.x < v_clip.x || gl_FragCoord.x >= v_clip.z ||
+ gl_FragCoord.y < v_clip.y || gl_FragCoord.y >= v_clip.w)) discard;
+ o_col = vec4(v_bg * clamp(u_ui.background_opacity, 0.0, 1.0), v_fg.r);
+}
diff --git a/shaders/prebuilt/decor.frag.glsl b/shaders/prebuilt/decor.frag.glsl
new file mode 100644
index 00000000..4c2bd6d8
--- /dev/null
+++ b/shaders/prebuilt/decor.frag.glsl
@@ -0,0 +1,24 @@
+#version 450
+
+// Decor: a flat, hard-edged fill (a rule, a rail, a grip mark, a bar cursor)
+// that ui.vert.glsl places like a cell, its track's transition included. Its
+// colour rides in the cell's ground (a_bg) and its coverage in a_fg.r. The
+// pipeline blends it as the overlay pipeline does (backgroundLayerBlend), so
+// a fading rule shows through, and the result is that pipeline's exactly.
+// Ink (a cursor) is drawn with the opacity uniform at 1.
+
+layout(std140, set = 3, binding = 0) uniform UiUniforms {
+ float background_opacity;
+} u_ui;
+
+layout(location = 1) in vec3 v_fg;
+layout(location = 2) in vec3 v_bg;
+layout(location = 5) flat in vec4 v_clip;
+
+layout(location = 0) out vec4 o_col;
+
+void main() {
+ if (v_clip.z >= v_clip.x && (gl_FragCoord.x < v_clip.x || gl_FragCoord.x >= v_clip.z ||
+ gl_FragCoord.y < v_clip.y || gl_FragCoord.y >= v_clip.w)) discard;
+ o_col = vec4(v_bg * clamp(u_ui.background_opacity, 0.0, 1.0), v_fg.r);
+}
diff --git a/shaders/prebuilt/decor.frag.spv b/shaders/prebuilt/decor.frag.spv
new file mode 100644
index 00000000..7e162f5a
--- /dev/null
+++ b/shaders/prebuilt/decor.frag.spv
Binary files differ
diff --git a/shaders/prebuilt/ui.frag.glsl b/shaders/prebuilt/ui.frag.glsl
index 3c366602..0b099db3 100644
--- a/shaders/prebuilt/ui.frag.glsl
+++ b/shaders/prebuilt/ui.frag.glsl
@@ -17,6 +17,7 @@ layout(location = 1) in vec3 v_fg;
layout(location = 2) in vec3 v_bg;
layout(location = 3) in vec2 v_cell_uv;
layout(location = 4) flat in uvec4 v_effect;
+layout(location = 5) flat in vec4 v_clip;
layout(location = 0) out vec4 o_col;
@@ -31,6 +32,8 @@ float cellNoise(uint serial, uint col, uint row) {
}
void main() {
+ if (v_clip.z >= v_clip.x && (gl_FragCoord.x < v_clip.x || gl_FragCoord.x >= v_clip.z ||
+ gl_FragCoord.y < v_clip.y || gl_FragCoord.y >= v_clip.w)) discard;
float a = texture(u_atlas, v_uv).r;
bool oldLayer = (v_effect.x & 0x80000000u) != 0u;
// A block cursor is foreground chrome even though its fill occupies the
diff --git a/shaders/prebuilt/ui.frag.spv b/shaders/prebuilt/ui.frag.spv
index 120438e7..033c10ab 100644
--- a/shaders/prebuilt/ui.frag.spv
+++ b/shaders/prebuilt/ui.frag.spv
Binary files differ
diff --git a/shaders/prebuilt/ui.vert.glsl b/shaders/prebuilt/ui.vert.glsl
index d23b609c..c1779add 100644
--- a/shaders/prebuilt/ui.vert.glsl
+++ b/shaders/prebuilt/ui.vert.glsl
@@ -19,12 +19,17 @@ layout(location = 5) in vec4 a_present_rect;
// the id is what is left below them; integer transport preserves the exact
// cross-backend hash.
layout(location = 6) in uvec4 a_effect;
+// A clip rectangle in target pixels (x0, y0, x1, y1): the fragment shader
+// drops what lies outside it. x1 < x0 is none. A vertical transition clips
+// its pane's instances to the box the pane is growing into.
+layout(location = 7) in vec4 a_clip;
layout(location = 0) out vec2 v_uv;
layout(location = 1) out vec3 v_fg;
layout(location = 2) out vec3 v_bg;
layout(location = 3) out vec2 v_cell_uv;
layout(location = 4) flat out uvec4 v_effect;
+layout(location = 5) flat out vec4 v_clip;
void main() {
vec2 pos;
@@ -70,5 +75,6 @@ void main() {
v_fg = a_fg;
v_bg = a_bg;
v_effect = a_effect;
+ v_clip = a_clip;
gl_Position = vec4(pos, 0.0, 1.0);
}
diff --git a/shaders/prebuilt/ui.vert.spv b/shaders/prebuilt/ui.vert.spv
index 659049dd..619a9690 100644
--- a/shaders/prebuilt/ui.vert.spv
+++ b/shaders/prebuilt/ui.vert.spv
Binary files differ
diff --git a/shaders/ui.frag.glsl b/shaders/ui.frag.glsl
index 3c366602..0b099db3 100644
--- a/shaders/ui.frag.glsl
+++ b/shaders/ui.frag.glsl
@@ -17,6 +17,7 @@ layout(location = 1) in vec3 v_fg;
layout(location = 2) in vec3 v_bg;
layout(location = 3) in vec2 v_cell_uv;
layout(location = 4) flat in uvec4 v_effect;
+layout(location = 5) flat in vec4 v_clip;
layout(location = 0) out vec4 o_col;
@@ -31,6 +32,8 @@ float cellNoise(uint serial, uint col, uint row) {
}
void main() {
+ if (v_clip.z >= v_clip.x && (gl_FragCoord.x < v_clip.x || gl_FragCoord.x >= v_clip.z ||
+ gl_FragCoord.y < v_clip.y || gl_FragCoord.y >= v_clip.w)) discard;
float a = texture(u_atlas, v_uv).r;
bool oldLayer = (v_effect.x & 0x80000000u) != 0u;
// A block cursor is foreground chrome even though its fill occupies the
diff --git a/shaders/ui.vert.glsl b/shaders/ui.vert.glsl
index d23b609c..c1779add 100644
--- a/shaders/ui.vert.glsl
+++ b/shaders/ui.vert.glsl
@@ -19,12 +19,17 @@ layout(location = 5) in vec4 a_present_rect;
// the id is what is left below them; integer transport preserves the exact
// cross-backend hash.
layout(location = 6) in uvec4 a_effect;
+// A clip rectangle in target pixels (x0, y0, x1, y1): the fragment shader
+// drops what lies outside it. x1 < x0 is none. A vertical transition clips
+// its pane's instances to the box the pane is growing into.
+layout(location = 7) in vec4 a_clip;
layout(location = 0) out vec2 v_uv;
layout(location = 1) out vec3 v_fg;
layout(location = 2) out vec3 v_bg;
layout(location = 3) out vec2 v_cell_uv;
layout(location = 4) flat out uvec4 v_effect;
+layout(location = 5) flat out vec4 v_clip;
void main() {
vec2 pos;
@@ -70,5 +75,6 @@ void main() {
v_fg = a_fg;
v_bg = a_bg;
v_effect = a_effect;
+ v_clip = a_clip;
gl_Position = vec4(pos, 0.0, 1.0);
}
diff --git a/src/Presentation.zig b/src/Presentation.zig
index e1f1b585..3b5bb3e0 100644
--- a/src/Presentation.zig
+++ b/src/Presentation.zig
@@ -44,6 +44,8 @@ pending: bool = false,
previous_cells: []pardes.Cell = &.{},
previous_body_layers: [MAX_PANES]pardes.Layer = @splat(.{}),
previous_tag_layers: [pardes.MAX_TAG_LAYERS]pardes.Layer = @splat(.{}),
+previous_regions: [pardes.MAX_REGIONS]pardes.Region = undefined,
+previous_nregions: u16 = 0,
previous_cols: u16 = 0,
previous_rows: u16 = 0,
previous_valid: bool = false,
@@ -346,6 +348,8 @@ pub fn capturePrevious(self: *Presentation, gpa: std.mem.Allocator, surface: *co
old.cells = storage;
}
@memcpy(self.previous_cells, cells);
+ @memcpy(self.previous_regions[0..surface.nregions], surface.regionList());
+ self.previous_nregions = surface.nregions;
self.previous_cols = surface.cols;
self.previous_rows = surface.rows;
self.previous_valid = true;
@@ -584,6 +588,7 @@ pub fn submit(self: *Presentation, p: *Pardes, s: *pardes.Surface) !void {
s.previous_cells = self.previous_cells;
s.previous_body_layers = &self.previous_body_layers;
s.previous_tag_layers = &self.previous_tag_layers;
+ s.previous_regions = self.previous_regions[0..self.previous_nregions];
s.cell_diffs = self.diffs;
}
s.npanel_tracks = paintOrder(
diff --git a/src/detached/client.zig b/src/detached/client.zig
index 3347f609..9fa85fb4 100644
--- a/src/detached/client.zig
+++ b/src/detached/client.zig
@@ -107,6 +107,8 @@ pub const Client = struct {
tag_layers: [pardes.MAX_TAG_LAYERS]pardes.Layer = @splat(.{}),
/// The session's placed regions, as its last frame gave them.
regions: std.ArrayListUnmanaged(pardes.Region) = .empty,
+ /// The palette its chrome is drawn in, as its last frame gave it.
+ chrome: pardes.Chrome = .{},
in: std.ArrayListUnmanaged(u8) = .empty,
out: std.ArrayListUnmanaged(u8) = .empty,
/// Bytes of `in` belonging to the message `next` returned last. Compacted at
@@ -322,7 +324,7 @@ pub const Client = struct {
try c.reshape(f.cols, f.rows);
}
try f.apply(c.grid.items);
- f.applyLayers(c.gpa, &c.body_layers, &c.tag_layers, &c.regions) catch |err| return switch (err) {
+ f.applyLayers(c.gpa, &c.body_layers, &c.tag_layers, &c.regions, &c.chrome) catch |err| return switch (err) {
error.OutOfMemory => error.Closed,
else => @as(wire.Error, @errorCast(err)),
};
diff --git a/src/detached/server.zig b/src/detached/server.zig
index 46fb8fcf..9e18e125 100644
--- a/src/detached/server.zig
+++ b/src/detached/server.zig
@@ -760,6 +760,7 @@ pub const Session = struct {
&surface.body_layers,
&surface.tag_layers,
surface.regionList(),
+ surface.chrome,
) catch |err| {
log.debug("frame {d}x{d} not encodable: {t}", .{ surface.cols, surface.rows, err });
return;
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index 684c869b..32cd5229 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -169,8 +169,9 @@ pub const Frame = struct {
nruns: u32,
runs: []const u8,
- pub fn applyLayers(f: Frame, gpa: std.mem.Allocator, layers: *[pardes.MAX_PANES]pardes.Layer, tags: *[pardes.MAX_TAG_LAYERS]pardes.Layer, regions: *std.ArrayListUnmanaged(pardes.Region)) !void {
+ pub fn applyLayers(f: Frame, gpa: std.mem.Allocator, layers: *[pardes.MAX_PANES]pardes.Layer, tags: *[pardes.MAX_TAG_LAYERS]pardes.Layer, regions: *std.ArrayListUnmanaged(pardes.Region), chrome: *pardes.Chrome) !void {
regions.clearRetainingCapacity();
+ chrome.* = .{};
for (layers) |*layer| {
gpa.free(layer.cells);
layer.* = .{};
@@ -188,6 +189,8 @@ pub const Frame = struct {
gpa.free(layer.cells);
layer.* = .{};
}
+ regions.clearRetainingCapacity();
+ chrome.* = .{};
}
if (f.layers.len == 0) return;
var r: Reader = .init(f.layers);
@@ -255,6 +258,15 @@ pub const Frame = struct {
region.clip > region.rect.w or region.thumb_y > region.rect.h or region.thumb_h > region.rect.h - region.thumb_y) return error.BadValue;
regions.appendAssumeCapacity(region);
}
+ inline for (@typeInfo(pardes.Chrome).@"struct".fields) |field| {
+ const rgb = &@field(chrome, field.name);
+ if (field.type == ?[3]u8) {
+ if (try r.getBool()) {
+ rgb.* = .{ 0, 0, 0 };
+ for (&rgb.*.?) |*channel| channel.* = try r.getByte();
+ }
+ } else for (rgb) |*channel| channel.* = try r.getByte();
+ }
try r.end();
}
@@ -697,7 +709,7 @@ pub fn encodeFramePointer(
cells: []const pardes.Cell,
prev: []const pardes.Cell,
) Error![]const u8 {
- return encodeFrameLayers(out, cols, rows, cursor, pointer_shape, cells, prev, &.{}, &.{}, &.{});
+ return encodeFrameLayers(out, cols, rows, cursor, pointer_shape, cells, prev, &.{}, &.{}, &.{}, .{});
}
pub fn layersBound(layers: []const pardes.Layer, tags: []const pardes.Layer, regions: []const pardes.Region) usize {
@@ -708,12 +720,15 @@ pub fn layersBound(layers: []const pardes.Layer, tags: []const pardes.Layer, reg
for (tags) |layer| if (layer.rows != 0) {
n += 28 + layer.cells.len * cell_max;
};
- return n + regions.len * region_len;
+ return n + regions.len * region_len + chrome_len;
}
const region_len = 1 + 1 + 4 + 8 + 1 + 2 + 2 + 1 + 2;
+/// The palette: each colour three bytes, a theme's optional ones a flag
+/// before them.
+const chrome_len = @typeInfo(pardes.Chrome).@"struct".fields.len * 4;
-pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, pointer_shape: pardes.Surface.PointerShape, cells: []const pardes.Cell, prev: []const pardes.Cell, layers: []const pardes.Layer, tags: []const pardes.Layer, regions: []const pardes.Region) Error![]const u8 {
+pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, pointer_shape: pardes.Surface.PointerShape, cells: []const pardes.Cell, prev: []const pardes.Cell, layers: []const pardes.Layer, tags: []const pardes.Layer, regions: []const pardes.Region, chrome: pardes.Chrome) Error![]const u8 {
// The protocol's ceiling, enforced by the ENCODER too, and BEFORE the
// assert below so a caller can be told rather than tripped. `max_payload`
// is derived from these two, so a larger grid is a frame this decoder
@@ -793,6 +808,13 @@ pub fn encodeFrameLayers(out: []u8, cols: u16, rows: u16, cursor: ?Cursor, point
try w.putByte(region.notice);
try w.putU16(region.clip);
}
+ inline for (@typeInfo(pardes.Chrome).@"struct".fields) |field| {
+ const rgb = @field(chrome, field.name);
+ if (field.type == ?[3]u8) {
+ try w.putBool(rgb != null);
+ if (rgb) |value| for (value) |channel| try w.putByte(channel);
+ } else for (rgb) |channel| try w.putByte(channel);
+ }
}
std.mem.writeInt(u32, w.buf[layers_len_at..][0..4], @intCast(w.n - layers_start), .little);
const nruns_at = w.n;
@@ -1871,7 +1893,8 @@ test "detached wire carries compact body rows and exact pointer metrics" {
const placed = [_]pardes.Region{.{ .kind = .rail, .owner = 3, .serial = 42, .rect = .{ .x = 0, .y = 1, .w = 2, .h = 3 }, .active = true, .thumb_y = 1, .thumb_h = 2, .clip = 1 }};
const out = try gpa.alloc(u8, frameBound(2, 4) + layersBound(&.{layer}, &.{tag}, &placed));
defer gpa.free(out);
- const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}, &.{tag}, &placed);
+ const palette: pardes.Chrome = .{ .page = .{ 1, 2, 3 }, .border = .{ 4, 5, 6 }, .tag_focus_bg = .{ 7, 8, 9 }, .scroll_thumb = .{ 10, 11, 12 } };
+ const encoded = try encodeFrameLayers(out, 2, 4, null, .link, &cells, &cells, &.{layer}, &.{tag}, &placed, palette);
const packet = (try framed(encoded)).?;
const frame = (try decodeServer(packet.tag, packet.payload)).frame;
var decoded: [pardes.MAX_PANES]pardes.Layer = @splat(.{});
@@ -1881,8 +1904,10 @@ test "detached wire carries compact body rows and exact pointer metrics" {
try frame.apply(&cells);
var regions: std.ArrayListUnmanaged(pardes.Region) = .empty;
defer regions.deinit(gpa);
- try frame.applyLayers(gpa, &decoded, &tags, &regions);
+ var chrome: pardes.Chrome = .{};
+ try frame.applyLayers(gpa, &decoded, &tags, &regions, &chrome);
try std.testing.expectEqualDeep(placed[0..], regions.items);
+ try std.testing.expectEqualDeep(palette, chrome);
try std.testing.expectEqual(@as(u16, 6), decoded[0].rows);
try std.testing.expect(decoded[0].context_separators.isSet(0));
try std.testing.expect(!decoded[0].context_separators.isSet(1));
@@ -1908,7 +1933,7 @@ test "detached wire carries compact body rows and exact pointer metrics" {
try std.testing.expect(mouse_encoded.len <= clientBound(message));
var truncated = frame;
truncated.layers = frame.layers[0 .. frame.layers.len - 1];
- try std.testing.expectError(error.Truncated, truncated.applyLayers(gpa, &decoded, &tags, &regions));
+ try std.testing.expectError(error.Truncated, truncated.applyLayers(gpa, &decoded, &tags, &regions, &chrome));
for (decoded) |item| {
try std.testing.expectEqual(@as(u16, 0), item.rows);
try std.testing.expectEqual(@as(usize, 0), item.cells.len);
@@ -1922,6 +1947,6 @@ test "detached wire carries compact body rows and exact pointer metrics" {
const no_layers = try encodeFramePointer(out, 2, 4, null, .arrow, &cells, &cells);
const cleared_packet = (try framed(no_layers)).?;
const cleared = (try decodeServer(cleared_packet.tag, cleared_packet.payload)).frame;
- try cleared.applyLayers(gpa, &decoded, &tags, &regions);
+ try cleared.applyLayers(gpa, &decoded, &tags, &regions, &chrome);
try std.testing.expectEqual(@as(u16, 0), decoded[0].rows);
}
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(), "╮", .{});
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index ff99bd39..35fbc99d 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -21,6 +21,7 @@ const c_heap = @import("c_heap");
const ninep_io = @import("../9p_io.zig");
const detached_client = @import("../detached/client.zig");
+const tracy = @import("../tracy.zig");
pub const c = @cImport({
@cDefine("SDL_DISABLE_OLD_NAMES", "1");
@@ -37,6 +38,7 @@ const log = std.log.scoped(.gui);
const font_ttf = @embedFile("AdwaitaMono-Regular.ttf");
const vert_spv = @embedFile("ui.vert.spv");
const frag_spv = @embedFile("ui.frag.spv");
+const decor_frag_spv = @embedFile("decor.frag.spv");
const overlay_vert_spv = @embedFile("overlay.vert.spv");
const overlay_frag_spv = @embedFile("overlay.frag.spv");
const image_vert_spv = @embedFile("image.vert.spv");
@@ -443,6 +445,11 @@ const CellInstance = extern struct {
progress_bits: u32 = @bitCast(@as(f32, 1)),
serial: u32 = 0,
cell_coord: u32 = 0,
+ /// A clip rectangle in target pixels; x1 < x0 is none (ui.vert.glsl).
+ clip_x0: f32 = 0,
+ clip_y0: f32 = 0,
+ clip_x1: f32 = -1,
+ clip_y1: f32 = -1,
};
const OverlayVertex = extern struct {
@@ -545,7 +552,8 @@ test "saved native placement retains identity and geometry but no producer bytes
}
test "shader instance ABI carries aligned transition vectors" {
- try std.testing.expectEqual(@as(usize, 104), @sizeOf(CellInstance));
+ try std.testing.expectEqual(@as(usize, 120), @sizeOf(CellInstance));
+ try std.testing.expectEqual(@as(usize, 104), @offsetOf(CellInstance, "clip_x0"));
try std.testing.expectEqual(@as(usize, 56), @offsetOf(CellInstance, "panel_x0"));
try std.testing.expectEqual(@as(usize, 72), @offsetOf(CellInstance, "present_x0"));
try std.testing.expectEqual(@as(usize, 88), @offsetOf(CellInstance, "effect"));
@@ -560,12 +568,17 @@ test "shader instance ABI carries aligned transition vectors" {
.{ .pane = 1, .phase = .moving, .effect = .slide },
.{ .pane = 2, .phase = .moving, .effect = .slide },
};
- const plan = makePaintPlan(&tracks, true);
- try std.testing.expectEqual(@as(usize, 4), plan.len);
- try std.testing.expect(plan.batches[0].track == null);
- try std.testing.expectEqual(@as(u8, 1), plan.batches[1].track.?.pane);
- try std.testing.expectEqual(@as(u8, 2), plan.batches[2].track.?.pane);
- try std.testing.expectEqual(@as(u8, 0), plan.batches[3].track.?.pane);
+ // Tier 0, the tracks as the core ordered them, then tiers 4 and 5.
+ const groups = makeGroups(&tracks, true);
+ try std.testing.expectEqual(@as(usize, 7), groups.len);
+ try std.testing.expect(groups.items[0].track == null);
+ try std.testing.expectEqual(@as(u8, 0), groups.items[1].track.?.pane);
+ try std.testing.expectEqual(@as(u8, 1), groups.items[2].track.?.pane);
+ try std.testing.expectEqual(@as(u8, 2), groups.items[3].track.?.pane);
+ try std.testing.expectEqual(@as(usize, 4), groups.notices);
+ try std.testing.expectEqual(@as(usize, 5), groups.guides);
+ try std.testing.expectEqual(@as(usize, 6), groups.ink);
+ for ([_]usize{ groups.notices, groups.guides, groups.ink }) |index| try std.testing.expect(groups.items[index].track == null);
}
test "GUI paint plan snaps history effects without a frozen grid" {
@@ -573,11 +586,11 @@ test "GUI paint plan snaps history effects without a frozen grid" {
.{ .pane = 0, .phase = .opening, .effect = .ascii },
.{ .pane = 1, .phase = .closing, .effect = .vertical },
};
- try std.testing.expectEqual(@as(usize, 1), makePaintPlan(&tracks, false).len);
- const ready = makePaintPlan(&tracks, true);
- try std.testing.expectEqual(@as(usize, 3), ready.len);
- try std.testing.expectEqual(pardes.animation.Phase.opening, ready.batches[1].track.?.phase);
- try std.testing.expectEqual(pardes.animation.Phase.closing, ready.batches[2].track.?.phase);
+ try std.testing.expectEqual(@as(usize, 0), makeGroups(&tracks, false).tracks);
+ const ready = makeGroups(&tracks, true);
+ try std.testing.expectEqual(@as(usize, 2), ready.tracks);
+ try std.testing.expectEqual(pardes.animation.Phase.opening, ready.items[1].track.?.phase);
+ try std.testing.expectEqual(pardes.animation.Phase.closing, ready.items[2].track.?.phase);
}
test "closing tombstone overlays but never owns canonical cells" {
@@ -588,10 +601,10 @@ test "closing tombstone overlays but never owns canonical cells" {
.from = .{ .x = 2, .y = 3, .w = 10, .h = 4 },
.to = .{ .x = 2, .y = -1, .w = 10, .h = 4 },
};
- const plan = makePaintPlan(&.{closing}, true);
- try std.testing.expectEqual(@as(usize, 0), paintBatchAt(&plan, 4, 4));
- try std.testing.expectEqual(@as(usize, 2), plan.len);
- try std.testing.expectEqual(pardes.animation.Phase.closing, plan.batches[1].track.?.phase);
+ const groups = makeGroups(&.{closing}, true);
+ try std.testing.expectEqual(@as(usize, 0), groupAt(&groups, 4, 4));
+ try std.testing.expectEqual(@as(usize, 1), groups.tracks);
+ try std.testing.expectEqual(pardes.animation.Phase.closing, groups.items[1].track.?.phase);
}
fn updateCoreResize(core: *pardes.Pardes, cols: u16, rows: u16, cell_w: u32, cell_h: u32, tag_w: u32, tag_h: u32) bool {
@@ -1745,6 +1758,7 @@ const Gui = struct {
device: *c.SDL_GPUDevice,
swapchain_format: c.SDL_GPUTextureFormat,
pipeline: *c.SDL_GPUGraphicsPipeline,
+ decor_pipeline: *c.SDL_GPUGraphicsPipeline,
overlay_pipeline: *c.SDL_GPUGraphicsPipeline,
image_pipeline: *c.SDL_GPUGraphicsPipeline,
crt_pipeline: *c.SDL_GPUGraphicsPipeline,
@@ -1764,9 +1778,11 @@ const Gui = struct {
native_images: std.AutoHashMapUnmanaged(pardes.ImageCacheKey, *c.SDL_GPUTexture) = .empty,
presented_images: std.ArrayListUnmanaged(SavedImagePlace) = .empty,
prepared_images: std.ArrayListUnmanaged(PreparedImage) = .empty,
- /// renderFrame's cover of the grid by layers (coverLayers): this frame's
- /// layers, then the layers a panel transition is leaving.
- layer_cover: std.ArrayListUnmanaged(bool) = .empty,
+ /// renderFrame's cover of the grid (coverFrame): this frame's, then the
+ /// frame a panel transition is leaving.
+ cover: std.ArrayListUnmanaged(Cover) = .empty,
+ /// renderFrame's decor instances, group by group (buildDecor).
+ decor: std.ArrayListUnmanaged(CellInstance) = .empty,
font: *c.UIFont,
font_bytes: []u8 = &.{},
@@ -1996,47 +2012,127 @@ fn taglineLayoutForCell(g: *const Gui, col: u16) CellLayout {
const boxContains = pardes.animation.Box.contains;
-const PaintBatch = struct {
+/// What one tier draws, or one track in tiers 2 and 3 (docs/render-pipeline.md
+/// §3.4), in this order: its cells, its images, then its decor.
+const Group = struct {
track: ?pardes.animation.Track = null,
cell_start: u32 = 0,
cell_count: u32 = 0,
image_start: u32 = 0,
image_count: u32 = 0,
+ decor_start: u32 = 0,
+ decor_count: u32 = 0,
};
-const PaintPlan = struct {
- batches: [pardes.MAX_PANES * 2 + 1]PaintBatch = @splat(.{}),
- len: usize = 1,
+/// The frame's draw groups, bottom to top: tier 0 (the page and the panes
+/// standing still), a group for each track in paint order (tier 2 moving and
+/// opening, tier 3 closing), tier 4 (floating: the notices, then the guides
+/// and the debug box, which the core paints over them) and tier 5 (ink: the
+/// bar cursors; the pet and the touch overlay draw after it). Tier 1 is the
+/// focus lift, which does not exist yet.
+const Groups = struct {
+ items: [pardes.MAX_PANES * 2 + 4]Group = @splat(.{}),
+ /// Groups 1 through `tracks` are the tracks'.
+ tracks: usize = 0,
+ notices: usize = 1,
+ guides: usize = 2,
+ ink: usize = 3,
+ len: usize = 4,
};
-fn makePaintPlan(tracks: []const pardes.animation.Track, has_diff: bool) PaintPlan {
- var plan: PaintPlan = .{};
- for ([_]pardes.animation.Phase{ .moving, .opening, .closing }) |phase| {
- for (tracks) |track| {
- if (!track.active() or track.phase != phase) continue;
- if (track.effect.needsPreviousGrid() and !has_diff) continue;
- std.debug.assert(plan.len < plan.batches.len);
- plan.batches[plan.len].track = track;
- plan.len += 1;
- }
+fn makeGroups(tracks: []const pardes.animation.Track, has_diff: bool) Groups {
+ var groups: Groups = .{};
+ for (tracks) |track| {
+ if (!track.active()) continue;
+ if (track.effect.needsPreviousGrid() and !has_diff) continue;
+ std.debug.assert(groups.tracks < pardes.MAX_PANES * 2);
+ groups.tracks += 1;
+ groups.items[groups.tracks].track = track;
}
- return plan;
+ groups.notices = groups.tracks + 1;
+ groups.guides = groups.tracks + 2;
+ groups.ink = groups.tracks + 3;
+ groups.len = groups.tracks + 4;
+ return groups;
}
-fn paintBatchAt(plan: *const PaintPlan, col: u16, row: u16) usize {
- for (plan.batches[1..plan.len], 1..) |batch, index| {
- if (batch.track.?.phase == .closing) continue;
- if (boxContains(batch.track.?.to, col, row)) return index;
+/// The group of the track whose pane covers this cell, else tier 0.
+fn groupAt(groups: *const Groups, col: u16, row: u16) usize {
+ for (groups.items[1..][0..groups.tracks], 1..) |group, index| {
+ if (group.track.?.phase == .closing) continue;
+ if (boxContains(group.track.?.to, col, row)) return index;
}
return 0;
}
-fn paintBatchForSerial(plan: *const PaintPlan, serial: u32) usize {
- for (plan.batches[1..plan.len], 1..) |batch, index|
- if (batch.track.?.serial == serial) return index;
+fn groupOf(groups: *const Groups, serial: u32) usize {
+ for (groups.items[1..][0..groups.tracks], 1..) |group, index|
+ if (group.track.?.serial == serial) return index;
return 0;
}
+/// What draws a grid cell, marked once a frame from the layers and the
+/// regions (coverFrame), so the cell loops read it rather than search.
+const Cover = packed struct(u16) {
+ /// A layer draws it; the grid's copy is not drawn.
+ layer: bool = false,
+ /// Under a guide or the debug box: drawn from the grid over the panes.
+ floating: bool = false,
+ /// In a pane's grip, `grip_offset` cells from its left: the grip's
+ /// glyph is the cell that far to the left.
+ grip: bool = false,
+ grip_offset: u4 = 0,
+ /// In the focused pane's or column's tag rows.
+ focus: bool = false,
+ /// At a column's left edge, where a tag's anchor is inset.
+ anchor: bool = false,
+ _: u7 = 0,
+};
+
+/// Marks the grid cells a body or tag layer covers, which the layers paint
+/// themselves, and what the regions say of the rest. Filling each rectangle
+/// once replaces scanning every layer for every cell, which was a third of
+/// the gui's CPU on a full screen of text.
+fn coverFrame(cover: []Cover, cols: u16, rows: u16, body_layers: []const pardes.Layer, tag_layers: []const pardes.Layer, regions: []const pardes.Region) void {
+ @memset(cover, .{});
+ const mark = struct {
+ fn rect(into: []Cover, width: u16, height: u16, r: pardes.Rect, comptime field: []const u8, value: anytype) void {
+ const right = @min(width, @as(u32, r.x) + r.w);
+ if (r.x >= right) return;
+ var y: u32 = r.y;
+ while (y < @min(height, @as(u32, r.y) + r.h)) : (y += 1)
+ for (into[y * width ..][r.x..right]) |*cell| {
+ @field(cell, field) = value;
+ };
+ }
+ };
+ for (body_layers) |*layer| {
+ if (layer.rows != 0) mark.rect(cover, cols, rows, layer.viewport, "layer", true);
+ }
+ for (tag_layers) |*layer| {
+ if (layer.rows != 0) mark.rect(cover, cols, rows, .{ .x = layer.viewport.x, .y = layer.viewport.y, .w = layer.viewport.w, .h = layer.rows }, "layer", true);
+ }
+ // The column bar's rows, which the focused column's tint spans.
+ var bar: ?pardes.Rect = null;
+ for (regions) |region| {
+ if (region.kind == .column_grip) bar = region.rect;
+ }
+ for (regions) |region| switch (region.kind) {
+ .grip => {
+ mark.rect(cover, cols, rows, region.rect, "grip", true);
+ for (0..@min(region.rect.w, 16)) |offset| mark.rect(cover, cols, rows, .{ .x = region.rect.x +| @as(u16, @intCast(offset)), .y = region.rect.y, .w = 1, .h = region.rect.h }, "grip_offset", @as(u4, @intCast(offset)));
+ if (region.active) mark.rect(cover, cols, rows, region.rect, "focus", true);
+ },
+ .tag => if (region.active) mark.rect(cover, cols, rows, region.rect, "focus", true),
+ .column => {
+ mark.rect(cover, cols, rows, .{ .x = region.rect.x, .y = 0, .w = 1, .h = rows }, "anchor", true);
+ if (region.active) if (bar) |b| mark.rect(cover, cols, rows, .{ .x = region.rect.x, .y = b.y, .w = region.rect.w, .h = b.h }, "focus", true);
+ },
+ .guide, .debug => mark.rect(cover, cols, rows, region.rect, "floating", true),
+ else => {},
+ };
+}
+
fn panelCellCoord(track: pardes.animation.Track, col: u16, row: u16) u32 {
const source = track.contentBox();
const x0: u16 = @intFromFloat(@max(0.0, @floor(source.x)));
@@ -2101,6 +2197,15 @@ fn setTransitionFields(
instance.progress_bits = @bitCast(active.amount());
instance.serial = active.serial;
instance.cell_coord = cell_coord;
+ // A vertical transition shows its pane only inside the box it grows
+ // into, in whole pixels.
+ if (@hasField(@TypeOf(instance.*), "clip_x0") and active.effect == .vertical) {
+ const box = active.contentBox();
+ instance.clip_x0 = std.math.clamp(@floor(box.x * layout.w), 0, win_w);
+ instance.clip_y0 = std.math.clamp(@floor(box.y * layout.h), 0, win_h);
+ instance.clip_x1 = std.math.clamp(@ceil((box.x + box.w) * layout.w), instance.clip_x0, win_w);
+ instance.clip_y1 = std.math.clamp(@ceil((box.y + box.h) * layout.h), instance.clip_y0, win_h);
+ }
}
pub const run = runNative;
@@ -2276,7 +2381,8 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
var ax_info = c.SDL_GPUTransferBufferCreateInfo{ .usage = c.SDL_GPU_TRANSFERBUFFERUSAGE_UPLOAD, .size = atlas_w * atlas_h, .props = 0 };
const atlas_xfer = c.SDL_CreateGPUTransferBuffer(device, &ax_info) orelse return error.GpuCreate;
- const pipeline = try makePipeline(device, swapchain_format);
+ const pipeline = try makePipeline(device, swapchain_format, frag_spv, 1, std.mem.zeroes(c.SDL_GPUColorTargetBlendState));
+ const decor_pipeline = try makePipeline(device, swapchain_format, decor_frag_spv, 0, backgroundLayerBlend());
const overlay_pipeline = try makeOverlayPipeline(device, swapchain_format);
const image_pipeline = try makeImagePipeline(device, swapchain_format);
const crt_pipeline = try makeCrtPipeline(device, swapchain_format);
@@ -2306,6 +2412,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
.device = device,
.swapchain_format = swapchain_format,
.pipeline = pipeline,
+ .decor_pipeline = decor_pipeline,
.overlay_pipeline = overlay_pipeline,
.image_pipeline = image_pipeline,
.crt_pipeline = crt_pipeline,
@@ -2343,6 +2450,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
// regrown to; the image ones are released below.
defer {
c.SDL_ReleaseGPUGraphicsPipeline(device, g.pipeline);
+ c.SDL_ReleaseGPUGraphicsPipeline(device, g.decor_pipeline);
c.SDL_ReleaseGPUGraphicsPipeline(device, g.overlay_pipeline);
c.SDL_ReleaseGPUGraphicsPipeline(device, g.crt_pipeline);
c.SDL_ReleaseGPUTexture(device, g.atlas_tex);
@@ -2368,7 +2476,8 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
g.native_images.deinit(gpa);
g.presented_images.deinit(gpa);
g.prepared_images.deinit(gpa);
- g.layer_cover.deinit(gpa);
+ g.cover.deinit(gpa);
+ g.decor.deinit(gpa);
}
defer c.SDL_ReleaseGPUTransferBuffer(device, g.image_vxfer);
defer c.SDL_ReleaseGPUBuffer(device, g.image_vbuf);
@@ -2479,6 +2588,7 @@ fn localSession(
.watches = &watches,
.fs = fs,
.test_mode = test_mode,
+ .test_clock = if (test_mode) host_io.testClock() else null,
};
defer shell.shutdownPtys();
const host = shell.host();
@@ -2668,24 +2778,12 @@ fn attachedLoop(gpa: std.mem.Allocator, g: *Gui, client: *detached_client.Client
}
fn paintAttached(g: *Gui, gpa: std.mem.Allocator, client: *detached_client.Client) void {
- var surface: pardes.Surface = .{
- .cols = client.cols,
- .rows = client.rows,
- .cells = client.grid.items,
- .cursor = if (client.cursor) |cu| .{ .x = cu.x, .y = cu.y, .bar = cu.bar } else null,
- .pointer_shape = client.pointer_shape,
- .body_layers = client.body_layers,
- .tag_layers = client.tag_layers,
- };
- const chrome = frameChromeBg(&surface);
+ var surface = attachedSurface(client);
_ = renderFrame(
g,
gpa,
null,
&surface,
- null,
- chrome,
- chrome,
.{},
false,
) catch |err| blk: {
@@ -2694,17 +2792,67 @@ fn paintAttached(g: *Gui, gpa: std.mem.Allocator, client: *detached_client.Clien
};
}
-fn frameChromeBg(surface: *const pardes.Surface) [3]u8 {
- if (surface.rows == 0 or surface.cols == 0) return bg_default;
- for (surface.cells[0..surface.cols]) |cell| {
- if (cell.default or cell.style.font_role != .tagline) continue;
- return switch (cell.style.bg) {
- .default => bg_default,
- .index => |i| palColor(i),
- .rgb => |rgb| rgb,
- };
- }
- return bg_default;
+/// The session's frame as a surface: its cells, layers, regions and palette,
+/// so its chrome is drawn from them as a local one's is.
+fn attachedSurface(client: *const detached_client.Client) pardes.Surface {
+ var surface: pardes.Surface = .{
+ .cols = client.cols,
+ .rows = client.rows,
+ .cells = client.grid.items,
+ .cursor = if (client.cursor) |cu| .{ .x = cu.x, .y = cu.y, .bar = cu.bar } else null,
+ .pointer_shape = client.pointer_shape,
+ .body_layers = client.body_layers,
+ .tag_layers = client.tag_layers,
+ .chrome = client.chrome,
+ };
+ const placed = client.regions.items[0..@min(client.regions.items.len, surface.regions.len)];
+ @memcpy(surface.regions[0..placed.len], placed);
+ surface.nregions = @intCast(placed.len);
+ return surface;
+}
+
+test "an attached frontend draws the session's chrome from its frame alone" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ // A gui-shaped core, whose tags and notices are layers.
+ core.update(.{ .resize = .{ .cols = 80, .rows = 24, .row_metrics = .{ .body_w = 10, .body_h = 20, .tagline_w = 6, .tagline_h = 12 } } });
+ _ = try core.newShell(1, "");
+ try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
+ core.settings.focus_tint = true;
+ try std.testing.expect(core.executeBuiltinLine(0, "Msg a notice with a rule under it"));
+ pardes.test_api.sync(core);
+ // Past its slide into place, where its rule shows.
+ for (0..30) |_| core.update(.tick);
+ const s = try core.render(core.scratch.allocator());
+
+ const wire = @import("../detached/wire.zig");
+ var client: detached_client.Client = .{ .gpa = gpa };
+ defer client.deinit();
+ var welcome: [64]u8 = undefined;
+ try client.in.appendSlice(gpa, try wire.encodeServer(&welcome, .{ .welcome = .{ .slot = 0, .cols = s.cols, .rows = s.rows } }));
+ const out = try gpa.alloc(u8, wire.frameBound(s.cols, s.rows) + wire.layersBound(&s.body_layers, &s.tag_layers, s.regionList()));
+ defer gpa.free(out);
+ try client.in.appendSlice(gpa, try wire.encodeFrameLayers(out, s.cols, s.rows, null, s.pointer_shape, s.cells, &.{}, &s.body_layers, &s.tag_layers, s.regionList(), s.chrome));
+ _ = try client.next();
+ _ = try client.next();
+ const attached = attachedSurface(&client);
+
+ var g: Gui = undefined;
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 6;
+ g.tagline_height = 12;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 800, 480, true);
+ const local = try gpa.dupe(CellInstance, g.decor.items);
+ defer gpa.free(local);
+ try std.testing.expect(local.len > 8);
+ try std.testing.expect(groups.items[groups.notices].decor_count != 0);
+ try buildDecor(&g, gpa, &attached, &groups, fixedCellLayout(&g), 800, 480, true);
+ try std.testing.expectEqualSlices(u8, std.mem.sliceAsBytes(local), std.mem.sliceAsBytes(g.decor.items));
}
fn runGrid(init: std.process.Init, opts_in: pardes.Options) !void {
@@ -3708,6 +3856,8 @@ const Shell = struct {
saw_event: bool = false,
dump_err: ?anyerror = null,
retired_shells: [pardes.MAX_PANES]RetiredShell = @splat(.{}),
+ /// PARDES_TEST_CLOCK's virtual time (host_io.testClock), in test mode.
+ test_clock: ?u64 = null,
fn reap(s: *Shell) void {
const now = shellClock();
@@ -3898,13 +4048,31 @@ fn waitInput(ctx: ?*anyopaque, timeout_ms: u32) void {
var in: Input = .{ .core = core };
if (s.gui) |g| {
var sev = std.mem.zeroes(c.SDL_Event);
+ // Idle is asleep until an event: SDL's own, and the user events the
+ // shell's queue raises for a terminal's output, a completion, a file
+ // system request. What the shell moves or times itself polls every
+ // frame: a smooth scroll, a gamepad's sticks, the test stdin feed,
+ // a retired shell waiting to be killed, a present to retry. A
+ // minimized window sleeps through the core's animation too: nothing
+ // shows it, and it is caught up at once when the window is back.
+ const retiring = for (s.retired_shells) |child| {
+ if (child.kill_at != 0) break true;
+ } else for (s.ptys) |slot| {
+ if (slot) |pt| if (pt.kill_at != 0) break true;
+ } else false;
+ const polls = g.scroll_lag != 0 or g.scroll_delta != 0 or g.gamepad != null or s.test_mode or core.needs_frame or retiring;
+ const minimized = !s.test_mode and c.SDL_GetWindowFlags(g.window) & (c.SDL_WINDOW_MINIMIZED | c.SDL_WINDOW_OCCLUDED) != 0;
// A latency trace is fed on stdin, whose poll below paces the loop:
// waiting here too would make every tick cost two frames.
- const ms: c_int = if (latency_fd >= 0) 0 else if (timeout_ms != 0) @intCast(timeout_ms) else 16;
+ const ms: c_int = if (latency_fd >= 0) 0 else if (minimized) -1 else if (timeout_ms != 0) @intCast(timeout_ms) else if (polls) 16 else -1;
// The wait is the 9P connections' turn with the core.
pardes.turn.rest();
const got = c.SDL_WaitEventTimeout(&sev, ms);
pardes.turn.wake();
+ // The core's wake ran out: a virtual clock moves exactly there.
+ if (!got and timeout_ms != 0) if (s.test_clock) |*virtual| {
+ virtual.* = @max(virtual.*, core.nextWake() orelse virtual.*);
+ };
if (got) {
// A run of queued motions is one move to its last position (the
// handler reads only absolute x/y and held buttons): acting on
@@ -4044,9 +4212,6 @@ fn present(ctx: ?*anyopaque, surface: *const pardes.Surface) void {
s.gpa,
core,
frame,
- core.theme().bg,
- config.gui_topbar_pane_border_rgb orelse core.chromeTheme().border,
- core.chromeTheme().tag_bg,
core.settings.scene_effects,
core.settings.debug,
) catch |err| blk: {
@@ -4078,8 +4243,8 @@ fn postPresent(ctx: ?*anyopaque) void {
s.core.acknowledgePanelPresentation(frame.panelTracks());
}
-fn guiNow(_: ?*anyopaque) u64 {
- return c.SDL_GetTicksNS();
+fn guiNow(ctx: ?*anyopaque) u64 {
+ return shellOf(ctx).test_clock orelse c.SDL_GetTicksNS();
}
fn gridPollFrame(ctx: ?*anyopaque) void {
@@ -4846,7 +5011,7 @@ fn prepareNativeImages(
gpa: std.mem.Allocator,
cmd: *c.SDL_GPUCommandBuffer,
surface: *pardes.Surface,
- plan: *PaintPlan,
+ groups: *Groups,
sw: u32,
sh: u32,
) bool {
@@ -4872,9 +5037,9 @@ fn prepareNativeImages(
}
g.prepared_images.clearRetainingCapacity();
- for (plan.batches[0..plan.len], 0..) |*batch, batch_index| {
- batch.image_start = std.math.cast(u32, g.prepared_images.items.len) orelse return false;
- const track = batch.track;
+ for (groups.items[0..groups.len], 0..) |*group, group_index| {
+ group.image_start = std.math.cast(u32, g.prepared_images.items.len) orelse return false;
+ const track = group.track;
if (track) |active| {
if (active.phase == .closing) {
@@ -4894,7 +5059,7 @@ fn prepareNativeImages(
for (surface.images[0..surface.nimages]) |maybe| {
const current = maybe orelse continue;
if (validImageBytes(current) == null or
- paintBatchForSerial(plan, current.serial) != batch_index) continue;
+ groupOf(groups, current.serial) != group_index) continue;
const saved = SavedImagePlace.from(current);
const current_track = if (track) |active| switch (active.effect) {
.dissolve => if (snapshotContainsPlacement(g, current)) null else active,
@@ -4903,7 +5068,7 @@ fn prepareNativeImages(
appendPreparedImage(g, gpa, saved, current_track, null, false);
}
}
- if (batch_index == 0) for (plan.batches[1..plan.len]) |tracked| {
+ if (group_index == 0) for (groups.items[1..][0..groups.tracks]) |tracked| {
const active = tracked.track.?;
if (active.effect != .vertical or active.phase != .opening) continue;
for (g.presented_images.items) |saved| {
@@ -4912,7 +5077,7 @@ fn prepareNativeImages(
}
};
const image_end = std.math.cast(u32, g.prepared_images.items.len) orelse return false;
- batch.image_count = image_end - batch.image_start;
+ group.image_count = image_end - group.image_start;
}
const capacity = std.math.cast(u32, g.prepared_images.items.len) orelse return false;
@@ -4981,11 +5146,10 @@ fn drawNativeImagesGpu(
g: *Gui,
rp: ?*c.SDL_GPURenderPass,
surface: *pardes.Surface,
- plan: *const PaintPlan,
- batch_index: usize,
+ batch: Group,
+ whole: c.SDL_Rect,
) void {
const pass = rp orelse return;
- const batch = plan.batches[batch_index];
c.SDL_BindGPUGraphicsPipeline(pass, g.image_pipeline);
const max_w = @as(u32, surface.cols) * g.cell_w;
const max_h = @as(u32, surface.rows) * g.cell_h;
@@ -5004,12 +5168,8 @@ fn drawNativeImagesGpu(
idx += 1;
}
std.debug.assert(idx == batch.image_start + batch.image_count);
- const whole = c.SDL_Rect{
- .x = 0,
- .y = 0,
- .w = @intCast(@as(u32, surface.cols) * g.cell_w),
- .h = @intCast(@as(u32, surface.rows) * g.cell_h),
- };
+ // Back to the whole target: what draws after a picture may run on past
+ // the grid, as a rule and a band do.
c.SDL_SetGPUScissor(pass, &whole);
}
@@ -5018,9 +5178,6 @@ fn renderFrame(
gpa: std.mem.Allocator,
core: ?*const pardes.Pardes,
surface: *pardes.Surface,
- theme_bg: ?[3]u8,
- topbar_pane_border_rgb: [3]u8,
- tagline_rgb: [3]u8,
scene_effects: pardes.animation.SceneEffect,
debug_on: bool,
) !bool {
@@ -5084,45 +5241,39 @@ fn renderFrame(
};
const scene: *c.SDL_GPUTexture = if (scene_on) g.scene_tex.? else target;
+ // The frame's CPU side: groups, decor, cover and every instance, up to
+ // their upload.
+ const build_zone = tracy.zone(@src(), "gui frame build");
+ var building = true;
+ defer if (building) build_zone.end();
const layout = fixedCellLayout(g);
- var paint_plan = makePaintPlan(surface.panelTracks(), surface.hasPanelDiff());
- var overlay_count = buildOverlay(
- g,
- core,
- surface,
- layout,
- sw,
- sh,
- topbar_pane_border_rgb,
- tagline_rgb,
- paint_plan.len == 1,
- debug_on,
- );
+ const chrome = &surface.chrome;
+ var groups = makeGroups(surface.panelTracks(), surface.hasPanelDiff());
+ // Cursors stand still while panes move, as they always have.
+ const cursors_shown = groups.tracks == 0;
+ var overlay_count = buildOverlay(g, core, surface, sw, sh, debug_on);
if (overlay_count != 0 and !uploadOverlayGpu(g, cmd, overlay_count))
overlay_count = 0;
- if (!prepareNativeImages(g, gpa, cmd, surface, &paint_plan, sw, sh)) {
+ if (!prepareNativeImages(g, gpa, cmd, surface, &groups, sw, sh)) {
g.prepared_images.clearRetainingCapacity();
- for (paint_plan.batches[0..paint_plan.len]) |*batch| {
- batch.image_start = 0;
- batch.image_count = 0;
+ for (groups.items[0..groups.len]) |*group| {
+ group.image_start = 0;
+ group.image_count = 0;
}
}
const cells: u32 = @as(u32, surface.cols) * surface.rows;
- try g.layer_cover.resize(gpa, @as(usize, cells) * 2);
- const covered = g.layer_cover.items[0..cells];
- coverLayers(covered, surface.cols, surface.rows, surface.bodyLayers(), surface.tagLayers());
+ try g.cover.resize(gpa, @as(usize, cells) * 2);
+ const covered = g.cover.items[0..cells];
+ coverFrame(covered, surface.cols, surface.rows, surface.bodyLayers(), surface.tagLayers(), surface.regionList());
// Only a panel transition paints the grid under the layers it leaves.
- const previous_covered = g.layer_cover.items[cells..];
- if (paint_plan.len > 1)
- coverLayers(previous_covered, surface.cols, surface.rows, surface.previous_body_layers, surface.previous_tag_layers);
+ const previous_covered = g.cover.items[cells..];
+ if (groups.tracks != 0)
+ coverFrame(previous_covered, surface.cols, surface.rows, surface.previous_body_layers, surface.previous_tag_layers, surface.previous_regions);
+ try buildDecor(g, gpa, surface, &groups, layout, win_w, win_h, cursors_shown);
- const page = ground(theme_bg, g.transparent);
+ const page = ground(chrome.page, g.transparent);
const background_opacity = backgroundOpacity(g.applied_window_opacity);
- const tagline_base = pardes.Cell{
- .style = .{ .bg = .{ .rgb = tagline_rgb }, .font_role = .tagline },
- .default = false,
- };
var color_target = std.mem.zeroes(c.SDL_GPUColorTargetInfo);
color_target.texture = scene;
color_target.clear_color = if (page.clear) .{ .r = 0, .g = 0, .b = 0, .a = 0 } else .{
@@ -5139,28 +5290,21 @@ fn renderFrame(
while (row < surface.rows) : (row += 1) {
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
- if (covered[@as(usize, row) * surface.cols + col]) continue;
- const batch_index = paintBatchAt(&paint_plan, col, row);
- const track = paint_plan.batches[batch_index].track;
- const data_effect = if (track) |active|
- active.effect == .dissolve
- else
- false;
- const data_diff = data_effect and gripCellChanged(surface, col, row);
- const logical_idx: u32 = @as(u32, row) * surface.cols + col;
+ const at = @as(usize, row) * surface.cols + col;
+ if (covered[at].layer and !covered[at].floating) continue;
+ const group_index = if (covered[at].floating) groups.guides else groupAt(&groups, col, row);
+ const track = groups.items[group_index].track;
+ const data_effect = if (track) |active| active.effect == .dissolve else false;
+ const data_diff = data_effect and gripCellChanged(surface, covered, previous_covered, col, row);
const instance_count = if (data_diff)
- cellInstanceCount(&surface.previous_cells[logical_idx]) + cellInstanceCount(surface.at(col, row))
+ cellInstanceCount(&surface.previous_cells[at]) + cellInstanceCount(&surface.cells[at])
else
- cellInstanceCount(surface.at(col, row));
- paint_plan.batches[batch_index].cell_count = std.math.add(
- u32,
- paint_plan.batches[batch_index].cell_count,
- instance_count,
- ) catch return error.GpuCapacity;
+ cellInstanceCount(&surface.cells[at]);
+ groups.items[group_index].cell_count = std.math.add(u32, groups.items[group_index].cell_count, instance_count) catch return error.GpuCapacity;
}
}
- for (paint_plan.batches[1..paint_plan.len]) |*batch| {
- const track = batch.track.?;
+ for (groups.items[1..][0..groups.tracks]) |*group| {
+ const track = group.track.?;
const duplicate_under = track.effect == .vertical and track.phase == .opening;
if (!duplicate_under and track.phase != .closing) continue;
row = 0;
@@ -5168,33 +5312,29 @@ fn renderFrame(
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
if (!boxContains(track.contentBox(), col, row)) continue;
- if (previous_covered[@as(usize, row) * surface.cols + col]) continue;
- const destination = if (duplicate_under) &paint_plan.batches[0] else batch;
- const logical_idx: u32 = @as(u32, row) * surface.cols + col;
- destination.cell_count = std.math.add(
- u32,
- destination.cell_count,
- cellInstanceCount(&surface.previous_cells[logical_idx]),
- ) catch return error.GpuCapacity;
+ const at = @as(usize, row) * surface.cols + col;
+ if (previous_covered[at].layer) continue;
+ const destination = if (duplicate_under) &groups.items[0] else group;
+ destination.cell_count = std.math.add(u32, destination.cell_count, cellInstanceCount(&surface.previous_cells[at])) catch return error.GpuCapacity;
}
}
}
for (surface.bodyLayers()) |*layer| {
if (layer.rows == 0) continue;
- const batch = &paint_plan.batches[paintBatchAt(&paint_plan, layer.viewport.x, layer.viewport.y)];
+ const group = &groups.items[groupAt(&groups, layer.viewport.x, layer.viewport.y)];
const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h;
for (0..layer.rows) |lr| {
if (layer.rowTop(@intCast(lr), layout.h, @floatFromInt(g.tagline_height)) >= bottom) break;
- batch.cell_count += layer.cols + @as(u32, if (lr < layer.context_rows) 1 else 0) + @as(u32, @intFromBool(layer.hasContextBorderAfter(lr)));
+ group.cell_count += layer.cols + @as(u32, if (lr < layer.context_rows) 1 else 0) + @as(u32, @intFromBool(layer.hasContextBorderAfter(lr)));
}
}
- for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
- const track = batch.track.?;
+ for (groups.items[1..][0..groups.tracks], 1..) |group, index| {
+ const track = group.track.?;
const under = track.effect == .vertical and track.phase == .opening;
if (!under and track.phase != .closing and track.effect != .dissolve) continue;
for (surface.previous_body_layers) |*layer| {
if (layer.rows == 0 or layer.serial != track.serial) continue;
- const destination = if (under) &paint_plan.batches[0] else &paint_plan.batches[batch_index];
+ const destination = &groups.items[if (under) 0 else index];
const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h;
for (0..layer.rows) |lr| {
if (layer.rowTop(@intCast(lr), layout.h, @floatFromInt(g.tagline_height)) >= bottom) break;
@@ -5204,28 +5344,28 @@ fn renderFrame(
}
for (surface.tagLayers()) |*layer| {
if (layer.rows == 0) continue;
- const batch = &paint_plan.batches[paintBatchAt(&paint_plan, layer.viewport.x, layer.viewport.y)];
- batch.cell_count += (tagLayerCellCount(g, layer) + 1) * layer.rows;
+ const group_index = if (layer.kind == .notice) groups.notices else groupAt(&groups, layer.viewport.x, layer.viewport.y);
+ groups.items[group_index].cell_count += (tagLayerCellCount(g, layer) + 1) * layer.rows;
}
- for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
- const track = batch.track.?;
+ for (groups.items[1..][0..groups.tracks], 1..) |group, index| {
+ const track = group.track.?;
const under = track.effect == .vertical and track.phase == .opening;
if (!under and track.phase != .closing and track.effect != .dissolve) continue;
for (surface.previous_tag_layers) |*layer| {
if (layer.rows == 0 or layer.kind != .pane or layer.serial != track.serial) continue;
- const destination = if (under) &paint_plan.batches[0] else &paint_plan.batches[batch_index];
- destination.cell_count += (tagLayerCellCount(g, layer) + 1) * layer.rows;
+ groups.items[if (under) 0 else index].cell_count += (tagLayerCellCount(g, layer) + 1) * layer.rows;
}
}
- var cell_next: [pardes.MAX_PANES * 2 + 1]u32 = @splat(0);
+ var cell_next: [pardes.MAX_PANES * 2 + 4]u32 = @splat(0);
var cell_total: u32 = 0;
- for (paint_plan.batches[0..paint_plan.len], 0..) |*batch, batch_index| {
- batch.cell_start = cell_total;
- cell_next[batch_index] = cell_total;
- cell_total = std.math.add(u32, cell_total, batch.cell_count) catch return error.GpuCapacity;
+ for (groups.items[0..groups.len], 0..) |*group, index| {
+ group.cell_start = cell_total;
+ cell_next[index] = cell_total;
+ cell_total = std.math.add(u32, cell_total, group.cell_count) catch return error.GpuCapacity;
}
- if (cell_total != 0) {
- const capacity = std.math.add(u32, cell_total, cells) catch return error.GpuCapacity;
+ const decor_len: u32 = @intCast(g.decor.items.len);
+ if (cell_total != 0 or decor_len != 0) {
+ const capacity = std.math.add(u32, cell_total, cells + decor_len) catch return error.GpuCapacity;
try ensureVbuf(g, capacity);
const vptr: [*]u8 = @ptrCast(c.SDL_MapGPUTransferBuffer(g.device, g.vxfer.?, false) orelse {
@@ -5239,45 +5379,40 @@ fn renderFrame(
row = 0;
while (row < surface.rows) : (row += 1) {
const line = surface.cells[@as(usize, row) * surface.cols ..][0..surface.cols];
+ const line_cover = covered[@as(usize, row) * surface.cols ..][0..surface.cols];
const line_cursor: ?usize = if (surface.cursor) |cu| if (cu.y == row) cu.x else null else null;
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
- if (covered[@as(usize, row) * surface.cols + col]) continue;
- const batch_index = paintBatchAt(&paint_plan, col, row);
- const track = paint_plan.batches[batch_index].track;
- const logical_idx: u32 = @as(u32, row) * surface.cols + col;
- const data_effect = if (track) |active|
- active.effect == .dissolve
- else
- false;
- const data_diff = data_effect and gripCellChanged(surface, col, row);
- if (data_diff) {
- emitSurfaceCell(
- g,
- core,
- instances,
- &cell_next[batch_index],
- col,
- row,
- layout,
- win_w,
- win_h,
- track,
- surface.previous_cells[@as(usize, row) * surface.cols ..][0..surface.cols],
- col,
- null,
- &tagline_base,
- paneGripCell(surface.previous_tag_layers, surface.previous_cells, surface.cols, col, row),
- true,
- false,
- page,
- );
- }
+ const at = @as(usize, row) * surface.cols + col;
+ if (covered[at].layer and !covered[at].floating) continue;
+ const group_index = if (covered[at].floating) groups.guides else groupAt(&groups, col, row);
+ const track = groups.items[group_index].track;
+ const data_effect = if (track) |active| active.effect == .dissolve else false;
+ const data_diff = data_effect and gripCellChanged(surface, covered, previous_covered, col, row);
+ if (data_diff) emitSurfaceCell(
+ g,
+ chrome,
+ instances,
+ &cell_next[group_index],
+ col,
+ row,
+ layout,
+ win_w,
+ win_h,
+ track,
+ surface.previous_cells[@as(usize, row) * surface.cols ..][0..surface.cols],
+ previous_covered[@as(usize, row) * surface.cols ..][0..surface.cols],
+ col,
+ null,
+ true,
+ false,
+ page,
+ );
emitSurfaceCell(
g,
- core,
+ chrome,
instances,
- &cell_next[batch_index],
+ &cell_next[group_index],
col,
row,
layout,
@@ -5285,12 +5420,11 @@ fn renderFrame(
win_h,
if (data_effect and !data_diff) null else track,
line,
+ line_cover,
col,
line_cursor,
- &tagline_base,
- paneGripCell(surface.tagLayers(), surface.cells, surface.cols, col, row),
false,
- paint_plan.len == 1 and logical_idx == cursor_idx and !cursor_bar and bodyLayerAt(surface, col, row) == null,
+ cursors_shown and at == cursor_idx and !cursor_bar and bodyLayerAt(surface, col, row) == null,
page,
);
}
@@ -5299,36 +5433,36 @@ fn renderFrame(
// is a tag layer, and instances paint in emission order.
for (surface.bodyLayers()) |*layer| {
if (layer.rows == 0) continue;
- const batch_index = paintBatchAt(&paint_plan, layer.viewport.x, layer.viewport.y);
- emitBodyLayer(g, instances, &cell_next[batch_index], layer, win_w, win_h, paint_plan.batches[batch_index].track, page, paint_plan.len == 1, false, topbar_pane_border_rgb);
+ const group_index = groupAt(&groups, layer.viewport.x, layer.viewport.y);
+ emitBodyLayer(g, instances, &cell_next[group_index], layer, win_w, win_h, groups.items[group_index].track, page, cursors_shown, false, chrome.topbar_rule);
}
for (surface.tagLayers()) |*layer| {
if (layer.rows == 0) continue;
- const batch_index = paintBatchAt(&paint_plan, layer.viewport.x, layer.viewport.y);
- emitTagLayer(g, instances, &cell_next[batch_index], layer, win_w, win_h, paint_plan.batches[batch_index].track, page, paint_plan.len == 1, false);
+ const track_index = groupAt(&groups, layer.viewport.x, layer.viewport.y);
+ // A notice floats over the panes, moving with its own.
+ const group_index = if (layer.kind == .notice) groups.notices else track_index;
+ emitTagLayer(g, instances, &cell_next[group_index], layer, win_w, win_h, groups.items[track_index].track, page, cursors_shown, false);
}
- for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
- const track = batch.track.?;
+ for (groups.items[1..][0..groups.tracks], 1..) |group, index| {
+ const track = group.track.?;
const under = track.effect == .vertical and track.phase == .opening;
if (!under and track.phase != .closing and track.effect != .dissolve) continue;
for (surface.previous_tag_layers) |*layer| {
if (layer.rows == 0 or layer.kind != .pane or layer.serial != track.serial) continue;
- const destination: usize = if (under) 0 else batch_index;
- emitTagLayer(g, instances, &cell_next[destination], layer, win_w, win_h, if (under) null else track, page, false, track.effect == .dissolve);
+ emitTagLayer(g, instances, &cell_next[if (under) 0 else index], layer, win_w, win_h, if (under) null else track, page, false, track.effect == .dissolve);
}
}
- for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
- const track = batch.track.?;
+ for (groups.items[1..][0..groups.tracks], 1..) |group, index| {
+ const track = group.track.?;
const under = track.effect == .vertical and track.phase == .opening;
if (!under and track.phase != .closing and track.effect != .dissolve) continue;
for (surface.previous_body_layers) |*layer| {
if (layer.rows == 0 or layer.serial != track.serial) continue;
- const destination: usize = if (under) 0 else batch_index;
- emitBodyLayer(g, instances, &cell_next[destination], layer, win_w, win_h, if (under) null else track, page, false, track.effect == .dissolve, topbar_pane_border_rgb);
+ emitBodyLayer(g, instances, &cell_next[if (under) 0 else index], layer, win_w, win_h, if (under) null else track, page, false, track.effect == .dissolve, chrome.topbar_rule);
}
}
- for (paint_plan.batches[1..paint_plan.len], 1..) |batch, batch_index| {
- const track = batch.track.?;
+ for (groups.items[1..][0..groups.tracks], 1..) |group, index| {
+ const track = group.track.?;
const duplicate_under = track.effect == .vertical and track.phase == .opening;
if (!duplicate_under and track.phase != .closing) continue;
row = 0;
@@ -5336,13 +5470,12 @@ fn renderFrame(
var col: u16 = 0;
while (col < surface.cols) : (col += 1) {
if (!boxContains(track.contentBox(), col, row)) continue;
- if (previous_covered[@as(usize, row) * surface.cols + col]) continue;
- const destination: usize = if (duplicate_under) 0 else batch_index;
+ if (previous_covered[@as(usize, row) * surface.cols + col].layer) continue;
emitSurfaceCell(
g,
- core,
+ chrome,
instances,
- &cell_next[destination],
+ &cell_next[if (duplicate_under) 0 else index],
col,
row,
layout,
@@ -5350,10 +5483,9 @@ fn renderFrame(
win_h,
if (duplicate_under) null else track,
surface.previous_cells[@as(usize, row) * surface.cols ..][0..surface.cols],
+ previous_covered[@as(usize, row) * surface.cols ..][0..surface.cols],
col,
null,
- &tagline_base,
- paneGripCell(surface.previous_tag_layers, surface.previous_cells, surface.cols, col, row),
false,
false,
page,
@@ -5361,57 +5493,56 @@ fn renderFrame(
}
}
}
- for (paint_plan.batches[0..paint_plan.len], 0..) |batch, batch_index|
- std.debug.assert(cell_next[batch_index] == batch.cell_start + batch.cell_count);
+ for (groups.items[0..groups.len], 0..) |group, index|
+ std.debug.assert(cell_next[index] == group.cell_start + group.cell_count);
shifted = emitScrollRows(g, instances, cell_total, surface, layout, win_w, win_h, page);
+ @memcpy(instances[cell_total + shifted ..][0..decor_len], g.decor.items);
c.SDL_UnmapGPUTransferBuffer(g.device, g.vxfer.?);
if (g.atlas_dirty_top < g.atlas_dirty_bottom) uploadAtlas(g, cmd);
const copy = c.SDL_BeginGPUCopyPass(cmd);
const src = c.SDL_GPUTransferBufferLocation{ .transfer_buffer = g.vxfer.?, .offset = 0 };
- const dst = c.SDL_GPUBufferRegion{ .buffer = g.vbuf.?, .offset = 0, .size = (cell_total + shifted) * @sizeOf(CellInstance) };
+ const dst = c.SDL_GPUBufferRegion{ .buffer = g.vbuf.?, .offset = 0, .size = (cell_total + shifted + decor_len) * @sizeOf(CellInstance) };
c.SDL_UploadToGPUBuffer(copy, &src, &dst, false);
c.SDL_EndGPUCopyPass(copy);
}
+ build_zone.end();
+ building = false;
const rp = c.SDL_BeginGPURenderPass(cmd, &color_target, 1, null);
pushLayerOpacity(cmd, rp, background_opacity);
const whole = c.SDL_Rect{ .x = 0, .y = 0, .w = @intCast(sw), .h = @intCast(sh) };
- for (paint_plan.batches[0..paint_plan.len], 0..) |batch, batch_index| {
- const has_shifted = batch_index == 0 and shifted != 0;
- if ((batch.cell_count != 0 or has_shifted) and g.vbuf != null) {
+ const decor_base = cell_total + shifted;
+ for (groups.items[0..groups.len], 0..) |group, index| {
+ const has_shifted = index == 0 and shifted != 0;
+ if ((group.cell_count != 0 or has_shifted) and g.vbuf != null) {
c.SDL_BindGPUGraphicsPipeline(rp, g.pipeline);
const samp_binding = c.SDL_GPUTextureSamplerBinding{ .texture = g.atlas_tex, .sampler = g.atlas_sampler };
c.SDL_BindGPUFragmentSamplers(rp, 0, &samp_binding, 1);
- if (batch.cell_count != 0) {
- const panel_clipped = if (batch.track) |active| active.effect == .vertical else false;
- if (panel_clipped) {
- const clip = panelBoxScissor(g, batch.track.?.contentBox(), sw, sh);
- c.SDL_SetGPUScissor(rp, &clip);
- }
- const binding = c.SDL_GPUBufferBinding{
- .buffer = g.vbuf.?,
- .offset = batch.cell_start * @sizeOf(CellInstance),
- };
+ if (group.cell_count != 0) {
+ const binding = c.SDL_GPUBufferBinding{ .buffer = g.vbuf.?, .offset = group.cell_start * @sizeOf(CellInstance) };
c.SDL_BindGPUVertexBuffers(rp, 0, &binding, 1);
- c.SDL_DrawGPUPrimitives(rp, 6, batch.cell_count, 0, 0);
- if (panel_clipped) c.SDL_SetGPUScissor(rp, &whole);
+ c.SDL_DrawGPUPrimitives(rp, 6, group.cell_count, 0, 0);
}
if (has_shifted) {
const clip = scrollScissor(g, layout, sw, sh);
c.SDL_SetGPUScissor(rp, &clip);
- const binding = c.SDL_GPUBufferBinding{
- .buffer = g.vbuf.?,
- .offset = cell_total * @sizeOf(CellInstance),
- };
+ const binding = c.SDL_GPUBufferBinding{ .buffer = g.vbuf.?, .offset = cell_total * @sizeOf(CellInstance) };
c.SDL_BindGPUVertexBuffers(rp, 0, &binding, 1);
c.SDL_DrawGPUPrimitives(rp, 6, shifted, 0, 0);
c.SDL_SetGPUScissor(rp, &whole);
}
}
- if (batch.image_count != 0)
- drawNativeImagesGpu(g, rp, surface, &paint_plan, batch_index);
+ if (group.image_count != 0) drawNativeImagesGpu(g, rp, surface, group, whole);
+ if (group.decor_count != 0 and g.vbuf != null) {
+ c.SDL_BindGPUGraphicsPipeline(rp, g.decor_pipeline);
+ // Ink keeps its colour at any WindowOpacity.
+ pushLayerOpacity(cmd, rp, if (index == groups.ink) 1 else background_opacity);
+ const binding = c.SDL_GPUBufferBinding{ .buffer = g.vbuf.?, .offset = (decor_base + group.decor_start) * @sizeOf(CellInstance) };
+ c.SDL_BindGPUVertexBuffers(rp, 0, &binding, 1);
+ c.SDL_DrawGPUPrimitives(rp, 6, group.decor_count, 0, 0);
+ }
}
drawOverlayGpu(g, cmd, rp, overlay_count);
c.SDL_EndGPURenderPass(rp);
@@ -5455,23 +5586,23 @@ fn renderFrame(
}
if (g.capture) {
command_consumed = true;
- try captureFrame(g, gpa, cmd, target, sw, sh);
+ try captureFrame(g, gpa, cmd, target, sw, sh, if (groups.tracks != 0) groups.items[1].track.?.frame else null);
g.presented_scene = rendered_scene;
- if (paint_plan.len == 1) rememberPresentedImages(g, gpa, surface);
+ if (groups.tracks == 0) rememberPresentedImages(g, gpa, surface);
return true;
}
if (g.soft_present) {
command_consumed = true;
try softPresentFrame(g, cmd, target, sw, sh);
g.presented_scene = rendered_scene;
- if (paint_plan.len == 1) rememberPresentedImages(g, gpa, surface);
+ if (groups.tracks == 0) rememberPresentedImages(g, gpa, surface);
return true;
}
command_consumed = true;
const submitted = c.SDL_SubmitGPUCommandBuffer(cmd);
if (submitted) {
g.presented_scene = rendered_scene;
- if (paint_plan.len == 1) rememberPresentedImages(g, gpa, surface);
+ if (groups.tracks == 0) rememberPresentedImages(g, gpa, surface);
}
return submitted;
}
@@ -5487,24 +5618,6 @@ const ResolvedCell = struct {
const topbarPaneBorderPixels = pardes.topbarPaneBorderPixels;
const taglineBandOffset = pardes.taglineBandOffset;
-fn topbarPaneBorderHeight(surface: *const pardes.Surface, top: u16, cell_h: u32, tagline_h: u32) ?u32 {
- if (top == 0 or surface.rows <= top) return null;
- const height = topbarPaneBorderPixels(cell_h, tagline_h);
- if (height == 0) return null;
- const base = @as(usize, top) * surface.cols;
- for (surface.cells[base..][0..surface.cols]) |cell|
- if (!cell.default and cell.style.font_role == .tagline) return height;
- return null;
-}
-
-fn bottomTaglinePresent(surface: *const pardes.Surface) bool {
- if (surface.rows == 0) return false;
- const base = @as(usize, surface.rows - 1) * surface.cols;
- for (surface.cells[base..][0..surface.cols]) |cell|
- if (!cell.default and cell.style.font_role == .tagline) return true;
- return false;
-}
-
test "tagline bands center in their cells and Tagbottom faces the window edge" {
const cell_h: u32 = 20;
const tagline_h: u32 = 16;
@@ -5587,40 +5700,6 @@ fn cellInstanceCount(cell: *const pardes.Cell) u32 {
return if (cellFontRole(cell) == .tagline) 2 else 1;
}
-fn taglineBaseRgb(core: *const pardes.Pardes, col: u16, row: u16) [3]u8 {
- const chrome = core.chromeTheme();
- if (!core.settings.focus_tint) return chrome.tag_bg;
- if (row >= core.topBarHeight() and row < core.topBarHeight() + core.columnBarHeight()) {
- for (0..core.ncol) |column| for (core.col_panes[column][0..core.col_n[column]]) |id| {
- if (id != core.active) continue;
- const x = core.col_x[column];
- if (col >= x and col - x < core.col_w[column]) return chrome.tag_active_bg;
- };
- }
- const r = core.rects[core.active];
- const pane = core.panes[core.active] orelse return chrome.tag_bg;
- const tag_y = core.tagTop(pane, r);
- return if (r.h > 0 and row >= tag_y and row < tag_y + pane.tag_rows and col >= r.x and col - r.x < r.w) chrome.tag_active_bg else chrome.tag_bg;
-}
-
-test "compact column and pane underlays share focus tint without tinting the global tag" {
- const core = try pardes.Pardes.init(std.testing.allocator, .{ .cols = 120, .rows = 24 });
- defer core.deinit();
- _ = try core.newShell(1, "");
- try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
- pardes.test_api.sync(core);
- core.active = 1;
- const active = core.rects[1];
- const inactive = core.rects[0];
- const chrome = core.chromeTheme();
- try std.testing.expectEqual(chrome.tag_bg, taglineBaseRgb(core, active.x, 0));
- try std.testing.expectEqual(chrome.tag_active_bg, taglineBaseRgb(core, active.x, pardes.TOPBAR_H));
- try std.testing.expectEqual(chrome.tag_bg, taglineBaseRgb(core, inactive.x, pardes.TOPBAR_H));
- try std.testing.expectEqual(chrome.tag_active_bg, taglineBaseRgb(core, active.x, active.y));
- core.settings.focus_tint = false;
- try std.testing.expectEqual(chrome.tag_bg, taglineBaseRgb(core, active.x, pardes.TOPBAR_H));
-}
-
fn tagLayerIn(layers: []const pardes.Layer, col: u16, row: u16) ?*const pardes.Layer {
for (layers) |*layer| {
const r = layer.viewport;
@@ -5629,34 +5708,10 @@ fn tagLayerIn(layers: []const pardes.Layer, col: u16, row: u16) ?*const pardes.L
return null;
}
-/// Marks the grid cells a body or tag layer covers, which the layers paint
-/// themselves: the grid's cell loops skip them. Filling each layer's rectangle
-/// once replaces scanning every layer for every cell, which was a third of the
-/// gui's CPU on a full screen of text. It marks exactly the cells bodyLayerIn
-/// and tagLayerIn find a layer for: a tag layer covers its rows, and a
-/// viewport past the grid covers the grid's part of it.
-fn coverLayers(cover: []bool, cols: u16, rows: u16, body_layers: []const pardes.Layer, tag_layers: []const pardes.Layer) void {
- @memset(cover, false);
- for (body_layers) |*layer| {
- const r = layer.viewport;
- const right = @min(cols, @as(u32, r.x) + r.w);
- if (layer.rows == 0 or r.x >= right) continue;
- var y: u32 = r.y;
- while (y < @min(rows, @as(u32, r.y) + r.h)) : (y += 1) @memset(cover[y * cols ..][r.x..right], true);
- }
- for (tag_layers) |*layer| {
- const r = layer.viewport;
- const right = @min(cols, @as(u32, r.x) + r.w);
- if (layer.rows == 0 or r.x >= right) continue;
- var y: u32 = r.y;
- while (y < @min(rows, @as(u32, r.y) + layer.rows)) : (y += 1) @memset(cover[y * cols ..][r.x..right], true);
- }
-}
-
-fn expectCoverIsScan(cover: []const bool, cols: u16, rows: u16, body_layers: []const pardes.Layer, tag_layers: []const pardes.Layer) !void {
+fn expectCoverIsScan(cover: []const Cover, cols: u16, rows: u16, body_layers: []const pardes.Layer, tag_layers: []const pardes.Layer) !void {
for (0..rows) |row| for (0..cols) |col| {
const scan = bodyLayerIn(body_layers, @intCast(col), @intCast(row)) != null or tagLayerIn(tag_layers, @intCast(col), @intCast(row)) != null;
- try std.testing.expectEqual(scan, cover[row * cols + col]);
+ try std.testing.expectEqual(scan, cover[row * cols + col].layer);
};
}
@@ -5670,7 +5725,7 @@ test "the layer cover is the per-cell layer scan, over a core's layouts and rand
try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
pardes.layout.insert(core, 1, 1, 2);
pardes.layout.insert(core, 0, 1, 3);
- var cover: [97 * 31]bool = undefined;
+ var cover: [97 * 31]Cover = undefined;
var notices = false;
for (0..32) |step| {
core.settings.tag_bottom = step & 1 != 0;
@@ -5683,9 +5738,20 @@ test "the layer cover is the per-cell layer scan, over a core's layouts and rand
}
const s = try core.render(core.scratch.allocator());
try std.testing.expectEqual(@as(usize, cover.len), @as(usize, s.cols) * s.rows);
- @memset(&cover, true); // what an earlier frame left
- coverLayers(&cover, s.cols, s.rows, s.bodyLayers(), s.tagLayers());
+ @memset(&cover, .{ .layer = true, .floating = true, .grip = true, .focus = true }); // what an earlier frame left
+ coverFrame(&cover, s.cols, s.rows, s.bodyLayers(), s.tagLayers(), s.regionList());
try expectCoverIsScan(&cover, s.cols, s.rows, s.bodyLayers(), s.tagLayers());
+ // A grip's cells know their way to its glyph, and the focused
+ // pane's tag rows are the focused ones.
+ for (s.regionList()) |region| {
+ if (region.kind != .grip) continue;
+ for (0..region.rect.w) |offset| {
+ const cell = cover[@as(usize, region.rect.y) * s.cols + region.rect.x + offset];
+ try std.testing.expect(cell.grip);
+ try std.testing.expectEqual(offset, cell.grip_offset);
+ try std.testing.expectEqual(region.owner == core.active, cell.focus);
+ }
+ }
for (s.tagLayers()) |layer| notices = notices or (layer.rows != 0 and layer.kind == .notice);
}
try std.testing.expect(notices);
@@ -5705,18 +5771,18 @@ test "the layer cover is the per-cell layer scan, over a core's layouts and rand
const random = prng.random();
var body: [pardes.MAX_PANES]pardes.Layer = undefined;
var tag: [pardes.MAX_TAG_LAYERS]pardes.Layer = undefined;
- var grid: [64 * 40]bool = undefined;
+ var grid: [64 * 40]Cover = undefined;
for (0..3000) |_| {
const cols = random.intRangeAtMost(u16, 1, 64);
const rows = random.intRangeAtMost(u16, 1, 40);
const empty = random.intRangeAtMost(u8, 2, 5); // about one layer in `empty` is empty
for (&body) |*layer| layer.* = .{ .rows = @intFromBool(random.uintLessThan(u8, empty) != 0), .viewport = gen.rect(random, cols, rows) };
- for (&tag) |*layer| layer.* = .{ .cols = @intFromBool(random.uintLessThan(u8, empty) != 0), .viewport = gen.rect(random, cols, rows) };
+ for (&tag) |*layer| layer.* = .{ .rows = @intFromBool(random.uintLessThan(u8, empty) != 0), .viewport = gen.rect(random, cols, rows) };
const bodies = body[0..random.uintAtMost(usize, body.len)];
const tags = tag[0..random.uintAtMost(usize, tag.len)];
const cells = grid[0 .. @as(usize, cols) * rows];
- @memset(cells, random.boolean());
- coverLayers(cells, cols, rows, bodies, tags);
+ @memset(cells, .{ .layer = random.boolean() });
+ coverFrame(cells, cols, rows, bodies, tags, &.{});
try expectCoverIsScan(cells, cols, rows, bodies, tags);
}
}
@@ -5844,42 +5910,23 @@ fn emitBodyLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const
}
}
-const GripCell = struct { cell: *const pardes.Cell, offset: u16 };
-
-fn gripCellChanged(surface: *const pardes.Surface, col: u16, row: u16) bool {
+/// A dissolving cell changes with its grip's glyph, which is the cell its
+/// grip offset to the left, in this frame's grip or the last one's.
+fn gripCellChanged(surface: *const pardes.Surface, covered: []const Cover, previous_covered: []const Cover, col: u16, row: u16) bool {
if (surface.panelCellChanged(col, row)) return true;
- if (paneGripCell(surface.tagLayers(), surface.cells, surface.cols, col, row)) |grip| {
- if (surface.panelCellChanged(col - grip.offset, row)) return true;
- }
- if (paneGripCell(surface.previous_tag_layers, surface.previous_cells, surface.cols, col, row)) |grip| {
- if (surface.panelCellChanged(col - grip.offset, row)) return true;
- }
+ const at = @as(usize, row) * surface.cols + col;
+ for ([_]Cover{ covered[at], previous_covered[at] }) |cover|
+ if (cover.grip and surface.panelCellChanged(col -| cover.grip_offset, row)) return true;
return false;
}
-fn paneGripCell(layers: []const pardes.Layer, cells: []const pardes.Cell, cols: u16, col: u16, row: u16) ?GripCell {
- const at = @as(usize, row) * cols + col;
- if (at >= cells.len or cells[at].default or cells[at].style.font_role != .tagline) return null;
- for (layers) |layer| {
- if (layer.rows == 0 or layer.kind != .pane or layer.viewport.x < pardes.TAG_TEXT_INSET or row < layer.viewport.y or row - layer.viewport.y >= layer.rows) continue;
- const x = layer.viewport.x - pardes.TAG_TEXT_INSET;
- if (col < x or col - x >= config.GUTTER) continue;
- const index = @as(usize, row) * cols + x;
- if (index >= cells.len) return null;
- const cell = &cells[index];
- if (cell.default or cell.printableAscii() == ' ' or cell.style.invisible) return null;
- return .{ .cell = cell, .offset = col - x };
- }
- return null;
-}
-
fn anchorInset(g: *const Gui, row: u16, win_h: f32) u32 {
return taglineBandOffset(row, win_h, g.cell_h, g.tagline_height);
}
fn emitSurfaceCell(
g: *Gui,
- core: ?*const pardes.Pardes,
+ chrome: *const pardes.Chrome,
instances: [*]CellInstance,
next: *u32,
col: u16,
@@ -5889,18 +5936,20 @@ fn emitSurfaceCell(
win_h: f32,
track: ?pardes.animation.Track,
line: []const pardes.Cell,
+ line_cover: []const Cover,
at: usize,
cursor: ?usize,
- tagline_base: *const pardes.Cell,
- grip: ?GripCell,
old_layer: bool,
is_cursor: bool,
page: Ground,
) void {
const role = cellFontRole(&line[at]);
if (role == .tagline) {
- var band_base = tagline_base.*;
- if (core) |p| band_base.style.bg = .{ .rgb = taglineBaseRgb(p, col, row) };
+ const here = line_cover[at];
+ const band_base: pardes.Cell = .{
+ .style = .{ .bg = .{ .rgb = if (here.focus) chrome.tag_focus_bg else chrome.tag_bg }, .font_role = .tagline },
+ .default = false,
+ };
// The smaller font changes the ink, not the chrome's coverage. Painting
// this underlay at tagline height leaves page-colored gutters between
// adjacent workspace, column and pane tags at TaglineSize < 100.
@@ -5908,27 +5957,32 @@ fn emitSurfaceCell(
next.* += 1;
// Tag glyphs keep their font size inside a full physical-width grip.
emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .tagline, line, at, cursor, old_layer, is_cursor, page);
+ // A grip's glyph is the cell at its left edge, and a blank one is
+ // no glyph: its marks are decor.
+ const glyph = &line[at - @min(here.grip_offset, at)];
+ const grip = here.grip and !glyph.default and glyph.printableAscii() != ' ' and !glyph.style.invisible;
+ const offset: u16 = if (grip) @min(here.grip_offset, at) else 0;
// An anchor starts at its column's rule: its left edge is inset by
// its top margin so the band frames it on the left as above and
// below, and its mark moves right by half that to stay centered.
- const inset: u32 = if (core) |p| if (std.mem.indexOfScalar(u16, p.col_x[0..p.ncol], col -| (if (grip) |indicator| indicator.offset else 0)) != null) anchorInset(g, row, win_h) else 0 else 0;
- if (grip) |indicator| {
+ const inset: u32 = if (line_cover[at - offset].anchor) anchorInset(g, row, win_h) else 0;
+ if (grip) {
// A grip is one glyph centered across the gutter, not text: it is
// looked up by its codepoint's glyph, never shaped.
- const drawn = glyphForCodepoint(g, cellCodepoint(indicator.cell));
- const cached = cachedGlyph(g, .{ .face = drawn.face, .glyph = drawn.glyph, .role = .tagline, .decoration = GlyphDecoration.fromCell(indicator.cell), .centered = true });
+ const drawn = glyphForCodepoint(g, cellCodepoint(glyph));
+ const cached = cachedGlyph(g, .{ .face = drawn.face, .glyph = drawn.glyph, .role = .tagline, .decoration = GlyphDecoration.fromCell(glyph), .centered = true });
const slot = cached orelse g.space_slot;
- const offset: f32 = if (cached != null) @as(f32, @floatFromInt(@as(u32, indicator.offset) * g.cell_w)) - @as(f32, @floatFromInt(inset / 2)) else 0;
- const resolved = resolveCell(g, indicator.cell[0..1], 0, null, .tagline, false, page);
+ const shift: f32 = if (cached != null) @as(f32, @floatFromInt(@as(u32, offset) * g.cell_w)) - @as(f32, @floatFromInt(inset / 2)) else 0;
+ const resolved = resolveCell(g, glyph[0..1], 0, null, .tagline, false, page);
instances[next.*].fr = @as(f32, @floatFromInt(resolved.fg[0])) / 255;
instances[next.*].fg = @as(f32, @floatFromInt(resolved.fg[1])) / 255;
instances[next.*].fb = @as(f32, @floatFromInt(resolved.fg[2])) / 255;
- instances[next.*].u0 = (@as(f32, @floatFromInt(slot.u)) + offset) / atlas_w;
+ instances[next.*].u0 = (@as(f32, @floatFromInt(slot.u)) + shift) / atlas_w;
instances[next.*].v0 = @as(f32, @floatFromInt(slot.v)) / atlas_h;
instances[next.*].v1 = @as(f32, @floatFromInt(slot.v + g.tagline_height)) / atlas_h;
}
instances[next.*].u1 = instances[next.*].u0 + @as(f32, @floatFromInt(g.cell_w)) / atlas_w;
- if (inset > 0 and (grip == null or grip.?.offset == 0)) {
+ if (inset > 0 and (!grip or offset == 0)) {
instances[next.*].x0 += @as(f32, @floatFromInt(inset)) / win_w * 2;
instances[next.*].u0 += @as(f32, @floatFromInt(inset)) / atlas_w;
}
@@ -5940,8 +5994,7 @@ fn emitSurfaceCell(
}
test "small tagline fonts leave no background gutters between adjacent tags" {
- const core = try pardes.Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 });
- defer core.deinit();
+ const chrome: pardes.Chrome = .{};
var g: Gui = undefined;
g.words = .{};
g.cell_w = 10;
@@ -5954,7 +6007,7 @@ test "small tagline fonts leave no background gutters between adjacent tags" {
var next: u32 = 0;
for (0..3) |row| {
// Column 1: past the anchor's inset edge at the column's left.
- emitSurfaceCell(&g, core, &instances, &next, 1, @intCast(row), fixedCellLayout(&g), 800, 480, null, (&cell)[0..1], 0, null, &cell, null, false, false, Ground.opaqueRgb(bg_default));
+ emitSurfaceCell(&g, &chrome, &instances, &next, 1, @intCast(row), fixedCellLayout(&g), 800, 480, null, (&cell)[0..1], &.{.{}}, 0, null, false, false, Ground.opaqueRgb(bg_default));
const base = instances[row * 2];
const ink = instances[row * 2 + 1];
try std.testing.expectApproxEqAbs(@as(f32, 20.0 / 480.0 * 2), base.y0 - base.y1, 0.0001);
@@ -5967,12 +6020,12 @@ test "small tagline fonts leave no background gutters between adjacent tags" {
// Column 0 is an anchor's edge: its ink is inset by the band's margin
// (2 px here) and its underlay still covers the whole cell.
next = 0;
- emitSurfaceCell(&g, core, &instances, &next, 0, 1, fixedCellLayout(&g), 800, 480, null, (&cell)[0..1], 0, null, &cell, null, false, false, Ground.opaqueRgb(bg_default));
+ emitSurfaceCell(&g, &chrome, &instances, &next, 0, 1, fixedCellLayout(&g), 800, 480, null, (&cell)[0..1], &.{.{ .anchor = true }}, 0, null, false, false, Ground.opaqueRgb(bg_default));
try std.testing.expectApproxEqAbs(@as(f32, 10.0 / 800.0 * 2), instances[0].x1 - instances[0].x0, 0.0001);
try std.testing.expectApproxEqAbs(@as(f32, 8.0 / 800.0 * 2), instances[1].x1 - instances[1].x0, 0.0001);
try std.testing.expectApproxEqAbs(@as(f32, 8) / atlas_w, instances[1].u1 - instances[1].u0, 0.0001);
next = 0;
- emitSurfaceCell(&g, null, &instances, &next, 0, 2, fixedCellLayout(&g), 800, 480, null, (&cell)[0..1], 0, null, &cell, null, true, false, Ground.opaqueRgb(bg_default));
+ emitSurfaceCell(&g, &chrome, &instances, &next, 0, 2, fixedCellLayout(&g), 800, 480, null, (&cell)[0..1], &.{.{}}, 0, null, true, false, Ground.opaqueRgb(bg_default));
try std.testing.expectEqual(@as(u32, 2), next);
try std.testing.expectApproxEqAbs(@as(f32, 16.0 / 480.0 * 2), instances[1].y0 - instances[1].y1, 0.0001);
}
@@ -6086,24 +6139,25 @@ test "insert cursor overlays without replacing the character beneath it" {
};
try std.testing.expectEqual(@as(u32, 'x'), cellCodepoint(&cell));
- var vertices: [6]OverlayVertex = undefined;
- var builder: OverlayBuilder = .{
- .vertices = &vertices,
- .win_w = 100,
- .win_h = 100,
- };
- addCursorBar(&builder, 2, 3, true, 8, .{
- .x_off = 0,
- .y_off = 0,
- .w = 10,
- .h = 20,
- }, .{ 32, 48, 64 });
- try std.testing.expectEqual(@as(usize, 6), builder.len);
- try std.testing.expectApproxEqAbs(@as(f32, -0.32), vertices[0].y, 0.0001);
- try std.testing.expectApproxEqAbs(@as(f32, -0.48), vertices[2].y, 0.0001);
- try std.testing.expectApproxEqAbs(@as(f32, 32.0 / 255.0), vertices[0].r, 0.0001);
- for (vertices) |vertex| try std.testing.expectEqual(@as(u32, 1), vertex.ink);
- try std.testing.expect(!builder.ink);
+ // A bar at a tag cell: tagline-tall, centered in its row, in the
+ // cell's ink, drawn as decor over it.
+ var cells: [25]pardes.Cell = @splat(.{});
+ cells[3 * 5 + 2] = .{ .default = false, .style = .{ .fg = .{ .rgb = .{ 32, 48, 64 } }, .font_role = .tagline } };
+ const surface: pardes.Surface = .{ .cols = 5, .rows = 5, .cells = &cells, .cursor = .{ .x = 2, .y = 3, .bar = true } };
+ var g: Gui = undefined;
+ g.decor = .empty;
+ defer g.decor.deinit(std.testing.allocator);
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 8;
+ g.tagline_height = 8;
+ try cursorDecor(&g, std.testing.allocator, &surface, fixedCellLayout(&g), 100, 100);
+ try std.testing.expectEqual(@as(usize, 1), g.decor.items.len);
+ const bar = g.decor.items[0];
+ try std.testing.expectApproxEqAbs(@as(f32, -0.32), bar.y0, 0.0001);
+ try std.testing.expectApproxEqAbs(@as(f32, -0.48), bar.y1, 0.0001);
+ try std.testing.expectApproxEqAbs(@as(f32, 32.0 / 255.0), bar.br, 0.0001);
+ try std.testing.expectEqual(@as(f32, 1), bar.fr);
}
fn palColor(idx: u8) [3]u8 {
@@ -6613,17 +6667,24 @@ test "pane indicator halves share dissolve changes and safe exhausted atlas" {
cells[0].text[0] = '$';
var previous = cells;
previous[0].text[0] = '^';
- var layers = [_]pardes.Layer{.{ .kind = .pane, .serial = 1, .viewport = .{ .x = pardes.TAG_TEXT_INSET, .y = 0, .w = 3, .h = 1 }, .cols = 3, .rows = 1 }};
var diffs: [12]pardes.PanelCellDiff = @splat(.unchanged);
diffs[0] = pardes.PanelCellDiff.between(&previous[0], &cells[0]);
- var surface: pardes.Surface = .{ .cols = 6, .rows = 2, .cells = &cells, .previous_cells = &previous, .previous_tag_layers = &layers, .cell_diffs = &diffs };
- surface.tag_layers[0] = layers[0];
- try std.testing.expect(gripCellChanged(&surface, 0, 0));
- try std.testing.expect(gripCellChanged(&surface, 1, 0));
- try std.testing.expect(!gripCellChanged(&surface, 2, 0));
- try std.testing.expect(paneGripCell(surface.tagLayers(), &cells, 6, 2, 0) == null);
- layers[0].kind = .column;
- try std.testing.expect(paneGripCell(&layers, &cells, 6, 0, 0) == null);
+ const surface: pardes.Surface = .{ .cols = 6, .rows = 2, .cells = &cells, .previous_cells = &previous, .cell_diffs = &diffs };
+ // A grip two cells wide at the top left, in both frames.
+ const grip = [_]pardes.Region{.{ .kind = .grip, .serial = 1, .rect = .{ .x = 0, .y = 0, .w = 2, .h = 1 } }};
+ var cover: [12]Cover = undefined;
+ var previous_cover: [12]Cover = undefined;
+ coverFrame(&cover, 6, 2, &.{}, &.{}, &grip);
+ coverFrame(&previous_cover, 6, 2, &.{}, &.{}, &grip);
+ try std.testing.expect(gripCellChanged(&surface, &cover, &previous_cover, 0, 0));
+ try std.testing.expect(gripCellChanged(&surface, &cover, &previous_cover, 1, 0));
+ try std.testing.expect(!gripCellChanged(&surface, &cover, &previous_cover, 2, 0));
+ try std.testing.expect(!cover[2].grip);
+ // A column's grip has no glyph of a pane's.
+ coverFrame(&cover, 6, 2, &.{}, &.{}, &.{.{ .kind = .column_grip, .rect = .{ .x = 0, .y = 0, .w = 2, .h = 1 } }});
+ try std.testing.expect(!cover[0].grip);
+ coverFrame(&cover, 6, 2, &.{}, &.{}, &grip);
+ const chrome: pardes.Chrome = .{};
const font = c.ui_font_new(font_ttf.ptr, @intCast(font_ttf.len)).?;
defer c.ui_font_free(font);
@@ -6647,7 +6708,7 @@ test "pane indicator halves share dissolve changes and safe exhausted atlas" {
var instances: [4]CellInstance = undefined;
var next: u32 = 0;
for (0..2) |col| {
- emitSurfaceCell(&g, null, &instances, &next, @intCast(col), 0, fixedCellLayout(&g), 100, 100, null, cells[0..6], col, null, &cells[2], .{ .cell = &cells[0], .offset = @intCast(col) }, old, false, Ground.opaqueRgb(bg_default));
+ emitSurfaceCell(&g, &chrome, &instances, &next, @intCast(col), 0, fixedCellLayout(&g), 100, 100, null, cells[0..6], cover[0..6], col, null, old, false, Ground.opaqueRgb(bg_default));
try std.testing.expectEqual(@as(f32, 0), instances[col * 2 + 1].u0);
try std.testing.expectEqual(old, instances[col * 2 + 1].effect & old_layer_bit != 0);
}
@@ -6802,10 +6863,12 @@ fn ensureVbuf(g: *Gui, cells: u32) !void {
g.vbuf_cells = capacity;
}
-fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat) !*c.SDL_GPUGraphicsPipeline {
+/// A pipeline over cell instances: the cells' own (ui.frag, the atlas, no
+/// blending) or the decor's (decor.frag, blended like the overlay).
+fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat, frag: []const u8, samplers: u32, blend: c.SDL_GPUColorTargetBlendState) !*c.SDL_GPUGraphicsPipeline {
const vs = try makeShader(device, vert_spv, c.SDL_GPU_SHADERSTAGE_VERTEX, 0, 0);
defer c.SDL_ReleaseGPUShader(device, vs);
- const fs = try makeShader(device, frag_spv, c.SDL_GPU_SHADERSTAGE_FRAGMENT, 1, 1);
+ const fs = try makeShader(device, frag, c.SDL_GPU_SHADERSTAGE_FRAGMENT, samplers, 1);
defer c.SDL_ReleaseGPUShader(device, fs);
var vbuf_desc = c.SDL_GPUVertexBufferDescription{
@@ -6822,8 +6885,9 @@ fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat)
.{ .location = 4, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(CellInstance, "panel_x0") },
.{ .location = 5, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(CellInstance, "present_x0") },
.{ .location = 6, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_UINT4, .offset = @offsetOf(CellInstance, "effect") },
+ .{ .location = 7, .buffer_slot = 0, .format = c.SDL_GPU_VERTEXELEMENTFORMAT_FLOAT4, .offset = @offsetOf(CellInstance, "clip_x0") },
};
- var col_desc = c.SDL_GPUColorTargetDescription{ .format = color_format, .blend_state = std.mem.zeroes(c.SDL_GPUColorTargetBlendState) };
+ var col_desc = c.SDL_GPUColorTargetDescription{ .format = color_format, .blend_state = blend };
var info = std.mem.zeroes(c.SDL_GPUGraphicsPipelineCreateInfo);
info.vertex_shader = vs;
@@ -7135,7 +7199,10 @@ fn latencyNote(comptime fmt: []const u8, args: anytype) void {
_ = libc.write(latency_fd, line.ptr, line.len);
}
-fn captureFrame(g: *Gui, gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTexture, sw: u32, sh: u32) !void {
+/// Writes the frame to latest.ppm in the capture directory, and a panel
+/// transition's frame `transition` to transition-<frame>.ppm too, so a test
+/// can hold one partway (test/gui_golden.py).
+fn captureFrame(g: *Gui, gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, target: *c.SDL_GPUTexture, sw: u32, sh: u32, transition: ?u16) !void {
const size = c.SDL_CalculateGPUTextureFormatSize(g.swapchain_format, sw, sh, 1);
if (size == 0) {
_ = c.SDL_SubmitGPUCommandBuffer(cmd);
@@ -7168,10 +7235,12 @@ fn captureFrame(g: *Gui, gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, t
const mapped = c.SDL_MapGPUTransferBuffer(g.device, xfer, false) orelse return error.GpuMap;
defer c.SDL_UnmapGPUTransferBuffer(g.device, xfer);
const pixels: [*]const u8 = @ptrCast(mapped);
- try writeCapturePpm(g, gpa, pixels[0..size], sw, sh);
+ try writeCapturePpm(g, gpa, pixels[0..size], sw, sh, "latest");
+ var name: [24]u8 = undefined;
+ if (transition) |frame| try writeCapturePpm(g, gpa, pixels[0..size], sw, sh, std.fmt.bufPrint(&name, "transition-{d}", .{frame}) catch unreachable);
}
-fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u32, height: u32) !void {
+fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u32, height: u32, name: []const u8) !void {
// A latency trace times frames, and an 8 MB file write per frame would
// stall the loop it is timing.
if (g.capture_dir.len == 0 or latency_fd >= 0) return;
@@ -7185,8 +7254,8 @@ fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u
var tmp_buf: [4096]u8 = undefined;
var final_buf: [4096]u8 = undefined;
- const tmp_path = std.fmt.bufPrintSentinel(&tmp_buf, "{s}/latest.ppm.tmp", .{g.capture_dir}, 0) catch return error.CapturePathTooLong;
- const final_path = std.fmt.bufPrintSentinel(&final_buf, "{s}/latest.ppm", .{g.capture_dir}, 0) catch return error.CapturePathTooLong;
+ const tmp_path = std.fmt.bufPrintSentinel(&tmp_buf, "{s}/{s}.ppm.tmp", .{ g.capture_dir, name }, 0) catch return error.CapturePathTooLong;
+ const final_path = std.fmt.bufPrintSentinel(&final_buf, "{s}/{s}.ppm", .{ g.capture_dir, name }, 0) catch return error.CapturePathTooLong;
const fd = libc.open(tmp_path, .{ .ACCMODE = .WRONLY, .CREAT = true, .TRUNC = true }, @as(libc.mode_t, 0o644));
if (fd < 0) return error.CaptureWriteFailed;
@@ -7212,116 +7281,263 @@ fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u
if (libc.rename(tmp_path, final_path) != 0) return error.CaptureWriteFailed;
}
-fn addCursorBar(
- builder: *OverlayBuilder,
- x: u16,
- y: u16,
- bar: bool,
- visual_height: f32,
- layout: CellLayout,
- rgb: [3]u8,
-) void {
- if (!bar) return;
- const previous_ink = builder.ink;
- builder.ink = true;
- defer builder.ink = previous_ink;
- const x0 = layout.x_off + @as(f32, @floatFromInt(x)) * layout.w;
- const height = std.math.clamp(visual_height, 1.0, layout.h);
- const y0 = layout.y_off + @as(f32, @floatFromInt(y)) * layout.h +
- @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, @intFromFloat(layout.h), @intFromFloat(height))));
- builder.addRect(
- x0,
- y0,
- x0 + @max(1.0, layout.w / 8.0),
- y0 + height,
- .{
- .r = @as(f32, @floatFromInt(rgb[0])) / 255.0,
- .g = @as(f32, @floatFromInt(rgb[1])) / 255.0,
- .b = @as(f32, @floatFromInt(rgb[2])) / 255.0,
- .a = 1.0,
- },
- );
+/// One flat, hard-edged rectangle of decor, in window pixels: clipped to the
+/// window and dropped when empty, as the triangle overlay drew it, and moved
+/// by `track` as its pane's cells are.
+fn addDecor(g: *Gui, gpa: std.mem.Allocator, x0_in: f32, y0_in: f32, x1_in: f32, y1_in: f32, rgb: [3]u8, alpha: f32, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
+ const x0 = std.math.clamp(@min(x0_in, x1_in), 0.0, win_w);
+ const x1 = std.math.clamp(@max(x0_in, x1_in), 0.0, win_w);
+ const y0 = std.math.clamp(@min(y0_in, y1_in), 0.0, win_h);
+ const y1 = std.math.clamp(@max(y0_in, y1_in), 0.0, win_h);
+ if (x1 <= x0 or y1 <= y0) return;
+ // decor.frag.glsl reads the colour from the ground and the coverage
+ // from the ink's red.
+ var instance: CellInstance = .{
+ .x0 = (x0 / win_w) * 2.0 - 1.0,
+ .y0 = 1.0 - (y0 / win_h) * 2.0,
+ .x1 = (x1 / win_w) * 2.0 - 1.0,
+ .y1 = 1.0 - (y1 / win_h) * 2.0,
+ .u0 = 0,
+ .v0 = 0,
+ .u1 = 0,
+ .v1 = 0,
+ .fr = alpha,
+ .fg = 0,
+ .fb = 0,
+ .br = @as(f32, @floatFromInt(rgb[0])) / 255.0,
+ .bg = @as(f32, @floatFromInt(rgb[1])) / 255.0,
+ .bb = @as(f32, @floatFromInt(rgb[2])) / 255.0,
+ };
+ setTransitionFields(&instance, track, fixedCellLayout(g), win_w, win_h, 0);
+ try g.decor.append(gpa, instance);
}
-fn buildOverlay(
- g: *Gui,
- core: ?*const pardes.Pardes,
- surface: *const pardes.Surface,
- layout: CellLayout,
- sw: u32,
- sh: u32,
- topbar_pane_border_rgb: [3]u8,
- tagline_rgb: [3]u8,
- transient_on: bool,
- debug_on: bool,
-) u32 {
- if (g.overlay_vertices.len == 0 or sw == 0 or sh == 0) return 0;
- const win_w: f32 = @floatFromInt(sw);
- const win_h: f32 = @floatFromInt(sh);
- var builder = OverlayBuilder{ .vertices = g.overlay_vertices, .win_w = win_w, .win_h = win_h };
- appendWorkspacePet(g, core, surface, &builder);
- // The workspace tag grows a row per line, so the border follows its foot.
- const top_rows: u16 = if (core) |p| p.topBarHeight() else pardes.TOPBAR_H;
- if (topbarPaneBorderHeight(surface, top_rows, g.cell_h, g.tagline_height)) |height| {
- const y0: f32 = @floatFromInt(top_rows * g.cell_h - height / 2);
- const rgb = topbar_pane_border_rgb;
- builder.addRect(0, y0, win_w, y0 + @as(f32, @floatFromInt(height)), .{
- .r = @as(f32, @floatFromInt(rgb[0])) / 255.0,
- .g = @as(f32, @floatFromInt(rgb[1])) / 255.0,
- .b = @as(f32, @floatFromInt(rgb[2])) / 255.0,
- .a = 1.0,
- });
+/// The frame's decor, group by group (docs/render-pipeline.md §3.4), all of it
+/// read from the regions and the palette: the page's rules and the still
+/// panes' rails, rules and grip marks in tier 0, a moving or closing pane's
+/// in its track's group, notice rules in tier 4, bar cursors in tier 5.
+fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, groups: *Groups, layout: CellLayout, win_w: f32, win_h: f32, cursors_shown: bool) !void {
+ g.decor.clearRetainingCapacity();
+ const chrome = &surface.chrome;
+ const regions = surface.regionList();
+ for (groups.items[0..groups.len], 0..) |*group, index| {
+ group.decor_start = @intCast(g.decor.items.len);
+ defer group.decor_count = @as(u32, @intCast(g.decor.items.len)) - group.decor_start;
+ if (index == 0) {
+ const top = for (regions) |region| {
+ if (region.kind == .workspace_tag) break region.rect.h;
+ } else 0;
+ // The workspace tag is ruled off from the tags right under it.
+ const tag_under = for (regions) |region| switch (region.kind) {
+ .grip, .tag, .column_grip, .column_tag => if (region.rect.y == top) break true,
+ else => {},
+ } else false;
+ const height = topbarPaneBorderPixels(g.cell_h, g.tagline_height);
+ if (top != 0 and top < surface.rows and tag_under and height != 0) {
+ const y0: f32 = @floatFromInt(top * g.cell_h - height / 2);
+ try addDecor(g, gpa, 0, y0, win_w, y0 + @as(f32, @floatFromInt(height)), chrome.topbar_rule, 1, null, win_w, win_h);
+ }
+ // The column bar is ruled off from the panes under it.
+ for (regions) |bar| {
+ if (bar.kind != .column_grip) continue;
+ const y = @as(f32, @floatFromInt(bar.rect.y + bar.rect.h)) * layout.h;
+ try addDecor(g, gpa, 0, y - 1, win_w, y, chrome.border, 1, null, win_w, win_h);
+ break;
+ }
+ // A tag on the last row runs on through the pixels under the grid.
+ const grid_bottom = @as(f32, @floatFromInt(surface.rows)) * layout.h;
+ const tag_last = for (regions) |region| switch (region.kind) {
+ .grip, .tag, .column_grip, .column_tag, .workspace_tag => if (region.rect.h != 0 and region.rect.y + region.rect.h == surface.rows) break true,
+ else => {},
+ } else false;
+ if (grid_bottom < win_h and tag_last) try addDecor(g, gpa, 0, grid_bottom, win_w, win_h, chrome.tag_bg, 1, null, win_w, win_h);
+ }
+ if (index <= groups.tracks) {
+ const track = group.track;
+ // A closing pane is gone: its chrome is the last frame's.
+ const closing = if (track) |active| active.phase == .closing else false;
+ const placed = if (closing) surface.previous_regions else regions;
+ const grid = if (closing) surface.previous_cells else surface.cells;
+ try paneDecor(g, gpa, placed, groups, index, chrome, layout, surface.cols, surface.rows, win_w, win_h, track);
+ // A single spine runs down each column, through every tag and body.
+ if (index == 0) for (regions) |column| {
+ if (column.kind != .column) continue;
+ const x = @as(f32, @floatFromInt(column.rect.x)) * layout.w;
+ const y0 = @as(f32, @floatFromInt(column.rect.y)) * layout.h;
+ const grid_y1 = @as(f32, @floatFromInt(surface.rows)) * layout.h;
+ const y1 = if (win_h - grid_y1 < layout.h) @max(grid_y1, win_h) else grid_y1;
+ if (y1 > y0) try addDecor(g, gpa, x, y0, x + 1, y1, chrome.border, 1, null, win_w, win_h);
+ };
+ try gripDecor(g, gpa, placed, grid, groups, index, layout, surface.cols, surface.rows, win_w, win_h, track);
+ }
+ // A notice chip is tagline hung over the body's top rows, so it is
+ // ruled off from the text beneath it the way a tag is: the same
+ // colour and thickness along its bottom, running to the window's
+ // edge when it does, fading up and dissolving with its band.
+ if (index == groups.notices) for (surface.tagLayers()) |*layer| {
+ if (layer.rows == 0 or layer.kind != .notice) continue;
+ const x0 = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;
+ const x1 = x0 + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w;
+ const rule_x1 = if (win_w - x1 < layout.w) @max(x1, win_w) else x1;
+ const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h;
+ // A band still falling into place is cut at its row's top; its
+ // rule comes with it, and shows only once it is below that cut.
+ const rule_y = top + (1 + layer.slide) * layout.h;
+ if (rule_y - 1 < top) continue;
+ const track = groups.items[groupAt(groups, layer.viewport.x, layer.viewport.y)].track;
+ try addDecor(g, gpa, x0, rule_y - 1, rule_x1, rule_y, chrome.border, layer.fade, track, win_w, win_h);
+ };
+ if (index == groups.ink and cursors_shown) try cursorDecor(g, gpa, surface, layout, win_w, win_h);
+ }
+}
+
+/// Each pane's rule between its tag and body, and its scroll rail: a page
+/// coloured gutter with a thin track and its thumb, running down through the
+/// pixels past the last whole row. Only the panes of group `index`.
+fn paneDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, groups: *const Groups, index: usize, chrome: *const pardes.Chrome, layout: CellLayout, cols: u16, rows: u16, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void {
+ const page = chrome.page orelse bg_default;
+ for (regions) |rail| {
+ if (rail.kind != .rail or rail.rect.h == 0 or groupOf(groups, rail.serial) != index) continue;
+ const tag = for (regions) |region| {
+ if (region.kind == .tag and region.owner == rail.owner) break region;
+ } else continue;
+ if (@as(u32, tag.rect.x) + tag.rect.w > cols or @as(u32, tag.rect.y) + tag.rect.h > rows or
+ @as(u32, rail.rect.y) + rail.rect.h > rows) continue;
+ const x0 = @as(f32, @floatFromInt(rail.rect.x)) * layout.w;
+ const x1 = @as(f32, @floatFromInt(tag.rect.x + tag.rect.w)) * layout.w;
+ // Like the band above it, the rule of a pane at the right edge runs
+ // on through the pixels past the last whole cell. Under a tag on
+ // top, over one below its body.
+ const rule_x1 = if (win_w - x1 < layout.w) @max(x1, win_w) else x1;
+ const tag_bottom = tag.rect.y > rail.rect.y;
+ const rule_y = @as(f32, @floatFromInt(if (tag_bottom) tag.rect.y else tag.rect.y + tag.rect.h)) * layout.h;
+ try addDecor(g, gpa, x0, if (tag_bottom) rule_y else rule_y - 1, rule_x1, if (tag_bottom) rule_y + 1 else rule_y, chrome.border, 1, track, win_w, win_h);
+
+ const y0 = @as(f32, @floatFromInt(rail.rect.y)) * layout.h;
+ const body_end = rail.rect.y + rail.rect.h;
+ const body_y1 = @as(f32, @floatFromInt(body_end)) * layout.h;
+ const y1 = if (body_end == rows and win_h - body_y1 < layout.h) @max(body_y1, win_h) else body_y1;
+ try addDecor(g, gpa, x0, y0, x0 + layout.w, y1, page, 1, track, win_w, win_h);
+ try addDecor(g, gpa, x0, y0, x0 + @min(layout.w, 2), y1, chrome.scroll_track, 1, track, win_w, win_h);
+ if (rail.thumb_h != 0) {
+ const thumb_y0 = @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y)) * layout.h;
+ const thumb_y1 = if (rail.thumb_y + rail.thumb_h == rail.rect.h) y1 else @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y + rail.thumb_h)) * layout.h;
+ try addDecor(g, gpa, x0, thumb_y0, x0 + @min(layout.w, @max(2, @floor(layout.w * 0.5))), thumb_y1, chrome.scroll_thumb, 1, track, win_w, win_h);
+ }
}
- if (core) |p| if (p.columnBarHeight() > 0) {
- const y = @as(f32, @floatFromInt(top_rows + p.columnBarHeight())) * layout.h;
- builder.addRect(0, y - 1, win_w, y, overlayRgb(p.chromeTheme().border));
+}
+
+/// The two short bars on each row of a grip with no mode glyph there,
+/// centered in its anchor. Only the panes of group `index`.
+fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, cells: []const pardes.Cell, groups: *const Groups, index: usize, layout: CellLayout, cols: u16, rows: u16, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void {
+ if (g.tagline_height < 6 or g.tagline_width < 3) return;
+ for (regions) |grip| {
+ if (grip.kind != .grip or groupOf(groups, grip.serial) != index) continue;
+ const x = grip.rect.x;
+ for (0..grip.rect.h) |line| {
+ const y = grip.rect.y + @as(u16, @intCast(line));
+ if (x >= cols or y >= rows) continue;
+ const cell = &cells[@as(usize, y) * cols + x];
+ if (cell.default or cell.printableAscii() != ' ') continue;
+ const rgb = switch (cell.style.fg) {
+ .rgb => |rgb| rgb,
+ else => continue,
+ };
+ const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w + @as(f32, @floatFromInt(anchorInset(g, y, win_h) / 2));
+ const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
+ const half_w = @max(1, @min(3, layout.w - 2));
+ try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), rgb, 1, track, win_w, win_h);
+ try addDecor(g, gpa, @floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), rgb, 1, track, win_w, win_h);
+ }
+ }
+}
+
+test "decor is read from the regions, each pane's in its own group" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.newShell(1, "");
+ try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ var g: Gui = undefined;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 6;
+ g.tagline_height = 12;
+ const layout = fixedCellLayout(&g);
+ const ndc = struct {
+ fn x(px: f32) f32 {
+ return px / 800 * 2 - 1;
+ }
};
- const grid_bottom = @as(f32, @floatFromInt(surface.rows)) * layout.h;
- if (grid_bottom < win_h and bottomTaglinePresent(surface)) {
- const rgb = tagline_rgb;
- builder.addRect(0, grid_bottom, win_w, win_h, .{
- .r = @as(f32, @floatFromInt(rgb[0])) / 255.0,
- .g = @as(f32, @floatFromInt(rgb[1])) / 255.0,
- .b = @as(f32, @floatFromInt(rgb[2])) / 255.0,
- .a = 1.0,
- });
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, layout, 800, 480, true);
+ // A spine one pixel wide at each column's left edge, down from under the
+ // workspace tag; a rail two pixels wide at each pane's.
+ for (core.col_x[0..core.ncol]) |x| {
+ const left = ndc.x(@as(f32, @floatFromInt(x)) * 10);
+ const spine = for (g.decor.items) |item| {
+ if (item.x0 == left and @abs(item.x1 - ndc.x(@as(f32, @floatFromInt(x)) * 10 + 1)) < 0.0001) break item;
+ } else return error.TestExpectedSpine;
+ try std.testing.expectApproxEqAbs(@as(f32, 1 - 20.0 / 480.0 * 2), spine.y0, 0.0001);
+ try std.testing.expectEqual(@as(f32, -1), spine.y1);
+ const rail = for (g.decor.items) |item| {
+ if (item.x0 == left and @abs(item.x1 - ndc.x(@as(f32, @floatFromInt(x)) * 10 + 2)) < 0.0001) break item;
+ } else return error.TestExpectedRail;
+ try std.testing.expectApproxEqAbs(@as(f32, s.chrome.scroll_track[0]) / 255, rail.br, 0.0001);
}
- if (transient_on) {
- if (core) |p| appendPaneChrome(p, surface, layout, &builder);
- appendTagGrips(g, surface, layout, &builder);
- if (surface.cursor) |cursor| {
- if (cursor.x < surface.cols and cursor.y < surface.rows and bodyLayerAt(surface, cursor.x, cursor.y) == null and tagLayerIn(surface.tagLayers(), cursor.x, cursor.y) == null) {
- const cell = surface.cells[@as(usize, cursor.y) * surface.cols + cursor.x];
- const role = cellFontRole(&cell);
- const visual_height: f32 = if (role == .tagline)
- @floatFromInt(g.tagline_height)
- else
- layout.h;
- const cursor_layout = if (role == .tagline)
- taglineLayoutForCell(g, cursor.x)
- else
- layout;
- addCursorBar(&builder, cursor.x, cursor.y, cursor.bar, visual_height, cursor_layout, cursorColor(core, &cell, role));
- }
+ try std.testing.expectEqual(@as(u32, 0), groups.items[groups.notices].decor_count);
+ const still = groups.items[0].decor_count;
+
+ // Pane 1 moving: its rule and rail leave tier 0 for its track's group,
+ // which moves them with it.
+ const serial = core.panes[1].?.serial;
+ const moving: pardes.animation.Track = .{ .pane = 1, .serial = serial, .phase = .moving, .effect = .slide, .from = .{ .x = 0, .y = 1, .w = 40, .h = 23 }, .to = .{ .x = 40, .y = 1, .w = 40, .h = 23 } };
+ groups = makeGroups(&.{moving}, true);
+ try buildDecor(&g, gpa, s, &groups, layout, 800, 480, groups.tracks == 0);
+ const tracked = groups.items[1];
+ try std.testing.expect(tracked.decor_count >= 4);
+ try std.testing.expectEqual(still, groups.items[0].decor_count + tracked.decor_count);
+ for (g.decor.items[tracked.decor_start..][0..tracked.decor_count]) |item|
+ try std.testing.expectEqual(@as(u32, @intFromEnum(pardes.animation.Transition.slide)), item.effect);
+ // Cursors stand still while panes move.
+ try std.testing.expectEqual(@as(u32, 0), groups.items[groups.ink].decor_count);
+}
+
+/// The bar cursors: the grid's where no layer has the cell, and each layer's
+/// at its own pitch.
+fn cursorDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, layout: CellLayout, win_w: f32, win_h: f32) !void {
+ const chrome = &surface.chrome;
+ if (surface.cursor) |cursor| {
+ if (cursor.bar and cursor.x < surface.cols and cursor.y < surface.rows and bodyLayerAt(surface, cursor.x, cursor.y) == null and tagLayerIn(surface.tagLayers(), cursor.x, cursor.y) == null) {
+ const cell = &surface.cells[@as(usize, cursor.y) * surface.cols + cursor.x];
+ const role = cellFontRole(cell);
+ const width: f32 = if (role == .tagline) @floatFromInt(g.tagline_width) else layout.w;
+ const height = std.math.clamp(if (role == .tagline) @as(f32, @floatFromInt(g.tagline_height)) else layout.h, 1.0, layout.h);
+ const x0 = @as(f32, @floatFromInt(cursor.x)) * layout.w;
+ const y0 = @as(f32, @floatFromInt(cursor.y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(cursor.y, win_h, @intFromFloat(layout.h), @intFromFloat(height))));
+ try addDecor(g, gpa, x0, y0, x0 + @max(1.0, width / 8.0), y0 + height, cursorColor(chrome, cell, role), 1, null, win_w, win_h);
}
}
- if (transient_on) for (surface.bodyLayers()) |*layer| {
+ for (surface.bodyLayers()) |*layer| {
if (layer.rows == 0) continue;
+ const cursor = layer.cursor orelse continue;
+ if (!cursor.bar or cursor.y >= layer.rows or cursor.x >= layer.cols) continue;
const left = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;
const bottom = @as(f32, @floatFromInt(layer.viewport.y + layer.viewport.h)) * layout.h;
- if (layer.cursor) |cursor| {
- if (!cursor.bar or cursor.y >= layer.rows or cursor.x >= layer.cols) continue;
- const top = layer.rowTop(cursor.y, layout.h, @floatFromInt(g.tagline_height));
- if (top >= bottom) continue;
- const height = layer.rowHeight(cursor.y, layout.h, @floatFromInt(g.tagline_height));
- const role: pardes.FontRole = if (cursor.y < layer.context_rows) .tagline else .body;
- const width: f32 = if (role == .tagline) @floatFromInt(g.tagline_width) else layout.w;
- const x = left + @as(f32, @floatFromInt(cursor.x)) * width;
- const cell = &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x];
- builder.addRect(x, top, x + @max(1, width / 8), @min(bottom, top + height), overlayRgb(cursorColor(core, cell, role)));
- }
- };
- if (transient_on) for (surface.tagLayers()) |*layer| {
+ const top = layer.rowTop(cursor.y, layout.h, @floatFromInt(g.tagline_height));
+ if (top >= bottom) continue;
+ const height = layer.rowHeight(cursor.y, layout.h, @floatFromInt(g.tagline_height));
+ const role: pardes.FontRole = if (cursor.y < layer.context_rows) .tagline else .body;
+ const width: f32 = if (role == .tagline) @floatFromInt(g.tagline_width) else layout.w;
+ const x = left + @as(f32, @floatFromInt(cursor.x)) * width;
+ const cell = &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x];
+ try addDecor(g, gpa, x, top, x + @max(1, width / 8), @min(bottom, top + height), cursorColor(chrome, cell, role), 1, null, win_w, win_h);
+ }
+ for (surface.tagLayers()) |*layer| {
if (layer.rows == 0) continue;
const cursor = layer.cursor orelse continue;
if (!cursor.bar or cursor.x >= layer.cols or cursor.y >= layer.rows) continue;
@@ -7331,10 +7547,25 @@ fn buildOverlay(
const x = left + @as(f32, @floatFromInt(cursor.x)) * width;
if (x >= right) continue;
const y = layer.viewport.y + cursor.y;
- const top = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height)));
- builder.addRect(x, top, @min(right, x + @max(1, width / 8)), top + @as(f32, @floatFromInt(g.tagline_height)), overlayRgb(cursorColor(core, &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x], .tagline)));
- };
- if (transient_on) appendTouchOverlay(g, &builder, debug_on);
+ const top = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, win_h, g.cell_h, g.tagline_height)));
+ try addDecor(g, gpa, x, top, @min(right, x + @max(1, width / 8)), top + @as(f32, @floatFromInt(g.tagline_height)), cursorColor(chrome, &layer.cells[@as(usize, cursor.y) * layer.cols + cursor.x], .tagline), 1, null, win_w, win_h);
+ }
+}
+
+/// The triangle overlay: the workspace pet and the touch debug view, over
+/// everything.
+fn buildOverlay(
+ g: *Gui,
+ core: ?*const pardes.Pardes,
+ surface: *const pardes.Surface,
+ sw: u32,
+ sh: u32,
+ debug_on: bool,
+) u32 {
+ if (g.overlay_vertices.len == 0 or sw == 0 or sh == 0) return 0;
+ var builder = OverlayBuilder{ .vertices = g.overlay_vertices, .win_w = @floatFromInt(sw), .win_h = @floatFromInt(sh) };
+ appendWorkspacePet(g, core, surface, &builder);
+ appendTouchOverlay(g, &builder, debug_on);
return @intCast(builder.len);
}
@@ -7409,33 +7640,11 @@ test "pet overlay stays beyond workspace text and vanishes while its tag is edit
try std.testing.expectEqual(@as(usize, 0), builder.len);
}
-fn cursorColor(core: ?*const pardes.Pardes, cell: *const pardes.Cell, role: pardes.FontRole) [3]u8 {
- if (core) |p| return if (role == .tagline) switch (cell.style.fg) {
+fn cursorColor(chrome: *const pardes.Chrome, cell: *const pardes.Cell, role: pardes.FontRole) [3]u8 {
+ return if (role == .tagline) switch (cell.style.fg) {
.rgb => |rgb| rgb,
- else => p.chromeTheme().tag_fg,
- } else p.theme().fg orelse fg_default;
- return switch (cell.style.fg) {
- .rgb => |rgb| rgb,
- .index => |index| palColor(index),
- // Detached frames carry explicit page backgrounds but blank cells need
- // not carry foreground ink. Keep their insertion caret visible too.
- .default => switch (cell.style.bg) {
- .rgb => |rgb| if ((@as(u16, rgb[0]) * 3 + @as(u16, rgb[1]) * 6 + rgb[2]) / 10 > 140)
- .{ 32, 32, 32 }
- else
- fg_default,
- else => fg_default,
- },
- };
-}
-
-test "detached insertion caret contrasts with blank light and dark pages" {
- var cell: pardes.Cell = .{ .default = false, .style = .{ .bg = .{ .rgb = .{ 248, 246, 237 } } } };
- try std.testing.expectEqual([3]u8{ 32, 32, 32 }, cursorColor(null, &cell, .body));
- cell.style.bg = .{ .rgb = .{ 24, 28, 32 } };
- try std.testing.expectEqual(fg_default, cursorColor(null, &cell, .body));
- cell.style.fg = .{ .rgb = .{ 122, 188, 160 } };
- try std.testing.expectEqual([3]u8{ 122, 188, 160 }, cursorColor(null, &cell, .body));
+ else => chrome.tag_fg,
+ } else chrome.fg orelse fg_default;
}
fn overlayRgb(rgb: [3]u8) OverlayColor {
@@ -7447,185 +7656,6 @@ fn overlayRgb(rgb: [3]u8) OverlayColor {
};
}
-fn cellBackgroundIs(cell: *const pardes.Cell, rgb: [3]u8) bool {
- return !cell.default and switch (cell.style.bg) {
- .rgb => |bg| std.mem.eql(u8, &bg, &rgb),
- else => false,
- };
-}
-
-// The canonical surface still owns scrollbar positions and hit targets. Only
-// their pixel ink is inset here; no layout, selection or mouse coordinates move.
-fn appendPaneChrome(
- core: *const pardes.Pardes,
- surface: *const pardes.Surface,
- layout: CellLayout,
- builder: *OverlayBuilder,
-) void {
- const chrome = core.chromeTheme();
- const page = overlayRgb(core.theme().bg orelse bg_default);
- for (core.panes, core.rects) |slot, r| {
- const pane = slot orelse continue;
- if (r.w <= config.GUTTER or r.h <= pane.tag_rows) continue;
- if (@as(u32, r.x) + r.w > surface.cols or @as(u32, r.y) + r.h > surface.rows) continue;
- const tag_y = core.tagTop(pane, r);
- const body_y = core.bodyTop(pane, r);
- const body_end = body_y + (r.h -| pane.tag_rows);
- const x0 = @as(f32, @floatFromInt(r.x)) * layout.w;
- const x1 = x0 + @as(f32, @floatFromInt(r.w)) * layout.w;
- const tag_cells = surface.cells[@as(usize, tag_y) * surface.cols + r.x + pardes.TAG_TEXT_INSET ..][0 .. r.w - pardes.TAG_TEXT_INSET];
- const tag_visible = for (tag_cells) |*cell| {
- if (cellFontRole(cell) != .tagline) break false;
- } else true;
- if (tag_visible) {
- const rule_y = @as(f32, @floatFromInt(tag_y + (if (core.settings.tag_bottom) 0 else pane.tag_rows))) * layout.h;
- // Like the band above it, the rule of a pane at the right edge
- // runs on through the pixels past the last whole cell.
- const rule_x1 = if (builder.win_w - x1 < layout.w) @max(x1, builder.win_w) else x1;
- builder.addRect(x0, rule_y - @as(f32, if (core.settings.tag_bottom) 0 else 1), rule_x1, rule_y + @as(f32, if (core.settings.tag_bottom) 1 else 0), overlayRgb(chrome.border));
- }
-
- // Do not paint over resize previews or debug text occupying the rail.
- var thumb_start: ?u16 = null;
- var thumb_end: u16 = body_y;
- var row = body_y;
- while (row < body_end) : (row += 1) {
- const cell = &surface.cells[@as(usize, row) * surface.cols + r.x];
- if (cell.printableAscii() != ' ') break;
- if (cellBackgroundIs(cell, chrome.scroll_thumb)) {
- if (thumb_start == null) thumb_start = row;
- thumb_end = row + 1;
- } else if (!cellBackgroundIs(cell, chrome.scroll_track)) break;
- }
- if (row != body_end) continue;
- const y0 = @as(f32, @floatFromInt(body_y)) * layout.h;
- const body_y1 = @as(f32, @floatFromInt(body_end)) * layout.h;
- const y1 = if (body_end == surface.rows and builder.win_h - body_y1 < layout.h) @max(body_y1, builder.win_h) else body_y1;
- builder.addRect(x0, y0, x0 + layout.w, y1, page);
- appendSquareRail(builder, x0, y0, y1, layout.w, false, overlayRgb(chrome.scroll_track));
- if (thumb_start) |first| {
- const thumb_y0 = @as(f32, @floatFromInt(first)) * layout.h;
- const thumb_y1 = if (thumb_end == body_end) y1 else @as(f32, @floatFromInt(thumb_end)) * layout.h;
- appendSquareRail(builder, x0, thumb_y0, thumb_y1, layout.w, true, overlayRgb(chrome.scroll_thumb));
- }
- }
- // A notice chip is tagline hung over the body's top rows, so it is ruled
- // off from the text beneath it the way a tagline is: the same colour and
- // thickness along its bottom, running to the window edge when it does.
- for (surface.tagLayers()) |*layer| {
- if (layer.rows == 0 or layer.kind != .notice) continue;
- const x0 = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;
- const x1 = x0 + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w;
- const rule_x1 = if (builder.win_w - x1 < layout.w) @max(x1, builder.win_w) else x1;
- const top = @as(f32, @floatFromInt(layer.viewport.y)) * layout.h;
- // A band still falling into place is cut at its row's top; its rule
- // comes with it, and shows only once it is below that cut.
- const rule_y = top + (1 + layer.slide) * layout.h;
- if (rule_y - 1 < top) continue;
- // ...and fades up and dissolves with it rather than standing alone.
- var rule = overlayRgb(chrome.border);
- rule.a = layer.fade;
- builder.addRect(x0, rule_y - 1, rule_x1, rule_y, rule);
- }
- appendColumnSpines(core, surface.rows, layout, builder);
-}
-
-// Tag handles use the canonical physical cells, including detached frames.
-// Their tag text begins at the layer viewport and may use a narrower font.
-fn appendTagGrips(g: *const Gui, surface: *const pardes.Surface, layout: CellLayout, builder: *OverlayBuilder) void {
- if (g.tagline_height < 6 or g.tagline_width < 3) return;
- for (surface.tagLayers()) |layer| {
- if (layer.rows == 0 or layer.kind != .pane or layer.viewport.x < pardes.TAG_TEXT_INSET) continue;
- const x = layer.viewport.x - pardes.TAG_TEXT_INSET;
- // A mark on each of the tag's rows whose grip cell is blank: every
- // row but the one with the mode's glyph.
- for (0..layer.rows) |index| {
- const y = layer.viewport.y + @as(u16, @intCast(index));
- if (x >= surface.cols or y >= surface.rows) continue;
- const cell = &surface.cells[@as(usize, y) * surface.cols + x];
- if (cell.default or cell.printableAscii() != ' ') continue;
- const rgb = switch (cell.style.fg) {
- .rgb => |rgb| rgb,
- else => continue,
- };
- const center_x = (@as(f32, @floatFromInt(x)) + @as(f32, @floatFromInt(config.GUTTER)) / 2) * layout.w + @as(f32, @floatFromInt(anchorInset(g, y, builder.win_h) / 2));
- const center_y = @as(f32, @floatFromInt(y)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(y, builder.win_h, g.cell_h, g.tagline_height))) + @as(f32, @floatFromInt(g.tagline_height)) / 2;
- const half_w = @max(1, @min(3, layout.w - 2));
- builder.addRect(@floor(center_x - half_w), @floor(center_y - 2), @floor(center_x + half_w), @floor(center_y - 1), overlayRgb(rgb));
- builder.addRect(@floor(center_x - half_w), @floor(center_y + 1), @floor(center_x + half_w), @floor(center_y + 2), overlayRgb(rgb));
- }
- }
-}
-
-test "column grips stay unmarked while pane marks retain physical centering" {
- var g: Gui = undefined;
- g.tagline_height = 12;
- g.tagline_width = 6;
- g.cell_h = 20;
- var cells: [80]pardes.Cell = @splat(.{ .default = false, .style = .{ .fg = .{ .rgb = .{ 100, 120, 140 } } } });
- var surface: pardes.Surface = .{ .cols = 20, .rows = 4, .cells = &cells };
- surface.tag_layers[0] = .{ .kind = .column, .viewport = .{ .x = 8, .y = 1, .w = 13, .h = 1 }, .cols = 20, .rows = 1 };
- surface.tag_layers[1] = .{ .kind = .pane, .viewport = .{ .x = 8, .y = 3, .w = 13, .h = 1 }, .cols = 20, .rows = 1 };
- var vertices: [24]OverlayVertex = undefined;
- var builder: OverlayBuilder = .{ .vertices = &vertices, .win_w = 200, .win_h = 80 };
- appendTagGrips(&g, &surface, .{ .w = 10, .h = 20, .x_off = 0, .y_off = 0 }, &builder);
- try std.testing.expectEqual(@as(usize, 12), builder.len);
- // Centered in the anchor, whose left edge is inset by the band's offset
- // (8 px on this last row): the mark moves right by half of it.
- try std.testing.expectApproxEqAbs(@as(f32, 61.0 / 200.0 * 2.0 - 1.0), vertices[0].x, 0.0001);
-}
-
-fn appendSquareRail(builder: *OverlayBuilder, x: f32, y0: f32, y1: f32, cell_w: f32, thumb: bool, color: OverlayColor) void {
- const width = @min(cell_w, if (thumb) @max(2, @floor(cell_w * 0.5)) else 2);
- builder.addRect(x, y0, x + width, y1, color);
-}
-
-fn appendColumnSpines(core: *const pardes.Pardes, rows: u16, layout: CellLayout, builder: *OverlayBuilder) void {
- const y0 = @as(f32, @floatFromInt(core.topBarHeight())) * layout.h;
- const grid_y1 = @as(f32, @floatFromInt(rows)) * layout.h;
- // Down through the pixels past the last whole row, as tags run right.
- const y1 = if (builder.win_h - grid_y1 < layout.h) @max(grid_y1, builder.win_h) else grid_y1;
- if (y1 <= y0) return;
- // A single boundary spans all panes, including their tags. Its left edge
- // agrees with the square scroll markers and existing move-box hit area.
- for (core.col_x[0..core.ncol], core.col_w[0..core.ncol]) |col, width| {
- if (width == 0) continue;
- const x = @as(f32, @floatFromInt(col)) * layout.w;
- builder.addRect(x, y0, x + 1, y1, overlayRgb(core.chromeTheme().border));
- }
-}
-
-test "square scroll markers share the column edge and have no rounded end inset" {
- var vertices: [12]OverlayVertex = undefined;
- var builder: OverlayBuilder = .{ .vertices = &vertices, .win_w = 800, .win_h = 480 };
- appendSquareRail(&builder, 400, 40, 480, 10, false, overlayRgb(.{ 40, 40, 40 }));
- appendSquareRail(&builder, 400, 80, 160, 10, true, overlayRgb(.{ 100, 100, 100 }));
- try std.testing.expectEqual(@as(usize, 12), builder.len);
- try std.testing.expectEqual(vertices[0].x, vertices[6].x);
- try std.testing.expectApproxEqAbs(@as(f32, 1 - 80.0 / 480.0 * 2), vertices[6].y, 0.0001);
- try std.testing.expectApproxEqAbs(@as(f32, 1 - 160.0 / 480.0 * 2), vertices[8].y, 0.0001);
- try std.testing.expectApproxEqAbs(@as(f32, 5.0 / 800.0 * 2), vertices[7].x - vertices[6].x, 0.0001);
-}
-
-test "column spines remain continuous across every pane tag and body" {
- const core = try pardes.Pardes.init(std.testing.allocator, .{ .cols = 80, .rows = 24 });
- defer core.deinit();
- _ = try core.newShell(1, "");
- try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
- pardes.test_api.sync(core);
- var vertices: [12]OverlayVertex = undefined;
- var builder: OverlayBuilder = .{ .vertices = &vertices, .win_w = 800, .win_h = 480 };
- appendColumnSpines(core, 24, .{ .w = 10, .h = 20, .x_off = 0, .y_off = 0 }, &builder);
- try std.testing.expectEqual(@as(usize, 12), builder.len);
- for (0..2) |col| {
- const first = vertices[col * 6];
- const bottom = vertices[col * 6 + 2];
- try std.testing.expectApproxEqAbs(@as(f32, 1 - 20.0 / 480.0 * 2), first.y, 0.0001);
- try std.testing.expectEqual(@as(f32, -1), bottom.y);
- try std.testing.expectApproxEqAbs(@as(f32, @floatFromInt(core.col_x[col])) * 10.0 / 800.0 * 2 - 1, first.x, 0.0001);
- }
-}
-
fn appendTouchOverlay(g: *Gui, builder: *OverlayBuilder, debug_on: bool) void {
if (!debug_on) {
g.touch.click_flash_frames = 0;
diff --git a/src/pardes.zig b/src/pardes.zig
index f8087bdb..616747fa 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -39,6 +39,7 @@ pub const ImagePatch = @import("surface.zig").ImagePatch;
pub const ImagePlace = @import("surface.zig").ImagePlace;
pub const Surface = @import("surface.zig").Surface;
pub const Region = @import("surface.zig").Region;
+pub const Chrome = @import("surface.zig").Chrome;
pub const MAX_REGIONS = @import("surface.zig").MAX_REGIONS;
pub const builtins = @import("builtins.zig");
const limits = memory.limits;
diff --git a/src/surface.zig b/src/surface.zig
index db7dd468..07b593b1 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -344,6 +344,13 @@ pub const Region = struct {
column_grip,
column_tag,
workspace_tag,
+ /// A column below the workspace tag, its bar and panes: its left
+ /// edge is the spine a pixel shell rules down it.
+ column,
+ /// A drag's guide or a seam's hover hint, drawn over the panes.
+ guide,
+ /// The debug stats box, drawn over the panes.
+ debug,
};
pub const none = std.math.maxInt(u8);
@@ -368,7 +375,30 @@ pub const Region = struct {
clip: u16 = 0,
};
-pub const MAX_REGIONS = 1 + MAX_PANES * (4 + panes.Pane.Notices.max) + pardes.MAX_COLS * 2 + 1;
+/// Guides a frame can draw: a run of a column-move rail per pane it skips
+/// the tags of, and a handful of border and hover dashes.
+pub const MAX_GUIDES = MAX_PANES + 4;
+pub const MAX_REGIONS = 1 + MAX_PANES * (4 + panes.Pane.Notices.max) + pardes.MAX_COLS * 3 + 1 + MAX_GUIDES + 1;
+
+/// The colours a pixel shell draws its chrome in (rules, rails, grips, the
+/// tag underlay), resolved by the core each frame, so a frontend attached
+/// to a detached session draws them from the frame alone.
+pub const Chrome = struct {
+ /// The theme's ground and ink; null where it leaves them to the shell.
+ page: ?[3]u8 = null,
+ fg: ?[3]u8 = null,
+ /// Rules and column spines.
+ border: [3]u8 = .{ 0, 0, 0 },
+ /// The rule under the workspace tag.
+ topbar_rule: [3]u8 = .{ 0, 0, 0 },
+ tag_bg: [3]u8 = .{ 0, 0, 0 },
+ /// The focused pane's and column's tag underlay: `tag_bg` unless
+ /// FocusTint is on.
+ tag_focus_bg: [3]u8 = .{ 0, 0, 0 },
+ tag_fg: [3]u8 = .{ 0, 0, 0 },
+ scroll_track: [3]u8 = .{ 0, 0, 0 },
+ scroll_thumb: [3]u8 = .{ 0, 0, 0 },
+};
/// The canonical interface: what a frame of pardes IS. The tty shell writes
/// these cells to vaxis one-to-one; the SDL shells rasterize them.
@@ -385,6 +415,7 @@ pub const Surface = struct {
rows: u16 = 0,
regions: [MAX_REGIONS]Region = undefined,
nregions: u16 = 0,
+ chrome: Chrome = .{},
cells: []Cell = &.{},
/// bar: draw an insert-style thin cursor instead of the block
cursor: ?struct { x: u16, y: u16, bar: bool = false } = null,
@@ -395,6 +426,7 @@ pub const Surface = struct {
previous_cells: []const Cell = &.{},
previous_body_layers: []const Layer = &.{},
previous_tag_layers: []const Layer = &.{},
+ previous_regions: []const Region = &.{},
cell_diffs: []const PanelCellDiff = &.{},
pub fn tagLayers(s: *const Surface) []const Layer {
@@ -409,6 +441,12 @@ pub const Surface = struct {
return s.regions[0..s.nregions];
}
+ pub fn addRegion(s: *Surface, r: Region) void {
+ std.debug.assert(s.nregions < s.regions.len);
+ s.regions[s.nregions] = r;
+ s.nregions += 1;
+ }
+
/// The first part of `kind` that `owner` has: its tag, its rail.
pub fn region(s: *Surface, kind: Region.Kind, owner: usize) ?*Region {
for (s.regions[0..s.nregions]) |*r| if (r.kind == kind and r.owner == owner) return r;
diff --git a/test/gui-goldens.txt b/test/gui-goldens.txt
index e52155a3..27c39150 100644
--- a/test/gui-goldens.txt
+++ b/test/gui-goldens.txt
@@ -10,8 +10,9 @@
10-tagbottom dd8ba0d3ecabf3420d000b0c9591a57278179cbd2a9060f849d283f3881d1847
11-column-tags 6b1731d1611404597c96ad77f34c2d5cd5a81c1085ec68c6779388e07ce28614
12-multiline-tag 1b9ccd331814dc76111470f18448b1ca192e1edc3d79ea6eee19a6a78a4b1831
-13-image de18edee2ab2e0f2d4bf4162d86e3d8a1852fccfc344830491fa1a359095d4ea
-14-theme-ink 6e733d4212373c90b860b2e41102ced8719b07480591b5da3905267c8358db18
-15-theme-acme-light e099dc1a351de8efa63ecd0ec0899636ea31b722a945eb40137a18abd87a82e2
-16-debug e97ce74b7a3d4a08f6a529b1f0de111d62e5532c829a6b1633b966dd45218f55
-17-terminal 304a84d1ef871f581d0a3fade9bc1acdcc87a3612c607548ab0d2851871753ee
+13-image a52e2cf9f238447d885692cba01e68ef7fdc6ae3fc7c8fe1e95c8463c85f519c
+14-theme-ink cc2ea256ddda24e1c97871f75b71ddf62fa825bd02f5d1432592d05e478c707a
+15-theme-acme-light 2bde8a2bdfada7f061217f52cb2272913fc0657dbfa1d7448e1852f321f8e3bf
+16-debug 129ad6f5f85b0e60e81bc6e8e78434629ee751f49731cf2eb28fd0553efd914a
+17-terminal a1305fd755b6bc957228bbf28833f972c2355af9abf40b77f76b30741109574f
+18-mid-transition d4f353ef5fda80411cd9de9729e5b23f59ff7511c10774e69214152ee8070725
diff --git a/test/gui_golden.py b/test/gui_golden.py
index 327911cc..d97009c9 100644
--- a/test/gui_golden.py
+++ b/test/gui_golden.py
@@ -106,7 +106,9 @@ def run(args):
display = Path(os.environ.get('WAYLAND_DISPLAY', 'wayland-0'))
if not display.is_absolute():
display = Path(os.environ.get('XDG_RUNTIME_DIR', '/run/user/%d' % os.getuid())) / display
- inherited = {'PARDES_TEST': '1', 'PARDES_TEST_COLS': '120', 'PARDES_TEST_ROWS': '40',
+ # The virtual clock: a transition's frames are the same frames on any
+ # machine, so one partway through can be hashed too.
+ inherited = {'PARDES_TEST': '1', 'PARDES_TEST_CLOCK': '1', 'PARDES_TEST_COLS': '120', 'PARDES_TEST_ROWS': '40',
'PARDES_TEST_PAD': '7', 'PARDES_TEST_CAPTURE_DIR': str(root),
'SDL_VIDEODRIVER': 'wayland', 'WAYLAND_DISPLAY': str(display)}
launch = ['-c', 'stty cols 120 rows 40; exec "$@"', name, str(args.binary.resolve()),
@@ -117,11 +119,12 @@ def run(args):
first = require(wait_for(lambda: next((s for s, tag in panes(client).items()
if 'shapes.zig' in tag), None)), 'source pane missing')
- def scene(label):
- results.append((label, settled(root)))
+ def scene(label, frame='latest.ppm'):
+ settled(root)
+ results.append((label, hashlib.sha256((root / frame).read_bytes()).hexdigest()))
if args.keep:
args.keep.mkdir(parents=True, exist_ok=True)
- shutil.copyfile(root / 'latest.ppm', args.keep / (label + '.ppm'))
+ shutil.copyfile(root / frame, args.keep / (label + '.ppm'))
print(label, results[-1][1][:16], flush=True)
def mouse(kind, button, x, y):
@@ -196,6 +199,13 @@ def run(args):
client.write(f'/pane/{terminal}/pty/data', b"PS1='$ '; clear; printf '\\033[1;36mcolour\\033[0m plain \\033[32mgreen\\033[0m\\n'\r")
require(wait_for(lambda: b'green' in client.read(f'/pane/{terminal}/body')), 'terminal output missing')
scene('17-terminal')
+ # Partway through a transition: the panes, the picture among them,
+ # sliding over for a new column, frame 6 of PanelSlide's 12. Its
+ # chrome slides with each pane.
+ client.write('/ctl', b'PanelSlide\n')
+ client.write(f'/pane/{first}/exec', b'Newcol\n')
+ require(wait_for(lambda: (root / 'transition-6.ppm').exists()), 'transition frame 6 never drawn')
+ scene('18-mid-transition', 'transition-6.ppm')
lines = ''.join(f'{label} {digest}\n' for label, digest in results)
if args.update:
GOLDENS.write_text(lines)