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

commitbcb014e76f3e03c37941db01875cf484b3a0fa9d
parent7e95a55752
authorLucas Galante <[email protected]>
date2026-07-20 15:33
fix: window-corner clip gets the curvature-matched span, like plates

The window clip (shader2d/scene3d window_corner_distance) cut squircle
corners at the nominal radius while window-scale Prim::Plates widen
theirs by the curvature-match factor — so a clip-rounded window
(cce-designer) showed visibly tighter corners than a plate-rounded one
(cce-data-editor) at corner_shape > 2. Extract the factor into
layout::corner_span_factor() and apply it where the clip radius feeds
both shaders (clip_corner_radius, capped at half the smaller extent);
set_corner_radius callers keep passing the nominal radius.

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

 src/backend/window_runner.rs | 28 ++++++++++------------------
 src/layout.rs                | 18 ++++++++++++++++++
 src/vk/renderer.rs           | 18 ++++++++++++++++--
 3 files changed, 44 insertions(+), 20 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 84fda88..12ab40c 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1801,24 +1801,16 @@ fn plate_push_raised(
 ) -> crate::vk::PlatePush {
     let cap = rect.width.min(rect.height) * 0.5;
     let shape = crate::layout::corner_shape();
-    // A raw superellipse of exponent n at the circle's nominal radius turns
-    // TIGHTER at the diagonal than that circle — its radius of curvature there
-    // is √2·r / (2^(1/n)·(n − 1)) — and once the roll inset exceeds it, the
-    // offset curve the specular band follows creases into a visible square
-    // corner. For PLATES (`scale_corners`), scale the corner span so the
-    // diagonal curvature radius equals the configured radius: the corner reads
-    // as the same size, entered and exited smoothly (the same reason Apple's
-    // continuous corners run ~1.5·r along the edge), and every inset ≤ r stays
-    // crease-free. Continuous at n = 2, where the factor is exactly 1.
-    // Widget-scale overlay 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 && shape > 2.001 {
-        (shape - 1.0) * 2f32.powf(1.0 / shape) / std::f32::consts::SQRT_2
-    } else {
-        1.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
+    // same size as a circular one, and every roll inset ≤ r stays crease-free
+    // (past the diagonal curvature radius the offset curve the specular band
+    // follows creases into a visible square corner). Widget-scale overlay
+    // 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 };
     crate::vk::PlatePush {
         rect: [
             (rect.x + rect.width * 0.5) * scale,
diff --git a/src/layout.rs b/src/layout.rs
index b608722..9360647 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -1501,6 +1501,24 @@ pub fn corner_shape() -> f32 {
     get_style_registry().read().unwrap().get_float("corner_shape").unwrap_or(2.0).clamp(2.0, 16.0)
 }
 
+/// The curvature-matched corner-span factor for window-scale squircle corners.
+/// A raw superellipse of exponent n at a circle's nominal radius turns tighter
+/// at the diagonal than that circle — its radius of curvature there is
+/// √2·r / (2^(1/n)·(n − 1)). Scaling the corner span by this factor makes the
+/// diagonal curvature equal the configured radius, so the corner reads as the
+/// same size as a circular one (the same reason Apple's continuous corners run
+/// ~1.5·r along the edge). Exactly 1 at n = 2. Applied to window-scale corners
+/// only — `Prim::Plate` and the renderer's window-corner clip — never to
+/// widget-scale radii, which must match the nominal-radius squircles around them.
+pub fn corner_span_factor() -> f32 {
+    let n = corner_shape();
+    if n > 2.001 {
+        (n - 1.0) * 2f32.powf(1.0 / n) / std::f32::consts::SQRT_2
+    } else {
+        1.0
+    }
+}
+
 /// Whether plates/bevels/recesses render through shader2d's per-pixel SDF-lit
 /// plate branch (the default) or the legacy banded vertex shading. `bevel_shader 0`
 /// in config flips back to the old look for A/B comparison.
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index 7056ec6..f20630a 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -794,11 +794,21 @@ impl VkRenderer {
         renderer
     }
 
+    /// The window-clip corner radius as the shaders consume it: the nominal
+    /// radius widened by the curvature-match factor, so the clip cuts along
+    /// the same curve as window-scale plate corners (`plate_push_raised` with
+    /// `scale_corners`) and a clipped window reads the same as a plate-drawn
+    /// one. Capped at half the smaller extent, like the plate path's cap.
+    fn clip_corner_radius(&self) -> f32 {
+        let cap = 0.5 * self.extent.width.min(self.extent.height) as f32;
+        (self.corner_radius_px * crate::layout::corner_span_factor()).min(cap)
+    }
+
     fn write_window_info(&mut self) {
         let data = [
             self.extent.width as f32,
             self.extent.height as f32,
-            self.corner_radius_px,
+            self.clip_corner_radius(),
             crate::layout::corner_shape(),
         ];
         if let Some(allocation) = self.window_info.allocation.as_mut() {
@@ -1079,6 +1089,9 @@ impl VkRenderer {
     }
 
     // Used at cutover, when scale changes re-derive the radius; vk-smoke fixes it at init.
+    // Nominal (circle-equivalent) radius in physical px — the curvature-match
+    // widening for squircle corner shapes happens at consumption
+    // (`clip_corner_radius`), so callers pass the configured radius as-is.
     #[allow(dead_code)]
     pub fn set_corner_radius(&mut self, radius_px: f32) {
         self.corner_radius_px = radius_px;
@@ -1224,11 +1237,12 @@ impl VkRenderer {
                 frame2d.images,
                 self.extent,
             );
+            let clip_radius = self.clip_corner_radius();
             self.scene.write_frame_uniforms(
                 &self.core.device,
                 self.core.allocator.as_mut().unwrap(),
                 frame_index,
-                self.corner_radius_px,
+                clip_radius,
             );
             if let Some(rt) = self.rt.as_mut() {
                 rt.write_frame_uniforms(frame_index);