git.lucas.co / cce-gallery
widget gallery and compositor test bench

commitb8cfad692ddbb1684470919abb00c003e7aa1083
parent154ed530c0
authorLucas Galante <[email protected]>
date2026-09-08 15:36
feat: exhibits at the toolkit's own spacing, sized by the one occupied-height rule

cce-ui@90c03e1 makes a labeled control's preferred height its occupied height under
either label convention and lands it exactly through `layout`, so the gallery's
post-layout inflation workaround (measure a zero-height set_rect, subtract the
strip once or twice) is gone: the exhibits shift by the scroll and hand
`set_rect` their content height via `label_inflation`. The strategies run at
their default gap (`CONTROL_GAP`) instead of a gallery-chosen 24, so the page
shows the rhythm a container gets by default.

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

 README.md   |  8 ++++++--
 src/main.rs | 43 ++++++++++++++++++-------------------------
 2 files changed, 24 insertions(+), 27 deletions(-)

diff --git a/README.md b/README.md
index 93f8959..f725ab3 100644
--- a/README.md
+++ b/README.md
@@ -43,8 +43,12 @@ Every exhibit is drawn at its toolkit default size — the control's configured
 the page is a record of the defaults; only the draw-in canvases (Trackpad,
 Plate, TreeList, the previews, the Ramp) are sized here. The `Layout` dropdown
 lays the exhibits out with the toolkit's own `ContainerLayout` strategies —
-Vertical, Columns, Grid, Adaptive Grid, Mosaic, Reverse Mosaic, Overlay — so
-what you see is what a container using that strategy does, and the exhibit
+Vertical, Columns, Grid, Adaptive Grid, Mosaic, Reverse Mosaic, Overlay — at
+their default spacing (`layout::CONTROL_GAP`, the one gap every strategy and
+row builder uses), so what you see is what a container using that strategy
+does: every control lands in exactly the box it is allotted, label included,
+whichever label convention it follows, and every detached label sits at the
+same `DETACHED_LABEL_INSET`. The exhibit
 area below the dropdown scrolls (wheel, or the scrollbar at its right edge)
 when a layout runs past the window. `Color Ramp...` and `Ramp...` open the
 ColorRamp and Ramp editors in their own child windows.
diff --git a/src/main.rs b/src/main.rs
index 06472e5..252c045 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -216,9 +216,10 @@ pub struct Exhibit {
 }
 
 impl Exhibit {
-    /// Sized by the widget's own preferred height, `fallback_h` when it declares none.
+    /// Sized by the widget's own preferred content height (its occupied height less the
+    /// label strip an inflating widget adds back), `fallback_h` when it declares none.
     fn new<W: WidgetHost + 'static>(widget: W, w: f32, fallback_h: f32) -> Self {
-        let h = widget.preferred_height().unwrap_or(fallback_h);
+        let h = widget.preferred_height().map(|p| p - widget.label_inflation()).unwrap_or(fallback_h);
         Exhibit { widget: Box::new(widget), w, h, content_w: None }
     }
 
@@ -909,7 +910,7 @@ impl State {
     /// The exhibits' viewport: below the Layout dropdown, above the status bar.
     fn exhibit_viewport(&self) -> (f32, f32, f32, f32) {
         let (dx, dy, _, dh) = self.roster.get_dyn(15).rect();
-        let (x, y) = (dx, dy + dh + 24.0);
+        let (x, y) = (dx, dy + dh + GAP);
         let w = (self.width - 2.0 * x).max(300.0);
         let h = ((self.height - 24.0) - y).max(100.0);
         (x, y, w, h)
@@ -944,19 +945,11 @@ impl State {
         let scroll_y = self.exhibit_scroll.scroll_y;
         for (&child, &idx) in children.iter().zip(&indices) {
             let widget = unsafe { &mut *child };
-            // The adapter inflates SOME widgets' rects by their detached label on every
-            // set_rect and reports the inflated height from rect(); others place the
-            // label inside their rect and get no inflation. Measure it — the height a
-            // zero-height set_rect comes back as — and work in content height from there.
+            // The strategy landed each exhibit in exactly the box it allotted; shift it
+            // by the scroll and hand `set_rect` the content height (it re-adds the
+            // label strip an inflating widget grows by — `label_inflation`).
             let (cx, cy, cw, ch) = widget.rect();
-            widget.set_rect(cx, cy, cw, 0.0);
-            let l = widget.rect().3;
-            let mut content = ch - l;
-            // A strategy sizes a child without an intrinsic height from its inflated
-            // rect and assigns that back through set_rect, so the label landed twice.
-            if widget.preferred_height().is_none() {
-                content -= l;
-            }
+            let content = ch - widget.label_inflation();
             let cw = self.content_width(idx).unwrap_or(cw);
             widget.set_rect(cx, cy - scroll_y, cw, content);
         }
@@ -1765,19 +1758,19 @@ fn spawn_editor(kind: &str) {
 }
 
 /// The toolkit container layouts the Layout dropdown offers, by name.
+/// The strategies at the toolkit's own spacing (`layout::CONTROL_GAP`, every strategy's
+/// default gap), no padding — the exhibit viewport is already inset — so the page shows
+/// the rhythm a container gets by default.
 const LAYOUTS: [(&str, fn() -> Box<dyn cce_ui::layout::LayoutStrategy>); 7] = [
-    ("Vertical", || {
-        let mut v = VerticalLayout::default();
-        v.spacing = 24.0;
-        Box::new(v)
-    }),
-    ("Columns", || Box::new(ColumnsLayout { padding_x: 0.0, padding_y: 0.0, spacing: 24.0 })),
-    ("Grid", || Box::new(GridLayout { columns: 3, gap: 24.0, padding_x: 0.0, padding_y: 0.0, grid: None })),
-    ("Adaptive Grid", || Box::new(AdaptiveGridLayout { min_col_width: 190.0, gap: 24.0, padding_x: 0.0, padding_y: 0.0, grid: None })),
-    ("Mosaic", || Box::new(MosaicLayout { gap: 24.0, padding_x: 0.0, padding_y: 0.0 })),
-    ("Reverse Mosaic", || Box::new(ReverseMosaicLayout { gap: 24.0, padding_x: 0.0, padding_y: 0.0 })),
+    ("Vertical", || Box::new(VerticalLayout::default())),
+    ("Columns", || Box::new(ColumnsLayout { padding_x: 0.0, padding_y: 0.0, ..ColumnsLayout::default() })),
+    ("Grid", || Box::new(GridLayout { columns: 3, gap: GAP, padding_x: 0.0, padding_y: 0.0, grid: None })),
+    ("Adaptive Grid", || Box::new(AdaptiveGridLayout { min_col_width: 190.0, gap: GAP, padding_x: 0.0, padding_y: 0.0, grid: None })),
+    ("Mosaic", || Box::new(MosaicLayout { padding_x: 0.0, padding_y: 0.0, ..MosaicLayout::default() })),
+    ("Reverse Mosaic", || Box::new(ReverseMosaicLayout { padding_x: 0.0, padding_y: 0.0, ..ReverseMosaicLayout::default() })),
     ("Overlay", || Box::new(OverlayLayout::default())),
 ];
+const GAP: f32 = cce_ui::layout::CONTROL_GAP;
 const DEFAULT_LAYOUT: usize = 4;
 
 /// The fixed positions: the chrome and the Layout dropdown. The exhibits are laid out