summaryrefslogtreecommitdiff
path: root/src/gui/gui.zig
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/gui.zig')
-rw-r--r--src/gui/gui.zig129
1 files changed, 129 insertions, 0 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index 5bc1ea6d..92bec92c 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -487,6 +487,8 @@ const initial_image_capacity: u32 = pardes.MAX_PANES;
const old_layer_bit: u32 = 0x8000_0000;
const clear_bg_bit: u32 = 0x4000_0000;
const opaque_bg_bit: u32 = 0x2000_0000;
+/// Decor that is a soft shadow, not a flat fill (decor.frag.glsl).
+const soft_shadow_bit: u32 = 0x1000_0000;
const SavedImagePlace = struct {
key: pardes.ImageCacheKey,
@@ -1855,6 +1857,10 @@ const Gui = struct {
/// Pixels the capture holds past the grid (PARDES_TEST_PAD).
capture_pad: u32 = 0,
capture_dir: []const u8 = "",
+ /// PARDES_TEST_SERIES: every captured frame is kept too, numbered, for a
+ /// feel review's frame series (docs/render-pipeline.md §8.4).
+ capture_series: bool = false,
+ captured: u32 = 0,
capture_tex: ?*c.SDL_GPUTexture = null,
capture_tex_w: u32 = 0,
capture_tex_h: u32 = 0,
@@ -2457,6 +2463,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
.glyphs = std.AutoHashMap(GlyphKey, Slot).init(gpa),
.capture = test_mode,
.capture_pad = if (test_mode) envU16(env, "PARDES_TEST_PAD") orelse 0 else 0,
+ .capture_series = test_mode and env.get("PARDES_TEST_SERIES") != null,
.capture_dir = capture_dir orelse "",
.soft_present = soft_present,
.transparent = config.gui_transparent,
@@ -7301,6 +7308,8 @@ fn captureFrame(g: *Gui, gpa: std.mem.Allocator, cmd: *c.SDL_GPUCommandBuffer, t
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);
+ if (g.capture_series) try writeCapturePpm(g, gpa, pixels[0..size], sw, sh, std.fmt.bufPrint(&name, "frame-{d:0>5}", .{g.captured}) catch unreachable);
+ g.captured +%= 1;
}
fn writeCapturePpm(g: *Gui, gpa: std.mem.Allocator, pixels: []const u8, width: u32, height: u32, name: []const u8) !void {
@@ -7375,6 +7384,99 @@ fn addDecor(g: *Gui, gpa: std.mem.Allocator, x0_in: f32, y0_in: f32, x1_in: f32,
try g.decor.append(gpa, instance);
}
+/// How a lift of 0 to 1 stands off the page: the shadow's blur and offset
+/// in cell heights, and its darkest, where it leaves the caster's edge.
+/// Within §8.2's budget: sigma 0.4-0.8 cell, at most 30% darker at the edge.
+const Shadow = struct {
+ // A key light above: the drop is most of the blur, so the shadow hangs
+ // below and to the right and barely reaches over the caster's top.
+ const sigma: f32 = 0.42;
+ const drop: f32 = 0.34;
+ const side: f32 = 0.1;
+ const strength: f32 = 0.42;
+};
+
+/// A soft shadow under `caster` (window pixels), `lift` 0 to 1, no darker
+/// than `ceiling`, moved by `track` as its caster is.
+fn addShadow(g: *Gui, gpa: std.mem.Allocator, caster: [4]f32, lift: f32, ceiling: f32, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
+ const strength = @min(Shadow.strength * lift, ceiling);
+ if (lift <= 0.001 or strength <= 0.002) return;
+ const h: f32 = @floatFromInt(g.cell_h);
+ const sigma = @max(0.5, Shadow.sigma * h * lift);
+ const dx = Shadow.side * h * lift;
+ const dy = Shadow.drop * h * lift;
+ const x0 = caster[0] + dx - 3 * sigma;
+ const y0 = caster[1] + dy - 3 * sigma;
+ const x1 = caster[2] + dx + 3 * sigma;
+ const y1 = caster[3] + dy + 3 * sigma;
+ const w = x1 - x0;
+ const ht = y1 - y0;
+ if (w <= 0 or ht <= 0) return;
+ // In the quad's own 0..1 space (decor.frag.glsl): sigma per axis, the
+ // caster's offset from its shadow, the strength.
+ 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 = sigma / w,
+ .v0 = sigma / ht,
+ .u1 = sigma / w,
+ .v1 = sigma / ht,
+ .fr = strength,
+ .fg = dx / w,
+ .fb = dy / ht,
+ .br = 0,
+ .bg = 0,
+ .bb = 0,
+ };
+ setTransitionFields(&instance, track, fixedCellLayout(g), win_w, win_h, 0);
+ instance.effect |= soft_shadow_bit;
+ try g.decor.append(gpa, instance);
+}
+
+/// sRGB's relative luminance (WCAG).
+fn luminance(rgb: [3]u8) f32 {
+ var sum: f32 = 0;
+ for (rgb, [3]f32{ 0.2126, 0.7152, 0.0722 }) |channel, weight| {
+ const v = @as(f32, @floatFromInt(channel)) / 255;
+ sum += weight * (if (v <= 0.04045) v / 12.92 else std.math.pow(f32, (v + 0.055) / 1.055, 2.4));
+ }
+ return sum;
+}
+
+/// How dark a shadow may make the page and its text, both together, before
+/// the pair drops below its own contrast or 4.5, whichever is lower (§8.2):
+/// darkening in linear light by k leaves (k L1 + .05) / (k L2 + .05).
+fn shadowCeiling(fg: [3]u8, bg: [3]u8) f32 {
+ const a = luminance(fg);
+ const b = luminance(bg);
+ const light = @max(a, b);
+ const dark = @min(a, b);
+ const target = @min((light + 0.05) / (dark + 0.05), 4.5);
+ const room = light - target * dark;
+ if (room <= 0) return 0;
+ return std.math.clamp(1 - 0.05 * (target - 1) / room, 0, 1);
+}
+
+test "a shadow never darkens a native theme's text below its contrast or 4.5" {
+ var shaded: usize = 0;
+ for (pardes.themes) |theme| {
+ const fg = theme.fg orelse fg_default;
+ const bg = theme.bg orelse bg_default;
+ const ceiling = shadowCeiling(fg, bg);
+ const k = 1 - @min(Shadow.strength, ceiling);
+ const light = @max(luminance(fg), luminance(bg));
+ const dark = @min(luminance(fg), luminance(bg));
+ const before = (light + 0.05) / (dark + 0.05);
+ const after = (k * light + 0.05) / (k * dark + 0.05);
+ try std.testing.expect(after >= @min(before, 4.5) - 1e-3);
+ shaded += @intFromBool(ceiling > 0.05);
+ }
+ // And most themes have room for one.
+ try std.testing.expect(shaded * 2 > pardes.themes.len);
+}
+
/// 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
@@ -7383,6 +7485,10 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
g.decor.clearRetainingCapacity();
const chrome = &surface.chrome;
const regions = surface.regionList();
+ const ceiling = shadowCeiling(chrome.fg orelse fg_default, chrome.page orelse bg_default);
+ const notice_lift = for (regions) |region| {
+ if (region.kind == .notice) break region.lift;
+ } else 0;
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;
@@ -7432,11 +7538,34 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
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);
+ // Cast last in the group, over its cells, images and chrome: a
+ // lifted pane's shadow darkens its neighbours' rails and rules
+ // as it darkens their text, and never the pane itself.
+ for (placed) |grip| {
+ if (grip.kind != .grip or grip.lift <= 0.001 or groupOf(groups, grip.serial) != index) continue;
+ var box: [4]f32 = .{ @as(f32, @floatFromInt(grip.rect.x)) * layout.w, @as(f32, @floatFromInt(grip.rect.y)) * layout.h, 0, @as(f32, @floatFromInt(grip.rect.y + grip.rect.h)) * layout.h };
+ box[2] = box[0];
+ for (placed) |part| {
+ if (part.owner != grip.owner or (part.kind != .tag and part.kind != .rail and part.kind != .body)) continue;
+ box[1] = @min(box[1], @as(f32, @floatFromInt(part.rect.y)) * layout.h);
+ box[2] = @max(box[2], @as(f32, @floatFromInt(part.rect.x + part.rect.w)) * layout.w);
+ box[3] = @max(box[3], @as(f32, @floatFromInt(part.rect.y + part.rect.h)) * layout.h);
+ }
+ try addShadow(g, gpa, box, grip.lift, ceiling, track, win_w, win_h);
+ }
}
// 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.
+ // A notice floats: its shadow first, then every rule.
+ for (surface.tagLayers()) |*layer| {
+ if (layer.rows == 0 or layer.kind != .notice or noticeGroup(groups, layer) != index) continue;
+ const x = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;
+ const top = (@as(f32, @floatFromInt(layer.viewport.y)) + layer.slide) * layout.h;
+ const track = groups.items[groupAt(groups, layer.viewport.x, layer.viewport.y)].track;
+ try addShadow(g, gpa, .{ x, top, x + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w, top + layout.h }, notice_lift * layer.fade, ceiling, track, win_w, win_h);
+ }
for (surface.tagLayers()) |*layer| {
if (layer.rows == 0 or layer.kind != .notice or noticeGroup(groups, layer) != index) continue;
const x0 = @as(f32, @floatFromInt(layer.viewport.x)) * layout.w;