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

commitfc117ccdce0b996483b464ddbe8361d1da12992b
parent0bb8410c00
authorLucas Galante <[email protected]>
date2026-07-23 17:52
feat: concave fillets — composed carves get rounded inside corners

Box radii can only round convex corners, so every composed carve's inside
corner (a tab's right wall turning onto its body's top edge) met at a hard
90°. New Prim::ConcaveFillet / PaintCtx::concave_fillet: a quarter-arc
wall whose centre sits out in the corner's pocket, shaded by the shared
free-carve path with the distance/gradient swapped to radial (shader modes
6/7 → the 2/3 machinery via `eff`). The wedge is HARD-cut at its tangent
lines, where the neighboring straight walls continue the step profile
exactly; an inside fillet never host-fades. SDF path only — the legacy
banded path keeps the square corner.

Wired into all three composed-tab sites: section throats (SECTION_FILLET_R
6, served through the new section_fillets getter for legacy-hatch hosts),
the labeled Dropdown, and the labeled Slider — each straight top run now
starts a fillet radius past the throat, its crossfade landing under the
fillet's tangent cut.

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

 src/backend/window_runner.rs          | 23 +++++++++++++++++++++
 src/scene/paint.rs                    | 16 ++++++++++++++
 src/vk/shader2d.wgsl                  | 35 +++++++++++++++++++++++++------
 src/widget/container/parameters_bg.rs | 39 +++++++++++++++++++++++++++++++++--
 src/widget/input/dropdown.rs          | 25 +++++++++++++++++-----
 src/widget/input/slider.rs            | 20 ++++++++++++++++--
 src/widget/model.rs                   |  3 +++
 7 files changed, 146 insertions(+), 15 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 6021244..20547cf 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1756,6 +1756,29 @@ pub fn tessellate_display_list(
                 // Legacy path: the flat disc, exactly a Circle.
                 verts.extend(circle_vertices(*cx, *cy, *radius, sw, sh, *color, segs(*radius), no));
             }
+            Prim::ConcaveFillet { cx, cy, radius, depth, start: a0, raised } if shader_plates => {
+                // A quarter-arc carve wall (shader mode 6/7): one cover quad
+                // over the wedge's reach; the wall straddles the arc by ±t/2
+                // like every carve boundary. p_rect carries centre + radius,
+                // p_radii.x the wedge start angle. Host box pushed far out —
+                // an inside-corner fillet never fades.
+                let m = *depth * 0.5 + 2.0;
+                let r = *radius + m;
+                verts.extend(quad_vertices(cx - r, cy - r, 2.0 * r, 2.0 * r, sw, sh, [0.0; 4]));
+                plate = Some(crate::vk::PlatePush {
+                    rect: [cx * scale, cy * scale, *radius * scale, 0.0],
+                    radii: [*a0, 0.0, 0.0, 0.0],
+                    light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
+                    material: plate_mat,
+                    host: [0.0, 0.0, 1e6, 1e6],
+                    specular_tint: [1.0, 1.0, 1.0, 0.0],
+                    mode: if *raised { 7.0 } else { 6.0 },
+                    shape: crate::layout::corner_shape(),
+                });
+            }
+            // Legacy banded path has no radial wall — the composed corner
+            // stays square there (A/B comparison path only).
+            Prim::ConcaveFillet { .. } => {}
         }
         let end = verts.len() as u32;
         if end == start {
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 585fc80..35b5a2b 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -92,6 +92,14 @@ pub enum Prim {
     /// plate's face keeps the app's color. Falls back to a flat circle on the
     /// legacy (`bevel_shader 0`) path.
     Sphere { cx: f32, cy: f32, radius: f32, color: [f32; 4] },
+    /// A concave inside-corner fillet for composed carves: a quarter-arc wall
+    /// whose centre `(cx, cy)` sits out in the corner's pocket, shaded with the
+    /// same step profile as a `Recess`/`Boss` wall (`raised` flips the sign).
+    /// `start` is the wedge's start angle (quarter span, hard-cut at the
+    /// tangent lines — the neighboring straight walls continue the profile
+    /// exactly there). Box radii can only round convex corners; this is the
+    /// missing concave piece. SDF path only (no legacy fallback).
+    ConcaveFillet { cx: f32, cy: f32, radius: f32, depth: f32, start: f32, raised: bool },
     /// Text in sRGB u8 (the `TextLabel` convention). `font` is a font string for
     /// `get_text_buffer` (family, or "family:size"); `bounds` is a logical `[l, t, r, b]` clip
     /// for the glyph pass (Phase 6: the backend renders these through glyphon when the app
@@ -368,6 +376,14 @@ impl PaintCtx {
         self.push(Prim::Sphere { cx: cx + ox, cy: cy + oy, radius, color });
     }
 
+    /// A concave inside-corner fillet — see `Prim::ConcaveFillet`. `start` is
+    /// the quarter wedge's start angle; the arc's centre sits in the corner's
+    /// pocket and the wall descends (or rises, `raised`) away from it.
+    pub fn concave_fillet(&mut self, cx: f32, cy: f32, radius: f32, depth: f32, start: f32, raised: bool) {
+        let (ox, oy) = self.offset;
+        self.push(Prim::ConcaveFillet { cx: cx + ox, cy: cy + oy, radius, depth, start, raised });
+    }
+
     pub fn border(&mut self, rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32) {
         let rect = self.apply_offset(rect);
         self.push(Prim::Border { rect, radii, fill, border, thickness });
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index f8bb32f..d95b5b7 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -257,7 +257,7 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
     // the surface tilt meets the half-vector, ~a third of the way out toward
     // the light) — and, like a plate face, the shade is expressed relative to
     // the flat face so the color at the lit center is exactly the app's.
-    if (rrect_clip.rect1.z > 4.5) {
+    if (rrect_clip.rect1.z > 4.5 && rrect_clip.rect1.z < 5.5) {
         let c = frag - rrect_clip.p_rect.xy;
         let r = max(rrect_clip.p_rect.z, 0.001);
         let dist = length(c);
@@ -329,10 +329,33 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
     // All profiles straddle the boundary (span [-t/2, t/2]). Darkening is exact
     // multiplicative shading (black at alpha 1 - shade); brightening is a
     // translucent white screen.
-    let u = clamp(d / t + 0.5, 0.0, 1.0);
+    //
+    // Concave fillet (mode 6 = recessed, 7 = raised): the wall follows a
+    // quarter ARC whose centre sits out in the pocket — the inside-corner
+    // rounding the box SDF cannot express. p_rect.xy = centre, .z = radius;
+    // p_radii.x = the wedge's start angle (quarter span, HARD-cut at the
+    // tangent lines — the straight walls continue the profile exactly there).
+    // Distance/gradient swap to radial; everything downstream is the shared
+    // free-carve path via `eff` (6→2, 7→3).
+    var eff = rrect_clip.rect1.z;
+    var fd = d;
+    var fgd = gd.xy;
+    var wedge = 1.0;
+    if (eff > 5.5) {
+        eff = eff - 4.0;
+        let c = frag - rrect_clip.p_rect.xy;
+        let dist = max(length(c), 1e-4);
+        fd = dist - rrect_clip.p_rect.z;
+        fgd = -c / dist;
+        let a0 = rrect_clip.p_radii.x;
+        let ang = atan2(c.y, c.x);
+        let rel = ang - a0 - floor((ang - a0) / TAU) * TAU;
+        wedge = select(0.0, 1.0, rel <= 1.5707964);
+    }
+    let u = clamp(fd / t + 0.5, 0.0, 1.0);
     var slope = 0.0;
     var curv = 0.0;
-    if (rrect_clip.rect1.z > 3.5) {
+    if (eff > 3.5) {
         // Ridge bump: the carve profile mirrored about the boundary (rising
         // outer half, falling inner half), amplitude halved so the wall tilt
         // matches a step's despite the doubled profile rate.
@@ -345,7 +368,7 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         // tints the whole cover quad).
         curv = -rrect_clip.p_mat.w * sin(w * TAU);
     } else {
-        let dir = select(-1.0, 1.0, rrect_clip.rect1.z > 2.5);
+        let dir = select(-1.0, 1.0, eff > 2.5);
         // The profile slope is carve_slope's family: smoothstep-derived
         // normally, smootherstep (zero second derivative at the plateaus)
         // under a continuous-curvature corner_shape — shading eases in and out
@@ -356,14 +379,14 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
         // boss.
         curv = -dir * rrect_clip.p_mat.w * sin(u * TAU);
     }
-    let sv = gd.xy * slope;
+    let sv = fgd * slope;
     let n = normalize(vec3f(sv, 1.0));
     let diff = PLATE_AMBIENT + (1.0 - PLATE_AMBIENT) * max(dot(n, l), 0.0);
     let spec = roll_spec(sv);
     // Fade the carve out across the host plate's perimeter roll (see p_host).
     let hb = rrect_clip.p_host;
     let host_d = min(hb.z - abs(frag.x - hb.x), hb.w - abs(frag.y - hb.y));
-    let att = clamp(host_d / t, 0.0, 1.0);
+    let att = clamp(host_d / t, 0.0, 1.0) * wedge;
     let v = (diff / flat_shade - 1.0 + curv + spec) * strength * att;
     if (v >= 0.0) {
         return vec4f(1.0, 1.0, 1.0, min(v, 1.0));
diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 66176b7..3828eba 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -94,6 +94,10 @@ const SECTION_R: f32 = 8.0;
 /// The throat — the concave fillet where the tab's right side turns onto the content
 /// body's top edge (the tab sits flush on the body; there is no connector neck).
 const SECTION_THROAT_R: f32 = 3.0;
+/// The relief carve's concave inside-corner radius at the tab throat
+/// (`section_fillets`) — sized against SECTION_R so inside and outside
+/// corners read as one family.
+const SECTION_FILLET_R: f32 = 6.0;
 /// Narrowest a title box may be: both its corners plus the throat fillet. Titles run
 /// wider than this in practice; it only keeps the throat clear of the corners.
 const SECTION_TITLE_MIN_W: f32 = 2.0 * SECTION_R + SECTION_THROAT_R;
@@ -1035,10 +1039,12 @@ impl ParametersBg {
                 out.push((cx, cy, cw, ch, (0.0, 0.0, r, r), depth, false, (false, true, true, true)));
                 let throat_r = tx + tw;
                 if cx + cw > throat_r + 0.5 {
+                    // Starts a fillet radius past the throat — the concave
+                    // fillet ([`Self::section_fillets`]) carries the corner.
                     out.push((
-                        throat_r - depth,
+                        throat_r + SECTION_FILLET_R - depth,
                         cy,
-                        cx + cw - throat_r + depth,
+                        cx + cw - throat_r - SECTION_FILLET_R + depth,
                         ch,
                         (0.0, r, 0.0, 0.0),
                         depth,
@@ -1144,6 +1150,35 @@ impl ParametersBg {
         out
     }
 
+    /// The section carves' concave inside-corner fillets — `(cx, cy, radius,
+    /// depth, start angle)` for [`crate::scene::paint::PaintCtx::concave_fillet`]
+    /// (recessed), drawn by the host AFTER [`Self::reliefs`]. The box reliefs
+    /// can only round convex corners; this rounds the throat where a tab's
+    /// right wall turns onto its body's top edge.
+    pub fn section_fillets(&self) -> Vec<(f32, f32, f32, f32, f32)> {
+        if !self.visible || !crate::layout::control_relief() {
+            return Vec::new();
+        }
+        let mut out = Vec::new();
+        for (title, content) in self.section_boxes() {
+            let (tx, _ty, tw, _th) = title;
+            if let Some((cx, cy, cw, ch)) = content {
+                let depth = crate::layout::bevel_width().min(ch * 0.2).min(CHANNEL);
+                let throat_r = tx + tw;
+                if cx + cw > throat_r + 2.0 * SECTION_FILLET_R {
+                    out.push((
+                        throat_r + SECTION_FILLET_R,
+                        cy - SECTION_FILLET_R,
+                        SECTION_FILLET_R,
+                        depth,
+                        std::f32::consts::FRAC_PI_2,
+                    ));
+                }
+            }
+        }
+        out
+    }
+
     /// The scene-path companion to the legacy views: rows whose widgets paint
     /// prims NO flat tuple view can carry (the ramp rows' curve fill, key
     /// circles, and field controls). A host rendering this panel through the
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 28d4683..d069da8 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -369,12 +369,27 @@ impl Dropdown {
                     depth,
                     (false, true, true, true),
                 );
-                // Ring top wall, right of the tab (starts at the tab's throat;
-                // extended `depth` left so it crossfades against the tab's
-                // right wall instead of fading short of it).
-                if outer_r - tab_r > 0.5 {
+                // Ring top wall, right of the tab. A concave fillet rounds the
+                // throat; the straight run starts a fillet radius past it
+                // (extended `depth` left so its fade-in lands under the
+                // fillet's hard tangent cut instead of leaving a gap).
+                let fr = 6.0_f32.min(strip * 0.5);
+                if outer_r - tab_r > fr + 4.0 {
+                    ctx.concave_fillet(
+                        tab_r + fr,
+                        ring_top - fr,
+                        fr,
+                        depth,
+                        std::f32::consts::FRAC_PI_2,
+                        false,
+                    );
                     ctx.recess_edges(
-                        Rect { x: tab_r - depth, y: ring_top, width: outer_r - tab_r + depth, height: visual_h + 2.0 * g },
+                        Rect {
+                            x: tab_r + fr - depth,
+                            y: ring_top,
+                            width: outer_r - tab_r - fr + depth,
+                            height: visual_h + 2.0 * g,
+                        },
                         (0.0, orad.1, 0.0, 0.0),
                         depth,
                         (true, false, false, false),
diff --git a/src/widget/input/slider.rs b/src/widget/input/slider.rs
index d3c3581..6200c6b 100644
--- a/src/widget/input/slider.rs
+++ b/src/widget/input/slider.rs
@@ -370,9 +370,25 @@ impl Paint for Slider {
                     (true, true, false, true),
                 );
                 ctx.recess_edges(track_rect, (0.0, 0.0, radius, radius), recess_t, (false, true, true, true));
-                if g.track_x + g.track_w - tab_r > 0.5 {
+                // Concave fillet at the throat, the straight run starting a
+                // fillet radius past it (the labeled-Dropdown convention).
+                let fr = 6.0_f32.min(strip * 0.5);
+                if g.track_x + g.track_w - tab_r > fr + 4.0 {
+                    ctx.concave_fillet(
+                        tab_r + fr,
+                        g.y - fr,
+                        fr,
+                        recess_t,
+                        std::f32::consts::FRAC_PI_2,
+                        false,
+                    );
                     ctx.recess_edges(
-                        Rect { x: tab_r - recess_t, y: g.y, width: g.track_x + g.track_w - tab_r + recess_t, height: g.h },
+                        Rect {
+                            x: tab_r + fr - recess_t,
+                            y: g.y,
+                            width: g.track_x + g.track_w - tab_r - fr + recess_t,
+                            height: g.h,
+                        },
                         (0.0, radius, 0.0, 0.0),
                         recess_t,
                         (true, false, false, false),
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 07a6ef4..7fd8360 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1243,6 +1243,9 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 Prim::Vector { x1, y1, x2, y2, thickness, color, cap } => ctx.vector(x1, y1, x2, y2, thickness, color, cap),
                 Prim::Circle { cx, cy, radius, color } => ctx.circle(cx, cy, radius, color),
                 Prim::Sphere { cx, cy, radius, color } => ctx.sphere(cx, cy, radius, color),
+                Prim::ConcaveFillet { cx, cy, radius, depth, start, raised } => {
+                    ctx.concave_fillet(cx, cy, radius, depth, start, raised)
+                }
                 Prim::Image { image, rect, alpha } => ctx.image(image, rect, alpha),
             }
         }