GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: add a Recess paint primitive and a recessed MenuBar
Recess is the inverse of Bevel: edges only, no fill, with the light vector
negated so the light-facing edges fall into shadow. What it carves into is
whatever was painted below, so the surface color only shades the lip.
Bevel edge tessellation gains a light_sign parameter and per-corner radii, so
a recess along the top of a rounded plate keeps the plate's arc on its top
corners and square ones where it meets the content.
MenuBar::with_recess drops the bar's own background and carves the window
backplate instead.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
src/backend/window_runner.rs | 84 +++++++++++++++++++++++++++++++++++++-------
src/scene/paint.rs | 18 ++++++++++
src/widget/container/menu.rs | 47 +++++++++++++++++++++++--
src/widget/model.rs | 1 +
4 files changed, 135 insertions(+), 15 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index cab3275..5123568 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -824,11 +824,56 @@ pub fn push_plate_bevel_vertices(
clip_circle: [f32; 3],
out: &mut Vec<Vertex>,
) {
- let r = r.min(ww * 0.5).min(h * 0.5);
+ push_bevel_edge_vertices(x, y, ww, h, r, t, sw, sh, base_color, clip_circle, 1.0, out);
+}
+
+/// The bevel edge shading, with the light direction selectable: `light_sign` is `1.0`
+/// for a raised plate (edges facing `light_source_position` are lit) and `-1.0` for a
+/// recess (those same edges fall into shadow instead, and the far edges catch the
+/// light). Negating the whole light vector flips every edge and every corner segment
+/// consistently, because both the flat-edge factors and the arc-normal dot product
+/// below are linear in it.
+pub fn push_bevel_edge_vertices(
+ x: f32, y: f32, ww: f32, h: f32,
+ r: f32,
+ t: f32,
+ sw: f32, sh: f32,
+ base_color: [f32; 4],
+ clip_circle: [f32; 3],
+ light_sign: f32,
+ out: &mut Vec<Vertex>,
+) {
+ push_bevel_edge_vertices_radii(
+ x, y, ww, h, (r, r, r, r), t, sw, sh, base_color, clip_circle, light_sign, out,
+ );
+}
+
+/// As [`push_bevel_edge_vertices`], but with a per-corner radius (TL, TR, BR, BL) so the
+/// lip can follow a shape whose corners differ — a recess carved along the top of a
+/// rounded plate needs the plate's radius on its top corners and square ones where it
+/// meets the content below. A uniform radius there would either square off the plate's
+/// arc (painting a notch outside it) or wrongly round the inner corners.
+pub fn push_bevel_edge_vertices_radii(
+ 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,
+ out: &mut Vec<Vertex>,
+) {
+ let cap = ww.min(h) * 0.5;
+ let (tl, tr, br, bl) = (
+ radii.0.clamp(0.0, cap),
+ radii.1.clamp(0.0, cap),
+ radii.2.clamp(0.0, cap),
+ radii.3.clamp(0.0, cap),
+ );
let rad = crate::layout::light_source_position();
- let lx = rad.cos();
- let ly = -rad.sin();
+ let lx = rad.cos() * light_sign;
+ let ly = -rad.sin() * light_sign;
let edge_color = |factor: f32| -> [f32; 4] {
let max_offset = crate::layout::bevel_depth();
@@ -846,20 +891,26 @@ pub fn push_plate_bevel_vertices(
let bottom_color = edge_color(ly);
let right_color = edge_color(lx);
- out.extend_from_slice(&quad_vertices_with_clip(x + r, y, ww - 2.0 * r, t, sw, sh, top_color, clip_circle));
- out.extend_from_slice(&quad_vertices_with_clip(x, y + r, t, h - 2.0 * r, sw, sh, left_color, clip_circle));
- out.extend_from_slice(&quad_vertices_with_clip(x + r, y + h - t, ww - 2.0 * r, t, sw, sh, bottom_color, clip_circle));
- out.extend_from_slice(&quad_vertices_with_clip(x + ww - t, y + r, t, h - 2.0 * r, sw, sh, right_color, clip_circle));
+ // 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));
let segments = 16;
let corners = [
- (x + r, y + r, std::f32::consts::PI, 1.5 * std::f32::consts::PI), // Top-Left
- (x + ww - r, y + r, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI), // Top-Right
- (x + ww - r, y + h - r, 0.0, 0.5 * std::f32::consts::PI), // Bottom-Right
- (x + r, y + h - r, 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), // 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
];
- for &(cx, cy, start_angle, end_angle) in &corners {
+ 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 {
+ continue;
+ }
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);
@@ -1093,6 +1144,15 @@ 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 } => {
+ // 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(
+ rect.x, rect.y, rect.width, rect.height, *radii, *depth,
+ sw, sh, *surface, no, -1.0, &mut verts,
+ );
+ }
Prim::Arc { cx, cy, radius, thickness, start: sa, end: ea, color } => {
push_arc_background_vertices(*cx, *cy, *radius, *thickness, *sa, *ea, sw, sh, *color, segs(*radius), no, &mut verts);
}
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 43b3cc3..ed7ca28 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -43,6 +43,17 @@ pub enum Prim {
/// A beveled plate: an inset rounded fill plus lightened/darkened edges (mirrors
/// `push_widget_vertices`' bevel branch).
Bevel { rect: Rect, radii: Radii, color: [f32; 4], depth: f32 },
+ /// A recess carved into whatever is already painted underneath — the inverse of
+ /// `Bevel`. Emits ONLY the shaded edges, never a fill, so the surface below shows
+ /// through the middle: a relief cut into the backplate rather than a plate laid on
+ /// top of it. The light vector is negated relative to `Bevel`, so the edges facing
+ /// `light_source_position` fall into shadow and the far edges catch the light —
+ /// which is what reads as "lower" instead of "raised".
+ ///
+ /// `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 },
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] },
@@ -326,6 +337,13 @@ impl PaintCtx {
self.push(Prim::Bevel { rect, radii, color, depth });
}
+ /// 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) {
+ let rect = self.apply_offset(rect);
+ self.push(Prim::Recess { rect, radii, surface, depth });
+ }
+
pub fn arc(&mut self, cx: f32, cy: f32, radius: f32, thickness: f32, start: f32, end: f32, color: [f32; 4]) {
let (ox, oy) = self.offset;
self.push(Prim::Arc { cx: cx + ox, cy: cy + oy, radius, thickness, start, end, color });
diff --git a/src/widget/container/menu.rs b/src/widget/container/menu.rs
index 77412d0..4925bec 100644
--- a/src/widget/container/menu.rs
+++ b/src/widget/container/menu.rs
@@ -27,12 +27,20 @@ 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,
pub curved_circle: Option<(f32, f32, f32)>,
pub blur: bool,
pub color: Option<[f32; 4]>,
+ /// Draw as a recess carved into the window backplate instead of as an opaque bar:
+ /// no background fill of its own, just shaded edges, so the plate shows through.
+ /// `color` is ignored while this is set — see [`Adapted::<MenuBar>::with_recess`].
+ pub recessed: bool,
pub title: String,
pub menus: Adapted<ButtonStrip>,
pub menu_items: Vec<String>,
@@ -68,6 +76,7 @@ impl MenuBar {
curved_circle: None,
blur: false,
color: None,
+ recessed: false,
title: String::new(),
menus: Adapted::new(ButtonStrip::new(x, y, w, h).with_inherit_menubar_font(true)),
menu_items: Vec::new(),
@@ -349,6 +358,15 @@ impl Adapted<MenuBar> {
self
}
+ /// Drop the bar's own background and carve it into the window backplate instead, so
+ /// the plate reads as recessed under the menu — a relief cut into the surface rather
+ /// than a slab sitting on it. Shading follows the DE-wide `light_source_position` /
+ /// `bevel_depth` config, inverted so the light-facing edges are the shadowed ones.
+ pub fn with_recess(mut self, recessed: bool) -> Self {
+ self.recessed = recessed;
+ self
+ }
+
pub fn with_blur(mut self, blur: bool) -> Self {
self.blur = blur;
self
@@ -450,9 +468,32 @@ impl Paint for MenuBar {
}
fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
- // Background: always the plain quad — the rounded-against-parent variant required a
- // backplate parent, which no longer exists.
- ctx.quad(rect, self.bg_color());
+ if self.recessed {
+ // No background of our own: carve the backplate instead, so the plate below is
+ // what shows through the bar. The shading base has to be the plate's own color
+ // (matching how the window emits it: page_low tinted by the active-backplate
+ // opacity), because the recess edges are that surface catching/losing light —
+ // 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();
+ }
+ // `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()
+ } else {
+ 0.0
+ };
+ ctx.recess(rect, (plate_r, plate_r, 0.0, 0.0), surface, RECESS_EDGE_PX);
+ } else {
+ // Background: always the plain quad — the rounded-against-parent variant required a
+ // backplate parent, which no longer exists.
+ ctx.quad(rect, self.bg_color());
+ }
// Title highlight while the context dropdown is open / hovered.
if !self.context_options.is_empty() {
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 3b69a78..06c18aa 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1235,6 +1235,7 @@ 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::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),