GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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),