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

commitf8cdbe44cc2425cc73a3641c04d58271e6ecf2e7
parent529222bcb0
authorLucas Galante <[email protected]>
date2026-08-14 19:03
feat: breadcrumb segments abut on a slanted seam

The segments were separate raised plates with a 6px gap; they now abut, and
the boundary between two of them is a single seam leaning right at the top,
like the "/" it stands for.

The run is ONE plate rather than a plate per segment. Abutting per-segment
plates would put a boss wall falling and another rising within a pixel of
each other at every boundary — two lighting evaluations stacked at the crest,
which reads far hotter than a plate edge (the same reason Prim::Ridge exists
instead of a boss over a recess). So the divisions are engraved into the
shared plate instead.

Engraving them needs a mark that is not axis-aligned, which the box SDF
cannot express — hence Prim::Groove (shader mode 8): a SLAB carve, the band
of a given half-width about an arbitrary line. Distance is |signed distance
to the line| minus the half-width, so one profile evaluation yields both
walls (the gradient flips sign across the centre) and the groove costs a
single specular term. It reuses the free-carve path wholesale — same wall
profile, same host-box fade, so a seam dies into the plate's own rolled edge
instead of ending on a hard line. Push constants are unchanged in size: the
mode reinterprets p_rect as centre + half-width and p_radii.xy as the line's
unit normal, the way modes 5-7 already reinterpret them. No legacy banded
equivalent, like Ridge and ConcaveFillet.

Hit-testing follows the lean: the zones are parallelograms now, and an x-only
test put a segment's top-left corner in its neighbour — exactly where the
seam is drawn furthest from the nominal edge. Only interior edges lean; the
run's two outer ends stay upright.

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

 src/backend/window_runner.rs       |  42 +++++++
 src/scene/paint.rs                 |  30 +++++
 src/vk/renderer.rs                 |   5 +-
 src/vk/shader2d.wgsl               |  16 ++-
 src/widget/container/breadcrumb.rs | 244 ++++++++++++++++++++++++++++++-------
 src/widget/model.rs                |   1 +
 6 files changed, 293 insertions(+), 45 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index d60695b..350acc6 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -1922,6 +1922,48 @@ pub fn tessellate_display_list(
             // Legacy banded path has no radial wall — the composed corner
             // stays square there (A/B comparison path only).
             Prim::ConcaveFillet { .. } => {}
+            Prim::Groove { a, b, width, depth, host } if shader_plates => {
+                // A slab carve about the line a–b (shader mode 8): the cover
+                // quad is the segment's bounding box grown by the groove's own
+                // half-width plus the wall's reach. Off-band corners of that
+                // box sit at u = 1 (plateau), so the box overhang shades
+                // nothing — the slab is what bounds the mark, not the quad.
+                let m = *width * 0.5 + *depth * 0.5 + 2.0;
+                let (x0, x1) = (a.0.min(b.0) - m, a.0.max(b.0) + m);
+                let (y0, y1) = (a.1.min(b.1) - m, a.1.max(b.1) + m);
+                verts.extend(quad_vertices(x0, y0, x1 - x0, y1 - y0, sw, sh, [0.0; 4]));
+                // Unit normal of the line — the direction the slab's distance is
+                // measured along. A degenerate segment falls back to vertical so
+                // a zero-length groove is a no-op wall rather than a NaN.
+                let (dx, dy) = (b.0 - a.0, b.1 - a.1);
+                let len = (dx * dx + dy * dy).sqrt();
+                let n = if len > 1e-4 { (-dy / len, dx / len) } else { (1.0, 0.0) };
+                plate = Some(crate::vk::PlatePush {
+                    // Centre + slab half-width in physical px; .w unused.
+                    rect: [
+                        (a.0 + b.0) * 0.5 * scale,
+                        (a.1 + b.1) * 0.5 * scale,
+                        *width * 0.5 * scale,
+                        0.0,
+                    ],
+                    radii: [n.0, n.1, 0.0, 0.0],
+                    light: [plate_light[0], plate_light[1], plate_light[2], *depth * scale],
+                    material: plate_mat,
+                    host: [
+                        (host.x + host.width * 0.5) * scale,
+                        (host.y + host.height * 0.5) * scale,
+                        host.width * 0.5 * scale,
+                        host.height * 0.5 * scale,
+                    ],
+                    specular_tint: [1.0, 1.0, 1.0, 0.0],
+                    mode: 8.0,
+                    shape: crate::layout::corner_shape(),
+                });
+            }
+            // No legacy banded equivalent — the banded tessellators walk box
+            // edges, which is exactly the axis-aligned assumption a groove
+            // exists to escape. Same omission as `Ridge`/`ConcaveFillet`.
+            Prim::Groove { .. } => {}
         }
         let end = verts.len() as u32;
         if end == start {
diff --git a/src/scene/paint.rs b/src/scene/paint.rs
index 079ff50..d730bb3 100644
--- a/src/scene/paint.rs
+++ b/src/scene/paint.rs
@@ -112,6 +112,23 @@ pub enum Prim {
     /// exactly there). Box radii can only round convex corners; this is the
     /// missing concave piece. SDF path only (no legacy fallback).
     ConcaveFillet { cx: f32, cy: f32, radius: f32, depth: f32, start: f32, raised: bool },
+    /// An engraved line: a groove of half-width `width / 2` running along the
+    /// segment `a`–`b`, cut into whatever is painted beneath. Like [`Prim::Recess`]
+    /// it emits only shading, never a fill — but its shape is a SLAB (a band about
+    /// an arbitrary line) rather than a box, which is what lets it run at an angle.
+    /// A box SDF can only carve axis-aligned walls; this is the diagonal case.
+    ///
+    /// Both walls come from ONE profile evaluation on `|distance to the line|`, so
+    /// the groove carries a single specular/shoulder term — the same reason
+    /// [`Prim::Ridge`] exists instead of stacking a boss on a recess.
+    /// `width` 0 makes the two walls meet in a V.
+    ///
+    /// `depth` is the transition width in px (the wall's run), matching
+    /// [`Prim::Recess`]. `host` is the surface the groove is engraved into: the
+    /// shading fades out across that box's perimeter roll, so a seam cut across a
+    /// plate dies into the plate's own rolled edge instead of ending on a hard line.
+    /// SDF path only — the legacy banded tessellation draws nothing (like `Ridge`).
+    Groove { a: (f32, f32), b: (f32, f32), width: f32, depth: f32, host: Rect },
     /// Text in sRGB u8 (the `TextLabel` convention). `font` is a font string for
     /// `get_text_buffer` (family, or "family:size"); `bounds` is a logical `[l, t, r, b]` clip
     /// for the glyph pass (Phase 6: the backend renders these through the glyph pass when the app
@@ -410,6 +427,19 @@ impl PaintCtx {
         self.push(Prim::ConcaveFillet { cx: cx + ox, cy: cy + oy, radius, depth, start, raised });
     }
 
+    /// An engraved line from `a` to `b` cut into `host` — see [`Prim::Groove`].
+    pub fn groove(&mut self, a: (f32, f32), b: (f32, f32), width: f32, depth: f32, host: Rect) {
+        let (ox, oy) = self.offset;
+        let host = self.apply_offset(host);
+        self.push(Prim::Groove {
+            a: (a.0 + ox, a.1 + oy),
+            b: (b.0 + ox, b.1 + oy),
+            width,
+            depth,
+            host,
+        });
+    }
+
     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 });
diff --git a/src/vk/renderer.rs b/src/vk/renderer.rs
index 5c5c9b6..a49e88d 100644
--- a/src/vk/renderer.rs
+++ b/src/vk/renderer.rs
@@ -64,7 +64,10 @@ pub struct PlatePush {
     /// RGB multiplies the roll's specular color (w unused). Neutral white
     /// normally; the focused-pane bevel carries the highlight color here.
     pub specular_tint: [f32; 4],
-    /// 1.0 = raised lit plate, 2.0 = recess overlay.
+    /// 1.0 = raised lit plate, 2.0 = recess overlay, 3.0 = boss, 4.0 = ridge,
+    /// 5.0 = sphere, 6.0/7.0 = concave fillet (recessed/raised), 8.0 = groove
+    /// (slab carve about a line: `rect` = [cx, cy, half-width, _], `radii.xy` =
+    /// the line's unit normal, `host` = the surface it is engraved into).
     pub mode: f32,
     /// Corner shape exponent: 2.0 = circular arcs, > 2 = superellipse
     /// (continuous-curvature) corners — see shader2d's `plate_sdf_grad`.
diff --git a/src/vk/shader2d.wgsl b/src/vk/shader2d.wgsl
index 1751c8a..927b8f6 100644
--- a/src/vk/shader2d.wgsl
+++ b/src/vk/shader2d.wgsl
@@ -371,7 +371,21 @@ fn plate_shade(frag: vec2f, vcol: vec4f) -> vec4f {
     var fd = d;
     var fgd = gd.xy;
     var wedge = 1.0;
-    if (eff > 5.5) {
+    // Groove (mode 8): a SLAB carve — the band of half-width p_rect.z about the
+    // line through p_rect.xy with unit normal p_radii.xy. Distance is |signed
+    // distance to that line| minus the half-width, so ONE profile evaluation
+    // yields both walls (the gradient flips sign across the centre line, tilting
+    // them apart) and the groove costs a single specular term. The box SDF is
+    // axis-aligned by construction; this is how a mark runs at an angle.
+    // Rejoins the shared free-carve path as a recess (eff = 2).
+    if (eff > 7.5) {
+        let nrm = rrect_clip.p_radii.xy;
+        let c = frag - rrect_clip.p_rect.xy;
+        let s = dot(c, nrm);
+        fd = abs(s) - rrect_clip.p_rect.z;
+        fgd = nrm * select(-1.0, 1.0, s >= 0.0);
+        eff = 2.0;
+    } else if (eff > 5.5) {
         eff = eff - 4.0;
         let c = frag - rrect_clip.p_rect.xy;
         let dist = max(length(c), 1e-4);
diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index 8b9f20b..5594a64 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -16,11 +16,22 @@ use crate::widget::{
 /// the two stay in lockstep.
 const BREADCRUMB_FONT_SIZE: f32 = 12.0;
 
-/// Gap between segment buttons.
-const SEG_GAP: f32 = 6.0;
-
-/// Horizontal text inset inside each segment button.
-const SEG_PAD_X: f32 = 8.0;
+/// Gap between segment buttons. Zero: the segments ABUT, and the boundary
+/// between two of them is a single slanted seam (see [`SEG_SLANT`]) rather than
+/// a strip of the well floor showing through.
+const SEG_GAP: f32 = 0.0;
+
+/// Lean of a seam, as horizontal run per unit of height — the boundary's top is
+/// this fraction of the plate height to the RIGHT of its bottom, so it reads as
+/// a "/" cut between two segments. 0.36 ≈ 20°, the slope of a "/" glyph in the
+/// mono faces the breadcrumb is set in.
+const SEG_SLANT: f32 = 0.36;
+
+/// Horizontal text inset inside each segment button. Wider than the gapped
+/// layout needed: a seam leans ±`SEG_SLANT * h / 2` about its mid-height, so the
+/// padding has to clear the seam at the plate's top and bottom edges too, not
+/// just beside the text.
+const SEG_PAD_X: f32 = 11.0;
 
 /// A segment as actually laid out for painting/hit-testing: its text, its BUTTON BOX's left
 /// edge and width (text sits `SEG_PAD_X` in), and logical index in
@@ -153,32 +164,77 @@ impl Breadcrumb {
         place(kept)
     }
 
-    fn seg_at(&self, rect: Rect, px: f32) -> Option<usize> {
-        self.visible_segs(rect)
-            .iter()
-            .find(|s| px >= s.x && px < s.x + s.w)
-            .and_then(|s| s.logical)
+    /// The segment under `px` at height `py`. The interior boundaries LEAN
+    /// (see [`SEG_SLANT`]), so the hit zones are parallelograms, not columns —
+    /// testing x alone would put the top-left corner of a segment in its
+    /// neighbor, exactly where the seam is drawn furthest from the nominal edge.
+    fn seg_at(&self, rect: Rect, px: f32, py: f32) -> Option<usize> {
+        let segs = self.visible_segs(rect);
+        let (py0, ph) = Self::plate_band(rect);
+        let mid = py0 + ph * 0.5;
+        // Only interior edges lean; the run's two outer ends stay upright.
+        let lean = |i: usize| -> f32 {
+            if i == 0 || i >= segs.len() { 0.0 } else { SEG_SLANT * (mid - py) }
+        };
+        for (i, s) in segs.iter().enumerate() {
+            let left = s.x + lean(i);
+            let right = s.x + s.w + lean(i + 1);
+            if px >= left && px < right {
+                return s.logical;
+            }
+        }
+        None
     }
 
-    /// Vertical margin between the widget's recessed well and each raised
-    /// segment button inside it.
+    /// Vertical margin between the widget's recessed well and the raised
+    /// segment plate inside it.
     const SEG_INSET_Y: f32 = 3.0;
 
-    /// The per-segment button boxes as laid out for `rect`: (x, y, w, h), inset
-    /// vertically so the raised plates sit within the widget's full-width well.
-    /// For flat-path hosts (cce-files) that mirror the plates app-side — the
+    /// Width of a seam's flat floor in px. Zero would meet the two walls in a
+    /// perfect V; a hair of floor keeps the crease from aliasing into a dotted
+    /// line as the seam's subpixel position drifts with the path text. Public
+    /// because flat-path hosts engrave the seams themselves — see [`seams`].
+    ///
+    /// [`seams`]: Breadcrumb::seams
+    pub const SEAM_WIDTH: f32 = 0.75;
+
+    /// The plate band inside the well: (y, height).
+    fn plate_band(rect: Rect) -> (f32, f32) {
+        (rect.y + Self::SEG_INSET_Y, (rect.height - 2.0 * Self::SEG_INSET_Y).max(0.0))
+    }
+
+    /// The ONE raised plate the whole segment run shares — (x, y, w, h), inset
+    /// vertically inside the widget's well — or `None` when nothing is laid out.
+    ///
+    /// The run is a single plate, not a plate per segment: with the segments
+    /// abutting, per-segment plates would put a boss wall falling and another
+    /// rising within a pixel of each other at every boundary, which stacks two
+    /// lighting evaluations and reads far hotter than one seam (the same reason
+    /// `Prim::Ridge` exists). The divisions are engraved instead — see [`seams`].
+    ///
+    /// For flat-path hosts (cce-files) that mirror the relief app-side — the
     /// render_widget geometry path drops relief prims, same as the dropdown's.
-    pub fn segment_boxes(&self, rect: Rect) -> Vec<(f32, f32, f32, f32)> {
-        self.visible_segs(rect)
-            .iter()
-            .map(|vs| {
-                (
-                    vs.x,
-                    rect.y + Self::SEG_INSET_Y,
-                    vs.w,
-                    (rect.height - 2.0 * Self::SEG_INSET_Y).max(0.0),
-                )
-            })
+    ///
+    /// [`seams`]: Breadcrumb::seams
+    pub fn run_box(&self, rect: Rect) -> Option<(f32, f32, f32, f32)> {
+        let segs = self.visible_segs(rect);
+        let first = segs.first()?;
+        let last = segs.last()?;
+        let (y, h) = Self::plate_band(rect);
+        Some((first.x, y, last.x + last.w - first.x, h))
+    }
+
+    /// The seam between each pair of abutting segments, as (top, bottom) line
+    /// endpoints. Each leans right at the top by [`SEG_SLANT`], so it reads as a
+    /// "/" between the two names. Only interior boundaries appear here — the
+    /// run's outer ends are the plate's own upright edges.
+    pub fn seams(&self, rect: Rect) -> Vec<((f32, f32), (f32, f32))> {
+        let segs = self.visible_segs(rect);
+        let (y, h) = Self::plate_band(rect);
+        let run = SEG_SLANT * h * 0.5;
+        segs.iter()
+            .skip(1)
+            .map(|s| ((s.x + run, y), (s.x - run, y + h)))
             .collect()
     }
 
@@ -207,9 +263,10 @@ impl Paint for Breadcrumb {
     }
 
     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
-        // The full-width recessed well defines the bar (as it always did);
-        // each segment is a RAISED button plate within it — the dropdown
-        // pairing (recessed surround + raised face), per-segment.
+        // The full-width recessed well defines the bar (as it always did); the
+        // segment run is ONE raised plate within it — the dropdown pairing
+        // (recessed surround + raised face) — divided into segments by seams
+        // engraved across it at a "/" lean.
         let radius = crate::layout::breadcrumb_corner_radius();
         let relief = crate::layout::control_relief();
         if relief {
@@ -218,18 +275,38 @@ impl Paint for Breadcrumb {
         }
 
         let segs = self.visible_segs(rect);
-        let boxes = self.segment_boxes(rect);
-        for (vs, &(sx, sy, sw, sh)) in segs.iter().zip(boxes.iter()) {
-            let seg_rect = Rect { x: sx, y: sy, width: sw, height: sh };
-            let r = radius.min(sh * 0.5);
+        if let Some((rx, ry, rw, rh)) = self.run_box(rect) {
+            let run_rect = Rect { x: rx, y: ry, width: rw, height: rh };
+            let r = radius.min(rh * 0.5);
             if relief {
-                let seg_depth = crate::layout::bevel_width().min(sh * 0.2);
-                ctx.boss(seg_rect, (r, r, r, r), seg_depth);
+                let depth = crate::layout::bevel_width().min(rh * 0.2);
+                ctx.boss(run_rect, (r, r, r, r), depth);
+                for (a, b) in self.seams(rect) {
+                    ctx.groove(a, b, Self::SEAM_WIDTH, depth, run_rect);
+                }
             } else {
-                ctx.rounded_rect(seg_rect, r, (true, true, true, true), self.bg_color());
+                ctx.rounded_rect(run_rect, r, (true, true, true, true), self.bg_color());
+                for (a, b) in self.seams(rect) {
+                    ctx.vector(a.0, a.1, b.0, b.1, 1.0, [0.0, 0.0, 0.0, 0.25], crate::scene::paint::Cap::Flat);
+                }
             }
-            if vs.logical.is_some() && vs.logical == self.hovered_seg {
-                ctx.quad(seg_rect, [1.0, 1.0, 1.0, 0.06]);
+        }
+        // Hover tint. The segment is a parallelogram but the tint is the
+        // axis-aligned box inset to the seam's furthest lean, so it never
+        // crosses a boundary — a slanted fill would need a primitive of its own
+        // for a 6% wash.
+        if let Some(hovered) = self.hovered_seg {
+            let (hy, hh) = Self::plate_band(rect);
+            let run = SEG_SLANT * hh * 0.5;
+            if let Some(vs) = segs.iter().find(|s| s.logical == Some(hovered)) {
+                let first = segs.first().map(|s| s.x) == Some(vs.x);
+                let last = segs.last().map(|s| s.x + s.w) == Some(vs.x + vs.w);
+                let l = vs.x + if first { 0.0 } else { run };
+                let r = vs.x + vs.w - if last { 0.0 } else { run };
+                ctx.quad(
+                    Rect { x: l, y: hy, width: (r - l).max(0.0), height: hh },
+                    [1.0, 1.0, 1.0, 0.06],
+                );
             }
         }
 
@@ -261,7 +338,7 @@ impl Input for Breadcrumb {
                 self.hovered =
                     *px >= r.x && *px <= r.x + r.width && *py >= r.y && *py <= r.y + r.height;
                 let old = self.hovered_seg;
-                self.hovered_seg = if self.hovered { self.seg_at(r, *px) } else { None };
+                self.hovered_seg = if self.hovered { self.seg_at(r, *px, *py) } else { None };
                 was != self.hovered || old != self.hovered_seg
             }
             Event::MouseLeave => {
@@ -280,7 +357,7 @@ impl Input for Breadcrumb {
                 // Record the segment first: the shared menu's header reads it (via the
                 // `as_any` downcast in `UiContext::handle_right_click`) to title itself with
                 // that segment's path, and "Copy Path" copies it.
-                self.right_clicked_seg = self.seg_at(ectx.rect, *px);
+                self.right_clicked_seg = self.seg_at(ectx.rect, *px, *py);
                 ectx.open_context_menu(*px, *py);
                 true
             }
@@ -288,9 +365,10 @@ impl Input for Breadcrumb {
                 button: MouseButton::Left,
                 state: ElementState::Pressed,
                 x: px,
+                y: py,
                 ..
             } => {
-                if let Some(i) = self.seg_at(ectx.rect, *px) {
+                if let Some(i) = self.seg_at(ectx.rect, *px, *py) {
                     if i < self.path.len() {
                         self.clicked_seg = Some(i);
                         return true;
@@ -427,8 +505,11 @@ mod tests {
         // A kept trailing segment still hit-tests to its original logical index, so clicking
         // it navigates to the correct path.
         let visible_seg = segs.iter().rev().nth(1).unwrap();
-        let hit = breadcrumb
-            .seg_at(Rect { x: 0.0, y: 0.0, width: 160.0, height: 24.0 }, visible_seg.x + 1.0);
+        let hit = breadcrumb.seg_at(
+            Rect { x: 0.0, y: 0.0, width: 160.0, height: 24.0 },
+            visible_seg.x + 6.0,
+            12.0,
+        );
         assert_eq!(hit, visible_seg.logical);
     }
 
@@ -445,6 +526,83 @@ mod tests {
         assert_eq!(segs[0].text, "/");
     }
 
+    /// The seams lean like "/" — top edge to the RIGHT of the bottom — and there
+    /// is exactly one per interior boundary, sitting on the shared edge at
+    /// mid-height. The run plate spans all of them.
+    #[test]
+    fn seams_lean_right_at_the_top() {
+        let mut breadcrumb = Breadcrumb::new();
+        breadcrumb.set_path(&["home".to_string(), "lsgalante".to_string()]);
+        let rect = Rect { x: 10.0, y: 20.0, width: 300.0, height: 24.0 };
+        breadcrumb.set_rect(rect.x, rect.y, rect.width, rect.height);
+
+        let segs = breadcrumb.visible_segs(rect);
+        let seams = breadcrumb.seams(rect);
+        // Root + two components ⇒ two interior boundaries.
+        assert_eq!(seams.len(), 2);
+        assert_eq!(seams.len(), segs.len() - 1);
+
+        for (i, ((tx, ty), (bx, by))) in seams.iter().enumerate() {
+            assert!(tx > bx, "seam {i} must lean right at the top");
+            assert!(ty < by, "seam {i} top must be above its bottom");
+            // Centered on the boundary it divides.
+            let edge = segs[i + 1].x;
+            assert!(((tx + bx) * 0.5 - edge).abs() < 0.01);
+        }
+
+        // One plate under the lot, spanning first edge to last.
+        let (rx, _, rw, _) = breadcrumb.run_box(rect).expect("run laid out");
+        assert_eq!(rx, segs[0].x);
+        assert!((rx + rw - (segs[2].x + segs[2].w)).abs() < 0.01);
+    }
+
+    /// A point in a segment's top-left corner belongs to the segment on the
+    /// LEFT: the seam has leaned right there, so the boundary is no longer the
+    /// nominal edge. This is what an x-only hit test got wrong.
+    #[test]
+    fn hit_test_follows_the_seam_lean() {
+        let mut breadcrumb = Breadcrumb::new();
+        breadcrumb.set_path(&["home".to_string(), "lsgalante".to_string()]);
+        let rect = Rect { x: 10.0, y: 20.0, width: 300.0, height: 24.0 };
+        breadcrumb.set_rect(rect.x, rect.y, rect.width, rect.height);
+
+        let segs = breadcrumb.visible_segs(rect);
+        let (y, h) = Breadcrumb::plate_band(rect);
+        let edge = segs[1].x; // boundary between "/" and "home"
+        let lean = SEG_SLANT * h * 0.5;
+        assert!(lean > 1.0, "the test needs a lean wide enough to probe");
+
+        // Just right of the nominal edge, at the TOP: still segment 0.
+        assert_eq!(breadcrumb.seg_at(rect, edge + lean * 0.5, y + 0.5), Some(0));
+        // The same x at the BOTTOM, where the seam has leaned left: segment 1.
+        assert_eq!(breadcrumb.seg_at(rect, edge + lean * 0.5, y + h - 0.5), Some(1));
+        // At mid-height the seam sits on the nominal edge.
+        assert_eq!(breadcrumb.seg_at(rect, edge + 0.5, y + h * 0.5), Some(1));
+        assert_eq!(breadcrumb.seg_at(rect, edge - 0.5, y + h * 0.5), Some(0));
+    }
+
+    /// The run's outer ends stay upright — only edges that face another segment
+    /// lean, so the first segment's left edge is a plain vertical boundary.
+    #[test]
+    fn outer_ends_do_not_lean() {
+        let mut breadcrumb = Breadcrumb::new();
+        breadcrumb.set_path(&["home".to_string()]);
+        let rect = Rect { x: 10.0, y: 20.0, width: 300.0, height: 24.0 };
+        breadcrumb.set_rect(rect.x, rect.y, rect.width, rect.height);
+
+        let segs = breadcrumb.visible_segs(rect);
+        let (y, h) = Breadcrumb::plate_band(rect);
+        let left = segs[0].x;
+        let right = segs[1].x + segs[1].w;
+
+        for py in [y + 0.5, y + h * 0.5, y + h - 0.5] {
+            assert_eq!(breadcrumb.seg_at(rect, left + 0.5, py), Some(0));
+            assert_eq!(breadcrumb.seg_at(rect, left - 0.5, py), None);
+            assert_eq!(breadcrumb.seg_at(rect, right - 0.5, py), Some(1));
+            assert_eq!(breadcrumb.seg_at(rect, right + 0.5, py), None);
+        }
+    }
+
     #[test]
     fn path_controller_reachable_through_element() {
         let mut breadcrumb = Breadcrumb::new();
diff --git a/src/widget/model.rs b/src/widget/model.rs
index 3cdf1fb..80654ed 100644
--- a/src/widget/model.rs
+++ b/src/widget/model.rs
@@ -1287,6 +1287,7 @@ impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
                 Prim::ConcaveFillet { cx, cy, radius, depth, start, raised } => {
                     ctx.concave_fillet(cx, cy, radius, depth, start, raised)
                 }
+                Prim::Groove { a, b, width, depth, host } => ctx.groove(a, b, width, depth, host),
                 Prim::Image { image, rect, alpha } => ctx.image(image, rect, alpha),
             }
             if clip_circle.is_some() {