GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: the params pane lights the hovered control's own relief
Beside the label lift, the hovered row's carves — a text or colour
well, a spinbox's well and its -/+ run, a dropdown's seam ring, a
toggle's track and glider — go out through the tint channel the focus
treatment uses, with the lifted label's neutral colour instead of the
accent: the shader composites the rim's light in that colour at its
focus gain and the shadow in a quarter of it, so the control under the
pointer reads lit while every other carve keeps the plate's grouped
shading. A tinted carve shades through the overlay path by design, so
nothing about the pane's grouping changes; a section's well spans many
rows and never qualifies.
`PaintCtx::recess_edges_tinted` and `trough_edges_tinted` complete the
tinted emitter set beside `boss_edges_tinted`. The pane picks the carves
by the hovered row's rect in `paint_ui`, so the carve getters keep their
shapes. `a_hover_lifts_the_label_and_lights_only_its_own_carves` asserts
the emitted primitives are identical at rest and on hover except for the
tint on carves inside the hovered row. Verified in a shadow session with
the user's project: hovering a text well, a spinbox, a dropdown and a
toggle each changes only that control's ring and its label.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
src/scene/paint.rs | 27 ++++++++++++
src/widget/container/parameters_bg.rs | 82 ++++++++++++++++++++++++++++-------
2 files changed, 93 insertions(+), 16 deletions(-)
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index e1cf75f..85e620a 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -1513,6 +1513,20 @@ impl PaintCtx {
self.push(Prim::Trough { rect, radii, depth, edges: (true, true, true, true), tint: Some(tint) });
}
+ /// [`PaintCtx::trough_edges`] with a specular tint on the lit rim — see
+ /// `Prim::Trough::tint`.
+ pub fn trough_edges_tinted(
+ &mut self,
+ rect: Rect,
+ radii: Radii,
+ depth: f32,
+ edges: (bool, bool, bool, bool),
+ tint: [f32; 3],
+ ) {
+ let rect = self.apply_offset(rect);
+ self.push(Prim::Trough { rect, radii, depth, edges, tint: Some(tint) });
+ }
+
/// Raise a rim along `rect`'s boundary — see `Prim::Ridge`. `depth` is the
/// full width of the bump (it straddles the outline by ±depth/2).
pub fn ridge(&mut self, rect: Rect, radii: Radii, depth: f32) {
@@ -1540,6 +1554,19 @@ impl PaintCtx {
self.push(Prim::Recess { rect, radii, depth, edges, tint: None });
}
+ /// [`PaintCtx::recess_edges`] with a specular tint on the lit rim — see
+ /// `Prim::Recess::tint`. A tinted carve never groups into its host plate
+ /// (the tint could only land on the whole plate's specular), so it
+ /// shades through the overlay path with its own lit rim.
+ pub fn recess_edges_tinted(
+ &mut self, rect: Rect, radii: Radii, depth: f32,
+ edges: (bool, bool, bool, bool),
+ tint: [f32; 3],
+ ) {
+ let rect = self.apply_offset(rect);
+ self.push(Prim::Recess { rect, radii, depth, edges, tint: Some(tint) });
+ }
+
/// The window's glass slab: rounded fill at full size plus a rolled, lit perimeter.
/// `depth` is the roll-off width in px — pass [`crate::layout::bevel_width`] unless the
/// window wants a shallower edge than the DE default.
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index b830bd4..6a88c57 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -80,8 +80,8 @@ pub struct ParametersBg {
code_error_line: Option<usize>,
mouse_pos: Option<(f32, f32)>,
/// The parameter row under the pointer: its LABEL lifts
- /// (`own_text_labels`) — the pane's ONE hover rule, over every control
- /// kind. The controls keep their own hovers (a spinbox's +/- buttons, a
+ /// (`own_text_labels`) and its own carves light (`paint_ui`, the tint
+ /// channel) — the pane's ONE hover rule, over every control kind. The controls keep their own hovers (a spinbox's +/- buttons, a
/// slider's band), but under the DE relief style most of them draw none
/// at all — the spinbox's row border, the well frames and the dropdown
/// border are flat-style chrome the relief branches never paint — so
@@ -97,7 +97,8 @@ pub struct ParametersBg {
/// any hover; painted after them, its 5% white composited in LINEAR
/// light, which lifts a well's dark shade line from 18 to ~67 while
/// the pane face moves 46 to 76 — the hovered well's outline paled and
- /// read as the well lighting up. Text stands clear of the relief.
+ /// read as the well lighting up. Text stands clear of the relief, and
+ /// the tinted carve is the relief's own way of lighting ONE rim.
hover_row: Option<usize>,
pub sliders: Vec<Option<Adapted<Slider>>>,
pub float3s: Vec<Option<Adapted<Float3>>>,
@@ -887,6 +888,13 @@ impl ParametersBg {
/// reads as the dialog's hovered command does.
const LABEL: [u8; 3] = [0xaa, 0xaa, 0xbb];
const LABEL_HOVER: [u8; 3] = [0xf4, 0xf4, 0xfa];
+ /// The hovered row's carve tint: the lifted label's own colour, so the
+ /// lit rim and the label read as one cue. Through the tint channel the
+ /// shader composites the rim's light in this colour at its focus gain
+ /// and the shadow in a quarter of it — the carve lights, it is not
+ /// washed over (a wash over a carve pales its dark shade line in linear
+ /// blending; see `hover_row`).
+ const HOVER_TINT: [f32; 3] = [0xf4 as f32 / 255.0, 0xf4 as f32 / 255.0, 0xfa as f32 / 255.0];
fn update_slider_rects(&mut self) {
// Inline rows hand their control the row less the label column; the
@@ -1894,15 +1902,34 @@ impl Paint for ParametersBg {
for (acx, acy, ar, at, a0, a1, ac) in self.arcs() {
ctx.arc(acx, acy, ar, at, a0, a1, ac);
}
+ // The hovered row's carves are TINTED — the focus treatment's channel
+ // with a neutral light instead of the accent: the rim's own light and
+ // shadow, recoloured and gained, so the control under the pointer
+ // reads lit while every other carve keeps the plate's grouped shading.
+ // A tinted carve shades through the overlay path by design, and a
+ // section's well spans many rows, so it never qualifies.
+ let hover_rect = self.hover_row.and_then(|i| self.get_param_rects().get(i).copied()).filter(|r| r.3 > 0.0);
+ let hovered = |x: f32, y: f32, w: f32, h: f32| {
+ hover_rect.map_or(false, |(rx, ry, rw, rh)| {
+ x >= rx - 0.5 && y >= ry - 0.5 && x + w <= rx + rw + 0.5 && y + h <= ry + rh + 0.5
+ })
+ };
for (rx, ry, rw, rh, radii, rd, raised, edges) in self.reliefs() {
- if raised {
- ctx.boss_edges(Rect { x: rx, y: ry, width: rw, height: rh }, radii, rd, edges);
- } else {
- ctx.recess_edges(Rect { x: rx, y: ry, width: rw, height: rh }, radii, rd, edges);
+ let rect = Rect { x: rx, y: ry, width: rw, height: rh };
+ match (raised, hovered(rx, ry, rw, rh)) {
+ (true, true) => ctx.boss_edges_tinted(rect, radii, rd, edges, Self::HOVER_TINT),
+ (true, false) => ctx.boss_edges(rect, radii, rd, edges),
+ (false, true) => ctx.recess_edges_tinted(rect, radii, rd, edges, Self::HOVER_TINT),
+ (false, false) => ctx.recess_edges(rect, radii, rd, edges),
}
}
for (tx2, ty2, tw2, th2, radii, td, tedges) in self.troughs() {
- ctx.trough_edges(Rect { x: tx2, y: ty2, width: tw2, height: th2 }, radii, td, tedges);
+ let rect = Rect { x: tx2, y: ty2, width: tw2, height: th2 };
+ if hovered(tx2, ty2, tw2, th2) {
+ ctx.trough_edges_tinted(rect, radii, td, tedges, Self::HOVER_TINT);
+ } else {
+ ctx.trough_edges(rect, radii, td, tedges);
+ }
}
for (ga, gb, gw, gd, ghost) in self.grooves() {
ctx.groove(ga, gb, gw, gd, ghost);
@@ -4101,14 +4128,17 @@ mod tests {
assert_eq!(p.hover_row, None);
}
- /// The hover cue is the row's LABEL, and nothing else in the pane's
- /// paint changes with it: a hover adds no geometry at all, so it can
- /// neither close the pane plate's carve-grouping window (a wash before
- /// the wells did, and every well flipped shading on any hover) nor
- /// composite over a well's shade lines (a wash after them did, and the
- /// hovered well's outline paled). Both shipped for part of 2026-09-28.
+ /// The hover cue is the row's LABEL and the row's OWN carves lit through
+ /// the tint channel — and nothing else in the pane's paint changes with
+ /// it. A hover adds no geometry: it can neither close the pane plate's
+ /// carve-grouping window (a wash before the wells did, and every well
+ /// flipped shading on any hover) nor composite over a well's shade lines
+ /// (a wash after them did, and the hovered well's outline paled). Both
+ /// shipped for part of 2026-09-28. The other rows' carves keep their
+ /// untinted, grouped shading.
#[test]
- fn a_hover_lifts_the_label_and_adds_no_geometry() {
+ fn a_hover_lifts_the_label_and_lights_only_its_own_carves() {
+ use crate::scene::paint::Prim;
let mut ctx = UiContext::new();
let mut p = panel_with(&[("Name", "x", "text"), ("Count", "3", "spinbox:0:10"), ("Size", "0.5", "slider:0:1")]);
WidgetHost::set_rect(&mut p, 0.0, 0.0, 300.0, 400.0);
@@ -4121,7 +4151,13 @@ mod tests {
let label_of = |p: &Adapted<ParametersBg>, name: &str| {
p.own_text_labels().into_iter().find(|l| l.text == name).map(|l| l.color).expect("a row label")
};
+ // A carve's rect and tint, whatever its kind.
+ let carve = |prim: &Prim| match prim {
+ Prim::Recess { rect, tint, .. } | Prim::Boss { rect, tint, .. } | Prim::Trough { rect, tint, .. } => Some((*rect, *tint)),
+ _ => None,
+ };
let at_rest = prims(&p, &ctx);
+ assert!(at_rest.iter().all(|pr| carve(pr).map_or(true, |(_, t)| t.is_none())), "nothing is tinted at rest");
assert_eq!(label_of(&p, "Count"), ParametersBg::LABEL);
let (x, y, w, h) = p.get_param_rects()[1];
@@ -4129,7 +4165,21 @@ mod tests {
assert_eq!(p.hover_row, Some(1));
assert_eq!(label_of(&p, "Count"), ParametersBg::LABEL_HOVER, "the hovered row's label lifts");
assert_eq!(label_of(&p, "Name"), ParametersBg::LABEL, "and only that row's");
- assert_eq!(prims(&p, &ctx), at_rest, "a hover paints no geometry");
+
+ let hovered = prims(&p, &ctx);
+ assert_eq!(hovered.len(), at_rest.len(), "a hover adds no geometry");
+ let mut lit = 0;
+ for (a, b) in at_rest.iter().zip(&hovered) {
+ if a == b {
+ continue;
+ }
+ let ((ra, ta), (rb, tb)) = (carve(a).expect("only carves change"), carve(b).expect("only carves change"));
+ assert_eq!(ra, rb, "a carve keeps its place");
+ assert_eq!((ta, tb), (None, Some(ParametersBg::HOVER_TINT)), "the change is the hover tint");
+ assert!(rb.y >= y - 0.5 && rb.y + rb.height <= y + h + 0.5, "and only inside the hovered row: {rb:?}");
+ lit += 1;
+ }
+ assert!(lit > 0, "the hovered row's carves light");
}
/// The label lift is the pane's rule, not the control's: a spinbox row,