summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--docs/effects.md29
-rw-r--r--docs/themes.md1
-rw-r--r--shaders/decor.frag.glsl13
-rw-r--r--shaders/prebuilt/decor.frag.glsl13
-rw-r--r--shaders/prebuilt/decor.frag.spvbin6560 -> 7852 bytes
-rw-r--r--shaders/prebuilt/ui.frag.glsl32
-rw-r--r--shaders/prebuilt/ui.frag.spvbin5700 -> 8448 bytes
-rw-r--r--shaders/prebuilt/ui.vert.glsl10
-rw-r--r--shaders/prebuilt/ui.vert.spvbin5612 -> 5612 bytes
-rw-r--r--shaders/ui.frag.glsl32
-rw-r--r--shaders/ui.vert.glsl10
-rw-r--r--src/colors.zig49
-rw-r--r--src/detached/wire.zig2
-rw-r--r--src/draw.zig17
-rw-r--r--src/gui/gui.zig335
-rw-r--r--src/surface.zig20
-rw-r--r--src/themes/lapis.zig92
-rw-r--r--test/snapshots/theme.golden62
-rw-r--r--test/snapshots/themesel.golden126
19 files changed, 729 insertions, 114 deletions
diff --git a/docs/effects.md b/docs/effects.md
index 8073c50a..8a60d116 100644
--- a/docs/effects.md
+++ b/docs/effects.md
@@ -38,6 +38,7 @@ why. The verdict is the user's.
| G1 lift | `Lift shadow\|rim\|auto` (off), `InactiveDim <percent>` | opt-in until the focus-lift default is decided | lift-shots-2 (off, shadow, rim, auto on acme, dusk, forge); frame series + mp4 per Motion flavour | shadow on acme: keep as is. Round 1 dropped glow and surface (surface lowered the focused text's contrast) and made rim a hairline just above the focused tag (light on a dark page, shade on a light one). auto on a dark page recedes the others (InactiveDim 30) |
| G3 cursor | follows `Motion`; `CursorBlink on\|off` (on) | glide on by default through the default flavour (smooth); blink on (decision 6) | .scratch/render/cursor/cursor-flavours-grid.mp4 (and quarter speed), raw/<flavour>/<motion>/ | pending |
| G4 bloom, vignette, grain | `Bloom`, `Vignette`, `Grain` `0..3` (off) | bundled post passes; none touches a tag, grip, notice or the cursor (pardesSpare); each capped per theme (pardesCap) | .scratch/render/{bloom,vignette,grain}/ (levels 0-3 on forge, dusk, acme; crops; mp4 stepping through the levels) | pending |
+| G5 lapis | `Theme lapis` (its `decor`) | a native theme with ornament: tag plaques with hard shadows inside their bands (the focused one striped), dots, rail checker, title shadow | .scratch/render/lapis/ (whole window at 1x and 2x, crops, a slide mp4) | pending |
## Motion flavours
@@ -114,3 +115,31 @@ is round, starts halfway to a corner and reaches 10, 18 and 28%. Grain is
Found on the way: SDL GPU binds at most 4 KiB of a uniform block, so every
Shadertoy colour after the palette (iBackgroundColor and on) read zero; the
block is now cut in three with ghostty's names and order kept.
+
+Notes on G5: lapis (src/themes/lapis.zig) is 0x4200.cafe's lapis.css in
+pardes's chrome, drawn inside it (decision 1). The tags are the site's
+trail and title bars: every tag a plaque in its own band, framed in gold
+(2 px, the trail's border) with the trail's hard vermilion shadow (5 px)
+down and right, the frame and the shadow both inside the band's rows and
+columns, so no shadow reaches a body, a grip or the next pane; down, the
+shadow gets what the band's slack under the text leaves (a pixel or three
+at a tagline under the body's size, more at a larger one), to the right
+its full offset. The focused pane's plaque is the code window's title bar,
+gold and lapis stripes of 2 px, its words on plates of the focused ground
+(the words' cells stay opaque, the blanks between let the stripes show),
+so the focus indicator is plainer to see than any theme's tint, never
+less; the unfocused ones are plain. Column and workspace tags take the
+quieter object-label language: a 1 px frame and a 2 px shadow of vermilion
+toward the page. The plaques are decor drawn under their cells (a group's
+under range) with the cells blended premultiplied over them; blank tag
+cells on the tag's ground are see-through there. They need an opaque
+window: under WindowOpacity 100 the tags are plain. Also from the site:
+the colours (lapis-deep page, vellum text, gold names and rules,
+vermilion rubrics and tag rule, gold selection in lapis ink), the page's
+gold dot grid (22 px, #e8c46a22) on page-coloured cells only, fixed to the
+window; the dither frame's checker in the scroll track, anchored to its
+rail (the thumb solid gold); the titles' vermilion offset shadow under a
+file name's letters. Left out: Crimson Pro and Departure Mono, the
+ornaments, a decor crossfade (it snaps while the colours fade). The tty
+shows the colours alone: a tag row abuts its body and its neighbour, so
+there is no cell for a shadow row or column.
diff --git a/docs/themes.md b/docs/themes.md
index 2b15051e..38596819 100644
--- a/docs/themes.md
+++ b/docs/themes.md
@@ -121,6 +121,7 @@ so existing exported themes remain valid.
| `rule_px` | Pixel shells: the width of the rules between columns, between panes stacked in a column, and under the workspace and column tags, in `border` (the rule between a tag and its body is always one pixel), in logical pixels: doubled on a 2x display, as acme scales its Border | 2 |
| `box_border`, `box_dirty` | The grip is acme's button: `box` filled when focused, else a two-pixel ring of `box_border` round the tag's ground; `box_dirty` fills it inside either ring while its file is unsaved (a grid, which has no ring, shows a bold `*` on `box_dirty` in the grip's second cell) | `box_dim`; `diagnostic_warning`, then `num` |
| `rail_px` | Pixel shells: the scroll column's width, the grip's button over the scrollbar (thumb a pixel narrower), in pixels at a 17px tagline, scaled with it | 12, acme's Scrollwid |
+| `decor` | Pixel shells: a theme's ornament, drawn inside the chrome it has (lapis's): `page_dots` (a dot grid on the page's own ground, `page_dot_alpha` strong every `page_dot_px`), `rail_checker` (a checker in the scroll track, squares of `rail_checker_px`), `tag_border`, `tag_shadow`, `tag_stripe` (every tag a raised plaque inside its own band: a `tag_border_px` frame round its text and a hard `tag_shadow_px` shadow down and right, both inside the band's rows and columns; the focused pane's plaque striped `tag_stripe_px` with its words on plates of the tag's ground; column and workspace tags a quieter one; opaque windows only) and `title_shadow` (the file name's offset shadow, `title_shadow_px`), sizes in logical pixels. None of it touches a focus indicator | none |
Native palettes give filenames a distinct hue at approximately the same
brightness as the surrounding tag text. This also applies to output names
diff --git a/shaders/decor.frag.glsl b/shaders/decor.frag.glsl
index 3e2c286f..bb1ef0d6 100644
--- a/shaders/decor.frag.glsl
+++ b/shaders/decor.frag.glsl
@@ -8,6 +8,8 @@
// colour rides in the cell's ground (a_bg), its coverage in a_fg.r, so the
// result is the overlay pipeline's exactly.
//
+// Checker (flag 0x08000000): two colours in squares, or in stripes, see main.
+//
// Soft (flag 0x10000000): a rectangle's shadow, rim or glow blurred by a Gaussian,
// in closed form (the product of two erf edges), on one quad grown by 3
// sigma: no blur pass. Everything is in the quad's own 0..1 space, so a
@@ -51,6 +53,17 @@ void main() {
if (v_clip.z >= v_clip.x && (gl_FragCoord.x < v_clip.x || gl_FragCoord.x >= v_clip.z ||
gl_FragCoord.y < v_clip.y || gl_FragCoord.y >= v_clip.w)) discard;
float opacity = clamp(u_ui.background_opacity, 0.0, 1.0);
+ // Checker (flag 0x08000000): squares of v_uv.x pixels from the quad's
+ // own corner, so it moves with its rail, in the ground's colour and the
+ // ink's (lapis's scroll track).
+ vec2 in_quad = v_cell_uv / max(vec2(dFdx(v_cell_uv.x), dFdy(v_cell_uv.y)), vec2(1e-6));
+ if ((v_effect.x & 0x08000000u) != 0u) {
+ ivec2 q = ivec2(floor(in_quad / max(v_uv.x, 1.0)));
+ // v_uv.y set: stripes across the quad (lapis's title bar), not squares.
+ int k = v_uv.y > 0.5 ? q.y : q.x + q.y;
+ o_col = vec4(((k & 1) == 0 ? v_bg : v_fg) * opacity, 1.0);
+ return;
+ }
if ((v_effect.x & 0x10000000u) == 0u) {
o_col = vec4(v_bg * opacity, v_fg.r);
return;
diff --git a/shaders/prebuilt/decor.frag.glsl b/shaders/prebuilt/decor.frag.glsl
index 3e2c286f..bb1ef0d6 100644
--- a/shaders/prebuilt/decor.frag.glsl
+++ b/shaders/prebuilt/decor.frag.glsl
@@ -8,6 +8,8 @@
// colour rides in the cell's ground (a_bg), its coverage in a_fg.r, so the
// result is the overlay pipeline's exactly.
//
+// Checker (flag 0x08000000): two colours in squares, or in stripes, see main.
+//
// Soft (flag 0x10000000): a rectangle's shadow, rim or glow blurred by a Gaussian,
// in closed form (the product of two erf edges), on one quad grown by 3
// sigma: no blur pass. Everything is in the quad's own 0..1 space, so a
@@ -51,6 +53,17 @@ void main() {
if (v_clip.z >= v_clip.x && (gl_FragCoord.x < v_clip.x || gl_FragCoord.x >= v_clip.z ||
gl_FragCoord.y < v_clip.y || gl_FragCoord.y >= v_clip.w)) discard;
float opacity = clamp(u_ui.background_opacity, 0.0, 1.0);
+ // Checker (flag 0x08000000): squares of v_uv.x pixels from the quad's
+ // own corner, so it moves with its rail, in the ground's colour and the
+ // ink's (lapis's scroll track).
+ vec2 in_quad = v_cell_uv / max(vec2(dFdx(v_cell_uv.x), dFdy(v_cell_uv.y)), vec2(1e-6));
+ if ((v_effect.x & 0x08000000u) != 0u) {
+ ivec2 q = ivec2(floor(in_quad / max(v_uv.x, 1.0)));
+ // v_uv.y set: stripes across the quad (lapis's title bar), not squares.
+ int k = v_uv.y > 0.5 ? q.y : q.x + q.y;
+ o_col = vec4(((k & 1) == 0 ? v_bg : v_fg) * opacity, 1.0);
+ return;
+ }
if ((v_effect.x & 0x10000000u) == 0u) {
o_col = vec4(v_bg * opacity, v_fg.r);
return;
diff --git a/shaders/prebuilt/decor.frag.spv b/shaders/prebuilt/decor.frag.spv
index 3256f2b1..bd565d03 100644
--- a/shaders/prebuilt/decor.frag.spv
+++ b/shaders/prebuilt/decor.frag.spv
Binary files differ
diff --git a/shaders/prebuilt/ui.frag.glsl b/shaders/prebuilt/ui.frag.glsl
index dca224ba..62dbbfa2 100644
--- a/shaders/prebuilt/ui.frag.glsl
+++ b/shaders/prebuilt/ui.frag.glsl
@@ -10,6 +10,12 @@
layout(set = 2, binding = 0) uniform sampler2D u_atlas;
layout(std140, set = 3, binding = 0) uniform UiUniforms {
float background_opacity;
+ // A theme's ornament (colors.Decor): the page's dots (rgb, strength),
+ // their geometry (period, solid radius, faded radius, in pixels), and a
+ // file name's shadow (rgb, offset in pixels).
+ vec4 decor_dots;
+ vec4 decor_dot_geom;
+ vec4 decor_shadow;
} u_ui;
layout(location = 0) in vec2 v_uv;
@@ -43,7 +49,7 @@ void main() {
float bgA = (v_effect.x & 0x20000000u) != 0u ? 1.0
: (v_effect.x & 0x40000000u) != 0u ? 0.0
: clamp(u_ui.background_opacity, 0.0, 1.0);
- uint effect = v_effect.x & 0x0fffffffu;
+ uint effect = v_effect.x & 0x07ffffffu;
float progress = clamp(uintBitsToFloat(v_effect.y), 0.0, 1.0);
uint cellCol = v_effect.w & 0xffffu;
uint cellRow = v_effect.w >> 16;
@@ -52,7 +58,29 @@ void main() {
// Ink over ground, both premultiplied: v_fg*a + (v_bg*bgA)*(1-a), which is
// mix() with the ground already premultiplied. At bgA = 1 that is exactly
// the mix(v_bg, v_fg, a) at alpha 1 this shader always wrote.
- vec4 cell = vec4(mix(v_bg * bgA, v_fg, a), mix(bgA, 1.0, a));
+ vec3 ground = v_bg;
+ // 0x10000000: the page's ground under a pane body, dotted (lapis): a
+ // dot every period pixels, fixed to the window, under the text.
+ if ((v_effect.x & 0x10000000u) != 0u) {
+ float period = u_ui.decor_dot_geom.x;
+ vec2 d = mod(gl_FragCoord.xy, period) - 0.5 * period;
+ float dot_a = 1.0 - smoothstep(u_ui.decor_dot_geom.y, u_ui.decor_dot_geom.z, length(d));
+ ground = mix(ground, u_ui.decor_dots.rgb, u_ui.decor_dots.a * dot_a);
+ }
+ vec4 under = vec4(ground * bgA, bgA);
+ // Derivatives outside any branch: a pixel's step in the atlas and in
+ // the cell, for the shadow's offset.
+ vec2 step_uv = vec2(dFdx(v_uv.x), dFdy(v_uv.y)) * u_ui.decor_shadow.a;
+ vec2 step_cell = vec2(dFdx(v_cell_uv.x), dFdy(v_cell_uv.y)) * u_ui.decor_shadow.a;
+ // 0x08000000: a file name's letters cast a shadow down and right onto
+ // their own cell's ground (lapis), under the ink; what would fall past
+ // the cell's edge is not drawn.
+ if ((v_effect.x & 0x08000000u) != 0u) {
+ vec2 from = v_cell_uv - step_cell;
+ float s = all(greaterThanEqual(from, vec2(0.0))) ? texture(u_atlas, v_uv - step_uv).r : 0.0;
+ under = mix(under, vec4(u_ui.decor_shadow.rgb, 1.0), s);
+ }
+ vec4 cell = vec4(mix(under.rgb, v_fg, a), mix(under.a, 1.0, a));
// 3 = data dissolve. Changed cells retain the frozen old cell until their
// stable threshold, then atomically become the new cell. Exact endpoints
diff --git a/shaders/prebuilt/ui.frag.spv b/shaders/prebuilt/ui.frag.spv
index c8ee1604..d83d9c55 100644
--- a/shaders/prebuilt/ui.frag.spv
+++ b/shaders/prebuilt/ui.frag.spv
Binary files differ
diff --git a/shaders/prebuilt/ui.vert.glsl b/shaders/prebuilt/ui.vert.glsl
index 5551f3f1..de22adee 100644
--- a/shaders/prebuilt/ui.vert.glsl
+++ b/shaders/prebuilt/ui.vert.glsl
@@ -14,9 +14,11 @@ layout(location = 3) in vec3 a_bg;
layout(location = 4) in vec4 a_panel_rect;
layout(location = 5) in vec4 a_present_rect;
// effect id, eased-progress float bits, pane serial, local row/column packed
-// into one uint. The FOUR high effect bits are flags — the frozen-old cell
-// layer, a see-through cell background, an opaque block-cursor fill and a
-// soft shadow (decor.frag.glsl) — so the id is what is left below them; integer transport preserves the exact
+// into one uint. The FIVE high effect bits are flags — the frozen-old cell
+// layer, a see-through cell background, an opaque block-cursor fill, a soft
+// shadow (decor.frag.glsl) or dotted page ground (ui.frag.glsl), and a
+// checker (decor) or a file name's shadow (ui) — so the id is what is left
+// below them; integer transport preserves the exact
// cross-backend hash.
layout(location = 6) in uvec4 a_effect;
// A clip rectangle in target pixels (x0, y0, x1, y1): the fragment shader
@@ -61,7 +63,7 @@ void main() {
v_cell_uv = vec2(0.0, 1.0);
}
- uint effect = a_effect.x & 0x0fffffffu;
+ uint effect = a_effect.x & 0x07ffffffu;
if (effect == 1u || effect == 2u || effect == 5u) {
// NDC y points up, so a top-left/bottom-right box has a NEGATIVE y
// delta. Preserve that sign or every local y is mirrored/outside.
diff --git a/shaders/prebuilt/ui.vert.spv b/shaders/prebuilt/ui.vert.spv
index b1e87ec1..af5b0738 100644
--- a/shaders/prebuilt/ui.vert.spv
+++ b/shaders/prebuilt/ui.vert.spv
Binary files differ
diff --git a/shaders/ui.frag.glsl b/shaders/ui.frag.glsl
index dca224ba..62dbbfa2 100644
--- a/shaders/ui.frag.glsl
+++ b/shaders/ui.frag.glsl
@@ -10,6 +10,12 @@
layout(set = 2, binding = 0) uniform sampler2D u_atlas;
layout(std140, set = 3, binding = 0) uniform UiUniforms {
float background_opacity;
+ // A theme's ornament (colors.Decor): the page's dots (rgb, strength),
+ // their geometry (period, solid radius, faded radius, in pixels), and a
+ // file name's shadow (rgb, offset in pixels).
+ vec4 decor_dots;
+ vec4 decor_dot_geom;
+ vec4 decor_shadow;
} u_ui;
layout(location = 0) in vec2 v_uv;
@@ -43,7 +49,7 @@ void main() {
float bgA = (v_effect.x & 0x20000000u) != 0u ? 1.0
: (v_effect.x & 0x40000000u) != 0u ? 0.0
: clamp(u_ui.background_opacity, 0.0, 1.0);
- uint effect = v_effect.x & 0x0fffffffu;
+ uint effect = v_effect.x & 0x07ffffffu;
float progress = clamp(uintBitsToFloat(v_effect.y), 0.0, 1.0);
uint cellCol = v_effect.w & 0xffffu;
uint cellRow = v_effect.w >> 16;
@@ -52,7 +58,29 @@ void main() {
// Ink over ground, both premultiplied: v_fg*a + (v_bg*bgA)*(1-a), which is
// mix() with the ground already premultiplied. At bgA = 1 that is exactly
// the mix(v_bg, v_fg, a) at alpha 1 this shader always wrote.
- vec4 cell = vec4(mix(v_bg * bgA, v_fg, a), mix(bgA, 1.0, a));
+ vec3 ground = v_bg;
+ // 0x10000000: the page's ground under a pane body, dotted (lapis): a
+ // dot every period pixels, fixed to the window, under the text.
+ if ((v_effect.x & 0x10000000u) != 0u) {
+ float period = u_ui.decor_dot_geom.x;
+ vec2 d = mod(gl_FragCoord.xy, period) - 0.5 * period;
+ float dot_a = 1.0 - smoothstep(u_ui.decor_dot_geom.y, u_ui.decor_dot_geom.z, length(d));
+ ground = mix(ground, u_ui.decor_dots.rgb, u_ui.decor_dots.a * dot_a);
+ }
+ vec4 under = vec4(ground * bgA, bgA);
+ // Derivatives outside any branch: a pixel's step in the atlas and in
+ // the cell, for the shadow's offset.
+ vec2 step_uv = vec2(dFdx(v_uv.x), dFdy(v_uv.y)) * u_ui.decor_shadow.a;
+ vec2 step_cell = vec2(dFdx(v_cell_uv.x), dFdy(v_cell_uv.y)) * u_ui.decor_shadow.a;
+ // 0x08000000: a file name's letters cast a shadow down and right onto
+ // their own cell's ground (lapis), under the ink; what would fall past
+ // the cell's edge is not drawn.
+ if ((v_effect.x & 0x08000000u) != 0u) {
+ vec2 from = v_cell_uv - step_cell;
+ float s = all(greaterThanEqual(from, vec2(0.0))) ? texture(u_atlas, v_uv - step_uv).r : 0.0;
+ under = mix(under, vec4(u_ui.decor_shadow.rgb, 1.0), s);
+ }
+ vec4 cell = vec4(mix(under.rgb, v_fg, a), mix(under.a, 1.0, a));
// 3 = data dissolve. Changed cells retain the frozen old cell until their
// stable threshold, then atomically become the new cell. Exact endpoints
diff --git a/shaders/ui.vert.glsl b/shaders/ui.vert.glsl
index 5551f3f1..de22adee 100644
--- a/shaders/ui.vert.glsl
+++ b/shaders/ui.vert.glsl
@@ -14,9 +14,11 @@ layout(location = 3) in vec3 a_bg;
layout(location = 4) in vec4 a_panel_rect;
layout(location = 5) in vec4 a_present_rect;
// effect id, eased-progress float bits, pane serial, local row/column packed
-// into one uint. The FOUR high effect bits are flags — the frozen-old cell
-// layer, a see-through cell background, an opaque block-cursor fill and a
-// soft shadow (decor.frag.glsl) — so the id is what is left below them; integer transport preserves the exact
+// into one uint. The FIVE high effect bits are flags — the frozen-old cell
+// layer, a see-through cell background, an opaque block-cursor fill, a soft
+// shadow (decor.frag.glsl) or dotted page ground (ui.frag.glsl), and a
+// checker (decor) or a file name's shadow (ui) — so the id is what is left
+// below them; integer transport preserves the exact
// cross-backend hash.
layout(location = 6) in uvec4 a_effect;
// A clip rectangle in target pixels (x0, y0, x1, y1): the fragment shader
@@ -61,7 +63,7 @@ void main() {
v_cell_uv = vec2(0.0, 1.0);
}
- uint effect = a_effect.x & 0x0fffffffu;
+ uint effect = a_effect.x & 0x07ffffffu;
if (effect == 1u || effect == 2u || effect == 5u) {
// NDC y points up, so a top-left/bottom-right box has a NEGATIVE y
// delta. Preserve that sign or every local y is mirrored/outside.
diff --git a/src/colors.zig b/src/colors.zig
index 4a18bba9..b44e84f9 100644
--- a/src/colors.zig
+++ b/src/colors.zig
@@ -123,6 +123,42 @@ pub fn mix(a: [3]u8, b: [3]u8) [3]u8 {
return out;
}
+/// A theme's ornament, drawn by a pixel shell inside the chrome it already
+/// has (docs/render-pipeline.md §10: no gaps between panes): the lapis
+/// theme's. Sizes are logical pixels. None of it touches a focus indicator:
+/// the dots lie on the page's own ground, the checker in the scroll track
+/// (never its thumb), a tag's plaque inside its band beside its grip (and
+/// the focused one only the more striking), and the shadow under a file
+/// name's own letters.
+pub const Decor = struct {
+ /// A dot grid on the page's ground, `page_dot_alpha` of 255 strong, a
+ /// dot one logical pixel across (fading out by a pixel and a half) every
+ /// `page_dot_px`, fixed to the window.
+ page_dots: ?[3]u8 = null,
+ page_dot_alpha: u8 = 0x22,
+ page_dot_px: u8 = 22,
+ /// A checker in the scroll track, squares of `rail_checker_px`, this
+ /// colour and the track's.
+ rail_checker: ?[3]u8 = null,
+ rail_checker_px: u8 = 3,
+ /// Every tag drawn as a raised plaque inside its own band (the site's
+ /// trail and title bars): a `tag_border` frame round the text, a hard
+ /// `tag_shadow` offset down and right by `tag_shadow_px`, both within
+ /// the band's rows and columns, never past them; the focused pane's
+ /// plaque striped `tag_stripe` and the tag's ground, `tag_stripe_px`
+ /// each, its words on plates of the ground. Column and workspace tags
+ /// take a quieter one (a one-pixel frame, a shadow of two, no stripes).
+ tag_border: ?[3]u8 = null,
+ tag_border_px: u8 = 2,
+ tag_shadow: ?[3]u8 = null,
+ tag_shadow_px: u8 = 5,
+ tag_stripe: ?[3]u8 = null,
+ tag_stripe_px: u8 = 2,
+ /// The file name's offset shadow, down and right, in a pane's tag.
+ title_shadow: ?[3]u8 = null,
+ title_shadow_px: u8 = 2,
+};
+
pub const Theme = struct {
name: []const u8,
bg: ?[3]u8,
@@ -188,6 +224,8 @@ pub const Theme = struct {
/// in pixels at a 17px tagline, the thumb one narrower (acme's 12px
/// Scrollwid, the default).
rail_px: ?u8 = null,
+ /// A pixel shell's ornament (Decor): lapis's.
+ decor: ?Decor = null,
};
const curated = struct {
@@ -209,6 +247,7 @@ const curated = struct {
pub const helix = @import("themes/helix.zig");
pub const dark = @import("themes/dark.zig");
pub const acme = @import("themes/acme.zig");
+ pub const lapis = @import("themes/lapis.zig");
};
fn fold(comptime C: type) [@typeInfo(C).@"struct".decls.len]Theme {
@@ -219,7 +258,15 @@ fn fold(comptime C: type) [@typeInfo(C).@"struct".decls.len]Theme {
var t: Theme = undefined;
const source = @field(C, d.name).theme;
for (@typeInfo(Theme).@"struct".fields) |f| {
- @field(t, f.name) = if (@hasField(@TypeOf(source), f.name))
+ @field(t, f.name) = if (f.type == ?Decor and @hasField(@TypeOf(source), f.name)) decor: {
+ // A nested literal is folded field by field too.
+ var decor: Decor = .{};
+ const given = @field(source, f.name);
+ for (@typeInfo(Decor).@"struct".fields) |g| if (@hasField(@TypeOf(given), g.name)) {
+ @field(decor, g.name) = @field(given, g.name);
+ };
+ break :decor decor;
+ } else if (@hasField(@TypeOf(source), f.name))
@field(source, f.name)
else if (f.default_value_ptr) |ptr|
@as(*const f.type, @ptrCast(@alignCast(ptr))).*
diff --git a/src/detached/wire.zig b/src/detached/wire.zig
index 53a1a70d..9f9c716a 100644
--- a/src/detached/wire.zig
+++ b/src/detached/wire.zig
@@ -13,7 +13,7 @@ pub const version: u16 = 8;
/// The Chrome palette is carried field by field as its declaration lists
/// them, so any change to it changes the wire: bump `version`, then this.
-const chrome_layout: u64 = 0xc91196fcdef144e2;
+const chrome_layout: u64 = 0xf852cb229ae38f51;
comptime {
@setEvalBranchQuota(10_000);
var hash = std.hash.Fnv1a_64.init();
diff --git a/src/draw.zig b/src/draw.zig
index b8cf4e7d..5f14ae21 100644
--- a/src/draw.zig
+++ b/src/draw.zig
@@ -470,7 +470,24 @@ pub fn render(p: *Pardes, arena: std.mem.Allocator) !*Surface {
.grip_dirty = boxDirty(p),
.tag_rule = p.theme().tag_rule,
.rail_px = @intCast(@min(255, @as(u32, p.theme().rail_px orelse 12) * p.settings.grip_width / 100)),
+ .name_ink = chrome.tag_name_fg,
+ .active_name_ink = if (p.settings.focus_tint) chrome.tag_active_name_fg else chrome.tag_name_fg,
};
+ if (p.theme().decor) |decor| {
+ s.chrome.decor_dots = decor.page_dots;
+ s.chrome.decor_dot_alpha = decor.page_dot_alpha;
+ s.chrome.decor_dot_px = decor.page_dot_px;
+ s.chrome.decor_checker = decor.rail_checker;
+ s.chrome.decor_checker_px = decor.rail_checker_px;
+ s.chrome.decor_box_border = decor.tag_border;
+ s.chrome.decor_box_border_px = decor.tag_border_px;
+ s.chrome.decor_box_shadow = decor.tag_shadow;
+ s.chrome.decor_box_shadow_px = decor.tag_shadow_px;
+ s.chrome.decor_box_stripe = decor.tag_stripe;
+ s.chrome.decor_box_stripe_px = decor.tag_stripe_px;
+ s.chrome.decor_title = decor.title_shadow;
+ s.chrome.decor_title_px = decor.title_shadow_px;
+ }
place(p, s);
// Separators have their own role; a scrollbar need not frame the page.
const whole = s.regions[0].rect;
diff --git a/src/gui/gui.zig b/src/gui/gui.zig
index bde9aecd..092bb3bf 100644
--- a/src/gui/gui.zig
+++ b/src/gui/gui.zig
@@ -507,6 +507,12 @@ const clear_bg_bit: u32 = 0x4000_0000;
const opaque_bg_bit: u32 = 0x2000_0000;
/// Decor that is a soft shadow, not a flat fill (decor.frag.glsl).
const soft_shadow_bit: u32 = 0x1000_0000;
+/// Decor that is a checker (decor.frag.glsl).
+const checker_bit: u32 = 0x0800_0000;
+/// A cell on the page's ground under the theme's dots, and a file name's
+/// cell that casts the theme's shadow (ui.frag.glsl, Chrome.decor_*).
+const dotted_bit: u32 = 0x1000_0000;
+const title_bit: u32 = 0x0800_0000;
const SavedImagePlace = struct {
key: pardes.ImageCacheKey,
@@ -1787,6 +1793,10 @@ const Gui = struct {
device: *c.SDL_GPUDevice,
swapchain_format: c.SDL_GPUTextureFormat,
pipeline: *c.SDL_GPUGraphicsPipeline,
+ /// The cell pipeline, blending premultiplied over what is drawn under:
+ /// for a frame with a theme's tag plaques, whose blank tag cells let
+ /// them show (tagPlaques).
+ pipeline_over: *c.SDL_GPUGraphicsPipeline,
decor_pipeline: *c.SDL_GPUGraphicsPipeline,
overlay_pipeline: *c.SDL_GPUGraphicsPipeline,
image_pipeline: *c.SDL_GPUGraphicsPipeline,
@@ -2068,6 +2078,9 @@ const Group = struct {
image_count: u32 = 0,
decor_start: u32 = 0,
decor_count: u32 = 0,
+ /// The first `under_count` of the group's decor are drawn before its
+ /// cells, under them (a theme's tag plaques, whose cells let them show).
+ under_count: u32 = 0,
};
/// The frame's draw groups, bottom to top: tier 0 (the page and the panes
@@ -2455,6 +2468,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
const atlas_xfer = c.SDL_CreateGPUTransferBuffer(device, &ax_info) orelse return error.GpuCreate;
const pipeline = try makePipeline(device, swapchain_format, frag_spv, 1, std.mem.zeroes(c.SDL_GPUColorTargetBlendState));
+ const pipeline_over = try makePipeline(device, swapchain_format, frag_spv, 1, premultipliedBlend());
const decor_pipeline = try makePipeline(device, swapchain_format, decor_frag_spv, 0, backgroundLayerBlend());
const overlay_pipeline = try makeOverlayPipeline(device, swapchain_format);
const image_pipeline = try makeImagePipeline(device, swapchain_format);
@@ -2485,6 +2499,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
.device = device,
.swapchain_format = swapchain_format,
.pipeline = pipeline,
+ .pipeline_over = pipeline_over,
.decor_pipeline = decor_pipeline,
.overlay_pipeline = overlay_pipeline,
.image_pipeline = image_pipeline,
@@ -2523,6 +2538,7 @@ fn runNative(init: std.process.Init, opts_in: pardes.Options, attach: ?[]const u
// regrown to; the image ones are released below.
defer {
c.SDL_ReleaseGPUGraphicsPipeline(device, g.pipeline);
+ c.SDL_ReleaseGPUGraphicsPipeline(device, g.pipeline_over);
c.SDL_ReleaseGPUGraphicsPipeline(device, g.decor_pipeline);
c.SDL_ReleaseGPUGraphicsPipeline(device, g.overlay_pipeline);
c.SDL_ReleaseGPUTexture(device, g.atlas_tex);
@@ -5418,6 +5434,20 @@ fn renderFrame(
defer if (building) build_zone.end();
const layout = fixedCellLayout(g);
const chrome = &surface.chrome;
+ cell_decor = CellDecor.of(chrome);
+ // Plaques need an opaque window: with WindowOpacity under 100 the cells
+ // are drawn as they are, and the plaques not at all.
+ cell_decor.plaques_off = backgroundOpacity(g.applied_window_opacity) < 1;
+ if (chrome.decor_box_border != null and !cell_decor.plaques_off) {
+ cell_decor.grounds = .{ chrome.tag_bg, chrome.tag_focus_bg };
+ for (surface.regionList()) |region| {
+ if (region.kind != .tag and region.kind != .column_tag and region.kind != .workspace_tag) continue;
+ if (cell_decor.nplaques == cell_decor.plaques.len) break;
+ const plaque = plaqueBox(g, chrome, surface.regionList(), region, layout, win_w, win_h) orelse continue;
+ cell_decor.plaques[cell_decor.nplaques] = plaque.band;
+ cell_decor.nplaques += 1;
+ }
+ }
var groups = makeGroups(surface.panelTracks(), surface.hasPanelDiff());
// Cursors stand still while panes move, as they always have; while the
// focused one glides or blinks, it is the overlay's quad instead. One
@@ -5696,9 +5726,16 @@ fn renderFrame(
const whole = c.SDL_Rect{ .x = 0, .y = 0, .w = @intCast(sw), .h = @intCast(sh) };
const decor_base = cell_total + shifted;
for (groups.items[0..groups.len], 0..) |group, index| {
+ if (group.under_count != 0 and g.vbuf != null) {
+ c.SDL_BindGPUGraphicsPipeline(rp, g.decor_pipeline);
+ pushLayerOpacity(cmd, rp, background_opacity);
+ const binding = c.SDL_GPUBufferBinding{ .buffer = g.vbuf.?, .offset = (decor_base + group.decor_start) * @sizeOf(CellInstance) };
+ c.SDL_BindGPUVertexBuffers(rp, 0, &binding, 1);
+ c.SDL_DrawGPUPrimitives(rp, 6, group.under_count, 0, 0);
+ }
const has_shifted = index == 0 and shifted != 0;
if ((group.cell_count != 0 or has_shifted) and g.vbuf != null) {
- c.SDL_BindGPUGraphicsPipeline(rp, g.pipeline);
+ c.SDL_BindGPUGraphicsPipeline(rp, if (cell_decor.nplaques != 0) g.pipeline_over else g.pipeline);
const samp_binding = c.SDL_GPUTextureSamplerBinding{ .texture = g.atlas_tex, .sampler = g.atlas_sampler };
c.SDL_BindGPUFragmentSamplers(rp, 0, &samp_binding, 1);
if (group.cell_count != 0) {
@@ -5716,13 +5753,13 @@ fn renderFrame(
}
}
if (group.image_count != 0) drawNativeImagesGpu(g, rp, surface, group, whole);
- if (group.decor_count != 0 and g.vbuf != null) {
+ if (group.decor_count > group.under_count and g.vbuf != null) {
c.SDL_BindGPUGraphicsPipeline(rp, g.decor_pipeline);
// Ink keeps its colour at any WindowOpacity.
pushLayerOpacity(cmd, rp, if (index == groups.ink) 1 else background_opacity);
- const binding = c.SDL_GPUBufferBinding{ .buffer = g.vbuf.?, .offset = (decor_base + group.decor_start) * @sizeOf(CellInstance) };
+ const binding = c.SDL_GPUBufferBinding{ .buffer = g.vbuf.?, .offset = (decor_base + group.decor_start + group.under_count) * @sizeOf(CellInstance) };
c.SDL_BindGPUVertexBuffers(rp, 0, &binding, 1);
- c.SDL_DrawGPUPrimitives(rp, 6, group.decor_count, 0, 0);
+ c.SDL_DrawGPUPrimitives(rp, 6, group.decor_count - group.under_count, 0, 0);
}
}
drawOverlayGpu(g, cmd, rp, overlay_count);
@@ -6024,6 +6061,9 @@ fn emitTagLayer(g: *Gui, instances: [*]CellInstance, next: *u32, layer: *const p
const y = layer.viewport.y + @as(u16, @intCast(index));
const top = @as(f32, @floatFromInt(y)) * bh;
emitInstance(g, instances, next.*, 0, y, .{ .w = band_right - left, .h = bh, .x_off = left, .y_off = slide }, win_w, win_h, track, .body, (&background)[0..1], 0, null, old_layer, false, page);
+ // A theme's plaque is drawn under the band: the band's own ground
+ // lets it show (tagPlaques).
+ if (cell_decor.nplaques != 0 and cell_decor.inPlaque(left + 0.5, top + 0.5)) instances[next.*].effect |= clear_bg_bit;
if (slide != 0) clipInstanceTop(&instances[next.*], top, win_h);
next.* += 1;
const cells = layer.cells[index * layer.cols ..][0..layer.cols];
@@ -6229,6 +6269,10 @@ fn emitSurfaceCell(
// this underlay at tagline height leaves page-colored gutters between
// adjacent workspace, column and pane tags at TaglineSize < 100.
emitInstance(g, instances, next.*, col, row, body_layout, win_w, win_h, track, .body, (&band_base)[0..1], 0, null, old_layer, false, page);
+ // Over a theme's plaque (not a grip's button) the underlay lets it
+ // show (tagPlaques).
+ if (cell_decor.nplaques != 0 and !here.grip and cell_decor.inPlaque((@as(f32, @floatFromInt(col)) + 0.5) * body_layout.w + body_layout.x_off, @as(f32, @floatFromInt(row)) * body_layout.h + body_layout.y_off + 0.5))
+ instances[next.*].effect |= clear_bg_bit;
next.* += 1;
// Tag glyphs keep their font size inside a full physical-width grip.
// A pane's grip cells are the cell shells' form (a dirty `*`): the
@@ -6553,6 +6597,21 @@ fn emitInstance(
// A block cursor is foreground UI ink, even though its color is carried
// in the reversed cell background. Keep it visible at WindowOpacity 0.
if (is_cursor) instances[idx].effect |= opaque_bg_bit;
+ // The theme's ornament (cell_decor): dots on the page's own ground, a
+ // shadow under a file name's letters. Never on a cursor.
+ if (!is_cursor) {
+ // A blank cell on a tag's ground inside a plaque: the plaque shows.
+ if (cell_decor.nplaques != 0 and resolved.role == .tagline and cellCodepoint(&line[at]) == ' ' and
+ (std.mem.eql(u8, &resolved.bg, &cell_decor.grounds[0]) or std.mem.eql(u8, &resolved.bg, &cell_decor.grounds[1])) and
+ (cell_decor.inPlaque(px0 + 0.5, py0 + 0.5) or cell_decor.inPlaque(px0 + layout.w - 0.5, py0 + 0.5)))
+ instances[idx].effect |= clear_bg_bit;
+ if (cell_decor.dots) |dotted| {
+ if (!resolved.clear_bg and std.mem.eql(u8, &resolved.bg, &dotted)) instances[idx].effect |= dotted_bit;
+ }
+ if (cell_decor.title) |inks| {
+ if (resolved.role == .tagline and (std.mem.eql(u8, &resolved.fg, &inks[0]) or std.mem.eql(u8, &resolved.fg, &inks[1]))) instances[idx].effect |= title_bit;
+ }
+ }
}
test "block cursor alone overrides transparent cell background" {
@@ -6569,6 +6628,107 @@ test "block cursor alone overrides transparent cell background" {
}
}
+test "lapis dots only the page's own ground and shadows only a file name, never a cursor" {
+ var g: Gui = undefined;
+ g.words = .{};
+ g.cell_w = 10;
+ g.cell_h = 20;
+ g.tagline_width = 8;
+ g.tagline_height = 16;
+ g.space_slot = .{ .u = 0, .v = 0 };
+ const page: [3]u8 = .{ 0x0a, 0x10, 0x30 };
+ const name: [3]u8 = .{ 0xe8, 0xc4, 0x6a };
+ var chrome: pardes.Chrome = .{ .page = page, .decor_dots = .{ 1, 2, 3 }, .decor_dot_alpha = 0x22, .decor_title = .{ 4, 5, 6 }, .name_ink = name, .active_name_ink = name };
+ cell_decor = CellDecor.of(&chrome);
+ defer cell_decor = .{};
+ const on_page: pardes.Cell = .{ .default = false, .style = .{ .fg = .{ .rgb = .{ 200, 200, 200 } }, .bg = .{ .rgb = page } } };
+ const selected: pardes.Cell = .{ .default = false, .style = .{ .fg = .{ .rgb = .{ 0, 0, 0 } }, .bg = .{ .rgb = name } } };
+ const title: pardes.Cell = .{ .default = false, .style = .{ .fg = .{ .rgb = name }, .bg = .{ .rgb = .{ 0x0f, 0x1a, 0x4a } }, .font_role = .tagline } };
+ const plain_tag: pardes.Cell = .{ .default = false, .style = .{ .fg = .{ .rgb = .{ 0x9a, 0xa6, 0xd9 } }, .bg = .{ .rgb = .{ 0x0f, 0x1a, 0x4a } }, .font_role = .tagline } };
+ var instances: [1]CellInstance = undefined;
+ const Case = struct { cell: pardes.Cell, role: pardes.FontRole, cursor: bool, dotted: bool, shadowed: bool };
+ for ([_]Case{
+ .{ .cell = on_page, .role = .body, .cursor = false, .dotted = true, .shadowed = false },
+ .{ .cell = on_page, .role = .body, .cursor = true, .dotted = false, .shadowed = false },
+ .{ .cell = selected, .role = .body, .cursor = false, .dotted = false, .shadowed = false },
+ .{ .cell = title, .role = .tagline, .cursor = false, .dotted = false, .shadowed = true },
+ .{ .cell = title, .role = .tagline, .cursor = true, .dotted = false, .shadowed = false },
+ .{ .cell = plain_tag, .role = .tagline, .cursor = false, .dotted = false, .shadowed = false },
+ }) |case| {
+ emitInstance(&g, &instances, 0, 0, 0, fixedCellLayout(&g), 800, 480, null, case.role, (&case.cell)[0..1], 0, null, false, case.cursor, Ground.opaqueRgb(page));
+ try std.testing.expectEqual(case.dotted, instances[0].effect & dotted_bit != 0);
+ try std.testing.expectEqual(case.shadowed, instances[0].effect & title_bit != 0);
+ }
+ // A theme without decor marks nothing.
+ chrome = .{ .page = page };
+ cell_decor = CellDecor.of(&chrome);
+ emitInstance(&g, &instances, 0, 0, 0, fixedCellLayout(&g), 800, 480, null, .body, (&on_page)[0..1], 0, null, false, false, Ground.opaqueRgb(page));
+ try std.testing.expectEqual(@as(u32, 0), instances[0].effect & (dotted_bit | title_bit));
+}
+
+test "lapis plaques stay in their tag's band, clear of every grip; only the focused one is striped" {
+ const gpa = std.testing.allocator;
+ const core = try pardes.Pardes.init(gpa, .{ .cols = 80, .rows = 24 });
+ defer core.deinit();
+ _ = try core.newShell(1, "");
+ try std.testing.expect(pardes.layout.splitColumn(core, 0, 1, false));
+ try std.testing.expect(core.executeBuiltinLine(0, "Theme lapis"));
+ for (0..40) |_| core.update(.tick);
+ pardes.test_api.sync(core);
+ const s = try core.render(core.scratch.allocator());
+ try std.testing.expect(s.chrome.decor_box_border != null and s.chrome.decor_checker != null and s.chrome.decor_dots != null and s.chrome.decor_title != null);
+ var g: Gui = undefined;
+ g.decor = .empty;
+ defer g.decor.deinit(gpa);
+ g.cell_w = 16;
+ g.cell_h = 32;
+ g.tagline_width = 12;
+ g.tagline_height = 22;
+ display_scale = 1;
+ var groups = makeGroups(&.{}, false);
+ try buildDecor(&g, gpa, s, &groups, fixedCellLayout(&g), 1280, 768, true);
+ const layout = fixedCellLayout(&g);
+ const under = g.decor.items[groups.items[0].decor_start..][0..groups.items[0].under_count];
+ try std.testing.expect(under.len != 0);
+ var stripes: usize = 0;
+ var shadows: usize = 0;
+ for (under) |item| {
+ const r: [4]f32 = .{ (item.x0 + 1) / 2 * 1280, (1 - item.y0) / 2 * 768, (item.x1 + 1) / 2 * 1280, (1 - item.y1) / 2 * 768 };
+ // Inside one tag's band, whole: never on a body, a grip or the
+ // next pane.
+ const home = for (s.regionList()) |region| {
+ if (region.kind != .tag and region.kind != .column_tag and region.kind != .workspace_tag) continue;
+ const band = plaqueBox(&g, &s.chrome, s.regionList(), region, layout, 1280, 768) orelse continue;
+ if (r[0] >= band.band[0] - 0.01 and r[1] >= band.band[1] - 0.01 and r[2] <= band.band[2] + 0.01 and r[3] <= band.band[3] + 0.01) break region;
+ } else return error.PlaqueOutsideItsBand;
+ for (s.regionList()) |grip| {
+ if (grip.kind != .grip and grip.kind != .column_grip) continue;
+ const gx0 = @as(f32, @floatFromInt(grip.rect.x)) * 16;
+ const gx1 = @as(f32, @floatFromInt(grip.rect.x + grip.rect.w)) * 16;
+ const gy0 = @as(f32, @floatFromInt(grip.rect.y)) * 32;
+ const gy1 = @as(f32, @floatFromInt(grip.rect.y + grip.rect.h)) * 32;
+ try std.testing.expect(r[2] <= gx0 + 0.01 or r[0] >= gx1 - 0.01 or r[3] <= gy0 + 0.01 or r[1] >= gy1 - 0.01);
+ }
+ if (item.effect & checker_bit != 0) {
+ try std.testing.expect(home.kind == .tag and home.active);
+ stripes += 1;
+ }
+ const rgb: [3]u8 = .{ @intFromFloat(@round(item.br * 255)), @intFromFloat(@round(item.bg * 255)), @intFromFloat(@round(item.bb * 255)) };
+ if (std.mem.eql(u8, &rgb, &s.chrome.decor_box_shadow.?)) shadows += 1;
+ }
+ try std.testing.expectEqual(@as(usize, 1), stripes);
+ try std.testing.expect(shadows >= 2);
+ // The scroll track is chequered.
+ var checkers: usize = 0;
+ for (g.decor.items[groups.items[0].decor_start + groups.items[0].under_count ..]) |item| checkers += @intFromBool(item.effect & checker_bit != 0);
+ try std.testing.expect(checkers >= 1);
+ // No other theme draws any of it.
+ try std.testing.expect(core.executeBuiltinLine(0, "Theme forge"));
+ for (0..40) |_| core.update(.tick);
+ const plain = try core.render(core.scratch.allocator());
+ try std.testing.expect(plain.chrome.decor_box_border == null and plain.chrome.decor_checker == null and plain.chrome.decor_dots == null and plain.chrome.decor_title == null);
+}
+
fn cellCodepoint(cell: *const pardes.Cell) u32 {
const grapheme = cell.grapheme();
return if (cell.default or grapheme.len == 0) ' ' else firstCp(grapheme);
@@ -7352,6 +7512,20 @@ fn makePipeline(device: *c.SDL_GPUDevice, color_format: c.SDL_GPUTextureFormat,
return c.SDL_CreateGPUGraphicsPipeline(device, &info) orelse error.GpuCreate;
}
+/// Premultiplied over: an opaque cell writes itself exactly, a see-through
+/// one only its ink.
+fn premultipliedBlend() c.SDL_GPUColorTargetBlendState {
+ var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState);
+ blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE;
+ blend.dst_color_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
+ blend.color_blend_op = c.SDL_GPU_BLENDOP_ADD;
+ blend.src_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE;
+ blend.dst_alpha_blendfactor = c.SDL_GPU_BLENDFACTOR_ONE_MINUS_SRC_ALPHA;
+ blend.alpha_blend_op = c.SDL_GPU_BLENDOP_ADD;
+ blend.enable_blend = true;
+ return blend;
+}
+
fn backgroundLayerBlend() c.SDL_GPUColorTargetBlendState {
var blend = std.mem.zeroes(c.SDL_GPUColorTargetBlendState);
blend.src_color_blendfactor = c.SDL_GPU_BLENDFACTOR_SRC_ALPHA;
@@ -7742,6 +7916,29 @@ fn addDecor(g: *Gui, gpa: std.mem.Allocator, x0_in: f32, y0_in: f32, x1_in: f32,
try g.decor.append(gpa, instance);
}
+/// A checker (decor.frag.glsl): squares of `px` window pixels in `a` and
+/// `b` from the quad's corner (lapis's scroll track); `stripes`, bands of
+/// `px` across it instead (lapis's focused title bar), `a` first.
+fn addChecker(g: *Gui, gpa: std.mem.Allocator, x0: f32, y0: f32, x1: f32, y1: f32, a: [3]u8, b: [3]u8, px: f32, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
+ return addPattern(g, gpa, x0, y0, x1, y1, a, b, px, false, track, win_w, win_h);
+}
+
+fn addPattern(g: *Gui, gpa: std.mem.Allocator, x0: f32, y0: f32, x1: f32, y1: f32, a: [3]u8, b: [3]u8, px: f32, stripes: bool, track: ?pardes.animation.Track, win_w: f32, win_h: f32) !void {
+ const first = g.decor.items.len;
+ try addDecor(g, gpa, x0, y0, x1, y1, a, 1, track, win_w, win_h);
+ if (g.decor.items.len == first) return;
+ const instance = &g.decor.items[first];
+ instance.u0 = px;
+ instance.u1 = px;
+ instance.v0 = if (stripes) 1 else 0;
+ instance.v1 = if (stripes) 1 else 0;
+ instance.fr = @as(f32, @floatFromInt(b[0])) / 255.0;
+ instance.fg = @as(f32, @floatFromInt(b[1])) / 255.0;
+ instance.fb = @as(f32, @floatFromInt(b[2])) / 255.0;
+ instance.effect |= checker_bit;
+}
+
+
/// How a lift of 0 to 1 stands off the page: the shadow's blur and offset
/// in cell heights, and its darkest, where it leaves the caster's edge.
/// Within §8.2's budget: sigma 0.4-0.8 cell, at most 30% darker at the edge.
@@ -8062,6 +8259,13 @@ fn buildDecor(g: *Gui, gpa: std.mem.Allocator, surface: *const pardes.Surface, g
for (groups.items[0..groups.len], 0..) |*group, index| {
group.decor_start = @intCast(g.decor.items.len);
defer group.decor_count = @as(u32, @intCast(g.decor.items.len)) - group.decor_start;
+ // Under the cells: a theme's tag plaques, this group's panes' (and
+ // the column and workspace tags with the still ones).
+ if (index <= groups.tracks) {
+ const closing = if (group.track) |active| active.phase == .closing else false;
+ try tagPlaques(g, gpa, if (closing) surface.previous_regions else regions, groups, index, chrome, layout, win_w, win_h, group.track);
+ }
+ group.under_count = @as(u32, @intCast(g.decor.items.len)) - group.decor_start;
// The grips' buttons before any rule: the rules over their rows cut
// them, and the band round them never covers a rule.
if (index <= groups.tracks) {
@@ -8226,7 +8430,10 @@ fn paneDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, gr
// the track edges it.
const column = scrollColumn(g, chrome, rail.rect.x, layout);
try addDecor(g, gpa, x0, y0, x0 + layout.w, y1, page, 1, track, win_w, win_h);
- try addDecor(g, gpa, column[0], y0, column[1], y1, chrome.scroll_track, 1, track, win_w, win_h);
+ if (chrome.decor_checker) |gold|
+ try addChecker(g, gpa, column[0], y0, column[1], y1, chrome.scroll_track, gold, dp(@floatFromInt(@max(1, chrome.decor_checker_px))), track, win_w, win_h)
+ else
+ try addDecor(g, gpa, column[0], y0, column[1], y1, chrome.scroll_track, 1, track, win_w, win_h);
if (rail.thumb_h != 0) {
const thumb_y0 = @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y)) * layout.h;
const thumb_y1 = if (rail.thumb_y + rail.thumb_h == rail.rect.h) y1 else @as(f32, @floatFromInt(rail.rect.y + rail.thumb_y + rail.thumb_h)) * layout.h;
@@ -8269,11 +8476,126 @@ fn updateDisplayScale(window: *c.SDL_Window) void {
display_scale = std.math.clamp(display_scale, 1, 4);
}
+/// A theme's ornament as the cell shader draws it (Chrome.decor_*), for the
+/// frame being built: renderFrame sets it, writing a cell marks the cells
+/// it falls on, and pushLayerOpacity hands the shader its colours and
+/// sizes. ponytail: one window per process, as display_scale.
+var cell_decor: CellDecor = .{};
+const CellDecor = struct {
+ /// The page's ground, when the theme dots it.
+ dots: ?[3]u8 = null,
+ /// A file name's inks in a tag, when the theme shadows it.
+ title: ?[2][3]u8 = null,
+ /// UiUniforms after background_opacity (ui.frag.glsl): the dots' colour
+ /// and strength, their period and radii, the shadow's colour and offset.
+ uniforms: [12]f32 = @splat(0),
+ /// The tag plaques' bands (window pixels) and the tag grounds: a blank
+ /// tag cell there lets its plaque show through (tagPlaques).
+ plaques: [pardes.MAX_TAG_LAYERS][4]f32 = undefined,
+ nplaques: usize = 0,
+ grounds: [2][3]u8 = .{ .{ 0, 0, 0 }, .{ 0, 0, 0 } },
+ plaques_off: bool = false,
+
+ fn inPlaque(decor: *const CellDecor, x: f32, y: f32) bool {
+ for (decor.plaques[0..decor.nplaques]) |band|
+ if (x >= band[0] and x < band[2] and y >= band[1] and y < band[3]) return true;
+ return false;
+ }
+
+ fn of(chrome: *const pardes.Chrome) CellDecor {
+ var out: CellDecor = .{};
+ if (chrome.decor_dots) |rgb| if (chrome.page) |page| {
+ out.dots = page;
+ out.uniforms[0..4].* = .{ unit(rgb[0]), unit(rgb[1]), unit(rgb[2]), unit(chrome.decor_dot_alpha) };
+ out.uniforms[4..8].* = .{ dp(@floatFromInt(@max(2, chrome.decor_dot_px))), 0.75 * display_scale, 1.25 * display_scale, 0 };
+ };
+ if (chrome.decor_title) |rgb| {
+ out.title = .{ chrome.name_ink, chrome.active_name_ink };
+ out.uniforms[8..12].* = .{ unit(rgb[0]), unit(rgb[1]), unit(rgb[2]), dp(@floatFromInt(@max(1, chrome.decor_title_px))) };
+ }
+ return out;
+ }
+
+ fn unit(v: u8) f32 {
+ return @as(f32, @floatFromInt(v)) / 255;
+ }
+};
+
/// A logical pixel size in the display's pixels: rounded, at least one.
fn dp(logical: f32) f32 {
return @max(1, @round(logical * display_scale));
}
+/// A theme's tag plaques (Chrome.decor_box_*), drawn under the cells of
+/// group `index`: each pane tag, and with the still panes the column and
+/// workspace tags, becomes a framed box inside its own band with a hard
+/// shadow down and right that stays inside the band too, so it never falls
+/// on a body, a grip or the next pane. The focused pane's box is striped,
+/// its words on plates of the tag's ground (their cells stay opaque; the
+/// spaces between let the stripes through, emitInstance); a column or the
+/// workspace takes a quieter box, a one-pixel frame and a two-pixel shadow.
+fn tagPlaques(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, groups: *const Groups, index: usize, chrome: *const pardes.Chrome, layout: CellLayout, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void {
+ const border_rgb = chrome.decor_box_border orelse return;
+ if (cell_decor.plaques_off) return;
+ const page = chrome.page orelse bg_default;
+ for (regions) |tag| {
+ const pane = tag.kind == .tag;
+ if (!pane and tag.kind != .column_tag and tag.kind != .workspace_tag) continue;
+ if (tag.rect.w == 0 or tag.rect.h == 0) continue;
+ if (pane and groupOf(groups, tag.serial) != index) continue;
+ if (!pane and index != 0) continue;
+ const plaque = plaqueBox(g, chrome, regions, tag, layout, win_w, win_h) orelse continue;
+ const b = plaque.border;
+ const sh = plaque.shadow;
+ const fill = if (tag.active) chrome.tag_focus_bg else chrome.tag_bg;
+ // The band's own ground, then the shadow, the frame and the fill.
+ try addDecor(g, gpa, plaque.band[0], plaque.band[1], plaque.band[2], plaque.band[3], page, 1, track, win_w, win_h);
+ const shadow_rgb = chrome.decor_box_shadow orelse border_rgb;
+ try addDecor(g, gpa, plaque.box[0] + sh[0], plaque.box[1] + sh[1], plaque.box[2] + sh[0], plaque.box[3] + sh[1], if (pane) shadow_rgb else mixRgb(shadow_rgb, page), 1, track, win_w, win_h);
+ try addDecor(g, gpa, plaque.box[0], plaque.box[1], plaque.box[2], plaque.box[3], border_rgb, 1, track, win_w, win_h);
+ const inner: [4]f32 = .{ plaque.box[0] + b, plaque.box[1] + b, plaque.box[2] - b, plaque.box[3] - b };
+ if (pane and tag.active and chrome.decor_box_stripe != null)
+ try addPattern(g, gpa, inner[0], inner[1], inner[2], inner[3], chrome.decor_box_stripe.?, fill, dp(@floatFromInt(@max(1, chrome.decor_box_stripe_px))), true, track, win_w, win_h)
+ else
+ try addDecor(g, gpa, inner[0], inner[1], inner[2], inner[3], fill, 1, track, win_w, win_h);
+ }
+}
+
+const Plaque = struct { band: [4]f32, box: [4]f32, border: f32, shadow: [2]f32 };
+
+/// A tag's plaque in window pixels: its band (the rows, from after the
+/// grip to the end), the framed box round the tagline's text, and the
+/// frame's and the shadow's widths, the shadow cut to what the band's slack
+/// below the text and its right end leave. Null where there is no room.
+fn plaqueBox(g: *const Gui, chrome: *const pardes.Chrome, regions: []const pardes.Region, tag: pardes.Region, layout: CellLayout, win_w: f32, win_h: f32) ?Plaque {
+ const pane = tag.kind == .tag;
+ const first = tag.rect.y;
+ const last = tag.rect.y + tag.rect.h - 1;
+ // From its start, right of its grip: the gap cell before its text is
+ // the plaque's margin, so its frame is never under the first letter.
+ _ = regions;
+ const x0 = @as(f32, @floatFromInt(tag.rect.x)) * layout.w;
+ const x1_cells = @as(f32, @floatFromInt(tag.rect.x + tag.rect.w)) * layout.w;
+ const x1 = if (win_w - x1_cells < layout.w) @max(x1_cells, win_w) else x1_cells;
+ const row_top = @as(f32, @floatFromInt(first)) * layout.h;
+ const row_bottom = @as(f32, @floatFromInt(last + 1)) * layout.h;
+ const text_top = row_top + @as(f32, @floatFromInt(taglineBandOffset(first, win_h, g.cell_h, g.tagline_height)));
+ const text_bottom = @as(f32, @floatFromInt(last)) * layout.h + @as(f32, @floatFromInt(taglineBandOffset(last, win_h, g.cell_h, g.tagline_height) + g.tagline_height));
+ const border = @min(if (pane) dp(@floatFromInt(@max(1, chrome.decor_box_border_px))) else dp(1), @floor(text_top - row_top));
+ if (border < 1) return null;
+ // The shadow's full offset to the right; down, what the slack under the
+ // text leaves (a pixel or three at a tagline under the body's size).
+ const want: f32 = if (pane) dp(@floatFromInt(chrome.decor_box_shadow_px)) else dp(2);
+ const drop = std.math.clamp(@floor(row_bottom - text_bottom - border), 0, want);
+ if (x1 - x0 < 4 * border + want) return null;
+ return .{
+ .band = .{ x0, row_top, x1, row_bottom },
+ .box = .{ x0, text_top - border, x1 - want, text_bottom + border },
+ .border = border,
+ .shadow = .{ want, drop },
+ };
+}
+
/// The two short bars on each row of a grip with no mode glyph there,
/// centered in its anchor. Only the panes of group `index`.
fn gripDecor(g: *Gui, gpa: std.mem.Allocator, regions: []const pardes.Region, cells: []const pardes.Cell, groups: *const Groups, index: usize, chrome: *const pardes.Chrome, layout: CellLayout, cols: u16, rows: u16, win_w: f32, win_h: f32, track: ?pardes.animation.Track) !void {
@@ -8903,7 +9225,8 @@ fn uploadOverlayGpu(g: *Gui, cmd: *c.SDL_GPUCommandBuffer, vertex_count: u32) bo
}
fn pushLayerOpacity(cmd: *c.SDL_GPUCommandBuffer, pass: ?*c.SDL_GPURenderPass, opacity: f32) void {
- const uniforms = [4]f32{ opacity, 0, 0, 0 };
+ var uniforms = [4]f32{ opacity, 0, 0, 0 } ++ @as([12]f32, @splat(0));
+ uniforms[4..].* = cell_decor.uniforms;
c.SDL_PushGPUFragmentUniformData(cmd, 0, &uniforms, @sizeOf(@TypeOf(uniforms)));
// The source alpha is geometric/image coverage, not window opacity. Its
// alpha contribution is multiplied by this constant separately, so a
diff --git a/src/surface.zig b/src/surface.zig
index 523cba43..5b850a8b 100644
--- a/src/surface.zig
+++ b/src/surface.zig
@@ -428,6 +428,26 @@ pub const Chrome = struct {
/// The scroll column's width, button and scrollbar alike, in pixels at
/// a 17px tagline (acme's 12 at its font); a pixel shell scales it.
rail_px: u8 = 12,
+ /// The theme's ornament (colors.Decor), a pixel shell's alone: the dot
+ /// grid on the page, the scroll track's checker, the tags' plaques
+ /// (frame, shadow, the focused one's stripes) and the file name's shadow, each null when the theme has none, sizes
+ /// in logical pixels. `name_ink` and `active_name_ink` are the file
+ /// name's colours in a tag, which the shadow goes under.
+ decor_dots: ?[3]u8 = null,
+ decor_dot_alpha: u8 = 0,
+ decor_dot_px: u8 = 22,
+ decor_checker: ?[3]u8 = null,
+ decor_checker_px: u8 = 3,
+ decor_box_border: ?[3]u8 = null,
+ decor_box_border_px: u8 = 2,
+ decor_box_shadow: ?[3]u8 = null,
+ decor_box_shadow_px: u8 = 5,
+ decor_box_stripe: ?[3]u8 = null,
+ decor_box_stripe_px: u8 = 2,
+ decor_title: ?[3]u8 = null,
+ decor_title_px: u8 = 2,
+ name_ink: [3]u8 = .{ 0, 0, 0 },
+ active_name_ink: [3]u8 = .{ 0, 0, 0 },
};
/// The canonical interface: what a frame of pardes IS. The tty shell writes
diff --git a/src/themes/lapis.zig b/src/themes/lapis.zig
new file mode 100644
index 00000000..46623fc2
--- /dev/null
+++ b/src/themes/lapis.zig
@@ -0,0 +1,92 @@
+//! Lapis: 0x4200.cafe's illuminated manuscript (web/theme/lapis.css) as a
+//! pardes theme. Ultramarine vellum, gold leaf and vermilion rubrics: the
+//! page is lapis-deep, the tags lapis with their names in gold, the text
+//! vellum. The site's ornament comes with it as the theme's `decor`, drawn
+//! inside the existing chrome (docs/render-pipeline.md §10, decision 1):
+//! a gold dot grid on the page under the pane bodies, a gold and lapis
+//! checker in the scroll column, every tag a gold-framed plaque with the
+//! trail's hard vermilion shadow (inside its own band: in a tiled layout a
+//! shadow past it would fall on the next pane or its grip, and no ornament
+//! may cover a focus indicator), the focused one striped like the code
+//! window's title bar, and the file name's vermilion offset shadow.
+pub const theme = .{
+ .name = "lapis",
+ // --lapis-deep, --vellum
+ .bg = .{ 0x0a, 0x10, 0x30 },
+ .fg = .{ 0xee, 0xe6, 0xd2 },
+ // A gold highlight in lapis ink, as the directory's current entry.
+ .sel_bg = .{ 0xe8, 0xc4, 0x6a },
+ .sel_fg = .{ 0x0f, 0x1a, 0x4a },
+ // --lapis, --vellum-dim
+ .tag_bg = .{ 0x0f, 0x1a, 0x4a },
+ .tag_fg = .{ 0x9a, 0xa6, 0xd9 },
+ .tag_name_fg = .{ 0xe8, 0xc4, 0x6a },
+ .tag_active_name_fg = .{ 0xf2, 0xd2, 0x80 },
+ // The plaques' hover lapis, taken a step further: 1.5:1 off the tag.
+ .tag_active_bg = .{ 0x25, 0x39, 0x7d },
+ .tag_active_fg = .{ 0xee, 0xe6, 0xd2 },
+ .box = .{ 0xe8, 0xc4, 0x6a },
+ .box_dim = .{ 0x3a, 0x4a, 0x8a },
+ .box_border = .{ 0x9a, 0xa6, 0xd9 },
+ .box_dirty = .{ 0xff, 0x6a, 0x4a },
+ .column_box = .{ 0xff, 0x6a, 0x4a },
+ .column_box_dim = .{ 0x8a, 0x3a, 0x3a },
+ // Rubrics: vermilion keywords, gilded strings, the code window's green
+ // for numbers, vellum-dim comments.
+ .kw = .{ 0xff, 0x6a, 0x4a },
+ .str = .{ 0xe8, 0xc4, 0x6a },
+ .num = .{ 0xe7, 0xff, 0xe0 },
+ .comment = .{ 0x9a, 0xa6, 0xd9 },
+ .lineno = .{ 0x5a, 0x6a, 0xa8 },
+ .lineno_active = .{ 0x6a, 0x7a, 0xb8 },
+ // --lapis-ink under a gold thumb (the page's scrollbar-color).
+ .scroll_track = .{ 0x05, 0x0a, 0x24 },
+ .scroll_thumb = .{ 0xe8, 0xc4, 0x6a },
+ // Gold rules, a vermilion hairline between a tag and its text.
+ .border = .{ 0xe8, 0xc4, 0x6a },
+ // An empty column is the page, not a slab of gold leaf.
+ .empty_col = .{ 0x0a, 0x10, 0x30 },
+ .tag_rule = .{ 0xd4, 0x41, 0x2f },
+ .search_bg = .{ 0x6a, 0x2a, 0x1c },
+ .search_fg = .{ 0xff, 0xd0, 0xb0 },
+ .diagnostic_error = .{ 0xff, 0x6a, 0x4a },
+ .diagnostic_warning = .{ 0xe8, 0xc4, 0x6a },
+ .diagnostic_info = .{ 0x9a, 0xc6, 0xff },
+ .diagnostic_hint = .{ 0xa8, 0xe0, 0xc0 },
+ .decor = .{
+ .page_dots = .{ 0xe8, 0xc4, 0x6a },
+ // #e8c46a22 on the site: a dot a pixel and a half across every 22.
+ .page_dot_alpha = 0x22,
+ .page_dot_px = 22,
+ .rail_checker = .{ 0xe8, 0xc4, 0x6a },
+ .rail_checker_px = 3,
+ // The trail: a 2px gold frame and a 5px vermilion shadow; the code
+ // window's title bar: gold and lapis stripes of 2px.
+ .tag_border = .{ 0xe8, 0xc4, 0x6a },
+ .tag_border_px = 2,
+ .tag_shadow = .{ 0xd4, 0x41, 0x2f },
+ .tag_shadow_px = 5,
+ .tag_stripe = .{ 0xe8, 0xc4, 0x6a },
+ .tag_stripe_px = 2,
+ .title_shadow = .{ 0xd4, 0x41, 0x2f },
+ .title_shadow_px = 2,
+ },
+ .palette = .{
+ .{ 0x0a, 0x10, 0x30 },
+ .{ 0xff, 0x6a, 0x4a },
+ .{ 0xa8, 0xe0, 0xc0 },
+ .{ 0xe8, 0xc4, 0x6a },
+ .{ 0x9a, 0xc6, 0xff },
+ .{ 0xd8, 0xa0, 0xe8 },
+ .{ 0x8a, 0xd8, 0xd8 },
+ .{ 0xee, 0xe6, 0xd2 },
+ .{ 0x9a, 0xa6, 0xd9 },
+ .{ 0xff, 0x9a, 0x6a },
+ .{ 0xc8, 0xf0, 0xd8 },
+ .{ 0xff, 0xde, 0x90 },
+ .{ 0xc0, 0xdc, 0xff },
+ .{ 0xe8, 0xc0, 0xf4 },
+ .{ 0xb0, 0xec, 0xec },
+ .{ 0xff, 0xf8, 0xe8 },
+ },
+};
diff --git a/test/snapshots/theme.golden b/test/snapshots/theme.golden
index 4acf715d..a6bd6e60 100644
--- a/test/snapshots/theme.golden
+++ b/test/snapshots/theme.golden
@@ -645,34 +645,34 @@
|30: 0-1 d,#ffffea, 2-49 #000000,#ffffea, 50-51 d,#ffffea, 52-99 #000000,#ffffea,
== snap generated grid=100x31 cursor=4,6
== style generated grid=100x31
-|0: 0-50 #e0e0e0,#1e262e, 51-59 #e0e0e0,#303840, 60-70 #e0e0e0,#1e262e, 71-99 d,#1e262e,
-|1: 0-1 #ffffff,#4f7b96, 2-2 d,#1e262e, 3-34 #e0e0e0,#1e262e, 35-49 d,#1e262e, 50-51 #ffffff,#4f7b96, 52-52 d,#1e262e, 53-84 #e0e0e0,#1e262e, 85-99 d,#1e262e,
-|2: 0-1 #000000,#ffa060, 2-2 d,#1e262e, 3-49 #e0e0e0,#1e262e, 50-51 #ffffff,#303840, 52-52 d,#1e262e, 53-99 #e0e0e0,#1e262e,
-|3: 0-2 d,#1e262e, 3-20 #e0e0e0,#1e262e, 21-52 d,#1e262e, 53-70 #e0e0e0,#1e262e, 71-99 d,#1e262e,
-|4: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|5: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|6: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-99 d,#161e26,
-|7: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|8: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|9: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|10: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|11: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|12: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|13: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|14: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|15: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|16: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|17: 0-1 #ffffff,#303840, 2-2 d,#1e262e, 3-49 #e0e0e0,#1e262e, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|18: 0-2 d,#1e262e, 3-20 #e0e0e0,#1e262e, 21-49 d,#1e262e, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|19: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|20: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|21: 0-0 d,#303840, 1-49 d,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|22: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|23: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|24: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|25: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|26: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|27: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|28: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|29: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
-|30: 0-0 d,#303840, 1-1 d,#161e26, 2-49 #e0e0e0,#161e26, 50-50 d,#303840, 51-51 d,#161e26, 52-99 #e0e0e0,#161e26,
+|0: 0-50 #9aa6d9,#0f1a4a, 51-59 #0f1a4a,#e8c46a, 60-70 #9aa6d9,#0f1a4a, 71-99 d,#0f1a4a,
+|1: 0-1 #ffffff,#8a3a3a, 2-2 d,#25397d, 3-34 #eee6d2,#25397d, 35-49 d,#25397d, 50-51 #ffffff,#8a3a3a, 52-52 d,#0f1a4a, 53-84 #9aa6d9,#0f1a4a, 85-99 d,#0f1a4a,
+|2: 0-1 #000000,#e8c46a, 2-2 d,#25397d, 3-29 #eee6d2,#25397d, 30-37 #f2d280,#25397d, 38-49 #eee6d2,#25397d, 50-51 #ffffff,#6a78b1, 52-52 d,#0f1a4a, 53-79 #9aa6d9,#0f1a4a, 80-87 #e8c46a,#0f1a4a, 88-99 #9aa6d9,#0f1a4a,
+|3: 0-2 d,#25397d, 3-20 #eee6d2,#25397d, 21-49 d,#25397d, 50-52 d,#0f1a4a, 53-70 #9aa6d9,#0f1a4a, 71-99 d,#0f1a4a,
+|4: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|5: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|6: 0-0 d,#e8c46a, 1-49 d,#0a1030, 50-50 d,#e8c46a, 51-99 d,#0a1030,
+|7: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|8: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|9: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|10: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|11: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|12: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|13: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|14: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|15: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|16: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|17: 0-1 #ffffff,#6a78b1, 2-2 d,#0f1a4a, 3-29 #9aa6d9,#0f1a4a, 30-37 #e8c46a,#0f1a4a, 38-49 #9aa6d9,#0f1a4a, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|18: 0-2 d,#0f1a4a, 3-20 #9aa6d9,#0f1a4a, 21-49 d,#0f1a4a, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|19: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|20: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|21: 0-0 d,#e8c46a, 1-49 d,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|22: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|23: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|24: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|25: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|26: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|27: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|28: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|29: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
+|30: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-49 #eee6d2,#0a1030, 50-50 d,#e8c46a, 51-51 d,#0a1030, 52-99 #eee6d2,#0a1030,
diff --git a/test/snapshots/themesel.golden b/test/snapshots/themesel.golden
index 455808ab..8d453978 100644
--- a/test/snapshots/themesel.golden
+++ b/test/snapshots/themesel.golden
@@ -529,7 +529,7 @@
|27: 19 Theme helix
|28: 20 Theme dark
|29: 21 Theme acme
-|30: 22 Theme acid
+|30: 22 Theme lapis
== style step-helix grid=100x31
|0: 0-70 #949494,#121212, 71-99 d,#121212,
|1: 0-1 #ffffff,#826f46, 2-2 d,#3a3a3a, 3-34 #bdbdbd,#3a3a3a, 35-99 d,#3a3a3a,
@@ -561,7 +561,7 @@
|27: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808,b 7-17 #bdbdbd,#080808, 18-99 d,#080808,
|28: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-16 #bdbdbd,#080808, 17-99 d,#080808,
|29: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-16 #bdbdbd,#080808, 17-99 d,#080808,
-|30: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-16 #bdbdbd,#080808, 17-99 d,#080808,
+|30: 0-0 d,#121212, 1-1 d,#080808, 2-6 #626262,#080808, 7-17 #bdbdbd,#080808, 18-99 d,#080808,
== snap step-dark grid=100x31 cursor=7,27
|17: 10 Theme solarium Theme
|18: 11 Theme spectrum
@@ -575,8 +575,8 @@
|26: 19 Theme helix
|27: 20 Theme dark
|28: 21 Theme acme
-|29: 22 Theme acid
-|30: 23 Theme acme_helix
+|29: 22 Theme lapis
+|30: 23 Theme acid
== style step-dark grid=100x31
|0: 0-70 #969696,#2c2a3e, 71-99 d,#2c2a3e,
|1: 0-1 #ffffff,#867a4e, 2-2 d,#484566, 3-34 #c8c8c8,#484566, 35-99 d,#484566,
@@ -621,9 +621,9 @@
|25: 19 Theme helix
|26: 20 Theme dark
|27: 21 Theme acme
-|28: 22 Theme acid
-|29: 23 Theme acme_helix
-|30: 24 Theme adwaita_dark
+|28: 22 Theme lapis
+|29: 23 Theme acid
+|30: 24 Theme acme_helix
== style step-acme grid=100x31
|0: 0-70 #000000,#eaffff, 71-99 d,#eaffff,
|1: 0-1 #000000,#8888cc, 2-2 d,#eaffff, 3-34 #000000,#eaffff, 35-99 d,#eaffff,
@@ -653,9 +653,9 @@
|25: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-17 #000000,#ffffea, 18-99 d,#ffffea,
|26: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea,
|27: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea,b 7-16 #000000,#ffffea, 17-99 d,#ffffea,
-|28: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea,
-|29: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-22 #000000,#ffffea, 23-99 d,#ffffea,
-|30: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-24 #000000,#ffffea, 25-99 d,#ffffea,
+|28: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-17 #000000,#ffffea, 18-99 d,#ffffea,
+|29: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea,
+|30: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-22 #000000,#ffffea, 23-99 d,#ffffea,
== snap step-generated grid=100x31 cursor=7,27
|17: 12 Theme harvest Theme
|18: 13 Theme clay
@@ -667,42 +667,42 @@
|24: 19 Theme helix
|25: 20 Theme dark
|26: 21 Theme acme
-|27: 22 Theme acid
-|28: 23 Theme acme_helix
-|29: 24 Theme adwaita_dark
-|30: 25 Theme adwaita_light
+|27: 22 Theme lapis
+|28: 23 Theme acid
+|29: 24 Theme acme_helix
+|30: 25 Theme adwaita_dark
== style step-generated grid=100x31
-|0: 0-70 #e0e0e0,#1e262e, 71-99 d,#1e262e,
-|1: 0-1 #ffffff,#4f7b96, 2-2 d,#1e262e, 3-34 #e0e0e0,#1e262e, 35-99 d,#1e262e,
-|2: 0-1 #ffffff,#303840, 2-2 d,#1e262e, 3-59 #e0e0e0,#1e262e, 60-99 d,#1e262e,
-|3: 0-0 d,#303840, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-12 #e0e0e0,#161e26, 13-99 d,#161e26,
-|4: 0-0 d,#303840, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-12 #e0e0e0,#161e26, 13-99 d,#161e26,
-|5: 0-0 d,#303840, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-12 #e0e0e0,#161e26, 13-99 d,#161e26,
-|6: 0-0 d,#303840, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-12 #e0e0e0,#161e26, 13-99 d,#161e26,
-|7: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-12 #e0e0e0,#161e26, 13-99 d,#161e26,
-|8: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-12 #e0e0e0,#161e26, 13-99 d,#161e26,
-|9: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-12 #e0e0e0,#161e26, 13-99 d,#161e26,
-|10: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-12 #e0e0e0,#161e26, 13-99 d,#161e26,
-|11: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-12 #e0e0e0,#161e26, 13-99 d,#161e26,
-|12: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-13 #e0e0e0,#161e26, 14-99 d,#161e26,
-|13: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-13 #e0e0e0,#161e26, 14-99 d,#161e26,
-|14: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-13 #e0e0e0,#161e26, 14-99 d,#161e26,
-|15: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-13 #e0e0e0,#161e26, 14-99 d,#161e26,
-|16: 0-1 #000000,#ffa060, 2-2 d,#1e262e, 3-61 #e0e0e0,#1e262e, 62-99 d,#1e262e,
-|17: 0-0 d,#303840, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-19 #e0e0e0,#161e26, 20-92 d,#161e26, 93-93 d,#1e262e, 94-98 #e0e0e0,#1e262e, 99-99 d,#1e262e,
-|18: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-16 #e0e0e0,#161e26, 17-99 d,#161e26,
-|19: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-23 #e0e0e0,#161e26, 24-99 d,#161e26,
-|20: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-22 #e0e0e0,#161e26, 23-99 d,#161e26,
-|21: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-25 #e0e0e0,#161e26, 26-99 d,#161e26,
-|22: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-99 d,#161e26,
-|23: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-34 #e0e0e0,#161e26, 35-99 d,#161e26,
-|24: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-17 #e0e0e0,#161e26, 18-99 d,#161e26,
-|25: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-16 #e0e0e0,#161e26, 17-99 d,#161e26,
-|26: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-16 #e0e0e0,#161e26, 17-99 d,#161e26,
-|27: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26,b 7-16 #e0e0e0,#161e26, 17-99 d,#161e26,
-|28: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-22 #e0e0e0,#161e26, 23-99 d,#161e26,
-|29: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-24 #e0e0e0,#161e26, 25-99 d,#161e26,
-|30: 0-0 d,#1e262e, 1-1 d,#161e26, 2-6 #303840,#161e26, 7-25 #e0e0e0,#161e26, 26-99 d,#161e26,
+|0: 0-70 #9aa6d9,#0f1a4a, 71-99 d,#0f1a4a,
+|1: 0-1 #ffffff,#8a3a3a, 2-2 d,#25397d, 3-34 #eee6d2,#25397d, 35-99 d,#25397d,
+|2: 0-1 #ffffff,#6a78b1, 2-2 d,#0f1a4a, 3-32 #9aa6d9,#0f1a4a, 33-37 #e8c46a,#0f1a4a, 38-59 #9aa6d9,#0f1a4a, 60-99 d,#0f1a4a,
+|3: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-12 #eee6d2,#0a1030, 13-99 d,#0a1030,
+|4: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-12 #eee6d2,#0a1030, 13-99 d,#0a1030,
+|5: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-12 #eee6d2,#0a1030, 13-99 d,#0a1030,
+|6: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-12 #eee6d2,#0a1030, 13-99 d,#0a1030,
+|7: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-12 #eee6d2,#0a1030, 13-99 d,#0a1030,
+|8: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-12 #eee6d2,#0a1030, 13-99 d,#0a1030,
+|9: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-12 #eee6d2,#0a1030, 13-99 d,#0a1030,
+|10: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-12 #eee6d2,#0a1030, 13-99 d,#0a1030,
+|11: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-12 #eee6d2,#0a1030, 13-99 d,#0a1030,
+|12: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-13 #eee6d2,#0a1030, 14-99 d,#0a1030,
+|13: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-13 #eee6d2,#0a1030, 14-99 d,#0a1030,
+|14: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-13 #eee6d2,#0a1030, 14-99 d,#0a1030,
+|15: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-13 #eee6d2,#0a1030, 14-99 d,#0a1030,
+|16: 0-1 #000000,#e8c46a, 2-2 d,#25397d, 3-32 #eee6d2,#25397d, 33-39 #f2d280,#25397d, 40-61 #eee6d2,#25397d, 62-99 d,#25397d,
+|17: 0-0 d,#e8c46a, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-19 #eee6d2,#0a1030, 20-92 d,#0a1030, 93-93 d,#0f1a4a, 94-98 #9aa6d9,#0f1a4a, 99-99 d,#0f1a4a,
+|18: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-16 #eee6d2,#0a1030, 17-99 d,#0a1030,
+|19: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-23 #eee6d2,#0a1030, 24-99 d,#0a1030,
+|20: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-22 #eee6d2,#0a1030, 23-99 d,#0a1030,
+|21: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-25 #eee6d2,#0a1030, 26-99 d,#0a1030,
+|22: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-99 d,#0a1030,
+|23: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-34 #eee6d2,#0a1030, 35-99 d,#0a1030,
+|24: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-17 #eee6d2,#0a1030, 18-99 d,#0a1030,
+|25: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-16 #eee6d2,#0a1030, 17-99 d,#0a1030,
+|26: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-16 #eee6d2,#0a1030, 17-99 d,#0a1030,
+|27: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #6a7ab8,#0a1030,b 7-17 #eee6d2,#0a1030, 18-99 d,#0a1030,
+|28: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-16 #eee6d2,#0a1030, 17-99 d,#0a1030,
+|29: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-22 #eee6d2,#0a1030, 23-99 d,#0a1030,
+|30: 0-0 d,#050a24, 1-1 d,#0a1030, 2-6 #5a6aa8,#0a1030, 7-24 #eee6d2,#0a1030, 25-99 d,#0a1030,
== snap step-back grid=100x31 cursor=7,26
== style step-back grid=100x31
|0: 0-70 #000000,#eaffff, 71-99 d,#eaffff,
@@ -732,25 +732,25 @@
|24: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-17 #000000,#ffffea, 18-99 d,#ffffea,
|25: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea,
|26: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea,b 7-16 #000000,#ffffea, 17-99 d,#ffffea,
-|27: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea,
-|28: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-22 #000000,#ffffea, 23-99 d,#ffffea,
-|29: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-24 #000000,#ffffea, 25-99 d,#ffffea,
-|30: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-25 #000000,#ffffea, 26-99 d,#ffffea,
+|27: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-17 #000000,#ffffea, 18-99 d,#ffffea,
+|28: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-16 #000000,#ffffea, 17-99 d,#ffffea,
+|29: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-22 #000000,#ffffea, 23-99 d,#ffffea,
+|30: 0-0 d,#99994c, 1-1 d,#ffffea, 2-6 #999980,#ffffea, 7-24 #000000,#ffffea, 25-99 d,#ffffea,
== snap tail grid=100x31 cursor=7,29
-|17: 234 Theme wolf_alabaster_dark
-|18: 235 Theme wolf_alabaster_dark_bg
-|19: 236 Theme wolf_alabaster_dark_mono
-|20: 237 Theme wolf_alabaster_light
-|21: 238 Theme wolf_alabaster_light_bg
-|22: 239 Theme wolf_alabaster_light_mono
-|23: 240 Theme yellowed
-|24: 241 Theme yo
-|25: 242 Theme yo_berry
-|26: 243 Theme yo_light
-|27: 244 Theme zed_onedark
-|28: 245 Theme zed_onelight
-|29: 246 Theme zenburn
-|30: 247
+|17: 235 Theme wolf_alabaster_dark
+|18: 236 Theme wolf_alabaster_dark_bg
+|19: 237 Theme wolf_alabaster_dark_mono
+|20: 238 Theme wolf_alabaster_light
+|21: 239 Theme wolf_alabaster_light_bg
+|22: 240 Theme wolf_alabaster_light_mono
+|23: 241 Theme yellowed
+|24: 242 Theme yo
+|25: 243 Theme yo_berry
+|26: 244 Theme yo_light
+|27: 245 Theme zed_onedark
+|28: 246 Theme zed_onelight
+|29: 247 Theme zenburn
+|30: 248
== snap picked grid=100x31 cursor=7,3
|16: 14 line 14
|17: 15 line 15