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