GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat(scene): border/bevel/arc prims; paint_self mirrors push_widget_vertices (Phase 3)
Add Prim::Border (rounded fill + solid stroke), Prim::Bevel (inset rounded fill + beveled edges), and Prim::Arc to the display list, with PaintCtx emit methods (offset-applied). Rework Element::paint_self to mirror the toolkit's own-geometry generator push_widget_vertices: a beveled plate, else a bordered plate, else a plain rounded background (from corner_radii + rounded_corners), then arcs, decoration quads, circles, and text. So a widget's full own appearance is now expressible in the DisplayList. Backend tessellation of these prims + visual fidelity are validated when render() is routed through the walk (next, runtime-gated). Headless tests cover emission + offset; scene suite green.
Co-Authored-By: Claude Opus 4.8 <[email protected]>
src/scene/paint.rs | 43 ++++++++++++++++++++++++++++++++++++++++++-
src/widget/mod.rs | 30 ++++++++++++++++++++++--------
2 files changed, 64 insertions(+), 9 deletions(-)
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 0286291..4c6dc22 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -29,10 +29,21 @@ pub enum Cap {
/// A single paint primitive in logical pixels (absolute coordinates once emitted). These mirror
/// the toolkit's existing tessellators so a `DisplayList` maps directly onto them at draw time.
+/// Per-corner radii `(top_left, top_right, bottom_right, bottom_left)`, matching the toolkit's
+/// `CornerRadii` order.
+pub type Radii = (f32, f32, f32, f32);
+
#[derive(Clone, Debug, PartialEq)]
pub enum Prim {
Quad { rect: Rect, color: [f32; 4] },
RoundedRect { rect: Rect, radius: f32, corners: (bool, bool, bool, bool), color: [f32; 4] },
+ /// A rounded fill plus a solid border stroke — a widget's own "plate" (mirrors
+ /// `push_widget_vertices`' non-bevel branch: rounded bg + `push_plate_solid_border_vertices`).
+ Border { rect: Rect, radii: Radii, fill: [f32; 4], border: [f32; 4], thickness: f32 },
+ /// 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 },
+ 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] },
Text { text: String, x: f32, y: f32, font_size: f32, color: [u8; 3] },
@@ -163,6 +174,21 @@ impl PaintCtx {
self.push(Prim::Circle { cx: cx + ox, cy: cy + oy, radius, color });
}
+ 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 });
+ }
+
+ pub fn bevel(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
+ let rect = self.apply_offset(rect);
+ self.push(Prim::Bevel { rect, radii, color, 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 });
+ }
+
pub fn text(&mut self, text: impl Into<String>, x: f32, y: f32, font_size: f32, color: [u8; 3]) {
let (ox, oy) = self.offset;
self.push(Prim::Text { text: text.into(), x: x + ox, y: y + oy, font_size, color });
@@ -274,9 +300,24 @@ mod tests {
let mut ctx = PaintCtx::new();
ctx.quad(r(0.0, 0.0, 1.0, 1.0), [0.0; 4]);
ctx.rounded_rect(r(0.0, 0.0, 1.0, 1.0), 2.0, (true, false, true, false), [0.0; 4]);
+ ctx.border(r(0.0, 0.0, 10.0, 10.0), (2.0, 2.0, 2.0, 2.0), [0.1; 4], [0.9; 4], 1.5);
+ ctx.bevel(r(0.0, 0.0, 10.0, 10.0), (2.0, 2.0, 2.0, 2.0), [0.3; 4], 2.0);
+ ctx.arc(5.0, 5.0, 4.0, 1.0, 0.0, 3.14, [0.0; 4]);
ctx.vector(0.0, 0.0, 10.0, 0.0, 1.0, [0.0; 4], Cap::Arrow);
ctx.circle(5.0, 5.0, 3.0, [0.0; 4]);
ctx.text("hi", 1.0, 2.0, 12.0, [255, 255, 255]);
- assert_eq!(ctx.finish().len(), 5);
+ assert_eq!(ctx.finish().len(), 8);
+ }
+
+ #[test]
+ fn border_and_bevel_are_offset() {
+ let mut ctx = PaintCtx::new();
+ ctx.translate(10.0, 20.0, |ctx| {
+ ctx.border(r(0.0, 0.0, 5.0, 5.0), (1.0, 1.0, 1.0, 1.0), [0.0; 4], [1.0; 4], 1.0);
+ ctx.bevel(r(0.0, 0.0, 5.0, 5.0), (1.0, 1.0, 1.0, 1.0), [0.0; 4], 1.0);
+ });
+ let list = ctx.finish();
+ assert!(matches!(list.items[0].prim, Prim::Border { rect, .. } if rect.x == 10.0 && rect.y == 20.0));
+ assert!(matches!(list.items[1].prim, Prim::Bevel { rect, .. } if rect.x == 10.0 && rect.y == 20.0));
}
}
diff --git a/src/widget/mod.rs b/src/widget/mod.rs
index 812914f..2cf71cc 100644
--- a/src/widget/mod.rs
+++ b/src/widget/mod.rs
@@ -480,16 +480,30 @@ pub trait Element {
/// are handled by the paint walk (`scene::painter`), not here.
fn paint_self(&self, ui: &UiContext, ctx: &mut crate::scene::paint::PaintCtx) {
use crate::scene::layout::Rect;
- let (r1, r2, r3, r4) = self.rounded_corners();
- if r1 || r2 || r3 || r4 {
- let (x, y, w, h) = self.rect();
- let c = self.color();
- if c[3].abs() > 0.001 {
- ctx.rounded_rect(Rect { x, y, width: w, height: h }, self.corner_radius(), (r1, r2, r3, r4), c);
+ let (x, y, w, h) = self.rect();
+ let rect = Rect { x, y, width: w, height: h };
+ let color = self.color();
+ let cr = self.corner_radii();
+ let radii = (cr.top_left, cr.top_right, cr.bottom_right, cr.bottom_left);
+
+ // The widget's own plate — mirrors `push_widget_vertices`: a beveled plate, or a bordered
+ // plate, or (the simple case) a rounded background from `rounded_corners`.
+ if let Some(depth) = self.plate_bevel() {
+ ctx.bevel(rect, radii, color, depth);
+ } else if let Some((border_color, thickness)) = self.solid_border() {
+ ctx.border(rect, radii, color, border_color, thickness);
+ } else if color[3].abs() > 0.001 {
+ let (r1, r2, r3, r4) = self.rounded_corners();
+ if r1 || r2 || r3 || r4 {
+ ctx.rounded_rect(rect, self.corner_radius(), (r1, r2, r3, r4), color);
}
}
- for (x, y, w, h, c) in self.all_quads(ui) {
- ctx.quad(Rect { x, y, width: w, height: h }, c);
+
+ for (qx, qy, qw, qh, c) in self.all_quads(ui) {
+ ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, c);
+ }
+ for (cx, cy, r, t, start, end, c) in self.extra_arcs() {
+ ctx.arc(cx, cy, r, t, start, end, c);
}
for (cx, cy, r, c) in self.extra_circles() {
ctx.circle(cx, cy, r, c);