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

commitf065703f605c33c692bd1469e4820bc054d128ea
parent80073d5da9
authorLucas Galante <[email protected]>
date2026-09-09 11:05
fix(group): count a labelled member's strip once

A widget's rect is its content plus its detached-label strip (the
WidgetHost::label_strip contract; `layout` lands the block a strip above
the content origin). Group::hull subtracted the strip from that rect again,
so every labelled member lifted the hull — and the frame and its title tab —
one strip too high. In the gallery the Flat lasso's tab landed on the
exhibit above its first member. The hull now takes the rect as is; a test
with a labelled Slider pins it.

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

 src/widget/container/group.rs | 31 ++++++++++++++++++++++++-------
 1 file changed, 24 insertions(+), 7 deletions(-)

diff --git a/src/widget/container/group.rs b/src/widget/container/group.rs
index f05ed80..2654c17 100644
--- a/src/widget/container/group.rs
+++ b/src/widget/container/group.rs
@@ -111,15 +111,15 @@ impl Group {
             if !w.visible() {
                 continue;
             }
-            let (x, mut y, ww, mut hh) = w.rect();
+            let (x, y, ww, hh) = w.rect();
             if ww <= 0.0 || hh <= 0.0 || x + ww <= 0.0 || y + hh <= 0.0 {
                 continue;
             }
-            // A labelled control's detached label hangs in the strip above its
-            // rect: the lasso wraps the label too.
-            let strip = w.label_strip();
-            y -= strip;
-            hh += strip;
+            // The rect IS the block — a labelled control's detached label strip
+            // is already in it (`WidgetHost::label_strip`: "a widget's rect is
+            // always its content plus this strip"), so the lasso wraps the label
+            // by taking the rect as is. Subtracting the strip here again pushed
+            // every labelled member's hull one strip too high, and the tab with it.
             hull = Some(match hull {
                 None => (x, y, x + ww, y + hh),
                 Some((x0, y0, x1, y1)) => (x0.min(x), y0.min(y), x1.max(x + ww), y1.max(y + hh)),
@@ -277,7 +277,7 @@ impl Input for Group {
 #[cfg(test)]
 mod tests {
     use super::*;
-    use crate::widget::{Button, WidgetHost};
+    use crate::widget::{Button, Slider, WidgetHost};
 
     fn register(ctx: &mut UiContext, w: &mut dyn WidgetHost) -> WidgetId {
         let id = w.base().id();
@@ -303,6 +303,23 @@ mod tests {
         assert!(f.tab.is_none(), "no label, no tab");
     }
 
+    /// A labelled member's rect already holds its label strip; the hull takes the
+    /// rect as it is, not the rect less another strip.
+    #[test]
+    fn a_labelled_members_strip_is_counted_once() {
+        let mut ctx = UiContext::new();
+        let mut s = Slider::new().with_label("Amount");
+        let strip = s.label_strip();
+        assert!(strip > 0.0, "a detached label has a strip");
+        // The block: strip + control, as `layout` lands it.
+        WidgetHost::set_rect(&mut s, 100.0, 50.0, 160.0, 16.0 + strip);
+        let ids = vec![register(&mut ctx, &mut s)];
+        let g = Group::new(ids).with_padding(10.0);
+        let f = g.inner().frame(&ctx).unwrap();
+        assert_eq!(f.body.y, 40.0, "one padding above the block, not a strip more");
+        assert_eq!(f.body.height, 16.0 + strip + 20.0);
+    }
+
     /// Fit to plate: a side near the plate's edge takes it (one padding in), a
     /// far side keeps the hull, and a corner on the plate's corner is concentric.
     #[test]