summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 07:47:39 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commitab668719385c94df3e274f71cb744f5917c1da7e (patch)
tree3f3002e8a7dec61e0b2e467828490405e0a14463 /src/gui/gui.zig
parent87ad46cb7e7410c53419c20cef7df53ed4c15f60 (diff)
downloadpardes-ab668719385c94df3e274f71cb744f5917c1da7e.tar.gz
pardes-ab668719385c94df3e274f71cb744f5917c1da7e.zip
G12: Occlusion, pane bodies darkening faintly toward their edges
Occlusion on|off (off, a pixel shell's; Chrome.fx bit 2 — the glow bits field is now `fx`): four soft strips cast from just outside each pane body inward, clipped to the body and round a cursor, black pre-warped to linear light, 2% at most (§9.1) and under the lift's contrast ceiling, in the pane's own group. Tests: the shade stays inside the body, off the cursor, at most 2%. Gates as G11. Feel review: .scratch/render/occlusion/ (with an x40 diff).
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig91
1 files changed, 85 insertions, 6 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 45861ea4..6bde8b60 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -5815,9 +5815,9 @@ fn renderFrame(
if (g.glide.pending) g.glide.measure(gpa, surface);
g.sel_glow.now_ns = if (core) |p| p.now_ns else c.SDL_GetTicksNS();
// Off, or while panes move, no glow and nothing fading.
- if (surface.chrome.glows & 1 == 0 or surface.panelTracks().len != 0) g.sel_glow = .{ .now_ns = g.sel_glow.now_ns };
+ if (surface.chrome.fx & 1 == 0 or surface.panelTracks().len != 0) g.sel_glow = .{ .now_ns = g.sel_glow.now_ns };
g.hover_glow.now_ns = g.sel_glow.now_ns;
- if (surface.chrome.glows & 2 == 0 or surface.panelTracks().len != 0) g.hover_glow = .{ .now_ns = g.sel_glow.now_ns };
+ if (surface.chrome.fx & 2 == 0 or surface.panelTracks().len != 0) g.hover_glow = .{ .now_ns = g.sel_glow.now_ns };
if (g.glide.moving()) g.glide.offset = g.glide.spring.value(g.glide.now_ns);
cell_decor = CellDecor.of(chrome);
// Plaques need an opaque window: with WindowOpacity under 100 the cells
@@ -8708,9 +8708,11 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
const bodies = body_buf[0..nbodies];
try paneDecor(g, gpa, placed, groups, index, chrome, layout, surface.cols, surface.rows, win_w, win_h, track);
// G9, while nothing moves: a halo round the body's selection.
- if (index == 0 and groups.tracks == 0 and chrome.glows & 1 != 0) try glowBlocks(g, gpa, surface, .selection, layout, win_w, win_h);
+ if (index == 0 and groups.tracks == 0 and chrome.fx & 1 != 0) try glowBlocks(g, gpa, surface, .selection, layout, win_w, win_h);
// G11: a glow under the word a look-hover would open.
- if (index == 0 and groups.tracks == 0 and chrome.glows & 2 != 0) try glowBlocks(g, gpa, surface, .hover, layout, win_w, win_h);
+ if (index == 0 and groups.tracks == 0 and chrome.fx & 2 != 0) try glowBlocks(g, gpa, surface, .hover, layout, win_w, win_h);
+ // G12: the bodies' edges shaded, a pane's in its own group.
+ if (chrome.fx & 4 != 0) try occlusion(g, gpa, placed, groups, index, surface, ceilings, layout, 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;
@@ -9092,6 +9094,44 @@ fn glowBlocks(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, k
tracker.* = next;
}
+/// G12: ambient occlusion, a pane body darkening faintly toward its edges
+/// (§9.1: an inner shadow of 1 to 2%): four soft strips cast from just
+/// outside the body inward, clipped to it and round a cursor, black in
+/// linear light, under the same contrast ceiling as a lift.
+fn occlusion(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, groups: *const Groups, index: usize, surface: *const pardes.Surface, ceilings: LiftCeilings, layout: CellLayout, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void {
+ const strength = @min(0.02, ceilings.shadow);
+ if (strength <= 0.002) return;
+ const reach = 0.5 * layout.h;
+ for (regions) |body| {
+ if (body.kind != .body or body.rect.w == 0 or body.rect.h == 0 or groupOf(groups, body.serial) != index) continue;
+ const b = bodyPixels(body, layout);
+ // The body, less a cursor's cell in it: left, right, above, below.
+ var parts: [4][4]f32 = undefined;
+ var nparts: usize = 0;
+ const cursor: ?[4]f32 = if (surface.cursor) |cu| .{ @as(f32, @floatFromInt(cu.x)) * layout.w, @as(f32, @floatFromInt(cu.y)) * layout.h, @as(f32, @floatFromInt(cu.x + 1)) * layout.w, @as(f32, @floatFromInt(cu.y + 1)) * layout.h } else null;
+ if (cursor) |cb| if (!(cb[0] >= b[2] or cb[2] <= b[0] or cb[1] >= b[3] or cb[3] <= b[1])) {
+ parts = .{ .{ b[0], b[1], cb[0], b[3] }, .{ cb[2], b[1], b[2], b[3] }, .{ cb[0], b[1], cb[2], cb[1] }, .{ cb[0], cb[3], cb[2], b[3] } };
+ nparts = 4;
+ };
+ if (nparts == 0) {
+ parts[0] = b;
+ nparts = 1;
+ }
+ const t = reach / 3;
+ const strips = [_][4]f32{
+ .{ b[0], b[1] - t, b[2], b[1] },
+ .{ b[0], b[3], b[2], b[3] + t },
+ .{ b[0] - t, b[1], b[0], b[3] },
+ .{ b[2], b[1], b[2] + t, b[3] },
+ };
+ for (strips) |strip| {
+ const first = g.decor.items.len;
+ try addSoft(g, gpa, strip, reach / 3, 0, 0, strength, .{ 0, 0, 0 }, track, win_w, win_h);
+ try clipToBodies(g, gpa, first, parts[0..nparts]);
+ }
+ }
+}
+
/// A theme's tag plaques (Chrome.decor_box_*), drawn under the cells of
/// group `index`: each pane tag, and with the still panes the column and
/// workspace tags, becomes a framed box inside its own band with a hard
@@ -9536,7 +9576,7 @@ test "the selection glows round its block, on the body only, fading in over 100
core.settings.selection_glow = true;
pardes.test_api.sync(core);
const s = try core.render(core.scratch.allocator());
- try std.testing.expectEqual(@as(u8, 1), s.chrome.glows);
+ try std.testing.expectEqual(@as(u8, 1), s.chrome.fx);
// Two rows of a selection in the body, as the core paints one.
const body = for (s.regionList()) |region| {
if (region.kind == .body) break region;
@@ -9586,7 +9626,7 @@ test "a hovered word glows along its foot, never over the cursor, fading in over
core.settings.hover_glow = true;
pardes.test_api.sync(core);
const s = try core.render(core.scratch.allocator());
- try std.testing.expectEqual(@as(u8, 2), s.chrome.glows);
+ try std.testing.expectEqual(@as(u8, 2), s.chrome.fx);
const body = for (s.regionList()) |region| {
if (region.kind == .body) break region;
} else return error.NoBody;
@@ -9628,6 +9668,45 @@ test "a hovered word glows along its foot, never over the cursor, fading in over
try std.testing.expect(!g.hover_glow.fading);
}
+test "occlusion shades each body's edges, inside it, never a tag, a grip or the cursor" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.setTestFile("some text on a line\n" ** 30);
+ core.settings.occlusion = true;
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ try std.testing.expectEqual(@as(u8, 4), s.chrome.fx);
+ const body = for (s.regionList()) |region| {
+ if (region.kind == .body) break region;
+ } else return error.NoBody;
+ // The cursor on the body's top-left cell, where the shade is.
+ s.cursor = .{ .x = body.rect.x, .y = body.rect.y };
+ 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;
+ g.sel_glow = .{};
+ g.hover_glow = .{};
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 800, 480, true);
+ const r = bodyPixels(body, fixedCellLayout(&g));
+ var shades: usize = 0;
+ for (g.decor.items) |item| {
+ if (item.effect & soft_shadow_bit == 0) continue;
+ shades += 1;
+ try std.testing.expect(item.fr <= 0.02 + 1e-6);
+ try std.testing.expect(item.clip_x0 >= r[0] - 0.01 and item.clip_x1 <= r[2] + 0.01 and item.clip_y0 >= r[1] - 0.01 and item.clip_y1 <= r[3] + 0.01);
+ const cx: f32 = @floatFromInt(@as(u32, s.cursor.?.x) * 10);
+ const cy: f32 = @floatFromInt(@as(u32, s.cursor.?.y) * 20);
+ try std.testing.expect(item.clip_x1 <= cx + 0.01 or item.clip_x0 >= cx + 10 - 0.01 or item.clip_y1 <= cy + 0.01 or item.clip_y0 >= cy + 20 - 0.01);
+ }
+ try std.testing.expect(shades >= 4);
+}
+
/// What a bundled post pass leaves alone (Post.Frame.spare), in window
/// pixels: every tag with its grip, the column and workspace tags, and the
/// notices, the chrome the focus indicators live in. A grip, the gap after