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

commit432e8b24740ddcd02417d7d16a3ac944103f5160
parenta8ee3bb958
authorLucas Galante <[email protected]>
date2026-08-25 12:33
dropdown: popover_anchor — the menu can hang off a larger control

An opt-in anchor rect substituted at the top of popover_geom: the open menu
positions and sizes itself off THAT rect instead of the trigger's. The
parameter pane's textpick rows use it to anchor the picker's menu to the
whole text-box well, so the completion list drops as one continuous surface
spanning the field — the button stays a 24px sliver, but the menu is the
field's. Everything downstream (drawn/unified geometry, hit reach, popover
registration) follows popover_geom, so the override carries through
animation, painting, and input for free.

 src/widget/container/parameters_bg.rs | 21 +++++++++++++++++++++
 src/widget/input/dropdown.rs          |  7 +++++++
 2 files changed, 28 insertions(+)

diff --git a/src/widget/container/parameters_bg.rs b/src/widget/container/parameters_bg.rs
index 1c897f3..b8bbc0c 100644
--- a/src/widget/container/parameters_bg.rs
+++ b/src/widget/container/parameters_bg.rs
@@ -614,6 +614,14 @@ impl ParametersBg {
                     let by = r.1 + label_top + inset;
                     let bh = (r.3 - label_top - 2.0 * inset).max(8.0);
                     d.set_rect(r.0 + r.2 - PICK_W - inset - 2.0, by, PICK_W, bh);
+                    // The menu hangs off the WHOLE field, not the button
+                    // sliver: anchor the popover to the box's well band.
+                    d.popover_anchor = Some(Rect {
+                        x: r.0,
+                        y: r.1 + label_top,
+                        width: r.2,
+                        height: r.3 - label_top,
+                    });
                 } else {
                     d.set_rect(r.0, r.1, r.2, r.3);
                 }
@@ -2852,6 +2860,19 @@ mod tests {
 
         // Both text variants lay out at the same row height.
         assert_eq!(p.inner().row_height(0), p.inner().row_height(1));
+
+        // The menu anchors off the WHOLE field: the popover spans at least
+        // the box width and hangs below it, not off the button sliver.
+        let anchor = p.choices[0].as_ref().unwrap().popover_anchor.expect("anchor set");
+        assert_eq!(anchor.x, tx);
+        assert_eq!(anchor.width, tw);
+        let d = p.choices[0].as_ref().unwrap();
+        let (px_, py_, pw, _ph) = d.popover_geom(crate::scene::layout::Rect {
+            x: dx, y: dy, width: dw, height: dh,
+        });
+        assert_eq!(px_, tx, "menu left-aligns with the box");
+        assert!(pw >= tw, "menu at least as wide as the box");
+        assert!(py_ >= ty + th - 1.0, "menu hangs below the box");
     }
 
     #[test]
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 35d79e0..df9c81b 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -93,6 +93,11 @@ pub struct Dropdown {
     /// production writer). Read by the Ramp popover clamp and the fade-blend parent color,
     /// like the legacy `parent` pointer it replaces.
     pub parent_snapshot: Option<ParentSnapshot>,
+    /// Optional popover anchor override: the menu hangs off THIS rect instead
+    /// of the trigger's — for triggers embedded in a larger control (the
+    /// parameter pane's textpick picker button nested in its TextBox), where
+    /// the menu should span the whole field, not the button sliver.
+    pub popover_anchor: Option<Rect>,
     pub font_family: String,
     pub custom_display_text: Option<String>,
     pub open_upward: Option<bool>,
@@ -141,6 +146,7 @@ impl Dropdown {
             hovered_item: None,
             just_changed: false,
             parent_snapshot: None,
+            popover_anchor: None,
             font_family: "sans-serif".to_string(),
             custom_display_text: None,
             open_upward: None,
@@ -335,6 +341,7 @@ impl Dropdown {
     /// with the base-rect reads rewritten in content-rect terms (`base.y + base.h` ⇒
     /// `content.y + content.height`, `base.y + label_offset` ⇒ `content.y`).
     pub fn popover_geom(&self, content: Rect) -> (f32, f32, f32, f32) {
+        let content = self.popover_anchor.unwrap_or(content);
         let rw = self.popover_width(content);
         let rh = self.options.len() as f32 * 24.0;