GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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() {