GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: fill-less roll overlay — negative Plate depth (MODE_ROLL)
A window whose face is not a plate fill (the designer's full-bleed 3D
canvas) could not wear the window-edge roll: a Plate/Bevel owns fill and
roll together, and the Boss rim it used instead straddles the boundary —
the compositor clip eats the outer half, so the visible band ran half a
roll wide and started at mid-slope.
Negative depth on Prim::Plate is the fill-less sentinel: no fill, and
the roll — same profile, crest and specular as MODE_PLATE, full width
inside the silhouette — composites as an overlay (white screen / black
multiply) like the free carves. The batch is not opened as a carve host:
an overlay owns no surface for a CSG feature to cut into. The legacy
banded path draws nothing for it (no overlay compositing there). No
existing caller passes negative depth, so every current plate is
byte-identical.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/backend/window_runner.rs | 30 +++++++++++++++++++++++++-----
src/scene/paint.rs | 10 +++++++++-
src/vk/shader2d.wgsl | 28 ++++++++++++++++++++++++++++
3 files changed, 62 insertions(+), 6 deletions(-)
diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index de9847c..48e648f 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1706,11 +1706,24 @@ pub fn tessellate_display_list(
made_plate = Some(*rect);
}
Prim::Plate { rect, radii, color, depth } if shader_plates => {
- // Same lit-plate branch; the cover quad is the exact rect so the
- // silhouette and the compositor's rounded window corners agree.
- verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
- plate = Some(plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, true));
- made_plate = Some(*rect);
+ if *depth < 0.0 {
+ // Negative depth = fill-less roll overlay (MODE_ROLL): the
+ // window-edge roll shading alone, screened over whatever is
+ // beneath — for a root plate whose face is not a fill (the
+ // designer's 3D canvas). The cover quad carries no color,
+ // and the batch is NOT opened as a carve host: an overlay
+ // owns no surface for a CSG feature to cut into.
+ verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, [0.0; 4]));
+ let mut p = plate_push_raised(rect, *radii, -*depth, scale, plate_light, plate_mat, true);
+ p.mode = 11.0; // MODE_ROLL
+ plate = Some(p);
+ } else {
+ // Same lit-plate branch; the cover quad is the exact rect so the
+ // silhouette and the compositor's rounded window corners agree.
+ verts.extend(quad_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, *color));
+ plate = Some(plate_push_raised(rect, *radii, *depth, scale, plate_light, plate_mat, true));
+ made_plate = Some(*rect);
+ }
}
Prim::Recess { rect, radii, depth, edges, .. }
| Prim::Boss { rect, radii, depth, edges, .. }
@@ -1945,6 +1958,13 @@ pub fn tessellate_display_list(
push_plate_bevel_vertices(rect.x, rect.y, rect.width, rect.height, radii.0, *depth, sw, sh, *color, no, &mut verts);
}
Prim::Plate { rect, radii, color, depth } => {
+ if *depth < 0.0 {
+ // Fill-less roll overlay (negative-depth sentinel): the banded
+ // legacy tessellation has no overlay compositing, so the roll
+ // is simply absent here — the A/B path draws nothing rather
+ // than a wrong fill.
+ continue;
+ }
// Fill at full size (no inset — see Prim::Plate), then light the face,
// 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
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 4c2ad01..09e522f 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -52,7 +52,8 @@ pub struct PlateSpec {
pub blur: bool,
/// Which corners lie ON the window silhouette (TL, TR, BR, BL).
pub window_corners: (bool, bool, bool, bool),
- /// Transition-band width of the rolled perimeter.
+ /// Transition-band width of the rolled perimeter. Negative = the fill-less
+ /// roll-overlay sentinel (see [`PaintCtx::plate`]).
pub depth: f32,
}
@@ -1022,6 +1023,13 @@ impl PaintCtx {
/// 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.
+ ///
+ /// A NEGATIVE `depth` is the fill-less sentinel: no fill is drawn, and the
+ /// rolled perimeter (width `-depth`) renders as an overlay — translucent
+ /// white screen / black multiply — over whatever is beneath, for a root
+ /// plate whose face is not a fill (the designer's full-bleed 3D canvas).
+ /// `color` is ignored; the roll profile, crest and specular are exactly the
+ /// positive-depth plate's.
pub fn plate(&mut self, rect: Rect, radii: Radii, color: [f32; 4], depth: f32) {
let rect = self.apply_offset(rect);
self.push(Prim::Plate { rect, radii, color, depth });
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 92efaf0..2bac4df 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -130,6 +130,7 @@ const MODE_FILLET_UP: i32 = 7; // concave inside-corner wall, raised
const MODE_GROOVE: i32 = 8; // slab carve about an arbitrary line
const MODE_TROUGH: i32 = 9; // sunken valley straddling the boundary
const MODE_DROPLET: i32 = 10; // hanging water droplet clinging to the box top
+const MODE_ROLL: i32 = 11; // fill-less rolled perimeter, composited as an overlay
// Fillet modes rejoin the shared free-carve path as their flat equivalents.
const FILLET_TO_STEP: i32 = 4; // 6 -> RECESS, 7 -> BOSS
@@ -555,6 +556,33 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
return vec4f(base.rgb * shade + rrect_clip.p_spec_tint.rgb * (spec * strength), abs(base.a) * aa);
}
+ if (mode == MODE_ROLL) {
+ // Fill-less rolled perimeter: MODE_PLATE's roll — same profile, crest
+ // and specular, spanning the full width INSIDE the silhouette — for a
+ // window whose face is not a plate fill (the designer's full-bleed 3D
+ // canvas). With no fill to shade into, it composites like the free
+ // carves: darkening is a black multiply, brightening a translucent
+ // white screen, over whatever is beneath. No CSG features: an overlay
+ // owns no surface, so carves never group into it (the tessellator
+ // never opens it as a host).
+ let aa = clamp(d + 0.5, 0.0, 1.0);
+ if (aa <= 0.0) {
+ discard;
+ }
+ let u = clamp(d / t, 0.0, 1.0);
+ let f = 1.0 - u;
+ let sv = gd.xy * roll_slope(f);
+ let extra = PLATE_CREST * f * f * f;
+ 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);
+ let v = (diff / flat_shade - 1.0 + extra + spec) * strength * aa;
+ if (v >= 0.0) {
+ return vec4f(1.0, 1.0, 1.0, min(v, 1.0));
+ }
+ return vec4f(0.0, 0.0, 0.0, min(-v, 1.0));
+ }
+
// Free-floating recess, boss, or ridge (one not grouped into a host plate —
// e.g. in a widget's own paint): an overlay over whatever is painted
// beneath — no fill, no silhouette. Junction behavior here is the heuristic