summaryrefslogtreecommitdiff
path: root/src/pardes.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-08-05 15:05:34 -0300
committerGabriel Schneider <[email protected]>2026-08-10 09:17:07 -0300
commit6eaa81710fee8ee2191a0b967b069bb01952be2e (patch)
tree051f54ede02517a851538f8a853f52c7bdfa96a9 /src/pardes.zig
parent98199e573d678f6b29a5c122836ec393856d7bd8 (diff)
downloadpardes-6eaa81710fee8ee2191a0b967b069bb01952be2e.tar.gz
pardes-6eaa81710fee8ee2191a0b967b069bb01952be2e.zip
theme the selection colors
Diffstat (limited to 'src/pardes.zig')
-rw-r--r--src/pardes.zig105
1 files changed, 80 insertions, 25 deletions
diff --git a/src/pardes.zig b/src/pardes.zig
index 471817f7..aa2ec398 100644
--- a/src/pardes.zig
+++ b/src/pardes.zig
@@ -2226,18 +2226,18 @@ pub const bash_rc =
// ---- theme ----
-// per-button selection highlight: distinct muted colors per mouse button
-const SEL_BG = [3][3]u8{ .{ 0x2c, 0x46, 0x74 }, .{ 0x2c, 0x60, 0x38 }, .{ 0x64, 0x2c, 0x58 } };
-const sel_fg = [3]u8{ 0xd2, 0xd2, 0xd2 };
-// modal line/char selection: warm tan, reads as keyboard-driven
-const msel_bg = [3]u8{ 0x4a, 0x3a, 0x1e };
-const msel_fg = [3]u8{ 0xd8, 0xc8, 0xa8 };
-// the OTHER cursors of a multi-selection: the same tan turned down, so the
-// primary is the one that reads as "here" at a glance (helix does the same
-// with its two selection styles). Each secondary's own cursor cell is then
-// painted msel_fg-on-msel_bg — there is only one hardware cursor.
-const msel2_bg = [3]u8{ 0x2e, 0x26, 0x16 };
-const msel2_fg = [3]u8{ 0xa8, 0x9c, 0x84 };
+/// Halfway between two colors, channel by channel. Every selection tint pardes
+/// paints is a mix of theme colors — the per-mouse-button ones pull the theme's
+/// selection toward one of its own accents, a quarter of the way and so a mix
+/// of a mix; the extra cursors' pulls it back toward the page — and renderPane
+/// makes seven of those, which is six more than spelling the same three-channel
+/// loop out at the call site is worth.
+fn mix(a: [3]u8, b: [3]u8) [3]u8 {
+ var out: [3]u8 = undefined;
+ for (&out, a, b) |*c, x, y| c.* = @intCast((@as(u16, x) + y) / 2);
+ return out;
+}
+
const UNDO_MAX = 256;
const EDIT_UNDO_MAX = 64; // terminal snapshots copy the edit buffer; cap tighter
@@ -2249,10 +2249,18 @@ const EDIT_UNDO_MAX = 64; // terminal snapshots copy the edit buffer; cap tighte
/// piece of loud chrome on screen. The overlay/drag greys that were also
/// hardcoded turned out to be the dark theme's own scroll_track/lineno/tag_fg
/// spelled a second time, so they read those fields now instead.
+///
+/// Selection is ONE pair, `sel_bg`/`sel_fg`, and the rest is arithmetic: the
+/// three per-mouse-button tints and the dimmed extra cursors are mixed off it in
+/// renderPane rather than named here. A theme author gets their selection colour
+/// right and would get four more by accident — and every source gen_themes reads
+/// names exactly one selection anyway.
pub const Theme = struct {
name: []const u8,
bg: ?[3]u8,
fg: ?[3]u8,
+ sel_bg: [3]u8,
+ sel_fg: [3]u8,
tag_bg: [3]u8,
tag_fg: [3]u8,
box: [3]u8, // move box, focused pane
@@ -10194,9 +10202,15 @@ pub const Pardes = struct {
}
if (pv.selection) |selection| {
try highlights.ensureUnusedCapacity(arena, selection.quads.len);
+ // a page is not a grid of cells, so this is the one selection that
+ // paints as a translucent wash over rendered pixels rather than as
+ // a cell's background — but it is the same selection, so it is the
+ // same colour. The alpha stays hardcoded: it is what makes the
+ // glyphs underneath readable, and no theme has an opinion on it.
+ const sel = p.theme().sel_bg;
for (selection.quads) |quad| highlights.appendAssumeCapacity(pdf_impl.Highlight.init(
quad,
- .{ 0x55, 0xa7, 0xff, 0x78 },
+ .{ sel[0], sel[1], sel[2], 0x78 },
.selection,
));
}
@@ -11615,6 +11629,11 @@ pub const Pardes = struct {
// scrollbar's second column. `.default` means the host terminal's own
// background, which is what a themeless dark pane wants.
const pane_bg: Color = if (th.bg) |c| .{ .rgb = c } else .default;
+ // ...and the same background as a colour to do arithmetic on, which the
+ // dimmed selections below need. A theme with a null bg cannot say what
+ // the host's own cell looks like, so its tag bar stands in — the
+ // substitution pdfTintColors already makes.
+ const page_rgb = th.bg orelse th.tag_bg;
// text area resets to terminal-default cells (vaxis clear semantics);
// light themes paint the page over it.
@@ -11681,7 +11700,13 @@ pub const Pardes = struct {
if (w < tw) _ = s.print(tx + tw - w, r.y, w, ibuf[0..iw], tag_style);
}
// tag char selection highlight (helix v/x, or a tagline sweep),
- // inclusive [lo, hi] — rendered-tag columns, so no conversion
+ // inclusive [lo, hi] — rendered-tag columns, so no conversion.
+ //
+ // Every selection on screen paints in the LIVE theme (`th`) and not in
+ // `chrome`: a highlight is not attached to any geometry, it appears
+ // under the range you just swept, so it has to arrive with the theme
+ // the way syntax colours do rather than slide in over the chrome
+ // animation's frames.
if (pane.tag_edit and pane.tag_sel) {
const b = tagSelBounds(pane);
var col: usize = b.lo;
@@ -11689,8 +11714,8 @@ pub const Pardes = struct {
while (col <= end and col < tw) : (col += 1) {
const cell = s.at(tx + @as(u16, @intCast(col)), r.y);
cell.default = false;
- cell.style.bg = .{ .rgb = msel_bg };
- cell.style.fg = .{ .rgb = msel_fg };
+ cell.style.bg = .{ .rgb = th.sel_bg };
+ cell.style.fg = .{ .rgb = th.sel_fg };
}
}
// cursor while editing the tag: a rendered-tag column, verbatim
@@ -11774,6 +11799,28 @@ pub const Pardes = struct {
// mouse selections (pane-local coords), one pass per button — later
// buttons win on overlap. A left .done stays highlighted after release;
// middle/right .done are transient (they fire their action on release).
+ //
+ // Which button drew a sweep is worth seeing, so each gets the theme's
+ // selection tinted toward one of the theme's own syntax accents: three
+ // colours that are visibly not each other on a dark theme and on a
+ // light one, without asking a theme to name three more. A QUARTER of
+ // the accent, which is the mix of a mix — at a half, an accent as
+ // bright as the theme's text lands on top of sel_fg and the selected
+ // text stops being readable on a couple of dozen generated themes.
+ // `num` gives way to `comment` when a theme paints numbers and strings
+ // alike — the shipped helix theme does, 24 of the generated ones do —
+ // because two buttons landing on one colour is the whole thing this
+ // avoids.
+ // ponytail: 20 of the 228 themes colour two of these three scopes the
+ // same anyway and still collapse two buttons. Upgrade path is to walk
+ // the theme for a third colour far enough from the other two, rather
+ // than naming the scopes here.
+ const accent2 = if (std.mem.eql(u8, &th.num, &th.str)) th.comment else th.num;
+ const sel_btn = [3][3]u8{
+ mix(th.sel_bg, mix(th.sel_bg, th.kw)),
+ mix(th.sel_bg, mix(th.sel_bg, th.str)),
+ mix(th.sel_bg, mix(th.sel_bg, accent2)),
+ };
for (pane.sel, 0..) |sl, b| {
if (sl.state == .none) continue;
if (sl.state == .done and b != 0) continue;
@@ -11788,8 +11835,8 @@ pub const Pardes = struct {
while (col <= c1 and col < tw) : (col += 1) {
const cell = s.at(tx + @as(u16, @intCast(col)), r.y + row);
cell.default = false;
- cell.style.bg = .{ .rgb = SEL_BG[b] };
- cell.style.fg = .{ .rgb = sel_fg };
+ cell.style.bg = .{ .rgb = sel_btn[b] };
+ cell.style.fg = .{ .rgb = th.sel_fg };
}
}
}
@@ -11804,8 +11851,8 @@ pub const Pardes = struct {
while (col < tw) : (col += 1) {
const cell = s.at(tx + col, r.y + row);
cell.default = false;
- cell.style.bg = .{ .rgb = msel_bg };
- cell.style.fg = .{ .rgb = msel_fg };
+ cell.style.bg = .{ .rgb = th.sel_bg };
+ cell.style.fg = .{ .rgb = th.sel_fg };
}
}
}
@@ -11838,8 +11885,16 @@ pub const Pardes = struct {
.acol = if (pane.vsel.active) pane.vsel.col else pane.cur_col,
} else pane.sels[si];
const bnd = cellBounds(sr);
- const bg = if (primary) msel_bg else msel2_bg;
- const fg = if (primary) msel_fg else msel2_fg;
+ // an extra cursor is the selection colour turned down: the
+ // highlight pulled halfway back to the page, so the primary is
+ // the one that reads as "here" at a glance. On a light theme
+ // that dims toward white rather than toward black, which is the
+ // same statement. The INK stays put — helix's two selection
+ // styles differ in their background too, and dimming both ends
+ // walks the text and its cell toward each other until neither
+ // is readable on the themes whose selection is already close to
+ // the page.
+ const bg = if (primary) th.sel_bg else mix(th.sel_bg, page_rgb);
var ar: i32 = bnd.lo_row;
while (ar <= bnd.hi_row) : (ar += 1) {
const prow = ar - off + @as(i32, BOX_H);
@@ -11851,7 +11906,7 @@ pub const Pardes = struct {
const cell = s.at(tx + @as(u16, @intCast(col)), r.y + @as(u16, @intCast(prow)));
cell.default = false;
cell.style.bg = .{ .rgb = bg };
- cell.style.fg = .{ .rgb = fg };
+ cell.style.fg = .{ .rgb = th.sel_fg };
}
}
if (primary and !preview) continue; // the hardware cursor IS the primary's
@@ -11860,8 +11915,8 @@ pub const Pardes = struct {
if (crow >= BOX_H and crow < @as(i32, r.h) and ccol >= vpfx and ccol < tw) {
const cell = s.at(tx + @as(u16, @intCast(ccol)), r.y + @as(u16, @intCast(crow)));
cell.default = false;
- cell.style.bg = .{ .rgb = msel_fg };
- cell.style.fg = .{ .rgb = msel_bg };
+ cell.style.bg = .{ .rgb = th.sel_fg };
+ cell.style.fg = .{ .rgb = th.sel_bg };
}
}
}