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

commit69013f08779afaadec28ab093bb97db227bec91f
parent0a2f362012
authorLucas Galante <[email protected]>
date2026-07-28 23:25
feat: focus lights the TreeList recess rim instead of washing the tree

The focused tree used to opt into the legacy WidgetHost highlight overlay,
which screened the whole widget with highlight_primary at 12% — the teal
wash. Focus now tints the lit rim of the tree's recessed well with the
highlight accent instead, keeping the rows' own colors untouched.

- Prim::Recess gains an optional tint (PaintCtx::recess_tinted), the
  Bevel-tint convention extended to carved wells. A tinted recess never
  groups into a host plate's CSG features (features carry no color), so
  it always renders as the free-carve overlay with the tint in
  specular_tint, w = 1.0 as the flag.
- shader2d free-carve highlight branch mixes its white screen toward
  p_spec_tint.rgb when flagged, with a small alpha boost so the accent
  reads at the rim's low alphas; shadows stay black.
- TreeList mirrors ctx focus in a field via FocusIn/FocusOut (paint has
  no UiContext) and drops the legacy_focus_highlight opt-in.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/backend/window_runner.rs     | 17 ++++++++++++++---
 src/scene/paint.rs               | 16 ++++++++++++++--
 src/vk/shader2d.wgsl             |  7 ++++++-
 src/widget/container/treelist.rs | 34 ++++++++++++++++++++++------------
 src/widget/model.rs              |  5 ++++-
 5 files changed, 60 insertions(+), 19 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 3a59679..98af890 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1546,11 +1546,15 @@ pub fn tessellate_display_list(
                 plate = Some(plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, true));
                 made_plate = Some(*rect);
             }
-            Prim::Recess { rect, radii, depth, edges }
+            Prim::Recess { rect, radii, depth, edges, .. }
             | Prim::Boss { rect, radii, depth, edges }
             | Prim::Ridge { rect, radii, depth, edges }
                 if shader_plates =>
             {
+                let tint = match &item.prim {
+                    Prim::Recess { tint, .. } => *tint,
+                    _ => None,
+                };
                 // Recess carves down into the surface; Boss raises a plateau out
                 // of it (same machinery, depth sign flipped); Ridge is a raised
                 // rim straddling the boundary (its own overlay profile — never
@@ -1572,8 +1576,10 @@ pub fn tessellate_display_list(
                 // the plate's whole-surface draw does not have, so grouped it
                 // smears the extended walls across the plate. Union pieces
                 // (section wells, rocker halves) are exactly these.
+                // A tinted carve also never groups: a CSG feature is geometry only,
+                // so the tint could only land on the whole plate's specular.
                 let full_ring = *edges == (true, true, true, true);
-                if let Some((bi, prect)) = last_plate.filter(|_| mode < 3.5 && full_ring) {
+                if let Some((bi, prect)) = last_plate.filter(|_| mode < 3.5 && full_ring && tint.is_none()) {
                     let inside = rect.x >= prect.x - 0.5
                         && rect.y >= prect.y - 0.5
                         && rect.x + rect.width <= prect.x + prect.width + 0.5
@@ -1643,6 +1649,11 @@ 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);
                 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).
+                if let Some(t) = tint {
+                    p.specular_tint = [t[0], t[1], t[2], 1.0];
+                }
                 // Host-plate box for the roll fade: a suppressed wall means the
                 // recess runs flush to the host's edge there, so that side of
                 // the box sits at the original rect edge; enabled walls face
@@ -1693,7 +1704,7 @@ pub fn tessellate_display_list(
                     sw, sh, *color, no, 1.0, &mut verts,
                 );
             }
-            Prim::Recess { rect, radii, depth, edges } => {
+            Prim::Recess { rect, radii, depth, edges, .. } => {
                 // Edges only — no fill: the shading is an overlay, so whatever is painted
                 // below (fill, rim gradient, blur) shows through the carve modulated
                 // rather than repainted. `light_sign = -1.0` shadows the lit-facing edges,
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 35b5a2b..f0b0ee4 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -60,7 +60,12 @@ 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`.
-    Recess { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool) },
+    /// `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
+    /// 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.
+    Recess { rect: Rect, radii: Radii, depth: f32, edges: (bool, bool, bool, bool), tint: Option<[f32; 3]> },
     /// The inverse of [`Prim::Recess`]: a plateau RAISED out of the surface below.
     /// Like `Recess` it emits only the shaded edges, never a fill — the face is the
     /// untouched surface underneath — so a region outlined by raised rolled bumps
@@ -405,6 +410,13 @@ impl PaintCtx {
         self.recess_edges(rect, radii, depth, (true, true, true, true));
     }
 
+    /// [`PaintCtx::recess`] with the lit rim tinted — see `Prim::Recess::tint`
+    /// (the focused-well treatment).
+    pub fn recess_tinted(&mut self, rect: Rect, radii: Radii, depth: f32, tint: [f32; 3]) {
+        let rect = self.apply_offset(rect);
+        self.push(Prim::Recess { rect, radii, depth, edges: (true, true, true, true), tint: Some(tint) });
+    }
+
     /// Raise a plateau out of the already-painted surface below — the inverse of
     /// [`PaintCtx::recess`]. Only the edges are shaded; the face stays the surface
     /// beneath, so the raised region inherits the backplate's color. `depth` is the
@@ -476,7 +488,7 @@ impl PaintCtx {
         edges: (bool, bool, bool, bool),
     ) {
         let rect = self.apply_offset(rect);
-        self.push(Prim::Recess { rect, radii, depth, edges });
+        self.push(Prim::Recess { rect, radii, depth, edges, tint: None });
     }
 
     /// The window's glass slab: rounded fill at full size plus a rolled, lit perimeter.
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 31e1e4d..5031ab6 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -419,7 +419,12 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
     let att = clamp(host_d / t, 0.0, 1.0) * wedge;
     let v = (diff / flat_shade - 1.0 + curv + spec) * strength * att;
     if (v >= 0.0) {
-        return vec4f(1.0, 1.0, 1.0, min(v, 1.0));
+        // p_spec_tint.w = 1 marks a tinted carve (a focused well): the white
+        // highlight screen mixes toward the tint color, slightly boosted so the
+        // accent reads at the rim's low alphas. Plates leave w at 0.
+        let tw = rrect_clip.p_spec_tint.w;
+        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(0.0, 0.0, 0.0, min(-v, 1.0));
 }
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 9cd33d6..7eaa46b 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -179,6 +179,9 @@ pub struct TreeList {
     pub right_clicked_section: Option<String>,
     pub last_scroll_y: f32,
     pub scrollbar_activity_timer: f32,
+    /// Mirrors ctx focus (FocusIn/FocusOut + the grab in `mouse_body`) so `paint` —
+    /// which has no UiContext — can light the recess rim when the tree is focused.
+    focused: bool,
     pub deleted_key_path: Option<String>,
     pub edit_box: crate::widget::Adapted<TextBox>,
     pub editing_key_idx: Option<usize>,
@@ -209,6 +212,7 @@ impl TreeList {
             right_clicked_section: None,
             last_scroll_y: 0.0,
             scrollbar_activity_timer: 0.0,
+            focused: false,
             deleted_key_path: None,
             edit_box: TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true),
             editing_key_idx: None,
@@ -701,13 +705,6 @@ impl Paint for TreeList {
         true
     }
 
-    // Legacy TreeList kept the default WidgetHost focus/hover highlight overlay (the teal
-    // wash over the focused tree, drawn by the old all_quads default) — opt back in
-    // (the 5q TextBox trap).
-    fn legacy_focus_highlight(&self) -> bool {
-        true
-    }
-
     fn prepare_text(&mut self, fs: &mut glyphon::FontSystem, _rect: Rect) {
         self.search_box.prepare_text(fs);
         self.add_key_btn.prepare_text(fs);
@@ -920,10 +917,19 @@ impl Paint for TreeList {
         }
 
         // Recessed well like a text box or list: the tree floor sits below the
-        // pane surface, its wall carved over the bg and row quads above.
+        // pane surface, its wall carved over the bg and row quads above. Focus
+        // lights the rim in the highlight accent instead of washing the tree
+        // (the retired legacy_focus_highlight overlay).
         if crate::layout::control_relief() {
             let depth = crate::layout::bevel_width().min(h * 0.2);
-            pc.recess(Rect { x, y, width: w, height: h }, (radius, radius, radius, radius), depth);
+            let well = Rect { x, y, width: w, height: h };
+            let radii = (radius, radius, radius, radius);
+            if self.focused {
+                let hc = crate::color::highlight_primary_color();
+                pc.recess_tinted(well, radii, depth, [hc[0], hc[1], hc[2]]);
+            } else {
+                pc.recess(well, radii, depth);
+            }
         }
 
         // Row/header labels with the legacy header/list viewport bounds.
@@ -1128,10 +1134,14 @@ impl Input for TreeList {
                 let Some(ui) = ectx.ui.as_deref_mut() else { return false; };
                 self.key_body(&ev, ui)
             }
+            Event::FocusIn => {
+                self.focused = true;
+                false
+            }
             Event::FocusOut => {
-        self.add_key_popover_open = false;
-        self.add_key_popover_box.unfocus();
-    
+                self.focused = false;
+                self.add_key_popover_open = false;
+                self.add_key_popover_box.unfocus();
                 false
             }
             _ => false,
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 7fd8360..6e292b1 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1235,7 +1235,10 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 Prim::RoundedRect { rect, radius, corners, color } => ctx.rounded_rect(rect, radius, corners, color),
                 Prim::Border { rect, radii, fill, border, thickness } => ctx.border(rect, radii, fill, border, thickness),
                 Prim::Bevel { rect, radii, color, depth, tint } => ctx.bevel_tinted(rect, radii, color, depth, tint),
-                Prim::Recess { rect, radii, depth, edges } => ctx.recess_edges(rect, radii, depth, edges),
+                Prim::Recess { rect, radii, depth, edges, tint } => match tint {
+                    Some(t) => ctx.recess_tinted(rect, radii, depth, t),
+                    None => ctx.recess_edges(rect, radii, depth, edges),
+                },
                 Prim::Boss { rect, radii, depth, edges } => ctx.boss_edges(rect, radii, depth, edges),
                 Prim::Ridge { rect, radii, depth, edges } => ctx.ridge_edges(rect, radii, depth, edges),
                 Prim::Plate { rect, radii, color, depth } => ctx.plate(rect, radii, color, depth),