diff options
| author | Gabriel Schneider <[email protected]> | 2026-09-15 15:17:32 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-09-15 17:24:42 -0300 |
| commit | 3327666759d47cad41d97264cd1f7b8ec64eafed (patch) | |
| tree | dacb3cd5490f0a3a162f7ec8161ac138f6d35590 /src/pardes.zig | |
| parent | 7d75f5d7f1394a09c6a2cc42c2a9a60e6aef7b12 (diff) | |
| download | pardes-3327666759d47cad41d97264cd1f7b8ec64eafed.tar.gz pardes-3327666759d47cad41d97264cd1f7b8ec64eafed.zip | |
Match filename tint lightness in every canonical theme and focus state
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 73 |
1 files changed, 71 insertions, 2 deletions
diff --git a/src/pardes.zig b/src/pardes.zig index b6d6ed8f..51c9c3db 100644 --- a/src/pardes.zig +++ b/src/pardes.zig @@ -2939,8 +2939,10 @@ pub const Theme = struct { // editor palettes keep working; ChromeTheme resolves their legacy defaults. tag_active_bg: ?[3]u8 = null, tag_active_fg: ?[3]u8 = null, - /// Quiet filename/output-name accent; directories and commands keep tag ink. + /// Filename/output-name accent, also used by the Tty command. tag_name_fg: ?[3]u8 = null, + /// Active tint; absence preserves the shared name tint or active tag ink. + tag_active_name_fg: ?[3]u8 = null, border: ?[3]u8 = null, search_bg: ?[3]u8 = null, search_fg: ?[3]u8 = null, @@ -3049,6 +3051,51 @@ test "native themes keep text and UI roles readable on their own surfaces" { } } +test "native filename tints change hue without changing perceived lightness" { + const Perceptual = struct { + fn lab(rgb: [3]u8) [3]f64 { + var linear: [3]f64 = undefined; + for (rgb, 0..) |channel, i| { + const value = @as(f64, @floatFromInt(channel)) / 255; + linear[i] = if (value <= 0.04045) value / 12.92 else std.math.pow(f64, (value + 0.055) / 1.055, 2.4); + } + const r = linear[0]; + const g = linear[1]; + const b = linear[2]; + const l = std.math.cbrt(0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b); + const m = std.math.cbrt(0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b); + const n = std.math.cbrt(0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b); + return .{ + 0.2104542553 * l + 0.7936177850 * m - 0.0040720468 * n, + 1.9779984951 * l - 2.4285922050 * m + 0.4505937099 * n, + 0.0259040371 * l + 0.7827717662 * m - 0.8086757660 * n, + }; + } + }; + for (themes[0..native_theme_count]) |th| { + try std.testing.expect(th.tag_name_fg != null and th.tag_active_name_fg != null); + const chrome = ChromeTheme.fromTheme(&th); + const pairs = [2][2][3]u8{ + .{ chrome.tag_fg, chrome.tag_name_fg }, + .{ chrome.tag_active_fg, chrome.tag_active_name_fg }, + }; + for (pairs) |pair| { + const base = Perceptual.lab(pair[0]); + const name = Perceptual.lab(pair[1]); + // Half a lightness point allows 8-bit quantization without a + // basename becoming visibly brighter or darker than its tag. + try std.testing.expectApproxEqAbs(base[0], name[0], 0.005); + const base_chroma = @sqrt(base[1] * base[1] + base[2] * base[2]); + const name_chroma = @sqrt(name[1] * name[1] + name[2] * name[2]); + try std.testing.expect(base_chroma > 0.005 and name_chroma > 0.02); + // A normalized dot product <= cos(60 degrees) means a clear + // hue difference, independent of saturation or lightness. + const hue_cosine = (base[1] * name[1] + base[2] * name[2]) / (base_chroma * name_chroma); + try std.testing.expect(hue_cosine <= 0.5); + } + } +} + test "native dark palettes choose near-black or deliberately soft contrast" { for (themes[0..native_theme_count]) |th| { if (themeContrast(th.bg.?, .{ 0, 0, 0 }) > 10) continue; // light paper @@ -3122,6 +3169,7 @@ test "legacy ThemeFile documents inherit Pardes UI roles without new fields" { defer std.zon.parse.free(std.testing.allocator, parsed); try std.testing.expectEqual(@as(?[3]u8, null), parsed.tag_active_bg); try std.testing.expectEqual(@as(?[3]u8, null), parsed.tag_name_fg); + try std.testing.expectEqual(@as(?[3]u8, null), parsed.tag_active_name_fg); try std.testing.expectEqual(@as(?[3]u8, null), parsed.search_bg); const chrome = ChromeTheme.fromTheme(&parsed); try std.testing.expectEqual(parsed.tag_bg, chrome.tag_active_bg); @@ -3131,6 +3179,27 @@ test "legacy ThemeFile documents inherit Pardes UI roles without new fields" { try std.testing.expectEqual(parsed.fg.?, chrome.diagnostic_error); } +test "custom filename tints preserve optional active fallback precedence" { + var theme = themes[0]; + theme.tag_name_fg = null; + theme.tag_active_name_fg = null; + var chrome = ChromeTheme.fromTheme(&theme); + try std.testing.expectEqual(theme.tag_fg, chrome.tag_name_fg); + try std.testing.expectEqual(theme.tag_active_fg.?, chrome.tag_active_name_fg); + theme.tag_name_fg = .{ 120, 130, 140 }; + chrome = ChromeTheme.fromTheme(&theme); + try std.testing.expectEqual(theme.tag_name_fg.?, chrome.tag_name_fg); + try std.testing.expectEqual(theme.tag_name_fg.?, chrome.tag_active_name_fg); + theme.tag_active_name_fg = .{ 150, 160, 170 }; + chrome = ChromeTheme.fromTheme(&theme); + try std.testing.expectEqual(theme.tag_name_fg.?, chrome.tag_name_fg); + try std.testing.expectEqual(theme.tag_active_name_fg.?, chrome.tag_active_name_fg); + theme.tag_name_fg = null; + chrome = ChromeTheme.fromTheme(&theme); + try std.testing.expectEqual(theme.tag_fg, chrome.tag_name_fg); + try std.testing.expectEqual(theme.tag_active_name_fg.?, chrome.tag_active_name_fg); +} + comptime { if (themes.len > std.math.maxInt(u16) + 1) @compileError("runtime config theme index no longer fits u16"); @@ -3209,7 +3278,7 @@ pub const ChromeTheme = struct { .tag_active_bg = th.tag_active_bg orelse th.tag_bg, .tag_active_fg = th.tag_active_fg orelse th.tag_fg, .tag_name_fg = th.tag_name_fg orelse th.tag_fg, - .tag_active_name_fg = th.tag_name_fg orelse (th.tag_active_fg orelse th.tag_fg), + .tag_active_name_fg = th.tag_active_name_fg orelse (th.tag_name_fg orelse (th.tag_active_fg orelse th.tag_fg)), .border = th.border orelse th.scroll_track, .search_bg = th.search_bg orelse th.sel_bg, .search_fg = th.search_fg orelse th.sel_fg, |
