diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-05 15:05:34 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-10 09:17:07 -0300 |
| commit | 6eaa81710fee8ee2191a0b967b069bb01952be2e (patch) | |
| tree | 051f54ede02517a851538f8a853f52c7bdfa96a9 /src/pardes.zig | |
| parent | 98199e573d678f6b29a5c122836ec393856d7bd8 (diff) | |
| download | pardes-6eaa81710fee8ee2191a0b967b069bb01952be2e.tar.gz pardes-6eaa81710fee8ee2191a0b967b069bb01952be2e.zip | |
theme the selection colors
Diffstat (limited to 'src/pardes.zig')
| -rw-r--r-- | src/pardes.zig | 105 |
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 }; } } } |
