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

commita668e837000c911c01a01b8e7e3d7945214de5e0
parentfabfbcfd89
authorLucas Galante <[email protected]>
date2026-09-09 10:23
feat: the flat variants in one run, under a Flat lasso

The relief-off looks (`with_raised(false)` / `with_recessed(false)`, what
`control_relief = 0` renders) were interleaved with the other variants, one
per widget. They now form the tail of `variant_exhibits`, from FLAT_FIRST —
the plates first, then the wells and troughs — and a third Group lasso
titled `Flat` (slot 34) gathers them, so the page reads the flat style as
one section. The ctor debug-asserts the run: every exhibit from FLAT_FIRST
is labelled `(flat)` and none before it is. README describes the three
lassos, which it had not mentioned.

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

 README.md   |   7 ++++-
 src/main.rs | 102 ++++++++++++++++++++++++++++++++++++++++--------------------
 2 files changed, 74 insertions(+), 35 deletions(-)

diff --git a/README.md b/README.md
index 42f3e2f..9a57045 100644
--- a/README.md
+++ b/README.md
@@ -36,7 +36,12 @@ FontSelector's flush trough, the wells of KeybindRecorder, ColorSelector's
 hex field, ProgressBar, UsageBar and Trackpad — show their flat look too (`with_recessed(false)` / `with_raised(false)`, what
 `control_relief = 0` renders), and ColorSelector its alpha swatch. The four
 StatusDot statuses and the plain Label round it off. When a widget gains a
-style, it gains an exhibit here.
+style, it gains an exhibit here. The flat looks come last, in one run,
+so the page reads them as a section; a `Group` lasso titled `Flat` gathers
+them. Two more lassos lie over the named exhibits — `Group`, loose around the
+FontSelector and the StatusDot, and `Group (fitted)` around the top row,
+fitted to the exhibit area's edges. A lasso is laid out by its members, not
+by the strategy: under Mosaic it is the hull of wherever they landed.
 
 Every exhibit is drawn at its toolkit default size — the control's configured
 `style.control.<name>.height`, or the intrinsic size the widget declares — so
diff --git a/src/main.rs b/src/main.rs
index 03ec2f8..3aa946b 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -195,10 +195,12 @@ pub struct GallerySlots {
     pub ramp_preview_demo: Adapted<RampPreview>,
     pub separator_demo: Adapted<Separator>,
     pub splitter_demo: Adapted<Splitter>,
-    /// Two `Group` lassos over other exhibits (slots 32 and 33): overlays, not
-    /// exhibits — laid out by their members, drawn under the exhibit clip.
+    /// Three `Group` lassos over other exhibits (slots 32–34): overlays, not
+    /// exhibits — laid out by their members, drawn under the exhibit clip. The
+    /// third gathers the flat variants (`FLAT_FIRST..`) into one section.
     pub group_loose: Adapted<Group>,
     pub group_fitted: Adapted<Group>,
+    pub group_flat: Adapted<Group>,
     /// The variant exhibits (`variant_exhibits`): every further style of a widget one of
     /// the named slots already shows, addressed as slots `GALLERY_COUNT..`.
     pub extra: Vec<Exhibit>,
@@ -206,7 +208,7 @@ pub struct GallerySlots {
 
 /// The number of NAMED gallery slots; the variant exhibits follow them, so the roster's
 /// `len` is this plus `GallerySlots::extra.len()`.
-pub const GALLERY_COUNT: usize = 34;
+pub const GALLERY_COUNT: usize = 35;
 
 /// A variant exhibit: a widget in a style other than the one its named slot shows, with
 /// the content size `layout_exhibits` resets it to.
@@ -278,7 +280,7 @@ impl GallerySlots {
             29 => self.ramp_preview_demo.draggable(),
             30 => self.separator_demo.draggable(),
             31 => self.splitter_demo.draggable(),
-            32 | 33 => false,
+            32..=34 => false,
             _ => panic!("gallery slot index out of range: {idx}"),
         }
     }
@@ -320,7 +322,7 @@ impl GallerySlots {
             29 => self.ramp_preview_demo.is_dragging(),
             30 => self.separator_demo.is_dragging(),
             31 => self.splitter_demo.is_dragging(),
-            32 | 33 => false,
+            32..=34 => false,
             _ => panic!("gallery slot index out of range: {idx}"),
         }
     }
@@ -364,6 +366,7 @@ impl GallerySlots {
             31 => &self.splitter_demo,
             32 => &self.group_loose,
             33 => &self.group_fitted,
+            34 => &self.group_flat,
             _ => panic!("gallery slot index out of range: {idx}"),
         }
     }
@@ -407,6 +410,7 @@ impl GallerySlots {
             31 => &mut self.splitter_demo,
             32 => &mut self.group_loose,
             33 => &mut self.group_fitted,
+            34 => &mut self.group_flat,
             _ => panic!("gallery slot index out of range: {idx}"),
         }
     }
@@ -640,7 +644,7 @@ impl Roster {
             29 => s.ramp_preview_demo.take_click(),
             30 => s.separator_demo.take_click(),
             31 => s.splitter_demo.take_click(),
-            32 | 33 => false,
+            32..=34 => false,
             // The variant exhibits are looked at, not drained.
             i if i >= GALLERY_COUNT => false,
             _ => panic!("take_click: unwired gallery slot {idx}"),
@@ -739,6 +743,12 @@ fn load_bevel_ramp() -> (Vec<RampKey>, String) {
 /// variants — a constructor (`Button::new_reset`), a builder (`with_band`), or a
 /// per-widget override of a config style (`with_slide`) — so the page shows each
 /// look a widget can take, in the toolkit's default size for it.
+///
+/// The FLAT looks — what `control_relief = 0` renders, `with_raised(false)` /
+/// `with_recessed(false)` — come last, in one run from [`FLAT_FIRST`], so the page
+/// reads them as one section and the `Flat` lasso (`GallerySlots::group_flat`)
+/// can gather them; every flat exhibit's label ends in `(flat)`, and the ctor
+/// checks the run.
 fn variant_exhibits() -> Vec<Exhibit> {
     const W: f32 = 190.0;
     let bh = cce_ui::layout::button_height();
@@ -755,16 +765,15 @@ fn variant_exhibits() -> Vec<Exhibit> {
     const TABS_H: f32 = 120.0;
     const TAB_COLUMN_W: f32 = 40.0;
     let three = || vec!["One".to_string(), "Two".to_string(), "Three".to_string()];
-    vec![
-        // Button: the five kinds, and the flat (unraised) face.
+    let mut out = vec![
+        // ── Relief variants ──
+        // Button: the other four kinds.
         Exhibit::new(Button::new_reset(0.0, 0.0, W, bh).with_label("Button (reset)"), W, bh),
         Exhibit::new(Button::new_list_row(0.0, 0.0, W, bh).with_label("Button (list row)"), W, bh),
         Exhibit::new(Button::new_menu_item(0.0, 0.0, W, bh).with_label("Button (menu item)"), W, bh),
         Exhibit::new(Button::new_copy_icon(0.0, 0.0, bh, bh), bh, bh),
-        Exhibit::new(Button::new(0.0, 0.0, W, bh).with_label("Button (flat)").with_raised(false), W, bh),
-        // Toggle: the slide style (config `style.control.toggle.style`), and flat.
+        // Toggle: the slide style (config `style.control.toggle.style`).
         Exhibit::new(Toggle::new().with_label("Toggle (slide)").with_slide(true), W, tgh),
-        Exhibit::new(Toggle::new().with_label("Toggle (flat)").with_raised(false), W, tgh),
         // Slider: with a readout.
         Exhibit::new(Slider::new().with_label("Slider (readout)").with_readout(true), W, slh),
         // TextBox: multiline, chromeless, password.
@@ -775,14 +784,6 @@ fn variant_exhibits() -> Vec<Exhibit> {
         ),
         Exhibit::new(TextBox::new("TextBox (chromeless)".to_string()).with_draw_bg_border(false), W, tbh),
         Exhibit::new(TextBox::new("hunter2".to_string()).with_password(true).with_label("TextBox (password)"), W, tbh),
-        // Dropdown: flat.
-        Exhibit::new(
-            Dropdown::new(vec!["Flat".to_string(), "Raised".to_string()], 0)
-                .with_raised(false)
-                .with_label("Dropdown (flat)"),
-            W,
-            ddh,
-        ),
         // ButtonStrip: the vertical column (rotated tabs), and the Paginator sidebar built on it.
         Exhibit::sized(
             Adapted::new(ButtonStrip::new(0.0, 0.0, W, TABS_H).with_buttons(three()).with_selected(Some(0)).with_vertical(true))
@@ -792,33 +793,53 @@ fn variant_exhibits() -> Vec<Exhibit> {
         )
         .with_content_width(TAB_COLUMN_W),
         Exhibit::sized(Paginator::new(three()).with_label("Paginator"), W, TABS_H),
-        // The flat (relief-off) looks of the controls whose default is a well or a
-        // trough carved into the plate: what `control_relief = 0` renders.
+        // ColorSelector: the alpha swatch.
+        Exhibit::new(ColorSelector::new_rgba([64, 128, 255, 128]).with_label("ColorSelector (alpha)"), W, csh),
+        // Label: the plain text widget.
+        Exhibit::new(Label::new("Label"), W, ddh),
+        // StatusDot: the other three statuses.
+        Exhibit::new(StatusDot::new(DotStatus::Inactive).with_label("StatusDot (inactive)"), DOT_W, StatusDot::SIZE)
+            .with_content_width(StatusDot::SIZE),
+        Exhibit::new(StatusDot::new(DotStatus::Warning).with_label("StatusDot (warning)"), DOT_W, StatusDot::SIZE)
+            .with_content_width(StatusDot::SIZE),
+        Exhibit::new(StatusDot::new(DotStatus::Error).with_label("StatusDot (error)"), DOT_W, StatusDot::SIZE)
+            .with_content_width(StatusDot::SIZE),
+    ];
+    debug_assert_eq!(out.len(), FLAT_FIRST, "FLAT_FIRST must index the first flat exhibit");
+    out.extend([
+        // ── Flat variants: the relief-off look of every control that has one ──
+        // The plates: Button, Toggle, Dropdown, FontSelector.
+        Exhibit::new(Button::new(0.0, 0.0, W, bh).with_label("Button (flat)").with_raised(false), W, bh),
+        Exhibit::new(Toggle::new().with_label("Toggle (flat)").with_raised(false), W, tgh),
+        Exhibit::new(
+            Dropdown::new(vec!["Flat".to_string(), "Raised".to_string()], 0)
+                .with_raised(false)
+                .with_label("Dropdown (flat)"),
+            W,
+            ddh,
+        ),
+        Exhibit::new(FontSelector::new("Sans".to_string()).with_raised(false).with_label("FontSelector (flat)"), W, fsh),
+        // The wells and troughs: ButtonStrip, KeybindRecorder, ColorSelector,
+        // ProgressBar, UsageBar, Trackpad.
         Exhibit::sized(
             Adapted::new(ButtonStrip::new(0.0, 0.0, W, bh).with_buttons(three()).with_selected(Some(0)).with_recessed(false))
                 .with_label("ButtonStrip (flat)"),
             W,
             bh,
         ),
-        Exhibit::new(FontSelector::new("Sans".to_string()).with_raised(false).with_label("FontSelector (flat)"), W, fsh),
         Exhibit::new(KeybindRecorder::new("ctrl+1".to_string()).with_recessed(false).with_label("KeybindRecorder (flat)"), W, tbh),
         Exhibit::new(ColorSelector::new([64, 128, 255]).with_recessed(false).with_label("ColorSelector (flat)"), W, csh),
-        Exhibit::new(ColorSelector::new_rgba([64, 128, 255, 128]).with_label("ColorSelector (alpha)"), W, csh),
         Exhibit::new(ProgressBar::new(0.43).with_recessed(false).with_label("ProgressBar (flat)"), W, pbh),
         Exhibit::new(UsageBar::new(0.62).with_recessed(false).with_label("UsageBar (flat)"), W, pbh),
         Exhibit::sized(Trackpad::new().with_recessed(false).with_label("Trackpad (flat)"), W, TABS_H),
-        // Label: the plain text widget.
-        Exhibit::new(Label::new("Label"), W, ddh),
-        // StatusDot: the other three statuses.
-        Exhibit::new(StatusDot::new(DotStatus::Inactive).with_label("StatusDot (inactive)"), DOT_W, StatusDot::SIZE)
-            .with_content_width(StatusDot::SIZE),
-        Exhibit::new(StatusDot::new(DotStatus::Warning).with_label("StatusDot (warning)"), DOT_W, StatusDot::SIZE)
-            .with_content_width(StatusDot::SIZE),
-        Exhibit::new(StatusDot::new(DotStatus::Error).with_label("StatusDot (error)"), DOT_W, StatusDot::SIZE)
-            .with_content_width(StatusDot::SIZE),
-    ]
+    ]);
+    out
 }
 
+/// Index into `variant_exhibits()` of the first flat variant; the flat run is
+/// `FLAT_FIRST..` (see there).
+const FLAT_FIRST: usize = 16;
+
 /// The exhibits: every slot but the chrome (0, 1) and the Layout dropdown (15),
 /// which `layout_exhibits` lays out under the dropdown.
 fn is_exhibit(i: usize) -> bool {
@@ -828,7 +849,7 @@ fn is_exhibit(i: usize) -> bool {
 /// The Group lassos: drawn under the exhibit clip like exhibits, laid out by
 /// their members rather than by the strategy.
 fn is_overlay(i: usize) -> bool {
-    matches!(i, 32 | 33)
+    matches!(i, 32..=34)
 }
 
 impl State {
@@ -1182,6 +1203,7 @@ impl cce_ui::engine::Application for State {
                 // Tight padding: the gallery packs its rows closer than a settings page.
                 group_loose: Group::new(Vec::new()).with_label("Group").with_padding(6.0),
                 group_fitted: Group::new(Vec::new()).with_label("Group (fitted)").with_fit(true).with_padding(6.0),
+                group_flat: Group::new(Vec::new()).with_label("Flat").with_padding(6.0),
                 extra: variant_exhibits(),
             }))
         };
@@ -1196,8 +1218,20 @@ impl cce_ui::engine::Application for State {
                 let ids = |s: &GallerySlots, idx: &[usize]| idx.iter().map(|&i| s.get_dyn(i).base().id()).collect::<Vec<_>>();
                 let loose = ids(s, &[17, 23]);
                 let fitted = ids(s, &[2, 19, 3, 4]);
+                // The flat variants are the tail of `variant_exhibits`, one run,
+                // so the Flat lasso is the hull of wherever the strategy put them.
+                let flat_idx: Vec<usize> = (GALLERY_COUNT + FLAT_FIRST..GALLERY_COUNT + s.extra.len()).collect();
+                debug_assert!(
+                    (GALLERY_COUNT..GALLERY_COUNT + s.extra.len()).all(|i| {
+                        let is_flat = s.get_dyn(i).base().label.as_deref().is_some_and(|l| l.ends_with("(flat)"));
+                        is_flat == (i >= GALLERY_COUNT + FLAT_FIRST)
+                    }),
+                    "variant_exhibits: the flat variants must be the tail, from FLAT_FIRST"
+                );
+                let flat = ids(s, &flat_idx);
                 s.group_loose.inner_mut().set_members(loose);
                 s.group_fitted.inner_mut().set_members(fitted);
+                s.group_flat.inner_mut().set_members(flat);
             }
             roster
         };