diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-28 18:14:36 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-10-01 00:12:15 -0300 |
| commit | f41d0209f65237568c9f7d527d5989ac8921b74c (patch) | |
| tree | c313dc20fa0593ecff725de32d6ad191b01d82ac /src/gui/gui.zig | |
| parent | b4781b334a06d16aedb0b6a3d604cb249eb619de (diff) | |
| download | pardes-f41d0209f65237568c9f7d527d5989ac8921b74c.tar.gz pardes-f41d0209f65237568c9f7d527d5989ac8921b74c.zip | |
Lift takes a style: shadow, surface, rim, glow or auto
The user wants Lift to read on dark themes, where a shadow has nothing to
darken. `Lift shadow|surface|rim|glow|auto` (`on` is auto, bare flips
auto and off): surface lightens the lifted part's ground about 5.5%
(Material-dark), rim is a thin soft light off its top and left edges,
glow a faint halo in the theme's accent (Chrome.accent, the focused box
colour), auto a shadow on a light page and surface with half a rim on a
dark one (by the page's luminance). Rim and glow are the soft kernel in a
light colour (no pre-warp: coloured light is laid on as is, within §8.2's
budget); each style is capped per theme so text keeps min(its contrast,
4.5) (tests: darkening and lightening over the native themes). The style
travels in the palette (Chrome.lift_style, one byte on the wire). Config
reports and ctl reads `Lift <style>`.
Shared files touched: config.zig, builtins.zig, ninep/ctl.zig,
detached/wire.zig (palette byte), gui.zig. Not touched: pardes.zig,
Messages.zig, mouse.zig.
Diffstat (limited to 'src/gui/gui.zig')
| -rw-r--r-- | src/gui/gui.zig | 132 |
1 files changed, 117 insertions, 15 deletions
diff --git a/src/gui/gui.zig b/src/gui/gui.zig index 905caa48..5d9783e3 100644 --- a/src/gui/gui.zig +++ b/src/gui/gui.zig @@ -7406,15 +7406,13 @@ const Shadow = struct { 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; +/// A soft rectangle around `caster` (window pixels), blurred by `sigma` +/// and moved by (dx, dy): black is a shadow, a light colour a rim or a +/// glow. The caster's own rectangle is left alone, and `track` moves it all +/// as its caster moves. +fn addSoft(g: *Gui, gpa: std.mem.Allocator, caster: [4]f32, sigma_in: f32, dx: f32, dy: f32, strength: f32, rgb: [3]u8, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void { + if (strength <= 0.002) return; + const sigma = @max(0.5, sigma_in); const x0 = caster[0] + dx - 3 * sigma; const y0 = caster[1] + dy - 3 * sigma; const x1 = caster[2] + dx + 3 * sigma; @@ -7436,15 +7434,95 @@ fn addShadow(g: *Gui, gpa: std.mem.Allocator, caster: [4]f32, lift: f32, ceiling .fr = strength, .fg = dx / w, .fb = dy / ht, - .br = 0, - .bg = 0, - .bb = 0, + .br = @as(f32, @floatFromInt(rgb[0])) / 255, + .bg = @as(f32, @floatFromInt(rgb[1])) / 255, + .bb = @as(f32, @floatFromInt(rgb[2])) / 255, }; setTransitionFields(&instance, track, fixedCellLayout(g), win_w, win_h, 0); instance.effect |= soft_shadow_bit; try g.decor.append(gpa, instance); } +/// How far each lift style may go on this theme without its text falling +/// below min(its contrast, 4.5): a shadow darkens, the others lighten. +const LiftCeilings = struct { + shadow: f32, + white: f32, + accent: f32, + + fn of(chrome: *const pardes.Chrome) LiftCeilings { + const fg = chrome.fg orelse fg_default; + const bg = chrome.page orelse bg_default; + return .{ .shadow = shadowCeiling(fg, bg), .white = lightenCeiling(fg, bg, .{ 255, 255, 255 }), .accent = lightenCeiling(fg, bg, chrome.accent) }; + } +}; + +/// What stands a lifted part off the page, `lift` 0 to 1 (a pane's spring, +/// a notice's 1), in the style chosen; `auto` reads the page: a shadow on a +/// light one, a lighter surface and a faint rim on a dark one, where a +/// shadow has nothing to darken. +fn addLift(g: *Gui, gpa: std.mem.Allocator, style: pardes.config.Runtime.LiftStyle, chrome: *const pardes.Chrome, ceilings: LiftCeilings, caster: [4]f32, lift: f32, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void { + if (lift <= 0.001) return; + const h: f32 = @floatFromInt(g.cell_h); + const dark_page = luminance(chrome.page orelse bg_default) < 0.18; + const parts: struct { shadow: bool = false, surface: bool = false, rim: f32 = 0, glow: bool = false } = switch (style) { + .off => return, + .shadow => .{ .shadow = true }, + .surface => .{ .surface = true }, + .rim => .{ .rim = 1 }, + .glow => .{ .glow = true }, + .auto => if (dark_page) .{ .surface = true, .rim = 0.5 } else .{ .shadow = true }, + }; + if (parts.surface) { + // A raised surface catches more light: its ground, and all on it, + // a few percent lighter, in whole pixels. + const a = @min(Lift.surface * lift, ceilings.white); + if (a > 0.002) try addDecor(g, gpa, caster[0], caster[1], caster[2], caster[3], .{ 255, 255, 255 }, a, track, win_w, win_h); + } + if (parts.shadow) try addSoft(g, gpa, caster, Shadow.sigma * h * lift, Shadow.side * h * lift, Shadow.drop * h * lift, @min(Shadow.strength * lift, ceilings.shadow), .{ 0, 0, 0 }, track, win_w, win_h); + // Light from above and to the left: a thin halo off the top and left + // edges, gone by the bottom and the right. + if (parts.rim > 0) try addSoft(g, gpa, caster, Lift.rim_sigma * h * lift, -Lift.rim_offset * h * lift, -Lift.rim_offset * h * lift, @min(Lift.rim * parts.rim * lift, ceilings.white), .{ 255, 255, 255 }, track, win_w, win_h); + if (parts.glow) try addSoft(g, gpa, caster, Lift.glow_sigma * h * lift, 0, 0, @min(Lift.glow * lift, ceilings.accent), chrome.accent, track, win_w, win_h); +} + +/// The lighter styles' strengths, in the §8.2 budget: coloured light at +/// most 25%, a surface a few percent. +const Lift = struct { + const surface: f32 = 0.055; + const rim: f32 = 0.22; + const rim_sigma: f32 = 0.12; + const rim_offset: f32 = 0.1; + const glow: f32 = 0.2; + const glow_sigma: f32 = 0.5; +}; + +/// Like shadowCeiling for a light colour laid over at `a` (sRGB blend, as +/// the decor pipeline blends a colour that is not black): the largest `a` +/// that keeps text and page apart by min(their contrast, 4.5). +fn lightenCeiling(fg: [3]u8, bg: [3]u8, rgb: [3]u8) f32 { + const contrast = struct { + fn of(a: [3]u8, b: [3]u8) f32 { + const la = luminance(a); + const lb = luminance(b); + return (@max(la, lb) + 0.05) / (@min(la, lb) + 0.05); + } + fn blend(under: [3]u8, over: [3]u8, a: f32) [3]u8 { + var out: [3]u8 = undefined; + for (&out, under, over) |*o, x, y| o.* = @intFromFloat(@round(@as(f32, @floatFromInt(x)) * (1 - a) + @as(f32, @floatFromInt(y)) * a)); + return out; + } + }; + const target = @min(contrast.of(fg, bg), 4.5); + var lo: f32 = 0; + var hi: f32 = 1; + for (0..24) |_| { + const mid = (lo + hi) / 2; + if (contrast.of(contrast.blend(fg, rgb, mid), contrast.blend(bg, rgb, mid)) >= target - 1e-3) lo = mid else hi = mid; + } + return lo; +} + /// sRGB's relative luminance (WCAG). fn luminance(rgb: [3]u8) f32 { var sum: f32 = 0; @@ -7469,6 +7547,29 @@ fn shadowCeiling(fg: [3]u8, bg: [3]u8) f32 { return std.math.clamp(1 - 0.05 * (target - 1) / room, 0, 1); } +test "a rim, a glow or a surface never lightens a native theme's text below its contrast or 4.5" { + for (pardes.themes) |theme| { + const fg = theme.fg orelse fg_default; + const bg = theme.bg orelse bg_default; + for ([_][3]u8{ .{ 255, 255, 255 }, theme.box }) |light| { + const a = @min(Lift.rim, lightenCeiling(fg, bg, light)); + const blend = struct { + fn of(under: [3]u8, over: [3]u8, t: f32) [3]u8 { + var out: [3]u8 = undefined; + for (&out, under, over) |*o, x, y| o.* = @intFromFloat(@round(@as(f32, @floatFromInt(x)) * (1 - t) + @as(f32, @floatFromInt(y)) * t)); + return out; + } + }.of; + const ratio = struct { + fn of(x: [3]u8, y: [3]u8) f32 { + return (@max(luminance(x), luminance(y)) + 0.05) / (@min(luminance(x), luminance(y)) + 0.05); + } + }.of; + try std.testing.expect(ratio(blend(fg, light, a), blend(bg, light, a)) >= @min(ratio(fg, bg), 4.5) - 2e-3); + } + } +} + test "a shadow never darkens a native theme's text below its contrast or 4.5" { var shaded: usize = 0; for (pardes.themes) |theme| { @@ -7495,7 +7596,8 @@ 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 ceilings = LiftCeilings.of(chrome); + const lift_style = std.enums.fromInt(pardes.config.Runtime.LiftStyle, chrome.lift_style) orelse .off; const notice_lift = for (regions) |region| { if (region.kind == .notice) break region.lift; } else 0; @@ -7561,7 +7663,7 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g 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); + try addLift(g, gpa, lift_style, chrome, ceilings, box, grip.lift, track, win_w, win_h); } } // A notice chip is tagline hung over the body's top rows, so it is @@ -7574,7 +7676,7 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g 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); + try addLift(g, gpa, lift_style, chrome, ceilings, .{ x, top, x + @as(f32, @floatFromInt(layer.viewport.w)) * layout.w, top + layout.h }, notice_lift * layer.fade, track, win_w, win_h); } for (surface.tagLayers()) |*layer| { if (layer.rows == 0 or layer.kind != .notice or noticeGroup(groups, layer) != index) continue; |
