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