Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
feat: curvature-matched squircle corner spans, mirroring cce-ui
A raw superellipse of exponent n at a circle's nominal radius turns
tighter at the diagonal, so cce-ui widens plate corners by
(n − 1)·2^(1/n)/√2 to make diagonal curvature equal the configured
radius. Every compositor-side corner cut must widen the same way or it
lands outside the corners the clients draw: widen_corner_radius()
(nominal × span factor, capped at half the smaller content extent) now
applied to the blur region radius (both set_rendering_state paths and
the commit handler) and the background plate. Factor computed on config
load/reload next to the exponent push into scenefx, stored as f64 bits
in an atomic like scenefx's own global_corner_shape. Exactly 1 at n = 2.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/server/config.rs | 27 +++++++++++++++++++++++++--
src/server/window.rs | 26 +++++++++++++++++++++++++-
src/server/xdg_toplevel.rs | 6 ++++++
3 files changed, 56 insertions(+), 3 deletions(-)
diff --git a/src/server/config.rs b/src/server/config.rs
index 456e72f..2518142 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -476,6 +476,23 @@ const HOVER_LIGHTEN: f32 = 0.35;
/// Mix a premultiplied-alpha color toward white (which is `[a, a, a, a]` in
/// premultiplied space), keeping the alpha.
+/// Curvature-matched corner-span factor for window-scale squircle corners,
+/// mirroring cce-ui's `layout::corner_span_factor()` exactly: a raw
+/// superellipse of exponent n at a circle's nominal radius turns tighter at
+/// the diagonal, so the corner span is scaled by (n − 1)·2^(1/n)/√2 to make
+/// the diagonal curvature equal the configured radius. The clients widen
+/// their plate corners by this factor, so every compositor-side corner cut
+/// (blur, shadow, surface clip, background rect) must widen the same way or
+/// the cuts land outside the corners the clients draw. Exactly 1 at n = 2.
+/// Written on config load/reload (same place the exponent is pushed into
+/// scenefx), read on the render paths — f64 bits in an atomic, like
+/// scenefx's own global_corner_shape.
+static CORNER_SPAN_FACTOR: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0x3FF0000000000000); // 1.0f64
+
+pub fn corner_span_factor() -> f64 {
+ f64::from_bits(CORNER_SPAN_FACTOR.load(std::sync::atomic::Ordering::Relaxed))
+}
+
pub fn lighten_premultiplied(c: [f32; 4], t: f32) -> [f32; 4] {
[
c[0] + (c[3] - c[0]) * t,
@@ -1787,10 +1804,16 @@ pub fn parse_config(path: &str, state: &mut crate::window_manager::WindowManager
.as_ref()
.and_then(|wm| wm.corner_shape)
.unwrap_or(2.0)
- .clamp(2.0, 16.0) as f32;
+ .clamp(2.0, 16.0);
unsafe {
- crate::ffi::fx_renderer_set_corner_shape(corner_shape);
+ crate::ffi::fx_renderer_set_corner_shape(corner_shape as f32);
}
+ let span_factor = if corner_shape > 2.001 {
+ (corner_shape - 1.0) * 2f64.powf(1.0 / corner_shape) / std::f64::consts::SQRT_2
+ } else {
+ 1.0
+ };
+ CORNER_SPAN_FACTOR.store(span_factor.to_bits(), std::sync::atomic::Ordering::Relaxed);
state.layout.gap = config.layout.gap as i32;
state.layout.gap_top = config.layout.gap_top as i32;
diff --git a/src/server/window.rs b/src/server/window.rs
index 8268f2e..3c3f442 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -89,6 +89,21 @@ impl Border {
}
}
+/// A window-scale corner radius as scenefx should consume it: the configured
+/// nominal (circle-equivalent) radius widened by the curvature-match span
+/// factor, capped at half the smaller content extent so opposite corners
+/// can't overlap — the exact counterpart of cce-ui's
+/// `VkRenderer::clip_corner_radius`, which widens the clients' plate/clip
+/// corners the same way. `width`/`height` and the returned radius are in
+/// logical px; callers scale to device px where they already do.
+pub fn widen_corner_radius(nominal: i32, width: i32, height: i32) -> i32 {
+ if nominal <= 0 {
+ return nominal;
+ }
+ let widened = (nominal as f64 * crate::config::corner_span_factor()).round() as i32;
+ widened.min(width.min(height) / 2)
+}
+
/// One of the 8 interactive border zones. Each draws as its own visual
/// element (corners as two-rect Ls) and highlights independently on hover.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -1889,6 +1904,9 @@ impl Window {
};
let actual_w = if self.rendering_sent.width > 0 { self.rendering_sent.width } else { toplevel_w as u32 };
let actual_h = if self.rendering_sent.height > 0 { self.rendering_sent.height } else { toplevel_h as u32 };
+ // Widen squircle corners to the span the clients draw (see
+ // widen_corner_radius); circles already sit at the half-extent cap.
+ let radius = if requested.circular { radius } else { widen_corner_radius(radius, actual_w as i32, actual_h as i32) };
let width = (actual_w as f64 * self.scale) as i32;
let height = (actual_h as f64 * self.scale) as i32;
ffi::river_scene_node_enable_blur(
@@ -2278,6 +2296,9 @@ impl Window {
};
let actual_w = if self.rendering_sent.width > 0 { self.rendering_sent.width } else { toplevel_w as u32 };
let actual_h = if self.rendering_sent.height > 0 { self.rendering_sent.height } else { toplevel_h as u32 };
+ // Same span widening as set_rendering_state — the two paths
+ // drive the same blur node and must agree.
+ let radius = if requested.circular { radius } else { widen_corner_radius(radius, actual_w as i32, actual_h as i32) };
let width = (actual_w as f64 * self.scale) as i32;
let height = (actual_h as f64 * self.scale) as i32;
ffi::river_scene_node_enable_blur(
@@ -2380,7 +2401,10 @@ impl Window {
let bg_height = (self.box_geom.height as f64 * self.scale) as i32;
ffi::river_scene_rect_set_size_if_changed(self.window_background, bg_width, bg_height);
ffi::wlr_scene_rect_set_color(self.window_background, border_color.as_ptr());
- ffi::river_scene_rect_set_corner_radius(self.window_background, (border.corner_radius as f64 * self.scale) as i32);
+ // The background plate sits directly under the client's plate, so its
+ // corners take the same span widening as the blur/clip radius.
+ let bg_radius = widen_corner_radius(border.corner_radius, self.box_geom.width, self.box_geom.height);
+ ffi::river_scene_rect_set_corner_radius(self.window_background, (bg_radius as f64 * self.scale) as i32);
ffi::wlr_scene_node_set_enabled(self.window_background as *mut ffi::wlr_scene_node, !requested.hidden && self.wm_requested.ssd);
// The border draws as 8 zone segments (4 edge bars + 4 two-rect L
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index 98c5eb1..56a085f 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -464,6 +464,12 @@ unsafe extern "C" fn handle_commit(listener: *mut ffi::wl_listener, _data: *mut
} else {
0
};
+ // Same span widening as Window::set_rendering_state (part of the mirror).
+ let radius = if (*window).rendering_requested.circular {
+ radius
+ } else {
+ crate::window::widen_corner_radius(radius, actual_w as i32, actual_h as i32)
+ };
let legacy_blur = std::env::var_os("CCE_BLUR_LEGACY").is_some(); // TEMP DIAGNOSTIC
let use_optimized = if is_status || (radius > 0 && !legacy_blur) {
false