summaryrefslogtreecommitdiff
path: root/src
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
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')
-rw-r--r--src/config.zig12
-rw-r--r--src/detached/wire.zig2
-rw-r--r--src/draw.zig2
-rw-r--r--src/gui/gui.zig91
-rw-r--r--src/surface.zig9
5 files changed, 101 insertions, 15 deletions
diff --git a/src/config.zig b/src/config.zig
index 4e159d01..22e65c46 100644
--- a/src/config.zig
+++ b/src/config.zig
@@ -124,6 +124,7 @@ pub const leader_path = paths: {
if (builtins.capabilities.lift) table.set(.CursorBlink, null);
if (builtins.capabilities.lift) table.set(.SelectionGlow, null);
if (builtins.capabilities.lift) table.set(.HoverGlow, null);
+ if (builtins.capabilities.lift) table.set(.Occlusion, null);
if (builtins.capabilities.lift) table.set(.Lift, null);
table.set(.Motion, null);
table.set(.InactiveDim, null);
@@ -674,6 +675,9 @@ pub const Runtime = struct {
/// G11: a soft glow under the word a look-hover would open, fading in
/// over 80 ms, a pixel shell's.
hover_glow: bool = false,
+ /// G12: pane bodies darken faintly toward their edges (ambient
+ /// occlusion, at most 2%), a pixel shell's.
+ occlusion: bool = false,
/// How long a message stays after the key or click that dismisses it.
message_linger_ms: u16 = 800,
/// How long its fall into the row takes, easing out (§8.1: arriving,
@@ -897,7 +901,7 @@ pub const Runtime = struct {
/// The message timings a setting word sets, each in milliseconds.
pub const MessageTime = enum { linger, fall, dissolve };
- pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, syntax_bold, verbose, message_animation, ligatures, cursor_blink, selection_glow, hover_glow };
+ pub const Toggle = enum { colors, wrap, tree_context_tag_style, tag_bottom, debug, focus_tint, syntax_bold, verbose, message_animation, ligatures, cursor_blink, selection_glow, hover_glow, occlusion };
/// A bundled post pass, a Shadertoy file in shaders/post/: the CRT, and
/// G4's bloom, vignette and grain (docs/effects.md).
pub const Scene = enum { crt, bloom, vignette, grain };
@@ -958,6 +962,7 @@ pub const Runtime = struct {
.{ .word = "CursorBlink", .action = .{ .toggle = .cursor_blink }, .availability = .lift, .doc = "The cursor blinks, solid while typing, on or off." },
.{ .word = "SelectionGlow", .action = .{ .toggle = .selection_glow }, .availability = .lift, .doc = "A soft halo of the selection's colour round it in the body, on or off." },
.{ .word = "HoverGlow", .action = .{ .toggle = .hover_glow }, .availability = .lift, .doc = "A soft underline of light under the word a look-hover would open, on or off." },
+ .{ .word = "Occlusion", .action = .{ .toggle = .occlusion }, .availability = .lift, .doc = "Pane bodies darken faintly toward their edges, on or off." },
.{ .word = "Lift", .action = .lift, .availability = .lift, .doc = "How the focused pane stands off the page: off, shadow, rim or auto." },
.{ .word = "Motion", .action = .motion, .doc = "How animations move: off, crisp, smooth, bouncy or playful." },
.{ .word = "InactiveDim", .action = .inactive_dim, .doc = "Fade the unfocused panes' text by a percentage, 0 through 100, 0 off." },
@@ -1034,9 +1039,9 @@ pub const Runtime = struct {
.toggle => |field| if (field == .ligatures) {
if (setting.availability != .ligatures)
@compileError("the ligatures toggle must use the ligatures capability");
- } else if (field == .cursor_blink or field == .selection_glow or field == .hover_glow) {
+ } else if (field == .cursor_blink or field == .selection_glow or field == .hover_glow or field == .occlusion) {
if (setting.availability != .lift)
- @compileError("CursorBlink, SelectionGlow and HoverGlow must use the lift capability (a pixel shell's)");
+ @compileError("CursorBlink, SelectionGlow, HoverGlow and Occlusion must use the lift capability (a pixel shell's)");
} else if (setting.availability != null)
@compileError("unconditional toggles cannot carry a backend capability"),
else => if (setting.availability != null)
@@ -1693,6 +1698,7 @@ pub const Runtime = struct {
.cursor_blink => "CursorBlink off\n",
.selection_glow => "SelectionGlow on\n",
.hover_glow => "HoverGlow on\n",
+ .occlusion => "Occlusion on\n",
},
.shell => "Shell requested (new panes) fish\n",
.tagline_size => "TaglineSize 73\n",
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index cf5a8711..3a914e42 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -13,7 +13,7 @@ pub const version: u16 = 8;
/// The Chrome palette is carried field by field as its declaration lists
/// them, so any change to it changes the wire: bump `version`, then this.
-const chrome_layout: u64 = 0x3195816f182fe5bc;
+const chrome_layout: u64 = 0x5e758b47fb0cf8da;
comptime {
@setEvalBranchQuota(10_000);
var hash = std.hash.Fnv1a_64.init();
diff --git a/src/draw.zig b/src/draw.zig
index 5e40f0fa..027326fa 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -472,7 +472,7 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
.rail_px = @intCast(@min(255, @as(u32, p.theme().rail_px orelse 12) * p.settings.grip_width / 100)),
.name_ink = chrome.tag_name_fg,
.active_name_ink = if (p.settings.focus_tint) chrome.tag_active_name_fg else chrome.tag_name_fg,
- .glows = @as(u8, @intFromBool(p.settings.selection_glow)) | @as(u8, @intFromBool(p.settings.hover_glow)) << 1,
+ .fx = @as(u8, @intFromBool(p.settings.selection_glow)) | @as(u8, @intFromBool(p.settings.hover_glow)) << 1 | @as(u8, @intFromBool(p.settings.occlusion)) << 2,
};
if (p.theme().decor) |decor| {
s.chrome.decor_dots = decor.page_dots;
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
diff --git a/src/surface.zig b/src/surface.zig
index 26783df4..02e4bb5d 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -448,10 +448,11 @@ pub const Chrome = struct {
decor_title_px: u8 = 2,
name_ink: [3]u8 = .{ 0, 0, 0 },
active_name_ink: [3]u8 = .{ 0, 0, 0 },
- /// A pixel shell's glows: bit 0 round the body's selection (G9,
- /// SelectionGlow), bit 1 under the word a look-hover would open (G11,
- /// HoverGlow).
- glows: u8 = 0,
+ /// A pixel shell's small effects: bit 0 a glow round the body's
+ /// selection (G9, SelectionGlow), bit 1 one under the word a look-hover
+ /// would open (G11, HoverGlow), bit 2 the bodies' edge shade (G12,
+ /// Occlusion).
+ fx: u8 = 0,
};
/// The canonical interface: what a frame of pardes IS. The tty shell writes