git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commita530574f7fa1f0938049db92053a17df27756acb
parent750bea84a9
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-24 22:22
fix(focus): the ring tints the plate's light and shadow instead of replacing them

A tinted carve (Trough, Boss, Recess) dropped its relief and drew only the
wrapped accent glint, so a focused flush plate lost its light and shadow.
Both tinted branches now keep the neutral relief term for term and recolour
it: the light composites in the accent instead of white, the shadow in a
dark accent (FOCUS_SHADOW) instead of black, both at FOCUS_GAIN. A faced
Bevel does the same on its own roll, with its carves' neutral shading added
back on top. A fill-less tinted plate (the network cursor) keeps the wrap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 CLAUDE.md                    |   6 ++-
 src/backend/window_runner.rs |  13 +++---
 src/scene/paint.rs           |  19 ++++----
 src/vk/shader2d.wgsl         | 107 ++++++++++++++++++++++---------------------
 4 files changed, 77 insertions(+), 68 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index e7870f5..df5bc34 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -244,8 +244,12 @@ What this buys, and where the code is heading:
   through `RenderTarget::inset_plate_tinted` and `CarveKind::Boss { tint }`.
   `CCE_FOCUS_DEBUG=1` prints the stops in walk order.
   The focus ring is the plate's own silhouette: `ControlPlate::with_tint`
-  lights the rim (a tinted `Trough`, `Boss` or `Bevel`), the same treatment a
+  tints the rim (a tinted `Trough`, `Boss` or `Bevel`), the same treatment a
   well's `recess_tinted` gives its rim while editing — never extra geometry.
+  The tint recolours the relief rather than replacing it: the rim's light
+  composites in the accent instead of white and its shadow in a dark accent
+  instead of black (`FOCUS_SHADOW`, both at `FOCUS_GAIN`, in
+  `shader2d.wgsl`), so a focused plate still reads which edges face the lamp.
   A Checkbox lights the ring its mark already draws; a Toggle lights the
   rim of the plate that slides in its well.
   Roles today: Button, Checkbox, Toggle, Dropdown, FontSelector, ButtonStrip
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 03346e7..affe2b7 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1999,10 +1999,10 @@ pub fn tessellate_display_list(
                 p.host[2] = fz;
                 p.host[3] = fw;
                 // w = 1 marks an accent-tinted plate (the focused-pane
-                // treatment): the shader then colors the WHOLE rolled edge
-                // with the tint, not just the specular glint — matching the
-                // free-carve path's tinted-well convention. Neutral white
-                // keeps w = 0 (spec-only, a no-op multiply).
+                // treatment): the shader keeps the roll's light and shadow
+                // and recolours them — light toward the tint, shadow toward
+                // a dark tint — matching the free-carve path's tinted-well
+                // convention. Neutral white keeps w = 0 (a no-op multiply).
                 let full = if *tint == [1.0, 1.0, 1.0] { 0.0 } else { 1.0 };
                 p.specular_tint = [tint[0], tint[1], tint[2], full];
                 plate = Some(p);
@@ -2261,8 +2261,9 @@ pub fn tessellate_display_list(
                 let sdf_rect = crate::scene::layout::Rect { x: x0, y: y0, width: x1 - x0, height: y1 - y0 };
                 let mut p = plate_push_raised(&sdf_rect, *radii, *depth, scale, plate_light, plate_mat, false, None);
                 p.mode = mode;
-                // w = 1.0 flags the free-carve shader path to mix its white
-                // highlight screen toward the tint (plates leave w at 0.0).
+                // w = 1.0 flags the free-carve shader path to composite its
+                // light in the tint and its shadow in a dark tint instead of
+                // white and black (plates leave w at 0.0).
                 if let Some(t) = tint {
                     p.specular_tint = [t[0], t[1], t[2], 1.0];
                 }
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index e412c36..3c23c61 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -240,8 +240,9 @@ pub struct ControlPlate {
     pub stance: PlateStance,
     pub face: Option<Material>,
     pub depth: f32,
-    /// The rim lit in this colour: the keyboard-focus ring, drawn on the
-    /// plate's own silhouette rather than as extra geometry. `None` unlit.
+    /// The rim's light and shadow tinted this colour: the keyboard-focus
+    /// ring, drawn on the plate's own relief rather than as extra geometry.
+    /// `None` untinted.
     pub tint: Option<[f32; 3]>,
 }
 
@@ -512,11 +513,12 @@ pub enum Prim {
     /// `push_widget_vertices`' non-bevel branch: rounded bg + `push_plate_solid_border_vertices`).
     Border { rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32 },
     /// A beveled plate: a rounded fill at full size plus a light/shadow overlay lip
-    /// (mirrors `push_widget_vertices`' bevel branch). `tint` multiplies the lit
-    /// roll's specular color — neutral white normally; a host sets it to a
+    /// (mirrors `push_widget_vertices`' bevel branch). `tint` colours the
+    /// roll's light and shadow — neutral white normally; a host sets it to a
     /// highlight color to mark the plate (the focused-pane treatment) without a
-    /// separate border ring. Shader-plates path only; the legacy banded
-    /// tessellation ignores it.
+    /// separate border ring: the light goes to the tint, the shadow to a dark
+    /// tint, so the relief still reads. Shader-plates path only; the legacy
+    /// banded tessellation ignores it.
     Bevel { rect: Rect, radii: Radii, material: Material, depth: f32, tint: [f32; 3] },
     /// A recess carved into whatever is already painted underneath — the inverse of
     /// `Bevel`. Emits ONLY the shaded edges, never a fill, so the surface below shows
@@ -531,8 +533,9 @@ pub enum Prim {
     /// `edges` is (top, right, bottom, left): which walls of the carve actually exist.
     /// A region flush with the plate's own edge is a step, not a trough — see
     /// `push_bevel_edge_vertices_banded`.
-    /// `tint` colors the wall's lit rim — the same focused-pane treatment as
-    /// [`Prim::Bevel`]'s tint, for carved wells instead of raised plates. A tinted
+    /// `tint` colours the wall's light and shadow — the same focused-pane
+    /// treatment as [`Prim::Bevel`]'s tint, for carved wells instead of raised
+    /// plates. A tinted
     /// recess never groups into a host plate's CSG features (a feature carries no
     /// color), so it always renders as the free-carve overlay. Shader-plates path
     /// only; the legacy banded tessellation ignores it.
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index e11ffb4..ca9ac2e 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -171,6 +171,14 @@ const PLATE_CREST: f32 = 0.25;
 // The far-edge shade line's strength relative to the glint (roll_shade_line):
 // 1 is the exact mirror; 0.5 keeps dark faces from bottoming out at black.
 const PLATE_SHADE_LINE: f32 = 0.5;
+// The focus treatment (p_spec_tint.w = 1) keeps the relief and recolours it:
+// the light composites in the accent instead of white, the shadow in the
+// accent darkened to this fraction instead of black — so the lit and shaded
+// sides still differ exactly as an unfocused plate's do, in the accent's hue.
+const FOCUS_SHADOW: f32 = 0.25;
+// ...and at this gain over the neutral relief: the accent at white's alpha
+// reads dimmer than white, and a focused plate should read at least as lit.
+const FOCUS_GAIN: f32 = 1.5;
 // Recess depth as a fraction of the roll width (a recess is visually shallower
 // than a raised plate's full quarter-round).
 const RECESS_DEPTH: f32 = 0.6;
@@ -673,37 +681,50 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         let diff = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * max(dot(n, l), 0.0);
         let shade = 1.0 + (diff / flat_shade - 1.0 + extra) * strength;
         // p_spec_tint.w = 1 marks an accent-tinted plate (the focused-pane
-        // treatment): the specular line WRAPS — the exact glint the
-        // light-facing edges always carry runs the whole silhouette in the
-        // accent color, same inset, width, and peak. Nothing else about the
-        // plate's shading changes (accent-wash variants were tried and read
-        // as painted frames). Neutral plates (w = 0) keep the directional
-        // glint, byte-identical.
+        // treatment). Neutral plates (w = 0) take the plain return below,
+        // byte-identical.
         let tw = rrect_clip.p_spec_tint.w;
         if (tw > 0.0) {
-            // The wrap runs on the plate's own roll ONLY (sv_rim), never on
-            // the carves: with the full slope every well carved into a
-            // focused plate — each parameter control on the designer's
-            // parameter pane — drew its own accent ring, reading as if every
-            // control were focused alongside the pane.
-            let spec = roll_spec_wrap(sv_rim);
             // A FILL-LESS tinted plate is a pure focus ring (the network
-            // cursor): the wrapped glint alone, on the plate's own roll — so
+            // cursor): there is no surface to light, so the specular line
+            // WRAPS — the glint the light-facing edges carry, run round the
+            // whole silhouette in the accent, on the plate's own roll — so
             // the line traces the same superellipse silhouette, radius
             // family, and inset as every node and pane, which a
             // boundary-straddling carve band cannot (outward offsets of an
             // Lp corner round off).
             if (abs(base.a) < 0.004) {
-                return vec4f(rrect_clip.p_spec_tint.rgb, spec * strength * aa);
+                return vec4f(rrect_clip.p_spec_tint.rgb, roll_spec_wrap(sv_rim) * strength * aa);
             }
-            // The carves keep the neutral directional glint an unfocused
-            // plate gives them (white, as p_spec_tint.rgb is for w = 0);
-            // sv_rim is zero on the face, so this is exactly their term.
-            let carve_spec = roll_spec(sv - sv_rim);
-            return vec4f(
-                base.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength) + vec3f(carve_spec * strength),
-                abs(base.a) * aa,
-            );
+            // A faced one keeps its relief and recolours it, exactly as a
+            // tinted carve does: the roll's light composites toward the
+            // accent instead of white, its shadow toward the dark accent
+            // instead of black, at FOCUS_GAIN — so the lit and shaded sides
+            // still differ as they do unfocused. The roll alone (sv_rim, and
+            // the crest, which lives on it): with the full slope every well
+            // carved into a focused plate — each parameter control on the
+            // designer's parameter pane — wore the accent too, reading as if
+            // every control were focused alongside the pane. The carves'
+            // neutral shading is added back on top unchanged.
+            let tint = rrect_clip.p_spec_tint.rgb;
+            let n_r = normalize(vec3f(sv_rim, 1.0));
+            let diff_r = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * max(dot(n_r, l), 0.0);
+            let crest = PLATE_CREST * f * f * f * crest_weight(gd.xy);
+            let shade_r = 1.0 + (diff_r / flat_shade - 1.0 + crest) * strength;
+            let rim = base.rgb * shade_r + vec3f((roll_spec(sv_rim) - roll_shade_line(sv_rim)) * strength);
+            let full = base.rgb * shade + vec3f((roll_spec(sv) - roll_shade_line(sv)) * strength);
+            // The alpha a carve would composite white (or black) at for the
+            // same change in luminance, then that alpha in the accent.
+            let W = vec3f(0.2126, 0.7152, 0.0722);
+            let bl = dot(base.rgb, W);
+            let dl = dot(rim, W) - bl;
+            var lit = base.rgb;
+            if (dl >= 0.0) {
+                lit = mix(base.rgb, tint, min(dl / max(1.0 - bl, 0.05) * FOCUS_GAIN, 1.0));
+            } else {
+                lit = mix(base.rgb, tint * FOCUS_SHADOW, min(-dl / max(bl, 0.004) * FOCUS_GAIN, 1.0));
+            }
+            return vec4f(lit + (full - rim), abs(base.a) * aa);
         }
         let spec = roll_spec(sv);
         let dark = roll_shade_line(sv);
@@ -903,41 +924,21 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
     let hb = rrect_clip.p_host;
     let host_d = min(hb.z - abs(frag.x - hb.x), hb.w - abs(frag.y - hb.y));
     let att = clamp(host_d / t, 0.0, 1.0) * wedge;
-    var v = (diff / flat_shade - 1.0 + curv + spec) * strength * att;
-    // p_spec_tint.w = 1 marks a tinted carve — the FOCUS treatment. It
-    // renders as the wrapped specular line alone (roll_spec_wrap: the glint
-    // the light-facing edges normally carry, swept around the whole
-    // outline), matching the focused plates' accent glint exactly; the
-    // relief's diffuse/curvature terms drop so a standalone focus ring reads
-    // as the line, not a lit step. Plates leave w at 0.
+    let v = (diff / flat_shade - 1.0 + curv + spec) * strength * att;
+    // p_spec_tint.w = 1 marks a tinted carve — the FOCUS treatment. The
+    // relief is the unfocused carve's, term for term; only its colours
+    // change: the light composites in the accent instead of white and the
+    // shadow in a dark accent instead of black (FOCUS_SHADOW), both at
+    // FOCUS_GAIN. So the ring is the carve's own light and shadow, still
+    // reading which walls face the lamp. Plates leave w at 0.
     let tw = rrect_clip.p_spec_tint.w;
-    if (tw > 0.0) {
-        // The PLATE's monotonic roll profile, not the carve wall's: a wall's
-        // slope is a bell (rises then falls), so its tilt crosses the glint
-        // angle twice and drew two concentric lines. With roll_slope the ring
-        // is exactly a plate silhouette's glint — one line, same position.
-        let fr = clamp(1.0 - u, 0.0, 1.0);
-        // ...and ENDS at that silhouette (the rect outset by t/2), 1px
-        // anti-aliased like a plate's own. Past it `u` saturates at 0 and
-        // roll_slope(1) is the profile's steepest point, so every pixel of
-        // the cover quad outside the ring drew the full glint — a flat
-        // tinted block, square-cornered (the quad's own shape), around the
-        // rounded ring.
-        let sil = clamp(fd + 0.5 * t + 0.5, 0.0, 1.0);
-        v = roll_spec_wrap(fgd * roll_slope(fr)) * strength * att * sil;
-    }
+    let gain = mix(1.0, FOCUS_GAIN, tw);
     if (v >= 0.0) {
-        // Highlight: the white screen mixes toward the tint color, slightly
-        // boosted so the accent reads at the rim's low alphas.
         let hl = mix(vec3f(1.0), rrect_clip.p_spec_tint.rgb, tw);
-        return vec4f(hl, min(v * (1.0 + 0.5 * tw), 1.0));
+        return vec4f(hl, min(v * gain, 1.0));
     }
-    // Shadow: the complementary counter-tint (warm against a cool accent),
-    // kept dark (~22%) so it still reads as shadow with a hue cast, not a
-    // second glow — the painter's warm-light/cool-shadow trick. Untinted
-    // carves stay black.
-    let sh = (vec3f(1.0) - rrect_clip.p_spec_tint.rgb) * 0.22 * tw;
-    return vec4f(sh, min(-v * (1.0 + 0.5 * tw), 1.0));
+    let sh = rrect_clip.p_spec_tint.rgb * (FOCUS_SHADOW * tw);
+    return vec4f(sh, min(-v * gain, 1.0));
 }
 
 struct VertexOutput {