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

commit7cbd1839962becf7cac58f76ff07c4e81aae73c4
parent497a9a1b8b
authorLucas Galante <[email protected]>
date2026-08-28 13:19
fix: the breadcrumb claims its plate, not the column beneath it

seg_at tests x against leaning parallelograms but never bounded py — it
reads py only through `lean`, which slants the seams without ever
rejecting a point. That was harmless while seg_at merely disambiguated
which segment a press inside the strip belonged to. aa91476 made it the
widget's whole hit region, and the missing bound went with it: every
segment began claiming the full-height column under itself, all the way
down the host's window.

So a host's own right-click menu lost to the copy-path menu wherever the
two shared an x. In cce-files the file-row menu (Open / Open with… /
Delete) was replaced by the breadcrumb's two-line menu for every row
under the path run — and since the run's width tracks the path's length,
it looked intermittent rather than broken.

The band seg_at already computes for the lean now bounds it. The new
test probes a segment's centre at the band's top edge, middle and bottom
edge, then just above, just below and far below it, through both seg_at
and the hit_test hosts actually call; hit_test_follows_the_seam_lean
never left the band, which is why it passed throughout.

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

 src/widget/container/breadcrumb.rs | 38 ++++++++++++++++++++++++++++++++++++++
 1 file changed, 38 insertions(+)

diff --git a/src/widget/container/breadcrumb.rs b/src/widget/container/breadcrumb.rs
index 459d8a0..e3716c5 100644
--- a/src/widget/container/breadcrumb.rs
+++ b/src/widget/container/breadcrumb.rs
@@ -182,9 +182,17 @@ impl Breadcrumb {
     /// (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.
+    ///
+    /// The parallelograms are bounded vertically by the plate band. That bound
+    /// is what makes this usable as [`Input::hit`]: `py` otherwise enters only
+    /// through `lean`, which slants the seams without ever rejecting a point,
+    /// so every segment would claim the full-height column beneath it.
     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);
+        if py < py0 || py >= py0 + ph {
+            return None;
+        }
         let mid = py0 + ph * 0.5;
         // Only interior edges lean; the run's two outer ends stay upright.
         let lean = |i: usize| -> f32 {
@@ -661,6 +669,36 @@ mod tests {
         assert_eq!(breadcrumb.seg_at(rect, edge - 0.5, y + h * 0.5), Some(0));
     }
 
+    /// A segment claims its plate, not the full-height column under it. `hit()`
+    /// delegates to `seg_at`, so a missing vertical bound there hands the widget
+    /// every press sharing an x with the run — which is how a host's own content
+    /// menu (cce-files' file rows) lost its right-click to the copy-path menu.
+    #[test]
+    fn hit_test_stops_at_the_plate_band() {
+        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 (y, h) = Breadcrumb::plate_band(rect);
+        let x = seg_center_x(&breadcrumb, rect, 1);
+
+        // Inside the band the segment answers, at the top and bottom edges too.
+        assert_eq!(breadcrumb.seg_at(rect, x, y + h * 0.5), Some(1));
+        assert_eq!(breadcrumb.seg_at(rect, x, y), Some(1));
+        assert_eq!(breadcrumb.seg_at(rect, x, y + h - 0.5), Some(1));
+
+        // Above and below it, nothing — however far the seams have leaned.
+        assert_eq!(breadcrumb.seg_at(rect, x, y - 0.5), None);
+        assert_eq!(breadcrumb.seg_at(rect, x, y + h), None);
+        assert_eq!(breadcrumb.seg_at(rect, x, y + 400.0), None);
+
+        // And the same bound through the `hit_test` hosts actually call.
+        let ctx = UiContext::new();
+        assert!(breadcrumb.hit_test(x, y + h * 0.5, &ctx));
+        assert!(!breadcrumb.hit_test(x, y + 400.0, &ctx));
+    }
+
     /// 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]