git.lucas.co / cce-files
file manager
git clone https://git.lucas.co/cce-files.git

commit4671ae69fd78c25a643a2a69e7a46fc9b889754e
parent32b66764dd
authorLucas Galante <[email protected]>
date2026-08-14 19:03
feat: mirror the breadcrumb's slanted segment seams

render_widget drops the relief prims Breadcrumb::paint emits, so every page
showing a breadcrumb carves it app-side. That carve is now one plate for the
whole segment run plus a groove per seam, and it lives in one place —
pages::breadcrumb_relief — instead of the same block copied into all three
pages.

The seams did not appear at first: rebuild_layout merged each page's
PageContent field by hand, four sites of `pc.x.extend(other.x)`, and nothing
adds a line for a vec you just introduced. PageContent::absorb replaces those
runs and destructures the source, so the next new field is a compile error
rather than a mark missing from the screen.

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

 src/main.rs          | 56 +++++++++++++++++++++++++++++++++-------------------
 src/pages/browse.rs  | 14 +++++--------
 src/pages/mod.rs     | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++
 src/pages/network.rs | 10 +---------
 src/pages/space.rs   |  6 +-----
 5 files changed, 99 insertions(+), 43 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index f1c41d3..4c8781e 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -136,6 +136,11 @@ enum WidgetFx {
     /// A GPU-textured quad; the id comes from `cce_ui::vk::upload_rgba`
     /// (the preview pane's image). `color` is unused.
     Image { id: u32, alpha: f32 },
+    /// A line engraved from (ax, ay) to (bx, by) into the widget's rect, which
+    /// is the HOST surface here rather than the mark's own bounds — the shading
+    /// fades out across the host's rolled edge. The breadcrumb's slanted seams;
+    /// the only quad in this list that is not axis-aligned.
+    Groove { ax: f32, ay: f32, bx: f32, by: f32, width: f32, depth: f32 },
 }
 
 struct AppWidget {
@@ -523,13 +528,12 @@ impl FilesystemApp {
                 // The dissolved root plate that used to sit between them is now a
                 // Prim::Plate emitted FIRST in display_list — the lit window slab the
                 // rest of the frame sits on (and the surface the band carves CSG into).
-                let (plain, rounded): (Vec<_>, Vec<_>) = plain_pc.rects.into_iter().partition(|r| r.5 <= 0.1);
+                let mut plain_pc = plain_pc;
+                let (plain, rounded): (Vec<_>, Vec<_>) =
+                    std::mem::take(&mut plain_pc.rects).into_iter().partition(|r| r.5 <= 0.1);
                 window_pc.rects.extend(plain);
                 window_pc.rects.extend(rounded);
-                window_pc.texts.extend(plain_pc.texts);
-                window_pc.buttons.extend(plain_pc.buttons);
-                window_pc.reliefs.extend(plain_pc.reliefs);
-                window_pc.images.extend(plain_pc.images);
+                window_pc.absorb(plain_pc);
 
                 // The view dropdown's flush inset plate (control_relief) lives
                 // in its modern paint(); the flat view loses it — restore it as
@@ -549,31 +553,19 @@ impl FilesystemApp {
                 let (bx, by, bw, bh) = self.browse_split.left_rect();
                 let browse_pc = pages::browse::view(&mut self.browse, &mut self.view_dropdown, bx, by, bw, bh, self.select_mode, &mut self.ui_context);
 
-                pc.rects.extend(browse_pc.rects);
-                pc.texts.extend(browse_pc.texts);
-                pc.buttons.extend(browse_pc.buttons);
-                pc.reliefs.extend(browse_pc.reliefs);
-                pc.images.extend(browse_pc.images);
+                pc.absorb(browse_pc);
             }
             Page::Network => {
                 let (nx, ny, nw, nh) = self.network_split.left_rect();
                 let network_pc = pages::network::view(&mut self.network, &self.browse, &mut self.view_dropdown, nx, ny, nw, nh, &mut self.ui_context);
 
-                pc.rects.extend(network_pc.rects);
-                pc.texts.extend(network_pc.texts);
-                pc.buttons.extend(network_pc.buttons);
-                pc.reliefs.extend(network_pc.reliefs);
-                pc.images.extend(network_pc.images);
+                pc.absorb(network_pc);
             }
             Page::Space => {
                 let (sx, sy, sw, sh) = self.space_split.left_rect();
                 let space_pc = pages::space::view(&mut self.space, &self.browse, &mut self.view_dropdown, sx, sy, sw, sh, &mut self.ui_context);
 
-                pc.rects.extend(space_pc.rects);
-                pc.texts.extend(space_pc.texts);
-                pc.buttons.extend(space_pc.buttons);
-                pc.reliefs.extend(space_pc.reliefs);
-                pc.images.extend(space_pc.images);
+                pc.absorb(space_pc);
             }
         }
 
@@ -772,6 +764,27 @@ impl FilesystemApp {
                     },
                 });
             }
+            for (ax, ay, bx, by, gw, gd, hx, hy, hw, hh) in &pc_part.grooves {
+                // Clipped by the HOST rect, not the seam's own span: a groove
+                // whose host is scrolled out has nothing left to engrave.
+                let (mut wy, mut wh) = (*hy, *hh);
+                if is_page_content {
+                    match clip_to_viewport(wy, wh, content_y, content_y + content_h) {
+                        Some((cy, ch)) => { wy = cy; wh = ch; }
+                        None => continue,
+                    }
+                }
+                widgets.push(AppWidget {
+                    x: *hx,
+                    y: wy,
+                    w: *hw,
+                    h: wh,
+                    color: [0.0; 4],
+                    radius: 0.0,
+                    corners: (true, true, true, true),
+                    fx: WidgetFx::Groove { ax: *ax, ay: *ay, bx: *bx, by: *by, width: *gw, depth: *gd },
+                });
+            }
             for (btn, action) in &pc_part.buttons {
                 let base = btn.base();
                 let bg = btn.bg.unwrap_or([0.16, 0.16, 0.24, 1.0]);
@@ -1349,6 +1362,9 @@ impl Application for FilesystemApp {
                 WidgetFx::Recess(depth) => pc.recess(rect, radii, depth),
                 WidgetFx::Inset(depth) => pc.inset_plate(rect, radii, w.color, depth),
                 WidgetFx::Image { id, alpha } => pc.image(id, rect, alpha),
+                WidgetFx::Groove { ax, ay, bx, by, width, depth } => {
+                    pc.groove((ax, ay), (bx, by), width, depth, rect)
+                }
                 WidgetFx::Flat => {
                     if w.radius > 0.1 {
                         pc.rounded_rect(rect, w.radius, w.corners, w.color);
diff --git a/src/pages/browse.rs b/src/pages/browse.rs
index 7c1c92c..f272863 100644
--- a/src/pages/browse.rs
+++ b/src/pages/browse.rs
@@ -198,17 +198,13 @@ pub fn view(state: &mut BrowseState, view_dropdown: &mut cce_ui::widget::Adapted
     let breadcrumb_w = client_w - dropdown_w - gap;
     let (bx, by, bw, bh) = layout.allocate(breadcrumb_w, breadcrumb_h);
     cce_ui::layout::render_widget(&mut pc, &mut state.breadcrumb, bx, by, bw, bh, ctx);
-    // The breadcrumb's well + per-segment button plates live in its modern
-    // paint(); the flat view this host renders through loses them, so carve
-    // here: the full-width recessed well, then each segment's raised button
-    // within it (the dropdown-mirror pairing).
+    // The breadcrumb's well + segment plate live in its modern paint(); the flat
+    // view this host renders through loses them, so carve here: the full-width
+    // recessed well, then the run's raised plate within it (the dropdown-mirror
+    // pairing), divided by the slanted seams.
     {
-        let r = cce_ui::layout::breadcrumb_corner_radius();
-        pc.relief_recessed(bx, by, bw, bh, r);
         let rect = cce_ui::scene::layout::Rect { x: bx, y: by, width: bw, height: bh };
-        for (sx, sy, sw, sh) in state.breadcrumb.segment_boxes(rect) {
-            pc.relief_raised(sx, sy, sw, sh, r.min(sh * 0.5));
-        }
+        crate::pages::breadcrumb_relief(&mut pc, &state.breadcrumb, rect);
     }
     cce_ui::layout::render_widget(&mut pc, view_dropdown, bx + bw + gap, by, dropdown_w, breadcrumb_h, ctx);
 
diff --git a/src/pages/mod.rs b/src/pages/mod.rs
index daad095..9ceec00 100644
--- a/src/pages/mod.rs
+++ b/src/pages/mod.rs
@@ -36,6 +36,28 @@ impl Page {
     }
 }
 
+/// Mirror the breadcrumb's relief into a flat-path [`PageContent`]: the
+/// full-width recessed well, the ONE raised plate the segment run shares, and a
+/// slanted seam engraved at each boundary between two segments.
+///
+/// `render_widget` drops the relief prims `Breadcrumb::paint` emits, so every
+/// page that shows a breadcrumb has to carve it app-side — this is that carve,
+/// in one place, since all three pages want it identically.
+pub fn breadcrumb_relief(
+    pc: &mut PageContent,
+    breadcrumb: &cce_ui::widget::Adapted<cce_ui::widget::Breadcrumb>,
+    rect: cce_ui::scene::layout::Rect,
+) {
+    let r = cce_ui::layout::breadcrumb_corner_radius();
+    pc.relief_recessed(rect.x, rect.y, rect.width, rect.height, r);
+    let Some(run) = breadcrumb.run_box(rect) else { return };
+    let (rx, ry, rw, rh) = run;
+    pc.relief_raised(rx, ry, rw, rh, r.min(rh * 0.5));
+    for (a, b) in breadcrumb.seams(rect) {
+        pc.groove(a, b, cce_ui::widget::Breadcrumb::SEAM_WIDTH, run);
+    }
+}
+
 pub const RELIEF_RECESSED: u8 = 0;
 pub const RELIEF_RAISED: u8 = 1;
 pub const RELIEF_INSET: u8 = 2;
@@ -49,6 +71,13 @@ pub struct PageContent {
     /// own the faces; these are the edges-only walls emitted over them (the
     /// ParametersBg::reliefs idiom for flat-view hosts).
     pub reliefs: Vec<(f32, f32, f32, f32, f32, f32, u8)>,
+    /// Engraved lines over the flat rects — (ax, ay, bx, by, width, depth) plus
+    /// the (x, y, w, h) of the surface being engraved, which the shading fades
+    /// out against. Unlike [`reliefs`] these are not axis-aligned: this is the
+    /// breadcrumb's slanted segment seams.
+    ///
+    /// [`reliefs`]: PageContent::reliefs
+    pub grooves: Vec<(f32, f32, f32, f32, f32, f32, f32, f32, f32, f32)>,
     /// GPU-textured quads — (image id from `cce_ui::vk::upload_rgba`, x, y, w, h,
     /// alpha). Drawn after the part's rects, so a fill emitted earlier is the floor
     /// beneath the image and overlay parts still cover it.
@@ -62,10 +91,28 @@ impl PageContent {
             texts: Vec::new(),
             buttons: Vec::new(),
             reliefs: Vec::new(),
+            grooves: Vec::new(),
             images: Vec::new(),
         }
     }
 
+    /// Move every part of `other` into this content.
+    ///
+    /// Field-complete by construction: this replaces four hand-listed runs of
+    /// `self.x.extend(other.x)` in `rebuild_layout`, which silently dropped any
+    /// vec nobody remembered to add a line for — `grooves` was invisible for
+    /// exactly that reason. The destructuring below turns a new field into a
+    /// compile error instead of a missing mark on screen.
+    pub fn absorb(&mut self, other: PageContent) {
+        let PageContent { rects, texts, buttons, reliefs, grooves, images } = other;
+        self.rects.extend(rects);
+        self.texts.extend(texts);
+        self.buttons.extend(buttons);
+        self.reliefs.extend(reliefs);
+        self.grooves.extend(grooves);
+        self.images.extend(images);
+    }
+
     /// A GPU-textured quad (id from `cce_ui::vk::upload_rgba`).
     pub fn image(&mut self, id: u32, x: f32, y: f32, w: f32, h: f32, alpha: f32) {
         self.images.push((id, x, y, w, h, alpha));
@@ -93,6 +140,15 @@ impl PageContent {
         }
     }
 
+    /// A line engraved from `a` to `b` into the surface `host` — no-op when the
+    /// DE's control_relief styling is off.
+    pub fn groove(&mut self, a: (f32, f32), b: (f32, f32), width: f32, host: (f32, f32, f32, f32)) {
+        if cce_ui::layout::control_relief() {
+            let depth = cce_ui::layout::bevel_width().min(host.3 * 0.2);
+            self.grooves.push((a.0, a.1, b.0, b.1, width, depth, host.0, host.1, host.2, host.3));
+        }
+    }
+
     /// A flush inset button plate (groove ring + beveled lip, face level with the
     /// surface — the Button treatment) over the control at (x, y, w, h) — no-op
     /// when the DE's control_relief styling is off.
diff --git a/src/pages/network.rs b/src/pages/network.rs
index 926a13d..c389802 100644
--- a/src/pages/network.rs
+++ b/src/pages/network.rs
@@ -115,17 +115,9 @@ pub fn view(state: &mut NetworkState, browse: &BrowseState, view_dropdown: &mut
     let dropdown_w = 120.0;
     let breadcrumb_w = cw - 16.0 - dropdown_w - 12.0;
     cce_ui::layout::render_widget(&mut pc, &mut state.breadcrumb, cx + 4.0, cy + 6.0, breadcrumb_w, 24.0, ctx);
-    // The breadcrumb's well + per-segment button plates live in its modern
-    // paint(); the flat view this host renders through loses them, so carve
-    // here: the full-width recessed well, then each segment's raised button
-    // within it (the dropdown-mirror pairing).
     {
-        let r = cce_ui::layout::breadcrumb_corner_radius();
-        pc.relief_recessed(cx + 4.0, cy + 6.0, breadcrumb_w, 24.0, r);
         let rect = cce_ui::scene::layout::Rect { x: cx + 4.0, y: cy + 6.0, width: breadcrumb_w, height: 24.0 };
-        for (sx, sy, sw, sh) in state.breadcrumb.segment_boxes(rect) {
-            pc.relief_raised(sx, sy, sw, sh, r.min(sh * 0.5));
-        }
+        crate::pages::breadcrumb_relief(&mut pc, &state.breadcrumb, rect);
     }
     cce_ui::layout::render_widget(&mut pc, view_dropdown, cx + 4.0 + breadcrumb_w + 12.0, cy + 6.0, dropdown_w, 24.0, ctx);
 
diff --git a/src/pages/space.rs b/src/pages/space.rs
index 084e3df..e55a690 100644
--- a/src/pages/space.rs
+++ b/src/pages/space.rs
@@ -470,12 +470,8 @@ pub fn view(
     let breadcrumb_w = cw - 16.0 - dropdown_w - 12.0;
     cce_ui::layout::render_widget(&mut pc, &mut state.breadcrumb, cx + 4.0, cy + 6.0, breadcrumb_w, 24.0, ctx);
     {
-        let r = cce_ui::layout::breadcrumb_corner_radius();
-        pc.relief_recessed(cx + 4.0, cy + 6.0, breadcrumb_w, 24.0, r);
         let rect = cce_ui::scene::layout::Rect { x: cx + 4.0, y: cy + 6.0, width: breadcrumb_w, height: 24.0 };
-        for (sx, sy, sw, sh) in state.breadcrumb.segment_boxes(rect) {
-            pc.relief_raised(sx, sy, sw, sh, r.min(sh * 0.5));
-        }
+        crate::pages::breadcrumb_relief(&mut pc, &state.breadcrumb, rect);
     }
     cce_ui::layout::render_widget(&mut pc, view_dropdown, cx + 4.0 + breadcrumb_w + 12.0, cy + 6.0, dropdown_w, 24.0, ctx);