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.zig132
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;