git.lucas.co / cce-cloud
cloud storage client
git clone https://git.lucas.co/cce-cloud.git

commit530748ce9e8c7375e5e610aacdc3e2d1a9a78e78
parent243ec8aa3d
authorLucas Galante <[email protected]>
date2026-08-01 23:30
feat: bevel effects — beveled window plate, recessed search well, raised selection

Render through the display-list tessellator (draw_frame_2d with batches +
plate features) instead of the legacy extra_quads flattening, which dropped
every prim that wasn't a plain quad. That unlocks the toolkit's relief
vocabulary:

- window background: beveled plate rim (plate_bevel_width roll)
- search bar: recessed well with the highlight-tinted rim (the focused-well
  treatment; replaces the flat 1px border quads)
- selection: raised beveled chip instead of a flat tint

Shader-lit plate batches are dropped during the close fade — their shading
is not vertex-alpha and would linger at full strength.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/main.rs | 233 +++++++++++++++++++++++++-----------------------------------
 1 file changed, 96 insertions(+), 137 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index d9f6abd..ddce1f0 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -44,98 +44,44 @@ use calloop_wayland_source::WaylandSource;
 
 use glyphon::{Attrs, Buffer, FontSystem, Metrics, SwashCache};
 
-use cce_ui::vk::{TextSpan, VkRenderer};
-
-// Vertex and quad_vertices are shared from the cce-ui engine.
-pub(crate) use cce_ui::engine::{quad_vertices, Vertex};
-
-fn rounded_rect_vertices_corners(
-    x: f32, y: f32, ww: f32, h: f32,
-    r: f32,
+use cce_ui::vk::{Batch2D, Frame2D, TextSpan, VkRenderer};
+
+// Vertex is shared from the cce-ui engine.
+pub(crate) use cce_ui::engine::Vertex;
+
+/// Tessellate a display list into the flat vertex buffer plus the renderer
+/// batches, converting the tessellator's logical-px `DlBatch` clips to
+/// `Batch2D`'s physical px (the same mapping the engine runner applies). Going
+/// through the display list — instead of the old `extra_quads` flattening —
+/// is what lets bevel/recess prims survive to the tessellator.
+fn tessellate(
+    dl: &cce_ui::scene::paint::DisplayList,
     sw: f32, sh: f32,
-    color: [f32; 4],
-    corners: (bool, bool, bool, bool),
-) -> Vec<Vertex> {
-    let mut verts = Vec::new();
-    let r = r.min(ww * 0.5).min(h * 0.5);
-
-    let push_quad = |verts: &mut Vec<Vertex>, qx: f32, qy: f32, qw: f32, qh: f32| {
-        let x0 = qx;
-        let y0 = qy;
-        let x1 = qx + qw;
-        let y1 = qy + qh;
-        
-        let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
-        let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
-        let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
-        let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
-        
-        let clip_circle = [0.0, 0.0, 0.0];
-        verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
-        verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
-        verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
-        verts.push(Vertex { position: [ndc_x1, ndc_y0], color, clip_circle });
-        verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
-        verts.push(Vertex { position: [ndc_x0, ndc_y1], color, clip_circle });
-    };
-
-    if r <= 0.1 || (!corners.0 && !corners.1 && !corners.2 && !corners.3) {
-        push_quad(&mut verts, x, y, ww, h);
-        return verts;
-    }
-
-    push_quad(&mut verts, x + r, y, ww - 2.0 * r, h);
-    push_quad(&mut verts, x, y + r, r, h - 2.0 * r);
-    push_quad(&mut verts, x + ww - r, y + r, r, h - 2.0 * r);
-
-    let corner_configs = [
-        (corners.0, x, y, x + r, y + r, std::f32::consts::PI, 1.5 * std::f32::consts::PI),
-        (corners.1, x + ww - r, y, x + ww - r, y + r, 1.5 * std::f32::consts::PI, 2.0 * std::f32::consts::PI),
-        (corners.2, x + ww - r, y + h - r, x + ww - r, y + h - r, 0.0, 0.5 * std::f32::consts::PI),
-        (corners.3, x, y + h - r, x + r, y + h - r, 0.5 * std::f32::consts::PI, std::f32::consts::PI),
-    ];
-
-    let segments = 16;
-    for &(is_rounded, sqx, sqy, cx, cy, start, end) in &corner_configs {
-        if is_rounded {
-            for i in 0..segments {
-                let theta1 = start + (i as f32) * (end - start) / (segments as f32);
-                let theta2 = start + ((i + 1) as f32) * (end - start) / (segments as f32);
-                
-                let x0 = cx;
-                let y0 = cy;
-                let x1 = cx + r * theta1.cos();
-                let y1 = cy + r * theta1.sin();
-                let x2 = cx + r * theta2.cos();
-                let y2 = cy + r * theta2.sin();
-                
-                let ndc_x0 = (x0 / sw) * 2.0 - 1.0;
-                let ndc_y0 = 1.0 - (y0 / sh) * 2.0;
-                let ndc_x1 = (x1 / sw) * 2.0 - 1.0;
-                let ndc_y1 = 1.0 - (y1 / sh) * 2.0;
-                let ndc_x2 = (x2 / sw) * 2.0 - 1.0;
-                let ndc_y2 = 1.0 - (y2 / sh) * 2.0;
-                
-                let clip_circle = [0.0, 0.0, 0.0];
-                verts.push(Vertex { position: [ndc_x0, ndc_y0], color, clip_circle });
-                verts.push(Vertex { position: [ndc_x1, ndc_y1], color, clip_circle });
-                verts.push(Vertex { position: [ndc_x2, ndc_y2], color, clip_circle });
-            }
-        } else {
-            push_quad(&mut verts, sqx, sqy, r, r);
-        }
-    }
-
-    verts
-}
-
-fn widget_vertices(w: &dyn WidgetHost, sw: f32, sh: f32) -> Vec<Vertex> {
-    let (x, y, ww, h) = w.rect();
-    let mut verts = quad_vertices(x, y, ww, h, sw, sh, w.color()).to_vec();
-    for (qx, qy, qw, qh, qc) in w.extra_quads() {
-        verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
-    }
-    verts
+    scale: f32,
+) -> (Vec<Vertex>, Vec<Batch2D>, Vec<[f32; 12]>) {
+    let (verts, dl_batches, _images, features) =
+        cce_ui::backend::window_runner::tessellate_display_list(dl, sw, sh, scale);
+    let batches = dl_batches
+        .iter()
+        .map(|b| Batch2D {
+            scissor: b.scissor.map(|c| {
+                (
+                    (c.x * scale).max(0.0) as u32,
+                    (c.y * scale).max(0.0) as u32,
+                    (c.width * scale) as u32,
+                    (c.height * scale) as u32,
+                )
+            }),
+            clip_rrect: b.clip_rrect.map(|c| {
+                [c[0] * scale, c[1] * scale, c[2] * scale, c[3] * scale, c[4] * scale]
+            }),
+            start: b.start,
+            end: b.end,
+            plate: b.plate,
+            blur_behind: b.blur_behind,
+        })
+        .collect();
+    (verts, batches, features)
 }
 
 fn make_text_buffer(font_system: &mut FontSystem, text: &str, size: f32) -> Buffer {
@@ -590,22 +536,15 @@ impl cce_ui::widget::Paint for FuzzelWidget {
         let pad = 15.0;
         let search_h = 35.0;
 
-        // Search Bar Background
-        ctx.quad(
-            Rect { x: self.x + pad, y: self.y + pad, width: self.w - pad * 2.0, height: search_h },
-            [0.10, 0.10, 0.14, 1.0],
-        );
-
-        // Search Bar Border
-        let border_color = [0.25, 0.45, 0.85, 1.0];
-        let bx = self.x + pad;
-        let by = self.y + pad;
-        let bw = self.w - pad * 2.0;
-        let bh = search_h;
-        ctx.quad(Rect { x: bx, y: by, width: bw, height: 1.0 }, border_color);
-        ctx.quad(Rect { x: bx, y: by + bh - 1.0, width: bw, height: 1.0 }, border_color);
-        ctx.quad(Rect { x: bx, y: by, width: 1.0, height: bh }, border_color);
-        ctx.quad(Rect { x: bx + bw - 1.0, y: by, width: 1.0, height: bh }, border_color);
+        // Search bar — a well recessed into the plate, its rim lit in the
+        // highlight accent (the toolkit's focused-well treatment; the query
+        // line always holds keyboard focus here). Replaces the flat fill +
+        // 1px border quads.
+        let well = Rect { x: self.x + pad, y: self.y + pad, width: self.w - pad * 2.0, height: search_h };
+        ctx.quad(well, [0.10, 0.10, 0.14, 1.0]);
+        let depth = cce_ui::layout::bevel_width().min(search_h * 0.2);
+        let hc = cce_ui::color::highlight_primary_color();
+        ctx.recess_tinted(well, (0.0, 0.0, 0.0, 0.0), depth, [hc[0], hc[1], hc[2]]);
 
         // ScrollBox quads
         let mut quads = Vec::new();
@@ -620,15 +559,16 @@ impl cce_ui::widget::Paint for FuzzelWidget {
             let virtual_selected_y = self.selected as f32 * item_h;
             if let Some(draw_y) = self.scroll_box.get_draw_y(virtual_selected_y, item_h) {
                 let scrollbar_w = if self.scroll_box.content_h > self.scroll_box.viewport_h { 10.0 } else { 0.0 };
-                ctx.quad(
-                    Rect {
-                        x: self.x + pad + 2.0,
-                        y: draw_y,
-                        width: self.w - pad * 2.0 - 4.0 - scrollbar_w,
-                        height: item_h - 2.0,
-                    },
-                    [0.20, 0.35, 0.65, 0.9],
-                );
+                // A raised beveled chip, not a flat tint: the selection reads
+                // as sitting proud of the list the way focused panes do.
+                let sel = Rect {
+                    x: self.x + pad + 2.0,
+                    y: draw_y,
+                    width: self.w - pad * 2.0 - 4.0 - scrollbar_w,
+                    height: item_h - 2.0,
+                };
+                let depth = cce_ui::color::plate_bevel_width().min(sel.height * 0.2);
+                ctx.bevel(sel, (4.0, 4.0, 4.0, 4.0), [0.20, 0.35, 0.65, 0.9], depth);
             }
         }
 
@@ -707,6 +647,8 @@ struct State {
     // (the swapchain must not outlive its Wayland surface).
     renderer: Option<VkRenderer>,
     vertex_data: Vec<Vertex>,
+    frame_batches: Vec<Batch2D>,
+    plate_features: Vec<[f32; 12]>,
 
     fuzzel: cce_ui::widget::Adapted<FuzzelWidget>,
     json_layout: Option<cce_ui::widget::Adapted<JsonLayoutWidget>>,
@@ -1011,6 +953,8 @@ impl State {
             wl_surface,
             renderer: Some(renderer),
             vertex_data: Vec::new(),
+            frame_batches: Vec::new(),
+            plate_features: Vec::new(),
             fuzzel,
             json_layout,
             font_system,
@@ -1208,51 +1152,59 @@ impl State {
         }
     }
 
-    fn collect_vertices(&self) -> Vec<Vertex> {
-        let sw = self.width;
-        let sh = self.height;
-        let mut verts = Vec::new();
+    fn collect_display_list(&self) -> cce_ui::scene::paint::DisplayList {
+        use cce_ui::scene::layout::Rect;
+        let mut pc = cce_ui::scene::paint::PaintCtx::new();
 
-        // 1. Window background — the dissolved Backplate's exact emission: base color at
-        // the configured backplate opacity, all corners rounded when the radius is set.
+        // 1. Window background — the dissolved Backplate's emission (base color at
+        // the configured backplate opacity), now as a beveled plate: the rolled
+        // rim makes the popup read as a raised surface instead of a flat sheet.
         let mut bg_color = self.window_bg;
         if bg_color[3] > 0.001 {
             bg_color[3] = cce_ui::color::active_backplate_opacity();
         }
         if bg_color[3] > 0.0 {
-            let r = self.window_radius;
-            let corners = if r > 0.1 { (true, true, true, true) } else { (false, false, false, false) };
+            let r = if self.window_radius > 0.1 { self.window_radius } else { 0.0 };
             let (wx, wy, ww, wh) = self.window_rect;
-            verts.extend(rounded_rect_vertices_corners(wx, wy, ww, wh, r, sw, sh, bg_color, corners));
+            pc.bevel(
+                Rect { x: wx, y: wy, width: ww, height: wh },
+                (r, r, r, r),
+                bg_color,
+                cce_ui::color::plate_bevel_width(),
+            );
         }
 
-        // 3. Draw child widgets
+        // 2. Child widgets, through the paint walk: bevel/recess prims reach the
+        // tessellator instead of being flattened away by the legacy quad bridges.
         if self.mode == LauncherMode::Json {
             if let Some(jl) = &self.json_layout {
-                for (qx, qy, qw, qh, qc) in jl.all_quads(&self.ui_context) {
-                    verts.extend(quad_vertices(qx, qy, qw, qh, sw, sh, qc));
-                }
-                for (qx, qy, qw, qh, qr, qc, qcorners) in jl.all_rounded_quads(&self.ui_context) {
-                    verts.extend(rounded_rect_vertices_corners(qx, qy, qw, qh, qr, sw, sh, qc, qcorners));
-                }
+                jl.paint_self(&self.ui_context, &mut pc);
             }
         } else {
-            verts.extend(widget_vertices(&self.fuzzel, sw, sh));
+            self.fuzzel.paint_self(&self.ui_context, &mut pc);
         }
 
-        verts
+        pc.finish()
     }
 
     fn upload_vertices(&mut self) {
-        let mut verts = self.collect_vertices();
+        let dl = self.collect_display_list();
+        let (mut verts, mut batches, features) =
+            tessellate(&dl, self.width, self.height, self.scale as f32);
         if self.fade_factor < 1.0 {
             for v in &mut verts {
                 v.color[3] *= self.fade_factor;
             }
+            // SDF-plate overlays (the recess rims) are shader-lit, not
+            // vertex-alpha — drop them during the close fade rather than let
+            // their shading linger at full strength over fading geometry.
+            batches.retain(|b| b.plate.is_none());
         }
-        // The GPU upload happens in VkRenderer::draw_frame, which consumes
+        // The GPU upload happens in VkRenderer::draw_frame_2d, which consumes
         // vertex_data every frame.
         self.vertex_data = verts;
+        self.frame_batches = batches;
+        self.plate_features = features;
     }
 
     fn prepare_text(&mut self) {
@@ -1321,7 +1273,14 @@ impl State {
         self.fade_factor = fade_factor;
         self.upload_vertices();
         self.prepare_text();
-        self.renderer.as_mut().unwrap().draw_frame(&self.vertex_data);
+        self.renderer.as_mut().unwrap().draw_frame_2d(Frame2D {
+            verts: &self.vertex_data,
+            batches: &self.frame_batches,
+            overlay_verts: &[],
+            images: &[],
+            plate_features: &self.plate_features,
+            clear_color: [0.0; 4],
+        });
         false
     }
 }