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

commit19c9a48bd63c861636a0f858a8bd40a0165018c9
parent9eef922a49
authorLucas Galante <[email protected]>
date2026-07-18 18:50
feat: shade bevels as surfaces — gradient roll-off, rims vs steps, Plate

The lip was one flat color per edge plus 16 flat wedges per corner, which
reads as a painted stripe rather than a curved surface. It is now sliced into
Gouraud-interpolated bands (the WGSL shader smooth-interpolates vertex color,
so this costs geometry only) following the sine of the remaining tilt — the
curve of a quarter-round rather than a linear 45° chamfer. Corners emit one
quad per (sweep segment × band) with all four vertices shaded independently,
so the shading varies along the sweep and across the lip at once and the
faceting is gone.

Only RGB varies; alpha is held equal across every vertex, because a negative
alpha is the blur-behind sentinel and a gradient crossing zero would flip part
of a triangle into blur mode mid-primitive.

Edges now come in two kinds, because they are different shapes:

  Rim  — the surface ends (a plate's perimeter). Quarter-round peaking at the
         boundary and dying inward.
  Step — the surface continues at another height (a sunken menubar). A height
         field falling monotonically across the transition keeps its normal
         tilted toward the low side throughout, so the shading is a symmetric
         bump straddling the boundary. Hanging the band on one side, as a rim
         profile does, leaves a hard seam that reads as a drawn line.

New `Prim::Plate` (fill + rolled perimeter at full size, unlike `Bevel` which
insets its fill) and `Prim::Recess` gains a per-wall mask: a full-width bar
flush with the plate edge is a plateau one step down, not a trough, so only
the wall facing the content is real — the other three sides are the plate's
own rim.

StatusBar gains with_recess, mirroring MenuBar to the bottom edge. Roll-off
width is the new `window_manager.bevel_width` (default 14 logical px),
companion to the existing bevel_depth amplitude, capped per widget so a deep
setting cannot swallow a short bar.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>

 src/backend/window_runner.rs     | 268 +++++++++++++++++++++++++++++++++------
 src/layout.rs                    |  10 ++
 src/scene/paint.rs               |  29 ++++-
 src/widget/container/menu.rs     |  29 +++--
 src/widget/display/status_bar.rs |  36 +++++-
 src/widget/model.rs              |   3 +-
 6 files changed, 318 insertions(+), 57 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 5123568..41bf4a5 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -381,6 +381,30 @@ pub fn quad_vertices_with_clip(
     ]
 }
 
+/// A quad whose four corners each carry their own color, Gouraud-interpolated across both
+/// triangles by the shader (`@location(0) color` has no `flat` qualifier). Corner order is
+/// TL, TR, BR, BL. Keep the alpha equal on all four: negative alpha is the blur sentinel,
+/// so a gradient that crossed zero would tear the triangle in half.
+pub fn quad_vertices_shaded(
+    x: f32, y: f32, w: f32, h: f32,
+    sw: f32, sh: f32,
+    c_tl: [f32; 4], c_tr: [f32; 4], c_br: [f32; 4], c_bl: [f32; 4],
+    clip_circle: [f32; 3],
+) -> [Vertex; 6] {
+    let x0 = (x / sw) * 2.0 - 1.0;
+    let y0 = 1.0 - (y / sh) * 2.0;
+    let x1 = ((x + w) / sw) * 2.0 - 1.0;
+    let y1 = 1.0 - ((y + h) / sh) * 2.0;
+    [
+        Vertex { position: [x0, y0], color: c_tl, clip_circle },
+        Vertex { position: [x1, y0], color: c_tr, clip_circle },
+        Vertex { position: [x0, y1], color: c_bl, clip_circle },
+        Vertex { position: [x1, y0], color: c_tr, clip_circle },
+        Vertex { position: [x1, y1], color: c_br, clip_circle },
+        Vertex { position: [x0, y1], color: c_bl, clip_circle },
+    ]
+}
+
 pub fn quad_vertices_clipped(
     x: f32, y: f32, w: f32, h: f32,
     surface_w: f32, surface_h: f32,
@@ -862,6 +886,90 @@ pub fn push_bevel_edge_vertices_radii(
     clip_circle: [f32; 3],
     light_sign: f32,
     out: &mut Vec<Vertex>,
+) {
+    push_bevel_edge_vertices_banded(
+        x, y, ww, h, radii, t, sw, sh, base_color, clip_circle, light_sign,
+        default_bevel_bands(t), (true, true, true, true), EdgeKind::Rim, out,
+    );
+}
+
+/// What kind of height change an edge represents. The two shade differently because they
+/// are different shapes, and using one where the other belongs is what makes a bevel read
+/// as a drawn line instead of a surface.
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub enum EdgeKind {
+    /// The surface *ends* here: a quarter-round rolling from face-on at the inner edge of
+    /// the lip to fully in-plane at the outer boundary, where it drops away. The shading
+    /// therefore peaks exactly at the boundary and dies inward. This is a plate's outer
+    /// perimeter.
+    Rim,
+    /// The surface *continues* at a different height: one plateau steps down to another.
+    /// A height field that falls monotonically across the transition has its normal tilted
+    /// toward the low side the whole way, steepest in the middle and flat at both ends —
+    /// so the shading is a bump straddling the boundary, not a band butted against it.
+    /// Hanging the band on one side instead leaves the seam the eye reads as a drawn line.
+    Step,
+}
+
+/// Shading across an edge at signed distance `d` from the boundary (positive = toward the
+/// shape's interior), for a transition of width `t`. Returns the light term as a fraction
+/// of full tilt.
+#[inline]
+fn bevel_profile(kind: EdgeKind, d: f32, t: f32) -> f32 {
+    if t <= 0.0 {
+        return 0.0;
+    }
+    match kind {
+        // Normal rotates from in-plane (d = 0) to face-on (d = t): sine of what tilt is
+        // left. A linear ramp here reads as a flat 45° chamfer instead of a roll.
+        EdgeKind::Rim => ((1.0 - (d / t).clamp(0.0, 1.0)) * std::f32::consts::FRAC_PI_2).sin(),
+        // Symmetric bump over [-t/2, +t/2], zero at both ends so the transition blends into
+        // both plateaus with no seam.
+        EdgeKind::Step => {
+            let s = (d / t + 0.5).clamp(0.0, 1.0);
+            (s * std::f32::consts::PI).sin()
+        }
+    }
+}
+
+/// The signed distance range an edge's shading occupies, relative to the boundary.
+#[inline]
+fn bevel_span(kind: EdgeKind, t: f32) -> (f32, f32) {
+    match kind {
+        EdgeKind::Rim => (0.0, t),
+        EdgeKind::Step => (-0.5 * t, 0.5 * t),
+    }
+}
+
+/// How many gradient bands to slice a lip of thickness `t` into. Vertex colors interpolate
+/// linearly, so each band is a chord of [`bevel_profile`]'s curve; one band per ~1.5px
+/// keeps the error under a shade step without emitting geometry finer than the display
+/// resolves. A 2px lip stays a single ramp; a 12px rolled edge gets eight.
+fn default_bevel_bands(t: f32) -> usize {
+    ((t / 1.5).ceil() as usize).clamp(1, 8)
+}
+
+/// As [`push_bevel_edge_vertices_radii`], with the band count forced and the walls
+/// selectable — for callers that want a coarser or finer roll-off than thickness alone
+/// implies, or that are shading a step rather than a closed shape.
+///
+/// `edges` is (top, right, bottom, left). Suppressing a wall matters for a region that
+/// runs flush to the surface's own edge: a full-width menubar sunk into the top of a plate
+/// is a *plateau one step down*, not a trough, so its only real wall is the one facing the
+/// content. Drawing the other three would carve a lip along the plate's outer edge, where
+/// the plate's own roll already lives, and the two would fight.
+pub fn push_bevel_edge_vertices_banded(
+    x: f32, y: f32, ww: f32, h: f32,
+    radii: (f32, f32, f32, f32),
+    t: f32,
+    sw: f32, sh: f32,
+    base_color: [f32; 4],
+    clip_circle: [f32; 3],
+    light_sign: f32,
+    bands: usize,
+    edges: (bool, bool, bool, bool),
+    kind: EdgeKind,
+    out: &mut Vec<Vertex>,
 ) {
     let cap = ww.min(h) * 0.5;
     let (tl, tr, br, bl) = (
@@ -870,14 +978,22 @@ pub fn push_bevel_edge_vertices_radii(
         radii.2.clamp(0.0, cap),
         radii.3.clamp(0.0, cap),
     );
+    let t = t.clamp(0.0, cap);
+    if t <= 0.0 {
+        return;
+    }
+    let bands = bands.max(1);
 
     let rad = crate::layout::light_source_position();
     let lx = rad.cos() * light_sign;
     let ly = -rad.sin() * light_sign;
+    let depth = crate::layout::bevel_depth();
 
-    let edge_color = |factor: f32| -> [f32; 4] {
-        let max_offset = crate::layout::bevel_depth();
-        let offset = factor * max_offset;
+    // Only RGB moves; alpha is held at the base value on every vertex. The renderer reads
+    // a negative alpha as the blur-behind sentinel (shader2d.wgsl), so interpolating alpha
+    // could cross zero and flip part of a triangle into blur mode.
+    let shade = |factor: f32| -> [f32; 4] {
+        let offset = factor.clamp(-1.0, 1.0) * depth;
         [
             (base_color[0] + offset).clamp(0.0, 1.0),
             (base_color[1] + offset).clamp(0.0, 1.0),
@@ -885,46 +1001,106 @@ pub fn push_bevel_edge_vertices_radii(
             base_color[3],
         ]
     };
-
-    let top_color = edge_color(-ly);
-    let left_color = edge_color(-lx);
-    let bottom_color = edge_color(ly);
-    let right_color = edge_color(lx);
+    // Color at signed distance `d` from the boundary, for an edge whose outward normal is
+    // `dir`. A `Step` band runs negative — it straddles the boundary into the plateau
+    // outside the rect, which is exactly what removes the seam.
+    let band = |dir: (f32, f32), d: f32| shade(bevel_profile(kind, d, t) * (dir.0 * lx + dir.1 * ly));
+    let (span_lo, span_hi) = bevel_span(kind, t);
 
     // Each flat edge spans between its two adjoining corner radii, not a single uniform
-    // inset — that is what lets the corners differ.
-    out.extend_from_slice(&quad_vertices_with_clip(x + tl, y, ww - tl - tr, t, sw, sh, top_color, clip_circle));
-    out.extend_from_slice(&quad_vertices_with_clip(x, y + tl, t, h - tl - bl, sw, sh, left_color, clip_circle));
-    out.extend_from_slice(&quad_vertices_with_clip(x + bl, y + h - t, ww - bl - br, t, sw, sh, bottom_color, clip_circle));
-    out.extend_from_slice(&quad_vertices_with_clip(x + ww - t, y + tr, t, h - tr - br, sw, sh, right_color, clip_circle));
+    // inset — that is what lets the corners differ. At a square corner there is no arc to
+    // cover the t×t patch where two edges meet, so the horizontal edges claim it (they run
+    // the full span) and the vertical ones inset by `t`; overlapping them instead would
+    // double-blend that patch, which shows as a dark notch on a translucent surface.
+    let (left_top, left_bot) = (if tl > 0.0 { tl } else { t }, if bl > 0.0 { bl } else { t });
+    let (right_top, right_bot) = (if tr > 0.0 { tr } else { t }, if br > 0.0 { br } else { t });
+    let top_w = ww - tl - tr;
+    let bottom_w = ww - bl - br;
+    let left_h = h - left_top - left_bot;
+    let right_h = h - right_top - right_bot;
+
+    for k in 0..bands {
+        let d0 = span_lo + (span_hi - span_lo) * (k as f32 / bands as f32);
+        let d1 = span_lo + (span_hi - span_lo) * ((k + 1) as f32 / bands as f32);
+        let bw = d1 - d0;
+
+        // Top: outward normal (0,-1); the gradient runs downward, into the surface.
+        if top_w > 0.0 && edges.0 {
+            let (c0, c1) = (band((0.0, -1.0), d0), band((0.0, -1.0), d1));
+            out.extend_from_slice(&quad_vertices_shaded(
+                x + tl, y + d0, top_w, bw, sw, sh, c0, c0, c1, c1, clip_circle,
+            ));
+        }
+        // Bottom: outward normal (0,1); gradient runs upward.
+        if bottom_w > 0.0 && edges.2 {
+            let (c0, c1) = (band((0.0, 1.0), d0), band((0.0, 1.0), d1));
+            out.extend_from_slice(&quad_vertices_shaded(
+                x + bl, y + h - d1, bottom_w, bw, sw, sh, c1, c1, c0, c0, clip_circle,
+            ));
+        }
+        // Left: outward normal (-1,0); gradient runs rightward.
+        if left_h > 0.0 && edges.3 {
+            let (c0, c1) = (band((-1.0, 0.0), d0), band((-1.0, 0.0), d1));
+            out.extend_from_slice(&quad_vertices_shaded(
+                x + d0, y + left_top, bw, left_h, sw, sh, c0, c1, c1, c0, clip_circle,
+            ));
+        }
+        // Right: outward normal (1,0); gradient runs leftward.
+        if right_h > 0.0 && edges.1 {
+            let (c0, c1) = (band((1.0, 0.0), d0), band((1.0, 0.0), d1));
+            out.extend_from_slice(&quad_vertices_shaded(
+                x + ww - d1, y + right_top, bw, right_h, sw, sh, c1, c0, c0, c1, clip_circle,
+            ));
+        }
+    }
 
-    let segments = 16;
+    // A corner arc belongs to both of its adjoining walls, so it is drawn only when both
+    // are — otherwise a suppressed wall would still get a quarter of a lip.
     let corners = [
-        (x + tl, y + tl, tl, std::f32::consts::PI, 1.5 * std::f32::consts::PI), // Top-Left
-        (x + ww - tr, y + tr, tr, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI), // Top-Right
-        (x + ww - br, y + h - br, br, 0.0, 0.5 * std::f32::consts::PI), // Bottom-Right
-        (x + bl, y + h - bl, bl, 0.5 * std::f32::consts::PI, std::f32::consts::PI), // Bottom-Left
+        (x + tl, y + tl, tl, std::f32::consts::PI, 1.5 * std::f32::consts::PI, edges.0 && edges.3), // Top-Left
+        (x + ww - tr, y + tr, tr, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI, edges.0 && edges.1), // Top-Right
+        (x + ww - br, y + h - br, br, 0.0, 0.5 * std::f32::consts::PI, edges.2 && edges.1), // Bottom-Right
+        (x + bl, y + h - bl, bl, 0.5 * std::f32::consts::PI, std::f32::consts::PI, edges.2 && edges.3), // Bottom-Left
     ];
 
-    for &(cx, cy, r, start_angle, end_angle) in &corners {
-        // A square corner has no arc to sweep — the two flat edges already meet there.
-        if r <= 0.0 {
+    for &(cx, cy, r, start_angle, end_angle, enabled) in &corners {
+        // A square corner has no arc to sweep — the flat edges already met there.
+        if r <= 0.0 || !enabled {
             continue;
         }
+        // The corner is a quarter of a torus: shading varies along the sweep (the normal
+        // swings through 90° of the light) *and* across the lip (the roll-off). Both come
+        // out of the vertex colors, so one quad per (segment × band) cell is enough — no
+        // faceting, unlike the 16 flat wedges this replaced.
+        let segments = ((r * 0.75) as usize).clamp(8, 48);
+        let ct = t.min(r);
         for j in 0..segments {
-            let theta1 = start_angle + (j as f32) * (end_angle - start_angle) / (segments as f32);
-            let theta2 = start_angle + ((j + 1) as f32) * (end_angle - start_angle) / (segments as f32);
-            let theta_mid = 0.5 * (theta1 + theta2);
-
-            let factor = (theta_mid.cos() * lx + theta_mid.sin() * ly).clamp(-1.0, 1.0);
-            let segment_color = edge_color(factor);
-
-            push_arc_background_vertices(
-                cx, cy, r, t,
-                theta1, theta2,
-                sw, sh, segment_color, 1, clip_circle,
-                out,
-            );
+            let theta0 = start_angle + (j as f32) * (end_angle - start_angle) / (segments as f32);
+            let theta1 = start_angle + ((j + 1) as f32) * (end_angle - start_angle) / (segments as f32);
+            let (cos0, sin0) = (theta0.cos(), theta0.sin());
+            let (cos1, sin1) = (theta1.cos(), theta1.sin());
+            let f0 = cos0 * lx + sin0 * ly;
+            let f1 = cos1 * lx + sin1 * ly;
+            for k in 0..bands {
+                let d0 = span_lo + (span_hi - span_lo) * (k as f32 / bands as f32);
+                let d1 = span_lo + (span_hi - span_lo) * ((k + 1) as f32 / bands as f32);
+                // Inward along the corner's radius is the same signed distance as inward
+                // from a flat edge, so the arc scales the span the same way.
+                let (r0, r1) = (r - ct * (d0 / t), r - ct * (d1 / t));
+                let p = |rho: f32, c: f32, s: f32| -> [f32; 2] {
+                    [
+                        ((cx + rho * c) / sw) * 2.0 - 1.0,
+                        1.0 - ((cy + rho * s) / sh) * 2.0,
+                    ]
+                };
+                // Outer/inner × the two sweep ends; each vertex gets its own shade.
+                let (p0, p1) = (bevel_profile(kind, d0, t), bevel_profile(kind, d1, t));
+                let v00 = Vertex { position: p(r0, cos0, sin0), color: shade(p0 * f0), clip_circle };
+                let v10 = Vertex { position: p(r0, cos1, sin1), color: shade(p0 * f1), clip_circle };
+                let v11 = Vertex { position: p(r1, cos1, sin1), color: shade(p1 * f1), clip_circle };
+                let v01 = Vertex { position: p(r1, cos0, sin0), color: shade(p1 * f0), clip_circle };
+                out.extend_from_slice(&[v00, v10, v11, v00, v11, v01]);
+            }
         }
     }
 }
@@ -1144,13 +1320,31 @@ pub fn tessellate_display_list(
                 push_rounded_rect_vertices_corners(rect.x + t, rect.y + t, rect.width - 2.0 * t, rect.height - 2.0 * t, inner, sw, sh, *color, no, None, &mut verts);
                 push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, t, sw, sh, *color, no, &mut verts);
             }
-            Prim::Recess { rect, radii, surface, depth } => {
+            Prim::Plate { rect, radii, color, depth } => {
+                // Fill at full size (no inset — see Prim::Plate), then roll the perimeter.
+                // The lip rides on top of the fill's outer band rather than replacing it,
+                // so the plate's silhouette and the compositor's rounded window corners
+                // still agree exactly.
+                let corners = crate::widget::CornerRadii {
+                    top_left: radii.0, top_right: radii.1,
+                    bottom_right: radii.2, bottom_left: radii.3,
+                };
+                push_rounded_rect_vertices_corners(
+                    rect.x, rect.y, rect.width, rect.height, corners, sw, sh, *color, no, None, &mut verts,
+                );
+                push_bevel_edge_vertices_radii(
+                    rect.x, rect.y, rect.width, rect.height, *radii, *depth,
+                    sw, sh, *color, no, 1.0, &mut verts,
+                );
+            }
+            Prim::Recess { rect, radii, surface, depth, edges } => {
                 // Edges only — no fill, so the surface already painted below shows through
                 // the middle of the carve. `light_sign = -1.0` shadows the lit-facing edges,
                 // which is the raised->recessed inversion.
-                push_bevel_edge_vertices_radii(
+                push_bevel_edge_vertices_banded(
                     rect.x, rect.y, rect.width, rect.height, *radii, *depth,
-                    sw, sh, *surface, no, -1.0, &mut verts,
+                    sw, sh, *surface, no, -1.0, default_bevel_bands(*depth), *edges,
+                    EdgeKind::Step, &mut verts,
                 );
             }
             Prim::Arc { cx, cy, radius, thickness, start: sa, end: ea, color } => {
diff --git a/src/layout.rs b/src/layout.rs
index 09b842b..cc6bdc0 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -108,6 +108,7 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 "style.control.toggle.corner_radius" => "toggle_corner_radius",
                 "window_manager.light_source_position" => "light_source_position",
                 "window_manager.bevel_depth" => "bevel_depth",
+                "window_manager.bevel_width" => "bevel_width",
                 "style.control.ramp.height" => "ramp_height",
                 "style.layout.column.gap" => "column_gap",
                 "style.control.control_panel.padding" => "control_panel_padding",
@@ -1487,6 +1488,15 @@ pub fn bevel_depth() -> f32 {
     get_style_registry().read().unwrap().get_float("bevel_depth").unwrap_or(0.15)
 }
 
+/// How wide a rolled edge is, in logical px — the distance over which a plate's perimeter
+/// or a recess wall curves away from the flat surface. `bevel_depth` is the companion
+/// knob: it sets how hard the light falls across that distance. Wide and shallow reads as
+/// thick glass; narrow and deep reads as a stamped metal lip.
+pub fn bevel_width() -> f32 {
+    lazy_init_style_registry();
+    get_style_registry().read().unwrap().get_float("bevel_width").unwrap_or(14.0)
+}
+
 pub fn toggle_corner_radius() -> f32 {
     lazy_init_style_registry();
     get_style_registry().read().unwrap().get_float("toggle_corner_radius").unwrap_or(4.0)
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index ed7ca28..76d631e 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -53,7 +53,16 @@ pub enum Prim {
     /// `surface` is the color of what lies beneath (the thing being carved); it is only
     /// the base for the edge shading, and is never filled. Its alpha carries through to
     /// the edges, so a recess in a translucent plate stays translucent.
-    Recess { rect: Rect, radii: Radii, surface: [f32; 4], depth: f32 },
+    /// `edges` is (top, right, bottom, left): which walls of the carve actually exist.
+    /// A region flush with the plate's own edge is a step, not a trough — see
+    /// `push_bevel_edge_vertices_banded`.
+    Recess { rect: Rect, radii: Radii, surface: [f32; 4], depth: f32, edges: (bool, bool, bool, bool) },
+    /// The window's glass slab: a rounded fill plus a rolled, lit edge around its whole
+    /// perimeter, drawn at full size. Distinct from `Bevel`, which insets its fill by
+    /// `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 },
     Arc { cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4] },
     Vector { x1: f32, y1: f32, x2: f32, y2: f32, thickness: f32, color: [f32; 4], cap: Cap },
     Circle { cx: f32, cy: f32, radius: f32, color: [f32; 4] },
@@ -340,8 +349,24 @@ impl PaintCtx {
     /// Carve a recess into the already-painted surface below. Unlike `bevel`, this fills
     /// nothing — `surface` is the color being carved, used only to shade the edges.
     pub fn recess(&mut self, rect: Rect, radii: Radii, surface: [f32; 4], depth: f32) {
+        self.recess_edges(rect, radii, surface, depth, (true, true, true, true));
+    }
+
+    /// [`PaintCtx::recess`] with only some of the walls — see `Prim::Recess`.
+    pub fn recess_edges(
+        &mut self, rect: Rect, radii: Radii, surface: [f32; 4], depth: f32,
+        edges: (bool, bool, bool, bool),
+    ) {
+        let rect = self.apply_offset(rect);
+        self.push(Prim::Recess { rect, radii, surface, depth, edges });
+    }
+
+    /// The window's glass slab: rounded fill at full size plus a rolled, lit perimeter.
+    /// `depth` is the roll-off width in px — pass [`crate::layout::bevel_width`] unless the
+    /// window wants a shallower edge than the DE default.
+    pub fn plate(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
         let rect = self.apply_offset(rect);
-        self.push(Prim::Recess { rect, radii, surface, depth });
+        self.push(Prim::Plate { rect, radii, color, depth });
     }
 
     pub fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 4925bec..b2ce4fc 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -27,10 +27,6 @@ use crate::widget::{
     MenuController, MouseButton, NamedKey, PageSelector, Paint, DROPDOWN_ITEM_H,
 };
 
-/// Thickness in logical px of the shaded lip around a recessed menubar. Two px reads as
-/// a carved edge at a glance without turning into a drawn border.
-const RECESS_EDGE_PX: f32 = 2.0;
-
 pub struct MenuBar {
     pub visible: bool,
     pub network_opacity: f32,
@@ -478,17 +474,22 @@ impl Paint for MenuBar {
             if surface[3] > 0.001 {
                 surface[3] = colors::active_backplate_opacity();
             }
-            // `depth` is the edge's thickness in px (the color offset is a separate thing:
-            // the renderer applies `bevel_depth` itself). The top corners follow the plate's
-            // radius so the lip stays inside its arc — square ones there paint a notch out
-            // past the rounded plate, into the transparent corner. The bottom corners stay
-            // square: that edge meets the content below, not the window edge.
-            let plate_r = if rect.x <= 0.5 && rect.y <= 0.5 {
-                colors::backplate_corner_radius()
+            // `depth` is the roll-off width in px (the shading amplitude is separate: the
+            // renderer applies `bevel_depth` itself), capped so a deep DE-wide setting can
+            // never swallow a short bar — the two walls would meet in the middle and the
+            // flat floor would vanish.
+            let depth = crate::layout::bevel_width().min(rect.height * 0.4);
+            if rect.x <= 0.5 && rect.y <= 0.5 {
+                // Flush with the plate's top-left: the bar is a plateau one step down, not a
+                // trough, so its only wall is the one facing the content. The other three
+                // sides are the plate's outer edge, where the plate's own roll already lives
+                // — carving there too would cut a second lip into the same pixels.
+                ctx.recess_edges(rect, (0.0, 0.0, 0.0, 0.0), surface, depth, (false, false, true, false));
             } else {
-                0.0
-            };
-            ctx.recess(rect, (plate_r, plate_r, 0.0, 0.0), surface, RECESS_EDGE_PX);
+                // Inset from the plate edge: a real trough, walled all round, its corners
+                // rounded by the roll itself.
+                ctx.recess(rect, (depth, depth, depth, depth), surface, depth);
+            }
         } else {
             // Background: always the plain quad — the rounded-against-parent variant required a
             // backplate parent, which no longer exists.
diff --git a/src/widget/display/status_bar.rs b/src/widget/display/status_bar.rs
index fba1abd..180a1f6 100644
--- a/src/widget/display/status_bar.rs
+++ b/src/widget/display/status_bar.rs
@@ -20,6 +20,11 @@ pub struct StatusBar {
     pub text_offset_x: Option<f32>,
     pub text_color: Option<[f32; 4]>,
     pub bg_color: Option<[f32; 4]>,
+    /// Draw as a step carved into the window backplate instead of an opaque slab: no
+    /// background fill of its own, just the shaded wall facing the content, so the plate
+    /// shows through. `bg_color` is ignored while this is set — see
+    /// [`Adapted::<StatusBar>::with_recess`].
+    pub recessed: bool,
 }
 
 impl StatusBar {
@@ -31,6 +36,7 @@ impl StatusBar {
             text_offset_x: None,
             text_color: None,
             bg_color: None,
+            recessed: false,
         })
     }
 
@@ -79,6 +85,16 @@ impl Adapted<StatusBar> {
         self.bg_color = Some(color);
         self
     }
+
+    /// Drop the bar's own background and sink it into the window backplate instead, the
+    /// mirror of `MenuBar::with_recess`. A status bar always sits flush with the bottom of
+    /// the plate, so it is shaded as a plateau one step down whose only wall is the top one
+    /// (facing the content) — the other three sides are the plate's outer edge, which
+    /// carries its own roll.
+    pub fn with_recess(mut self, recessed: bool) -> Self {
+        self.recessed = recessed;
+        self
+    }
 }
 
 impl Layout for StatusBar {
@@ -121,9 +137,23 @@ impl Paint for StatusBar {
     /// default `all_rounded_quads` path) — plus the text label (the legacy `text_labels`
     /// body; deliberately no `widget_font`, see module docs).
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
-        // Always the plain background quad — the rounded-against-parent variant required a
-        // backplate parent, which no longer exists.
-        ctx.quad(rect, self.bg());
+        if self.recessed {
+            // The shading base is the plate's own color (page_low at the active backplate
+            // opacity, matching how the window emits it) — these edges are that surface
+            // catching and losing light, so shading a transparent color would just produce
+            // transparent edges.
+            let mut surface = colors::page_low_color();
+            if surface[3] > 0.001 {
+                surface[3] = colors::active_backplate_opacity();
+            }
+            // Capped against the bar's own height so a deep DE-wide roll can't swallow it.
+            let depth = crate::layout::bevel_width().min(rect.height * 0.4);
+            ctx.recess_edges(rect, (0.0, 0.0, 0.0, 0.0), surface, depth, (true, false, false, false));
+        } else {
+            // Always the plain background quad — the rounded-against-parent variant required a
+            // backplate parent, which no longer exists.
+            ctx.quad(rect, self.bg());
+        }
 
         if !self.text.is_empty() {
             let offset_x = self.text_offset_x.unwrap_or(12.0);
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 06c18aa..fd894ca 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1235,7 +1235,8 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 Prim::RoundedRect { rect, radius, corners, color } => ctx.rounded_rect(rect, radius, corners, color),
                 Prim::Border { rect, radii, fill, border, thickness } => ctx.border(rect, radii, fill, border, thickness),
                 Prim::Bevel { rect, radii, color, depth } => ctx.bevel(rect, radii, color, depth),
-                Prim::Recess { rect, radii, surface, depth } => ctx.recess(rect, radii, surface, depth),
+                Prim::Recess { rect, radii, surface, depth, edges } => ctx.recess_edges(rect, radii, surface, depth, edges),
+                Prim::Plate { rect, radii, color, depth } => ctx.plate(rect, radii, color, depth),
                 Prim::Arc { cx, cy, radius, thickness, start, end, color } => ctx.arc(cx, cy, radius, thickness, start, end, color),
                 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),