summaryrefslogtreecommitdiff
path: root/src/gui
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui')
-rw-r--r--src/gui/gui.zig1472
1 files changed, 751 insertions, 721 deletions
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;