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

commitc8578379939fb2e0cc344fbbebaa7b43237996d9
parent67bddb49aa
authorLucas Galante <[email protected]>
date2026-07-19 01:23
feat: light the plate face — the flat-fill-to-object fix

A Plate's interior was one uniform color, and a uniform face is why the bevel
read as frame decoration rather than shape: the eye integrates the (large)
face first, concludes flat, and discounts the edges. The face now carries a
single diagonal luminance gradient — brightest at the corner facing
light_source_position, darkest at the opposite one — emitted through the same
two-pass white/black overlay machinery as the bevels, at 0.35 of bevel_depth
so the plane reads as leaning toward the light, not rolling.

The overlay quad is square; its corners poke past a rounded plate's arcs, but
the compositor clips the window surface to the same radius so the overhang
never reaches the screen.

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

 src/backend/window_runner.rs | 57 ++++++++++++++++++++++++++++++++++++++++----
 1 file changed, 53 insertions(+), 4 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index f6f7cd0..8e6bbb8 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1212,6 +1212,54 @@ pub fn push_bevel_edge_vertices_banded(
     }
 }
 
+/// How strong the face gradient is, as a fraction of `bevel_depth` at the corner nearest
+/// the light. Deliberately well below the edge amplitude: the face is a plane, not a
+/// roll — it only *leans* toward the light.
+const FACE_RATIO: f32 = 0.35;
+
+/// The face lighting of a plate: a single diagonal luminance gradient across the whole
+/// surface, brightest at the corner facing `light_source_position` and darkest at the
+/// opposite one. This is the difference between an object and a sticker: a real surface
+/// under directional light is never uniform, and a perfectly flat fill makes the eye
+/// read the (much smaller) edge shading as frame decoration rather than shape.
+///
+/// Emitted as the same two-pass white/black overlays as the bevels (see
+/// [`overlay_light`]/[`overlay_dark`]): fixed RGB per pass, per-corner alphas clamped at
+/// the terminator, bilinear across the quad. The quad is square — its corners poke past
+/// a rounded plate's arcs — but the compositor clips the window surface to the same
+/// radius, so the overhang never reaches the screen.
+pub fn push_plate_face_vertices(
+    x: f32, y: f32, ww: f32, h: f32,
+    sw: f32, sh: f32,
+    clip_circle: [f32; 3],
+    out: &mut Vec<Vertex>,
+) {
+    let rad = crate::layout::light_source_position();
+    let (lx, ly) = (rad.cos(), -rad.sin());
+    let amp = crate::layout::bevel_depth() * FACE_RATIO;
+    // Corner value = how much its outward diagonal faces the light.
+    let inv = std::f32::consts::FRAC_1_SQRT_2;
+    let v_tl = amp * inv * (-lx - ly);
+    let v_tr = amp * inv * (lx - ly);
+    let v_br = amp * inv * (lx + ly);
+    let v_bl = amp * inv * (-lx + ly);
+    let vs = [v_tl, v_tr, v_br, v_bl];
+    if vs.iter().any(|&v| v > 0.0) {
+        out.extend_from_slice(&quad_vertices_shaded(
+            x, y, ww, h, sw, sh,
+            overlay_light(v_tl), overlay_light(v_tr), overlay_light(v_br), overlay_light(v_bl),
+            clip_circle,
+        ));
+    }
+    if vs.iter().any(|&v| v < 0.0) {
+        out.extend_from_slice(&quad_vertices_shaded(
+            x, y, ww, h, sw, sh,
+            overlay_dark(v_tl), overlay_dark(v_tr), overlay_dark(v_br), overlay_dark(v_bl),
+            clip_circle,
+        ));
+    }
+}
+
 pub fn push_plate_solid_border_vertices(
     x: f32, y: f32, ww: f32, h: f32,
     radii: crate::widget::CornerRadii,
@@ -1423,10 +1471,10 @@ 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 } => {
-                // 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.
+                // 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
+                // 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,
@@ -1434,6 +1482,7 @@ pub fn tessellate_display_list(
                 push_rounded_rect_vertices_corners(
                     rect.x, rect.y, rect.width, rect.height, corners, sw, sh, *color, no, None, &mut verts,
                 );
+                push_plate_face_vertices(rect.x, rect.y, rect.width, rect.height, sw, sh, no, &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,