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

commit5c84a50d288ab86b570994b531652082e5de5965
parentbc7240764c
authorLucas Galante <[email protected]>
date2026-07-21 17:10
feat: beveled plate borders with a specular tint (focused-pane highlight)

Under control_relief a bordered plate renders as an SDF-lit bevel
(style.surface.plate.bevel_width roll, default 6) instead of the flat
border line — the plates' counterpart of the controls' outline->relief
degradation; blur-behind negative alpha resolves in the plate branch as
before. Prim::Bevel carries a specular tint (PaintCtx::bevel_tinted,
append_widget_plate_tinted): PlatePush grows a specular_tint vec4 —
push constants 112 -> 128 bytes, exactly the Vulkan-guaranteed minimum
— and shader2d multiplies the roll and sphere specular terms by it.
Neutral white everywhere by default; a host marks a plate (focus) by
coloring its glint instead of ringing it.

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

 src/backend/window_runner.rs | 10 +++++++---
 src/color.rs                 | 20 ++++++++++++++++++++
 src/scene/paint.rs           | 15 ++++++++++++---
 src/scene/painter.rs         | 19 +++++++++++++++++--
 src/vk/renderer.rs           | 15 ++++++++++-----
 src/vk/shader2d.wgsl         |  7 +++++--
 src/widget/model.rs          |  2 +-
 7 files changed, 72 insertions(+), 16 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 56b5648..601c142 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1513,7 +1513,7 @@ pub fn tessellate_display_list(
                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, cr, sw, sh, *fill, no, None, &mut verts);
                 push_plate_solid_border_vertices(rect.x, rect.y, rect.width, rect.height, cr, *thickness, sw, sh, *border, no, &mut verts);
             }
-            Prim::Bevel { rect, radii, color, depth } if shader_plates => {
+            Prim::Bevel { rect, radii, color, depth, tint } if shader_plates => {
                 // SDF-lit raised plate: one cover quad; the shader owns fill,
                 // roll shading, corners, and silhouette AA. Nominal corner
                 // radii (scale_corners false): a Bevel is a WIDGET-scale plate
@@ -1521,7 +1521,9 @@ pub fn tessellate_display_list(
                 // the controls around it — only window-scale `Plate`s get the
                 // curvature-matched span.
                 verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
-                plate = Some(plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, false));
+                let mut p = plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, false);
+                p.specular_tint = [tint[0], tint[1], tint[2], 0.0];
+                plate = Some(p);
                 made_plate = Some(*rect);
             }
             Prim::Plate { rect, radii, color, depth } if shader_plates => {
@@ -1641,7 +1643,7 @@ pub fn tessellate_display_list(
                 ];
                 plate = Some(p);
             }
-            Prim::Bevel { rect, radii, color, depth } => {
+            Prim::Bevel { rect, radii, color, depth, tint: _ } => {
                 // Full-size fill: the lip is now a shading overlay, not a paint of the
                 // outer ring, so the fill must cover the whole rect (the old inset fill
                 // would leave the ring showing whatever lay beneath).
@@ -1737,6 +1739,7 @@ pub fn tessellate_display_list(
                     light: [plate_light[0], plate_light[1], plate_light[2], 0.0],
                     material: plate_mat,
                     host: [0.0; 4],
+                    specular_tint: [1.0, 1.0, 1.0, 0.0],
                     mode: 5.0,
                     shape: 2.0,
                 });
@@ -1829,6 +1832,7 @@ fn plate_push_raised(
         // Mode-1 semantics: [feature offset, feature count] — no carves yet;
         // the tessellator fills these in as recesses group into this plate.
         host: [0.0, 0.0, 0.0, 0.0],
+        specular_tint: [1.0, 1.0, 1.0, 0.0],
         mode: 1.0,
         shape,
     }
diff --git a/src/color.rs b/src/color.rs
index 0b4a32d..3bb23b6 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -633,6 +633,9 @@ fn parse_and_set_colors(content: &str) {
     if let Some(t) = val.pointer("/style/surface/plate/border_thickness").and_then(|v| v.as_f64()) {
         if let Ok(mut lock) = PLATE_BORDER_THICKNESS.write() { *lock = t as f32; }
     }
+    if let Some(t) = val.pointer("/style/surface/plate/bevel_width").and_then(|v| v.as_f64()) {
+        if let Ok(mut lock) = PLATE_BEVEL_WIDTH.write() { *lock = t as f32; }
+    }
     if let Some(blur) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_bool()) {
         if let Ok(mut lock) = PLATE_BLUR.write() { *lock = blur; }
     } else if let Some(blur_val) = val.pointer("/style/surface/plate/blur").and_then(|v| v.as_f64()) {
@@ -1675,6 +1678,9 @@ pub fn set_backplate_statusbar_blur(b: bool) {
 static PLATE_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.15, 0.15, 0.2, 0.95]));
 static PLATE_BORDER_COLOR: RwLock<Option<[f32; 4]>> = RwLock::new(Some([0.3, 0.3, 0.4, 1.0]));
 static PLATE_BORDER_THICKNESS: RwLock<f32> = RwLock::new(1.0);
+/// Roll width of the beveled plate border (the control_relief replacement for
+/// the flat border line) — `style.surface.plate.bevel_width`.
+static PLATE_BEVEL_WIDTH: RwLock<f32> = RwLock::new(6.0);
 
 pub fn plate_color() -> Option<[f32; 4]> {
     load_colors_once();
@@ -1718,6 +1724,20 @@ pub fn set_plate_border_thickness(t: f32) {
     }
 }
 
+pub fn plate_bevel_width() -> f32 {
+    load_colors_once();
+    if let Ok(lock) = PLATE_BEVEL_WIDTH.read() {
+        return *lock;
+    }
+    6.0
+}
+
+pub fn set_plate_bevel_width(t: f32) {
+    if let Ok(mut lock) = PLATE_BEVEL_WIDTH.write() {
+        *lock = t;
+    }
+}
+
 static PLATE_BLUR: RwLock<bool> = RwLock::new(false);
 
 pub fn plate_blur() -> bool {
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 57a83ef..585fc80 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -41,8 +41,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).
-    Bevel { rect: Rect, radii: Radii, color: [f32; 4], depth: f32 },
+    /// (mirrors `push_widget_vertices`' bevel branch). `tint` multiplies the lit
+    /// roll's specular color — 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.
+    Bevel { rect: Rect, radii: Radii, color: [f32; 4], 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
     /// through the middle: a relief cut into the backplate rather than a plate laid on
@@ -370,8 +374,13 @@ impl PaintCtx {
     }
 
     pub fn bevel(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
+        self.bevel_tinted(rect, radii, color, depth, [1.0, 1.0, 1.0]);
+    }
+
+    /// `bevel` with a specular tint — see `Prim::Bevel::tint`.
+    pub fn bevel_tinted(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32, tint: [f32; 3]) {
         let rect = self.apply_offset(rect);
-        self.push(Prim::Bevel { rect, radii, color, depth });
+        self.push(Prim::Bevel { rect, radii, color, depth, tint });
     }
 
     /// Carve a recess into the already-painted surface below. Unlike `bevel`, this fills
diff --git a/src/scene/painter.rs b/src/scene/painter.rs
index 06a75bf..edf36d3 100644
--- a/src/scene/painter.rs
+++ b/src/scene/painter.rs
@@ -88,14 +88,29 @@ pub fn paint_legacy_leaf(
 /// hand-build their display list in their own draw order (the designer) instead of walking
 /// `paint_self`, but want a widget's background exactly as the vertex path drew it.
 pub fn append_widget_plate(w: &dyn WidgetHost, pc: &mut PaintCtx) {
+    append_widget_plate_tinted(w, pc, None);
+}
+
+/// [`append_widget_plate`] with an optional specular tint for the plate's
+/// bevel — the focused-pane treatment: the highlight colors the lit roll's
+/// glint instead of drawing a separate border ring. Under `control_relief` a
+/// bordered plate renders as a bevel (`plate_bevel_width` roll) — the beveled
+/// counterpart of the flat border line, exactly the controls' own
+/// outline→relief degradation.
+pub fn append_widget_plate_tinted(w: &dyn WidgetHost, pc: &mut PaintCtx, tint: Option<[f32; 3]>) {
     let (x, y, ww, h) = w.rect();
     let rect = Rect { x, y, width: ww, height: h };
     let radii = w.corner_radii();
     let radii_tuple = (radii.top_left, radii.top_right, radii.bottom_right, radii.bottom_left);
+    let tint = tint.unwrap_or([1.0, 1.0, 1.0]);
     if let Some(thickness) = w.plate_bevel() {
-        pc.bevel(rect, radii_tuple, w.color(), thickness);
+        pc.bevel_tinted(rect, radii_tuple, w.color(), thickness, tint);
     } else if let Some((border_color, thickness)) = w.solid_border() {
-        pc.border(rect, radii_tuple, w.color(), border_color, thickness);
+        if crate::layout::control_relief() {
+            pc.bevel_tinted(rect, radii_tuple, w.color(), crate::colors::plate_bevel_width(), tint);
+        } else {
+            pc.border(rect, radii_tuple, w.color(), border_color, thickness);
+        }
     } else {
         pc.border(rect, radii_tuple, w.color(), [0.0; 4], 0.0);
     }
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index 0d5420a..f3680be 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -54,6 +54,9 @@ pub struct PlatePush {
     /// (center + half-extents) a free recess fades out against; far-away sides
     /// (±1e5) disable the fade.
     pub host: [f32; 4],
+    /// RGB multiplies the roll's specular color (w unused). Neutral white
+    /// normally; the focused-pane bevel carries the highlight color here.
+    pub specular_tint: [f32; 4],
     /// 1.0 = raised lit plate, 2.0 = recess overlay.
     pub mode: f32,
     /// Corner shape exponent: 2.0 = circular arcs, > 2 = superellipse
@@ -498,12 +501,13 @@ impl VkRenderer {
 
         let set_layouts = [descriptor_set_layout];
         // Push constants: the per-batch rounded-rect clip plus the SDF-lit
-        // plate block (seven vec4s, matching shader2d's `RRectClip`), read by
-        // shader2d's fragment stage.
+        // plate block (eight vec4s, matching shader2d's `RRectClip`), read by
+        // shader2d's fragment stage. 128 bytes — exactly the Vulkan-guaranteed
+        // minimum budget.
         let push_ranges = [vk::PushConstantRange::default()
             .stage_flags(vk::ShaderStageFlags::FRAGMENT)
             .offset(0)
-            .size(112)];
+            .size(128)];
         let pipeline_layout = device
             .create_pipeline_layout(
                 &vk::PipelineLayoutCreateInfo::default()
@@ -1484,7 +1488,7 @@ impl VkRenderer {
                             // batch is a plate cover quad.
                             let rr = batch.clip_rrect.unwrap_or([0.0; 5]);
                             let enabled = if batch.clip_rrect.is_some() { 1.0f32 } else { 0.0 };
-                            let mut pc = [0.0f32; 28];
+                            let mut pc = [0.0f32; 32];
                             pc[..5].copy_from_slice(&rr);
                             pc[5] = enabled;
                             pc[7] = clip_shape;
@@ -1496,6 +1500,7 @@ impl VkRenderer {
                                 pc[16..20].copy_from_slice(&p.light);
                                 pc[20..24].copy_from_slice(&p.material);
                                 pc[24..28].copy_from_slice(&p.host);
+                                pc[28..32].copy_from_slice(&p.specular_tint);
                                 if p.mode == 1.0 {
                                     // Rebase the feature offset onto this
                                     // frame's UBO slot.
@@ -1551,7 +1556,7 @@ impl VkRenderer {
                     .cmd_bind_vertex_buffers(cmd, 0, &[frame.vertex.buffer], &[0]);
                 // Push constants persist across binds — clear any batch's
                 // rounded clip and plate mode.
-                let pc = [0.0f32; 28];
+                let pc = [0.0f32; 32];
                 self.core.device.cmd_push_constants(
                     cmd,
                     self.pipeline_layout,
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index ed9855b..d7aad3d 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -94,6 +94,9 @@ struct RRectClip {
     // the carve fades out against — a wall flush with the host's edge dies
     // across the host's perimeter roll; far-away sides sit at ±1e5 (no fade).
     p_host: vec4f,
+    // RGB multiplies the lit roll's specular color — neutral white normally,
+    // a highlight color on a marked (focused) plate. w unused.
+    p_spec_tint: vec4f,
 }
 var<push_constant> rrect_clip: RRectClip;
 
@@ -239,7 +242,7 @@ 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) * strength;
         let spec = roll_spec(c / h);
-        return vec4f(vcol.rgb * shade + vec3f(spec * strength), vcol.a * aa);
+        return vec4f(vcol.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength), vcol.a * aa);
     }
 
     let gd = rr_sdf_grad(frag, rrect_clip.p_rect, rrect_clip.p_radii);
@@ -283,7 +286,7 @@ 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;
         let spec = roll_spec(sv);
-        return vec4f(base.rgb * shade + vec3f(spec * strength), abs(base.a) * aa);
+        return vec4f(base.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength), abs(base.a) * aa);
     }
 
     // Free-floating recess, boss, or ridge (one not grouped into a host plate —
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 5304bd7..07a6ef4 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1234,7 +1234,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 Prim::Quad { rect, color } => ctx.quad(rect, color),
                 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 } => ctx.bevel(rect, radii, color, depth),
+                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::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),