summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/colors.zig121
-rw-r--r--src/draw.zig54
-rw-r--r--src/themes/acme.zig5
3 files changed, 169 insertions, 11 deletions
diff --git a/src/colors.zig b/src/colors.zig
index 72e25aed..b6da0d2c 100644
--- a/src/colors.zig
+++ b/src/colors.zig
@@ -153,18 +153,22 @@ fn toOklab(rgb: [3]u8) [3]f32 {
};
}
-fn fromOklab(lab: [3]f32) [3]u8 {
+fn oklabLinear(lab: [3]f32) [3]f32 {
const lm = lab[0] + 0.3963377774 * lab[1] + 0.2158037573 * lab[2];
const mm = lab[0] - 0.1055613458 * lab[1] - 0.0638541728 * lab[2];
const sm = lab[0] - 0.0894841775 * lab[1] - 1.2914855480 * lab[2];
const l = lm * lm * lm;
const m = mm * mm * mm;
const s = sm * sm * sm;
- const linear: [3]f32 = .{
+ return .{
4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
-0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
};
+}
+
+fn fromOklab(lab: [3]f32) [3]u8 {
+ const linear = oklabLinear(lab);
var out: [3]u8 = undefined;
for (&out, linear) |*o, v| {
const x = std.math.clamp(v, 0, 1);
@@ -181,6 +185,72 @@ pub fn mixOklab(a: [3]u8, b: [3]u8, t: f32) [3]u8 {
return fromOklab(.{ x[0] + (y[0] - x[0]) * t, x[1] + (y[1] - x[1]) * t, x[2] + (y[2] - x[2]) * t });
}
+fn oklabDistance(a: [3]f32, b: [3]f32) f32 {
+ return @sqrt((a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]) + (a[2] - b[2]) * (a[2] - b[2]));
+}
+
+/// How far a name's tint stands off its tag's ink in OKLab: a hue you see
+/// at a glance, not a shade you look for.
+pub const name_tint_distance: f32 = 0.07;
+/// How much hue a colour needs to lend a name its tint.
+const name_tint_chroma: f32 = 0.06;
+
+/// A name's tint for a theme that sets none (the faithful ports, the
+/// imports, a theme file): a hue of the theme's own -- its keyword colour,
+/// else the first of its ANSI accents, syntax, grip, selection or
+/// diagnostic colours that has one -- at the lightness nearest the tag's
+/// ink where that hue holds, the name reads on its ground (nameTintFloor)
+/// and it stands off the ink. Null when no colour of the theme will do.
+fn derivedNameTint(th: *const Theme, ink: [3]u8, ground: [3]u8) ?[3]u8 {
+ const pal = th.palette orelse [_][3]u8{th.kw} ** 16;
+ const sources = [_][3]u8{ th.kw, pal[4], pal[5], pal[6], pal[3], pal[2], pal[1], th.num, th.str, th.box } ++
+ [_][3]u8{ th.column_box orelse th.num, th.box_dirty orelse th.num, th.sel_bg, th.search_bg orelse th.num } ++
+ [_][3]u8{ th.diagnostic_info orelse th.num, th.diagnostic_hint orelse th.num, th.diagnostic_warning orelse th.num } ++
+ // A theme with no hue of its own at all (flatwhite): acme's DMedblue's.
+ [_][3]u8{.{ 0x00, 0x00, 0x99 }};
+ // Near the ink's lightness first, from any of the theme's hues; only
+ // then further off it.
+ for ([2]f32{ 0.15, 1 }) |reach| for (sources) |source| {
+ if (nameTintFrom(toOklab(source), ink, ground, reach)) |tint| return tint;
+ };
+ return null;
+}
+
+fn nameTintFrom(source: [3]f32, ink: [3]u8, ground: [3]u8, reach: f32) ?[3]u8 {
+ const chroma = @sqrt(source[1] * source[1] + source[2] * source[2]);
+ if (chroma < name_tint_chroma) return null;
+ const ink_lab = toOklab(ink);
+ const floor = nameTintFloor(ink, ground);
+ var step: f32 = 0;
+ while (step <= reach) : (step += 0.01) {
+ for ([2]f32{ 1, -1 }) |sign| {
+ const lightness = ink_lab[0] + sign * step;
+ if (lightness < 0 or lightness > 1) continue;
+ // As much of the hue as that lightness holds, up to a tint's.
+ var want = @min(chroma, 0.12);
+ while (want >= 0.04) : (want -= 0.01) {
+ const lab: [3]f32 = .{ lightness, source[1] / chroma * want, source[2] / chroma * want };
+ const in_gamut = for (oklabLinear(lab)) |v| {
+ if (v < -0.001 or v > 1.001) break false;
+ } else true;
+ if (!in_gamut) continue;
+ const tint = fromOklab(lab);
+ if (contrast(tint, ground) < floor) continue;
+ if (oklabDistance(toOklab(tint), ink_lab) < name_tint_distance) continue;
+ return tint;
+ }
+ }
+ }
+ return null;
+}
+
+/// What a name's tint must read at on its ground: 4.5:1, or within half a
+/// point of the ink's own where the ink has less to spare (seoulbones_dark's
+/// near-white on grey leaves no hue at the ink's 4.53:1).
+pub fn nameTintFloor(ink: [3]u8, ground: [3]u8) f32 {
+ return @min(4.5, contrast(ink, ground) - 0.5);
+}
+
test "an OKLab fade holds its ends exactly and keeps a mid-way bright" {
for ([_][3]u8{ .{ 0, 0, 0 }, .{ 255, 255, 255 }, .{ 0x0c, 0x0c, 0x0e }, .{ 0xe8, 0xc4, 0x6a }, .{ 0x1c, 0x51, 0x72 } }) |rgb| {
try std.testing.expectEqual(rgb, fromOklab(toOklab(rgb)));
@@ -548,13 +618,21 @@ pub const ChromeTheme = struct {
pub fn fromTheme(th: *const Theme) ChromeTheme {
// The initial chrome is resolved at compile time: contrast takes pow.
@setEvalBranchQuota(200_000);
+ const active_bg = focusTint(th);
+ const active_fg = th.tag_active_fg orelse th.tag_fg;
+ // A name is always tinted: a theme that names no tint gets one of
+ // its own hues, on each ground. One that names only the plain tint
+ // keeps it when focused too.
+ const name_fg = th.tag_name_fg orelse (derivedNameTint(th, th.tag_fg, th.tag_bg) orelse th.tag_fg);
+ const active_name_fg = th.tag_active_name_fg orelse
+ (th.tag_name_fg orelse (derivedNameTint(th, active_fg, active_bg) orelse active_fg));
return .{
.tag_bg = th.tag_bg,
.tag_fg = th.tag_fg,
- .tag_active_bg = focusTint(th),
- .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_active_name_fg orelse (th.tag_name_fg orelse (th.tag_active_fg orelse th.tag_fg)),
+ .tag_active_bg = active_bg,
+ .tag_active_fg = active_fg,
+ .tag_name_fg = name_fg,
+ .tag_active_name_fg = active_name_fg,
.border = separatorOf(th),
.empty_col = th.empty_col orelse separatorOf(th),
.search_bg = th.search_bg orelse th.sel_bg,
@@ -1108,8 +1186,9 @@ test "legacy ThemeFile documents inherit Pardes UI roles without new fields" {
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);
- try std.testing.expectEqual(parsed.tag_fg, chrome.tag_name_fg);
- try std.testing.expectEqual(chrome.tag_active_fg, chrome.tag_active_name_fg);
+ // Its names take a tint of its own hues, never the plain ink.
+ try std.testing.expect(!std.meta.eql(parsed.tag_fg, chrome.tag_name_fg));
+ try std.testing.expect(!std.meta.eql(chrome.tag_active_fg, chrome.tag_active_name_fg));
try std.testing.expectEqual(parsed.num, chrome.column_box);
try std.testing.expectEqual(mix(parsed.num, parsed.tag_bg), chrome.column_box_dim);
try std.testing.expectEqual(parsed.sel_bg, chrome.search_bg);
@@ -1148,8 +1227,9 @@ test "custom filename tints preserve optional active fallback precedence" {
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);
+ // Neither named: each derived, on its own ground.
+ try std.testing.expectEqual(derivedNameTint(&theme, theme.tag_fg, theme.tag_bg).?, chrome.tag_name_fg);
+ try std.testing.expectEqual(derivedNameTint(&theme, theme.tag_active_fg.?, chrome.tag_active_bg).?, 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);
@@ -1160,6 +1240,25 @@ test "custom filename tints preserve optional active fallback precedence" {
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(derivedNameTint(&theme, theme.tag_fg, theme.tag_bg).?, chrome.tag_name_fg);
try std.testing.expectEqual(theme.tag_active_name_fg.?, chrome.tag_active_name_fg);
}
+
+test "every theme tints its names off the tag's ink, and they read on both grounds" {
+ for (&themes) |*th| {
+ const chrome = ChromeTheme.fromTheme(th);
+ const pairs = [2][3][3]u8{
+ .{ chrome.tag_fg, chrome.tag_name_fg, chrome.tag_bg },
+ .{ chrome.tag_active_fg, chrome.tag_active_name_fg, chrome.tag_active_bg },
+ };
+ for (pairs) |pair| {
+ const ink, const name, const ground = pair;
+ const apart = oklabDistance(toOklab(ink), toOklab(name));
+ const floor = nameTintFloor(ink, ground);
+ if (apart < name_tint_distance or contrast(name, ground) < floor - 0.01)
+ std.debug.print("{s}: name {x} on {x} is {d:.3} off the ink {x}, {d:.2}:1\n", .{ th.name, name, ground, apart, ink, contrast(name, ground) });
+ try std.testing.expect(apart >= name_tint_distance);
+ try std.testing.expect(contrast(name, ground) >= floor - 0.01);
+ }
+ }
+}
diff --git a/src/draw.zig b/src/draw.zig
index 9c9e11dd..10303117 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -1477,6 +1477,60 @@ test "a grip's four states, focused or not and clean or dirty, are each its own
pane.file.?.revision = pane.file.?.saved_revision;
}
+test "every theme draws a file's name, a +Errors and a Tty word in a tint, never the tag's plain ink" {
+ const gpa = std.testing.allocator;
+ const p = try Pardes.init(gpa, .{ .cols = 120, .rows = 40, .tty_only = true });
+ defer p.deinit();
+ _ = try p.setTestFile("text\n");
+ try std.testing.expect(p.executeBuiltinLine(0, "Tty+sh"));
+ const terminal = p.active;
+ p.acknowledgeShell(terminal, "/bin/sh", false);
+ try @import("Output.zig").openErrors(p, 0, try gpa.dupe(u8, "oops\n"));
+ while (p.nextEffect()) |_| {}
+ p.animate_theme_changes = false;
+ const errors = for (p.panes, 0..) |slot, id| {
+ const pane = slot orelse continue;
+ if (pane.file) |f| if (std.mem.endsWith(u8, f.path, "+Errors")) break id;
+ } else return error.NoErrorsPane;
+ const Probe = struct { id: usize, word: []const u8 };
+ const probes = [_]Probe{ .{ .id = 0, .word = "test.txt" }, .{ .id = terminal, .word = "Tty+sh" }, .{ .id = errors, .word = "+Errors" } };
+ var arena: std.heap.ArenaAllocator = .init(gpa);
+ defer arena.deinit();
+ for (pardes.themes, 0..) |theme, index| {
+ colors.setThemeIndex(p, index);
+ // Each pane focused in turn: every word on both grounds.
+ for (probes) |focused| {
+ p.active = focused.id;
+ _ = arena.reset(.retain_capacity);
+ const s = try p.render(arena.allocator());
+ for (probes) |probe| {
+ const r = p.rects[probe.id];
+ const y = p.tagTop(p.panes[probe.id].?, r);
+ // The row's text, and the column each byte of it is drawn at.
+ var row: [512]u8 = undefined;
+ var cols: [512]u16 = undefined;
+ var len: usize = 0;
+ for (r.x..r.x + r.w) |x| {
+ const g = s.at(@intCast(x), y).grapheme();
+ for (if (g.len == 0) " " else g) |byte| if (len < row.len) {
+ row[len] = byte;
+ cols[len] = @intCast(x);
+ len += 1;
+ };
+ }
+ const name = std.mem.indexOf(u8, row[0..len], probe.word) orelse return error.WordNotDrawn;
+ const plain = std.mem.indexOf(u8, row[0..len], " Del") orelse return error.WordNotDrawn;
+ const tint = s.at(cols[name], y).style.fg;
+ const ink = s.at(cols[plain + 1], y).style.fg;
+ if (std.meta.eql(tint, ink)) std.debug.print("{s}: {s} drawn in the tag's ink\n", .{ theme.name, probe.word });
+ try std.testing.expect(!std.meta.eql(tint, ink));
+ // The whole word, in the one tint.
+ try std.testing.expect(std.meta.eql(s.at(cols[name + probe.word.len - 1], y).style.fg, tint));
+ }
+ }
+ }
+}
+
test "a tag of three rows has its grip on the first, and band under it that still grabs the pane" {
const p = try Pardes.init(std.testing.allocator, .{ .tty_only = true, .cols = 100, .rows = 30 });
defer p.deinit();
diff --git a/src/themes/acme.zig b/src/themes/acme.zig
index 621ffa23..277249d3 100644
--- a/src/themes/acme.zig
+++ b/src/themes/acme.zig
@@ -18,6 +18,10 @@
//! acme has no focus colour; pardes needs one, so the focused pane's button
//! is filled #8888cc (the column button's look) and every other pane's is
//! acme's own: #eaffff in a 2px #8888cc ring, #000099 inside it when dirty.
+//!
+//! acme tints nothing in a tag; pardes tints the file's name, a +Name and
+//! the Tty word everywhere, so here it is done quietly, in that same #000099
+//! (draw.h's DMedblue): a navy that reads as ink until you look for it.
pub const theme = .{
.name = "acme",
.bg = .{ 0xff, 0xff, 0xea },
@@ -26,6 +30,7 @@ pub const theme = .{
.sel_fg = .{ 0x00, 0x00, 0x00 },
.tag_bg = .{ 0xea, 0xff, 0xff },
.tag_fg = .{ 0x00, 0x00, 0x00 },
+ .tag_name_fg = .{ 0x00, 0x00, 0x99 },
.box = .{ 0x88, 0x88, 0xcc },
.box_dim = .{ 0xea, 0xff, 0xff },
.kw = .{ 0x97, 0x0d, 0xae },