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

commita0eae3abf5f80ccefb085bb51cadd753de15a185
parentc8277504be
authorLucas Galante <lsgalante12@gmail.com>
date2026-10-02 14:12
fix(dropdown): a nested trigger's menu grows out of the trigger's own span

A trigger anchored inside a wider field (the textpick picker at the end of
its text box) grew its menu from the field's LEFT edge at the button's
width, so the opening plate was a sliver under the text and its union with
the button spanned the whole field from the first frame. Both horizontal
edges now travel from the trigger's span to the menu's: the picker opens as
a plate the button's width and widens out to the field. An unanchored
trigger sits at its menu's left edge and grows rightward as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

 src/widget/input/dropdown.rs | 55 +++++++++++++++++++++++++++++++++++++++++---
 1 file changed, 52 insertions(+), 3 deletions(-)

diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 9944913..eb0372d 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -500,6 +500,15 @@ impl Dropdown {
     /// progress (cubic-out, the status-interface module-menu curve). Rows keep
     /// their final positions and slide into view under the traveling edge; an
     /// upward popover anchors its bottom edge to the trigger instead.
+    ///
+    /// The width grows out of the TRIGGER'S OWN SPAN, both edges travelling:
+    /// a trigger nested in a wider field (a [`Self::popover_anchor`] — the
+    /// textpick picker at the right end of its text box) opens as a plate the
+    /// button's width under the button and widens out to the field. Growing
+    /// from the anchor's left edge put a button-wide sliver under the text
+    /// instead, and the union with the button spanned the whole field from
+    /// the first frame. A trigger at its menu's left edge — every unanchored
+    /// one — grows rightward exactly as it always did.
     fn popover_geom_drawn(&self, content: Rect) -> (f32, f32, f32, f32) {
         let (rx, ry, rw, rh) = self.popover_geom(content);
         let a = self.anim_snap;
@@ -509,11 +518,14 @@ impl Dropdown {
         let t = 1.0 - (1.0 - a) * (1.0 - a) * (1.0 - a);
         let base_y = content.y - self.label_top();
         let open_upward = self.open_upward.unwrap_or(base_y > 400.0);
-        let w0 = content.width.min(rw);
-        let aw = w0 + (rw - w0) * t;
+        let (r0, r1) = (rx, rx + rw);
+        let s0 = content.x.clamp(r0, r1);
+        let s1 = (content.x + content.width).clamp(s0, r1);
+        let x0 = s0 + (r0 - s0) * t;
+        let x1 = s1 + (r1 - s1) * t;
         let ah = rh * t;
         let ay = if open_upward { ry + rh - ah } else { ry };
-        (rx, ay, aw, ah)
+        (x0, ay, x1 - x0, ah)
     }
 
     /// Popover geometry against the laid-out content rect — the legacy `get_popover_geom`,
@@ -1873,4 +1885,41 @@ mod tests {
             "the rows end flush on the band",
         );
     }
+
+    /// A trigger nested at the right end of a wider field (the textpick
+    /// picker, through `popover_anchor`) opens as a plate its own width,
+    /// under itself, and widens out to the field — both edges travelling. An
+    /// unanchored trigger still grows rightward from its own left edge.
+    #[test]
+    fn a_nested_trigger_grows_its_menu_out_of_its_own_span() {
+        let trigger = Rect { x: 170.0, y: 10.0, width: 30.0, height: 24.0 };
+        let field = Rect { x: 10.0, y: 10.0, width: 190.0, height: 24.0 };
+        let mut dd = Dropdown::new(vec!["A".to_string(), "B".to_string()], 0).with_open_upward(false);
+        dd.popover_anchor = Some(field);
+
+        dd.anim_snap = 0.0;
+        let (x, _, w, h) = dd.popover_geom_drawn(trigger);
+        assert_eq!((x, w, h), (170.0, 30.0, 0.0), "it begins as the button");
+        let (ux, _, uw, _) = dd.unified_geom_drawn(trigger);
+        assert_eq!((ux, uw), (170.0, 30.0), "the open surface is the button alone at first");
+
+        dd.anim_snap = 0.5;
+        let (x, _, w, _) = dd.popover_geom_drawn(trigger);
+        assert!(x > 10.0 && x < 170.0, "the left edge travels from the button toward the field's: {x}");
+        assert_eq!(x + w, 200.0, "the right edge stays where the button's is");
+
+        dd.anim_snap = 1.0;
+        let (x, _, w, _) = dd.popover_geom_drawn(trigger);
+        assert_eq!((x, w), (10.0, 190.0), "it lands spanning the field");
+
+        let plain = {
+            let mut d = Dropdown::new(vec!["A".to_string(), "B".to_string()], 0).with_open_upward(false);
+            d.anim_snap = 0.5;
+            d
+        };
+        let wide = Rect { x: 10.0, y: 10.0, width: 100.0, height: 24.0 };
+        let (x, _, w, _) = plain.popover_geom_drawn(wide);
+        assert_eq!(x, 10.0, "an unanchored menu keeps its left edge");
+        assert!(w >= 100.0, "and grows rightward out of the trigger");
+    }
 }