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

commit8d253c8b0a597e2d4ae2e733900b7f3b3e1d1169
parent64858f458e
authorLucas Galante <[email protected]>
date2026-09-05 19:52
feat(paint): per-plate corner exponent — `plate_shaped` draws a true circle

`Prim::Plate` gains `shape: Option<f32>`, an override of the DE-wide
`corner_shape` for one plate; `PaintCtx::plate_shaped` sets it and
`plate` stays `None` (unchanged behaviour). The runner's plate push
takes the exponent actually used and derives the corner span from it
(`layout::corner_span_factor_for`), so `Some(2.0)` with half-extent
radii lands on a circle instead of the DE's squircle.

Wanted by cce-browser's circle menu: at `corner_shape 4.5` a 40px plate
with 20px radii renders as a rounded square, and there was no way for
a single plate to opt out of the DE exponent.

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

 src/backend/window_runner.rs | 23 +++++++++++++++--------
 src/layout.rs                | 10 +++++++++-
 src/scene/paint.rs           | 20 +++++++++++++++++---
 3 files changed, 41 insertions(+), 12 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 08691c5..2c233da 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1912,7 +1912,7 @@ 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));
-                let mut p = 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, None);
                 // 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
@@ -1923,7 +1923,7 @@ pub fn tessellate_display_list(
                 plate = Some(p);
                 made_plate = Some(*rect);
             }
-            Prim::Plate { rect, radii, color, depth } if shader_plates => {
+            Prim::Plate { rect, radii, color, depth, shape } if shader_plates => {
                 if *depth < 0.0 {
                     // Negative depth = fill-less roll overlay (MODE_ROLL): the
                     // window-edge roll shading alone, screened over whatever is
@@ -1932,14 +1932,14 @@ pub fn tessellate_display_list(
                     // and the batch is NOT opened as a carve host: an overlay
                     // owns no surface for a CSG feature to cut into.
                     verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, [0.0; 4]));
-                    let mut p = plate_push_raised(rect, *radii, -*depth, scale, plate_light, plate_mat, true);
+                    let mut p = plate_push_raised(rect, *radii, -*depth, scale, plate_light, plate_mat, true, *shape);
                     p.mode = 11.0; // MODE_ROLL
                     plate = Some(p);
                 } else {
                     // Same lit-plate branch; the cover quad is the exact rect so the
                     // silhouette and the compositor's rounded window corners agree.
                     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, true));
+                    plate = Some(plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, true, *shape));
                     made_plate = Some(*rect);
                 }
             }
@@ -2167,7 +2167,7 @@ pub fn tessellate_display_list(
                 if !edges.2 { y1 += ext; }
                 if !edges.3 { x0 -= ext; }
                 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);
+                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).
@@ -2206,7 +2206,7 @@ pub fn tessellate_display_list(
                 push_rounded_rect_vertices_corners(rect.x, rect.y, rect.width, rect.height, corners, sw, sh, *color, no, None, &mut verts);
                 push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, *depth, sw, sh, *color, no, &mut verts);
             }
-            Prim::Plate { rect, radii, color, depth } => {
+            Prim::Plate { rect, radii, color, depth, .. } => {
                 if *depth < 0.0 {
                     // Fill-less roll overlay (negative-depth sentinel): the banded
                     // legacy tessellation has no overlay compositing, so the roll
@@ -2616,6 +2616,12 @@ const RECESS_DEPTH_RATIO: f32 = 0.6;
 
 /// The push-constant block for a raised SDF-lit plate over `rect` (logical px in,
 /// physical px out). Corner radii clamp to the half-extent cap the SDF needs.
+///
+/// `shape` is a per-plate corner exponent (`Prim::Plate`'s override); `None`
+/// follows the DE-wide `layout::corner_shape`. The span factor follows the
+/// exponent actually used, so a circular override (2.0) spans nothing and a
+/// half-extent radius lands on a true circle.
+#[allow(clippy::too_many_arguments)]
 fn plate_push_raised(
     rect: &crate::scene::layout::Rect,
     radii: (f32, f32, f32, f32),
@@ -2624,12 +2630,13 @@ fn plate_push_raised(
     light: [f32; 3],
     material: [f32; 4],
     scale_corners: bool,
+    shape: Option<f32>,
 ) -> crate::vk::PlatePush {
     // Floored: a rect already shrunk past its padding (a window dragged
     // below what its layout can hold) has a NEGATIVE extent here, and
     // `clamp(0.0, cap)` with a negative cap is a panic, not a zero radius.
     let cap = (rect.width.min(rect.height) * 0.5).max(0.0);
-    let shape = crate::layout::corner_shape();
+    let shape = shape.map_or_else(crate::layout::corner_shape, |n| n.clamp(2.0, 16.0));
     // For PLATES (`scale_corners`), widen the corner span by the
     // curvature-match factor (see `layout::corner_span_factor`): the diagonal
     // curvature radius equals the configured radius, the corner reads as the
@@ -2639,7 +2646,7 @@ fn plate_push_raised(
     // reliefs (recess/boss/ridge fallbacks) pass false: their radii must MATCH
     // the nominal-radius squircles of the widget silhouettes around them, and
     // at their few-px roll widths the offset crease is subpixel.
-    let rscale = if scale_corners { crate::layout::corner_span_factor() } else { 1.0 };
+    let rscale = if scale_corners { crate::layout::corner_span_factor_for(shape) } else { 1.0 };
     crate::vk::PlatePush {
         rect: [
             (rect.x + rect.width * 0.5) * scale,
diff --git a/src/layout.rs b/src/layout.rs
index 99f5ed8..6cec05a 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1635,7 +1635,15 @@ pub fn window_silhouette_radius() -> f32 {
 }
 
 pub fn corner_span_factor() -> f32 {
-    let n = corner_shape();
+    corner_span_factor_for(corner_shape())
+}
+
+/// The span factor for an explicit corner exponent — what a plate carrying
+/// its own `shape` (see `scene::paint::Prim::Plate`) scales its radii by.
+/// Same clamp as [`corner_shape`], so an override cannot reach an exponent
+/// the shader would not accept.
+pub fn corner_span_factor_for(n: f32) -> f32 {
+    let n = n.clamp(2.0, 16.0);
     if n > 2.001 {
         (n - 1.0) * 2f32.powf(1.0 / n) / std::f32::consts::SQRT_2
     } else {
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 97c0ebf..87c46bf 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -409,7 +409,12 @@ pub enum Prim {
     /// `depth` — a plate must fill the window exactly, or the compositor's rounded window
     /// corners would show a gap. `depth` is the width of the roll-off in px, not a color
     /// offset (the shading amplitude is the DE-wide `bevel_depth`).
-    Plate { rect: Rect, radii: Radii, color: [f32; 4], depth: f32 },
+    ///
+    /// `shape` overrides the DE-wide corner exponent (`layout::corner_shape`)
+    /// for this one plate — `Some(2.0)` is circular arcs, so a plate whose
+    /// radii reach its half-extent is a true circle regardless of the
+    /// squircle the rest of the DE wears. `None` follows the DE.
+    Plate { rect: Rect, radii: Radii, color: [f32; 4], depth: f32, shape: Option<f32> },
     Arc { cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4] },
     /// A ring band with radial color interpolation — inner rim → crest
     /// (centerline) → outer rim — for rounded rim bevels (the Ramp's key
@@ -838,7 +843,9 @@ impl PaintCtx {
             },
             Prim::Ridge { rect, radii, depth, edges } => self.ridge_edges(rect, radii, depth, edges),
             Prim::Trough { rect, radii, depth, edges } => self.trough_edges(rect, radii, depth, edges),
-            Prim::Plate { rect, radii, color, depth } => self.plate(rect, radii, color, depth),
+            Prim::Plate { rect, radii, color, depth, shape } => {
+                self.plate_shaped(rect, radii, color, depth, shape)
+            }
             Prim::Arc { cx, cy, radius, thickness, start, end, color } => {
                 self.arc(cx, cy, radius, thickness, start, end, color)
             }
@@ -1055,8 +1062,15 @@ impl PaintCtx {
     /// `color` is ignored; the roll profile, crest and specular are exactly the
     /// positive-depth plate's.
     pub fn plate(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
+        self.plate_shaped(rect, radii, color, depth, None);
+    }
+
+    /// [`plate`](Self::plate) with an explicit corner exponent — see
+    /// [`Prim::Plate`]'s `shape`. `Some(2.0)` on a plate whose radii are its
+    /// half-extent draws a circle; `None` is exactly `plate`.
+    pub fn plate_shaped(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32, shape: Option<f32>) {
         let rect = self.apply_offset(rect);
-        self.push(Prim::Plate { rect, radii, color, depth });
+        self.push(Prim::Plate { rect, radii, color, depth, shape });
     }
 
     /// Emit the plate a [`PlateSpec`] describes: role-resolved per-corner