widget gallery and compositor test bench
feat: a Style dropdown switches every control between Relief and Flat
The ten flat variant exhibits — Button, Toggle, Dropdown, FontSelector,
ButtonStrip, KeybindRecorder, ColorSelector, ProgressBar, UsageBar and
Trackpad with `with_raised(false)` / `with_recessed(false)` — and the Flat
lasso that grouped them are gone. A Style dropdown beside Layout in the
header (slot 34) calls `cce_ui::layout::set_control_relief`, and since the
toolkit now reads the setting live (cce-ui@HEAD), every exhibit shows both
looks in place; no widget appears twice for its style alone. Header dropdown
selections are drained by `apply_header_dropdowns` after mouse AND key
input — a Dropdown selects on Enter too, and the Layout pick used to wait
for the next click. The flat run's second layout pass goes with the run.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
README.md | 35 ++++++-------
src/main.rs | 161 ++++++++++++++++++++++++------------------------------------
2 files changed, 83 insertions(+), 113 deletions(-)
diff --git a/README.md b/README.md
index d3cc259..fe35cd3 100644
--- a/README.md
+++ b/README.md
@@ -24,23 +24,24 @@ frame it.
After those come the variants: every further look a widget can take, one
exhibit each, labelled `<Widget> (<style>)`. The toolkit's own variants are a
constructor (`Button::new_reset`, `new_list_row`, `new_menu_item`,
-`new_copy_icon`), a builder (`with_raised(false)` on Button, Toggle and
-Dropdown; `with_readout` on Slider;
-`with_multiline`, `with_draw_bg_border(false)` and `with_password` on
-TextBox; `with_vertical` on ButtonStrip, and the Paginator built on it), or a
-per-widget override of a config-wide style (`Toggle::with_slide`, the
-`style.control.toggle.style` look, next to `Slider::with_band` for
-`style.control.slider.style`). The controls whose default look is worked
-into the plate — ButtonStrip's well with its raised selected segment,
-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. The flat looks come last, in one run,
-so the page reads them as a section; a `Group` lasso titled `Flat` gathers
-them, and the run is laid out as a second pass of the same strategy, one gap
-plus the lasso's headroom (`Group::headroom`, its padding and title tab)
-below the rest, so the tab clears the exhibit above it. Two more lassos lie over the named exhibits — `Group`, loose around the
+`new_copy_icon`), a builder (`with_readout` on Slider; `with_multiline`,
+`with_draw_bg_border(false)` and `with_password` on TextBox; `with_vertical`
+on ButtonStrip, and the Paginator built on it), or a per-widget override of a
+config-wide style (`Toggle::with_slide`, the `style.control.toggle.style`
+look, next to `Slider::with_band` for `style.control.slider.style`).
+ColorSelector shows its alpha swatch, and the four StatusDot statuses and the
+plain Label round it off. When a widget gains a style, it gains an exhibit
+here.
+
+The relief-off look is not a variant. The header's `Style` dropdown, beside
+`Layout`, switches every control between `Relief` and `Flat` at once
+(`cce_ui::layout::set_control_relief`, what `control_relief = 0` configures
+DE-wide): the toolkit reads the switch live, so each exhibit shows both looks
+in place and no widget appears twice for its style alone. A widget that does
+not follow the switch is a toolkit bug — every control's `with_raised` /
+`with_recessed` is an override on top of the live default, not a copy of it.
+
+Two `Group` 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.
diff --git a/src/main.rs b/src/main.rs
index 13c7d4c..30579da 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -195,12 +195,13 @@ pub struct GallerySlots {
pub ramp_preview_demo: Adapted<RampPreview>,
pub separator_demo: Adapted<Separator>,
pub splitter_demo: Adapted<Splitter>,
- /// 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.
+ /// Two `Group` lassos over other exhibits (slots 32 and 33): overlays, not
+ /// exhibits — laid out by their members, drawn under the exhibit clip.
pub group_loose: Adapted<Group>,
pub group_fitted: Adapted<Group>,
- pub group_flat: Adapted<Group>,
+ /// The Style dropdown (slot 34), beside Layout in the header: Relief or Flat
+ /// for every control at once (`cce_ui::layout::set_control_relief`).
+ pub style_dd: Adapted<Dropdown>,
/// 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>,
@@ -264,6 +265,7 @@ impl GallerySlots {
13 => self.bevel_ramp.draggable(),
14 => self.ramp_btn.draggable(),
15 => self.layout_dd.draggable(),
+ 34 => self.style_dd.draggable(),
16 => self.color_selector_demo.draggable(),
17 => self.font_selector_demo.draggable(),
18 => self.keybind_demo.draggable(),
@@ -280,7 +282,7 @@ impl GallerySlots {
29 => self.ramp_preview_demo.draggable(),
30 => self.separator_demo.draggable(),
31 => self.splitter_demo.draggable(),
- 32..=34 => false,
+ 32 | 33 => false,
_ => panic!("gallery slot index out of range: {idx}"),
}
}
@@ -306,6 +308,7 @@ impl GallerySlots {
13 => self.bevel_ramp.is_dragging(),
14 => self.ramp_btn.is_dragging(),
15 => self.layout_dd.is_dragging(),
+ 34 => self.style_dd.is_dragging(),
16 => self.color_selector_demo.is_dragging(),
17 => self.font_selector_demo.is_dragging(),
18 => self.keybind_demo.is_dragging(),
@@ -322,7 +325,7 @@ impl GallerySlots {
29 => self.ramp_preview_demo.is_dragging(),
30 => self.separator_demo.is_dragging(),
31 => self.splitter_demo.is_dragging(),
- 32..=34 => false,
+ 32 | 33 => false,
_ => panic!("gallery slot index out of range: {idx}"),
}
}
@@ -366,7 +369,7 @@ impl GallerySlots {
31 => &self.splitter_demo,
32 => &self.group_loose,
33 => &self.group_fitted,
- 34 => &self.group_flat,
+ 34 => &self.style_dd,
_ => panic!("gallery slot index out of range: {idx}"),
}
}
@@ -410,7 +413,7 @@ impl GallerySlots {
31 => &mut self.splitter_demo,
32 => &mut self.group_loose,
33 => &mut self.group_fitted,
- 34 => &mut self.group_flat,
+ 34 => &mut self.style_dd,
_ => panic!("gallery slot index out of range: {idx}"),
}
}
@@ -628,6 +631,7 @@ impl Roster {
13 => s.bevel_ramp.take_click(),
14 => s.ramp_btn.take_click(),
15 => s.layout_dd.take_click(),
+ 34 => s.style_dd.take_click(),
16 => s.color_selector_demo.take_click(),
17 => s.font_selector_demo.take_click(),
18 => s.keybind_demo.take_click(),
@@ -644,7 +648,7 @@ impl Roster {
29 => s.ramp_preview_demo.take_click(),
30 => s.separator_demo.take_click(),
31 => s.splitter_demo.take_click(),
- 32..=34 => false,
+ 32 | 33 => false,
// The variant exhibits are looked at, not drained.
i if i >= GALLERY_COUNT => false,
_ => panic!("take_click: unwired gallery slot {idx}"),
@@ -655,6 +659,7 @@ impl Roster {
let s = self.gallery();
match idx {
15 => s.layout_dd.value(),
+ 34 => s.style_dd.value(),
_ => panic!("value: unwired gallery slot {idx}"),
}
}
@@ -744,11 +749,9 @@ fn load_bevel_ramp() -> (Vec<RampKey>, String) {
/// 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.
+/// The relief-off look is NOT a variant here: the header's Style dropdown switches
+/// every control between Relief and Flat at once (`set_control_relief`), so each
+/// exhibit shows both, and no widget appears twice for its style alone.
fn variant_exhibits() -> Vec<Exhibit> {
const W: f32 = 190.0;
let bh = cce_ui::layout::button_height();
@@ -756,17 +759,14 @@ fn variant_exhibits() -> Vec<Exhibit> {
let slh = cce_ui::layout::slider_height();
let tbh = cce_ui::layout::textbox_height();
let ddh = cce_ui::layout::dropdown_height();
- let fsh = cce_ui::layout::font_selector_height();
let csh = cce_ui::layout::color_selector_height();
- let pbh = cce_ui::layout::progressbar_height();
// A StatusDot is 12px square; its exhibit is as wide as its label.
const DOT_W: f32 = 200.0;
// A column of three rotated tabs, and the sidebar width a vertical strip is drawn for.
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()];
- let mut out = vec![
- // ── Relief variants ──
+ vec![
// 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),
@@ -804,44 +804,12 @@ fn variant_exhibits() -> Vec<Exhibit> {
.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(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(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),
- ]);
- 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.
+/// The exhibits: every slot but the chrome (0, 1), the lassos (32, 33) and the
+/// header dropdowns (15 Layout, 34 Style), which `layout_exhibits` lays out under
+/// the header row.
fn is_exhibit(i: usize) -> bool {
matches!(i, 2..=14 | 16..=31) || i >= GALLERY_COUNT
}
@@ -849,7 +817,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..=34)
+ matches!(i, 32 | 33)
}
impl State {
@@ -983,24 +951,8 @@ impl State {
children.push(widget as *mut (dyn WidgetHost + 'static));
indices.push(idx);
}
- let make = LAYOUTS.get(self.layout_idx).unwrap_or(&LAYOUTS[DEFAULT_LAYOUT]).1;
- // Two passes of the one strategy: the flat run (`FLAT_FIRST..`, the tail of the
- // exhibits) starts one gap plus the Flat lasso's headroom below the rest, so the
- // lasso's title tab clears the block above it — the room a strategy would reserve
- // for the title if the lasso were its child. A lasso is an overlay the strategy
- // never sees, so the gallery reserves it.
- let flat_from = GALLERY_COUNT + FLAT_FIRST;
- let (split, headroom) = match &self.roster {
- Roster::Gallery(s) => (
- indices.iter().position(|&i| i >= flat_from).unwrap_or(indices.len()),
- s.group_flat.headroom(),
- ),
- _ => (indices.len(), 0.0),
- };
- let h1 = if split > 0 { make().layout(x, y, w, h, &children[..split], &mut self.ui_context) } else { 0.0 };
- let y2 = if split > 0 { y + h1 + GAP + headroom } else { y };
- let h2 = if split < children.len() { make().layout(x, y2, w, h, &children[split..], &mut self.ui_context) } else { 0.0 };
- let content_h = (y2 - y) + h2;
+ let strategy = (LAYOUTS.get(self.layout_idx).unwrap_or(&LAYOUTS[DEFAULT_LAYOUT]).1)();
+ let content_h = strategy.layout(x, y, w, h, &children, &mut self.ui_context);
self.exhibit_scroll.update_bounds(content_h, y, h);
let scroll_y = self.exhibit_scroll.scroll_y;
for (&child, &idx) in children.iter().zip(&indices) {
@@ -1042,6 +994,28 @@ impl State {
}
}
+ /// Drain the header dropdowns' selections and apply them: Layout (15) picks the
+ /// strategy, Style (34) switches every control between Relief and Flat
+ /// (`set_control_relief` — the toolkit reads it live at paint, so a rebuild is
+ /// all it takes). Called after mouse AND key input: a Dropdown selects from the
+ /// keyboard too (Down, Enter), and a selection must not wait for the next click.
+ fn apply_header_dropdowns(&mut self) -> bool {
+ let mut applied = false;
+ if self.roster.take_click(15) {
+ self.layout_idx = self.roster.value(15) as usize;
+ applied = true;
+ }
+ if self.roster.take_click(34) {
+ cce_ui::layout::set_control_relief(self.roster.value(34) == 0);
+ applied = true;
+ }
+ if applied {
+ self.relayout();
+ self.apply_layout();
+ }
+ applied
+ }
+
fn apply_layout(&mut self) {
for i in 0..self.roster.len() {
if self.roster.is_dragging(i) {
@@ -1054,9 +1028,9 @@ impl State {
let visible = self.is_widget_visible(i);
let (x, y, w, h) = self.positions[i];
let widget = self.roster.get_dyn_mut(i);
- if visible && i == 15 {
- // The Layout dropdown lands through `layout` at its own preferred
- // height: `y` is where its label goes, the content sits a strip below.
+ if visible && (i == 15 || i == 34) {
+ // The header dropdowns land through `layout` at their own preferred
+ // height: `y` is where the label goes, the content sits a strip below.
let h = widget.preferred_height().unwrap_or(h);
let strip = widget.label_strip();
widget.layout(Point { x, y: y + strip }, LayoutConstraints::new(w, w, h, h), &mut self.ui_context);
@@ -1182,6 +1156,10 @@ impl cce_ui::engine::Application for State {
LAYOUTS.iter().map(|(name, _)| name.to_string()).collect(),
DEFAULT_LAYOUT,
).with_label("Layout"),
+ style_dd: Dropdown::new(
+ vec!["Relief".to_string(), "Flat".to_string()],
+ if cce_ui::layout::control_relief() { 0 } else { 1 },
+ ).with_label("Style"),
color_selector_demo: ColorSelector::new([64, 128, 255]).with_label("ColorSelector"),
font_selector_demo: FontSelector::new("Sans".to_string()).with_label("FontSelector"),
keybind_demo: KeybindRecorder::new("ctrl+1".to_string()).with_label("KeybindRecorder"),
@@ -1219,7 +1197,6 @@ 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(),
}))
};
@@ -1234,20 +1211,8 @@ 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
};
@@ -1734,10 +1699,7 @@ impl cce_ui::engine::Application for State {
return Some("exit".to_string());
}
} else {
- if self.roster.take_click(15) {
- self.layout_idx = self.roster.value(15) as usize;
- self.relayout();
- self.apply_layout();
+ if self.apply_header_dropdowns() {
changed = true;
} else if self.roster.take_click(12) {
spawn_editor("ColorRamp");
@@ -1839,6 +1801,11 @@ impl cce_ui::engine::Application for State {
return Some("exit".to_string());
}
+ // A header dropdown driven by the keyboard selects on Enter: apply it now.
+ if !self.is_child && self.apply_header_dropdowns() {
+ changed = true;
+ }
+
if changed {
*needs_rebuild = true;
}
@@ -1903,10 +1870,12 @@ fn demo_positions(sw: f32, sh: f32, count: usize) -> Vec<(f32, f32, f32, f32)> {
let mut vec = vec![(0.0, 0.0, 0.0, 0.0); count];
vec[0] = (0.0, 0.0, sw, 40.0); // MenuBar
vec[1] = (0.0, sh - 24.0, sw, 24.0); // StatusBar
- // The Layout dropdown; the exhibits below it are laid out by `layout_exhibits`
- // with the strategy it selects. Its height is the widget's own preferred
- // height (`apply_layout`); the one here is a placeholder.
+ // The header dropdowns; the exhibits below them are laid out by
+ // `layout_exhibits` with the strategy Layout selects. Their heights are the
+ // widgets' own preferred heights (`apply_layout`); the ones here are placeholders.
vec[15] = (20.0, 60.0, 190.0, 0.0);
+ // The Style dropdown, one gap to its right.
+ vec[34] = (20.0 + 190.0 + GAP, 60.0, 190.0, 0.0);
vec
}