summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-15 15:17:32 -0300
committerGabriel Schneider <[email protected]>2026-09-15 17:24:42 -0300
commit3327666759d47cad41d97264cd1f7b8ec64eafed (patch)
treedacb3cd5490f0a3a162f7ec8161ac138f6d35590 /src/pardes.zig
parent7d75f5d7f1394a09c6a2cc42c2a9a60e6aef7b12 (diff)
downloadpardes-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.zig73
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,