widget gallery and compositor test bench
feat: every style of a widget, and labeled controls at their full default height
The variant exhibits: after the one-of-each roster come the further looks each
widget can take — Button's reset, list-row, menu-item, copy-icon and flat faces;
Toggle slide and flat; Slider band, flat and with a readout; TextBox multiline,
chromeless and password; Dropdown flat; the vertical ButtonStrip and the Paginator
on it; the plain Label; the other three StatusDot statuses. They live in
`GallerySlots::extra` as boxed widgets addressed as slots 32.., so adding a style is
one line in `variant_exhibits`.
The Layout dropdown, and every labeled control that eats its label from its rect, now
take the toolkit's preferred height (cce-ui@d48b622),
which is what made the dropdown, spinbox and sliders look short: they were drawn in a
16-24px rect minus an 18px label strip. Exhibits whose label is wider than they are (a
StatusDot, a vertical tab column) are laid out at the label's width and handed their
own width back afterwards.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
README.md | 29 ++++++++---
src/main.rs | 165 +++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
2 files changed, 173 insertions(+), 21 deletions(-)
diff --git a/README.md b/README.md
index c99e8a5..2869379 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,10 @@
A widget gallery and compositor-behaviour test bench for the `cce` desktop
environment. It is a single `cce-ui` client (a real Wayland surface, drawn as
-GPU primitives) that shows the toolkit's widgets on one page, spawns simulated
-client windows of different shell types on another, and exercises the XDG
-file-chooser portal on a third. It ships a `.desktop` entry under
-Utility/Development, so it appears in the launcher.
+GPU primitives) that shows the toolkit's widgets, in every style each can take,
+on one page, and doubles as the binary behind a set of test windows of
+different shell types. It ships a `.desktop` entry under Utility/Development,
+so it appears in the launcher.
It is a *visual* test harness: you look at what it draws and what the
compositor does with the windows it spawns. It has no automated test suite.
@@ -20,6 +20,18 @@ StatusDot, Separator, Splitter, InfoBox, InteractiveListItem, Breadcrumb,
TreeList, Plate, BevelPreview, RampPreview, Ramp. The MenuBar and StatusBar
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_band`, `with_recessed(false)` and `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 four StatusDot statuses and the plain
+Label round it off. When a widget gains a style, it gains an exhibit here.
+
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
the page is a record of the defaults; only the draw-in canvases (Trackpad,
@@ -86,10 +98,11 @@ for example). Nothing else is bound; the `cce-gallery` domain of
## Layout of the source
- `src/main.rs` — the `Application` impl. The gallery is a concretely typed
- roster of 32 named slots (`GallerySlots`) addressed by numeric index in the
- layout tables, visibility filters and dispatch loops; child windows use the
- five-slot `ChildSlots`. `demo_positions` and `child_positions` are the
- layout tables.
+ roster of 32 named slots (`GallerySlots`) followed by the variant exhibits
+ (`variant_exhibits`, a `Vec<Exhibit>` of boxed widgets), all addressed by
+ numeric index in the layout tables, visibility filters and dispatch loops;
+ child windows use the five-slot `ChildSlots`. `demo_positions` and
+ `child_positions` are the layout tables.
- `src/gallery_widgets.rs` — lookalikes of the retired toolkit `Plate` (an
exhibit) and root plate (the child windows' background).
diff --git a/src/main.rs b/src/main.rs
index c72122f..97eaa25 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,7 +3,7 @@ use cce_ui::widget::{
Toggle, WidgetHost, Trackpad, hover_animation, TextBox, MenuBar,
Ramp, RampKey, ColorRamp, MouseButton, ElementState, Key, NamedKey, KeyEvent, MouseScrollDelta,
ColorSelector, FontSelector, KeybindRecorder, ButtonStrip, Float3, UsageBar, StatusDot, DotStatus,
- InfoBox, InteractiveListItem, Breadcrumb, TreeList, BevelPreview, RampPreview, Separator, Splitter,
+ InfoBox, InteractiveListItem, Breadcrumb, TreeList, BevelPreview, RampPreview, Separator, Splitter, Paginator,
VerticalLayout, ColumnsLayout, GridLayout, AdaptiveGridLayout, MosaicLayout, ReverseMosaicLayout, OverlayLayout, ScrollBox,
};
mod gallery_widgets;
@@ -155,7 +155,7 @@ impl Visibility {
_ => false,
};
}
- index < GALLERY_COUNT
+ true
}
}
@@ -195,14 +195,52 @@ pub struct GallerySlots {
pub ramp_preview_demo: Adapted<RampPreview>,
pub separator_demo: Adapted<Separator>,
pub splitter_demo: Adapted<Splitter>,
+ /// 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>,
}
+/// 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 = 32;
+/// 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.
+pub struct Exhibit {
+ pub widget: Box<dyn WidgetHost + 'static>,
+ pub w: f32,
+ pub h: f32,
+ /// A content width narrower than `w`: the strategy lays the exhibit out at `w` (room
+ /// for its label) and the widget is then given this width — a StatusDot stays a dot.
+ pub content_w: Option<f32>,
+}
+
+impl Exhibit {
+ /// Sized by the widget's own preferred height, `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);
+ Exhibit { widget: Box::new(widget), w, h, content_w: None }
+ }
+
+ /// Sized by hand: for widgets whose declared height is a single row (a multiline
+ /// TextBox, a vertical ButtonStrip) when the exhibit wants several.
+ fn sized<W: WidgetHost + 'static>(widget: W, w: f32, h: f32) -> Self {
+ Exhibit { widget: Box::new(widget), w, h, content_w: None }
+ }
+
+ fn with_content_width(mut self, w: f32) -> Self {
+ self.content_w = Some(w);
+ self
+ }
+}
+
impl GallerySlots {
// Per-slot drag queries.
pub fn draggable(&self, idx: usize) -> bool {
+ if idx >= GALLERY_COUNT {
+ return false; // variant exhibits never drag
+ }
match idx {
0 => self.menu_bar.draggable(),
1 => self.status_bar.draggable(),
@@ -241,6 +279,9 @@ impl GallerySlots {
}
pub fn is_dragging(&self, idx: usize) -> bool {
+ if idx >= GALLERY_COUNT {
+ return false;
+ }
match idx {
0 => self.menu_bar.is_dragging(),
1 => self.status_bar.is_dragging(),
@@ -279,6 +320,9 @@ impl GallerySlots {
}
pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
+ if idx >= GALLERY_COUNT {
+ return &*self.extra[idx - GALLERY_COUNT].widget;
+ }
match idx {
0 => &self.menu_bar,
1 => &self.status_bar,
@@ -317,6 +361,9 @@ impl GallerySlots {
}
pub fn get_dyn_mut(&mut self, idx: usize) -> &mut (dyn WidgetHost + 'static) {
+ if idx >= GALLERY_COUNT {
+ return &mut *self.extra[idx - GALLERY_COUNT].widget;
+ }
match idx {
0 => &mut self.menu_bar,
1 => &mut self.status_bar,
@@ -495,7 +542,7 @@ pub enum Roster {
impl Roster {
pub fn len(&self) -> usize {
match self {
- Roster::Gallery(_) => GALLERY_COUNT,
+ Roster::Gallery(s) => GALLERY_COUNT + s.extra.len(),
Roster::Child(_) => CHILD_COUNT,
}
}
@@ -583,6 +630,8 @@ impl Roster {
29 => s.ramp_preview_demo.take_click(),
30 => s.separator_demo.take_click(),
31 => s.splitter_demo.take_click(),
+ // The variant exhibits are looked at, not drained.
+ i if i >= GALLERY_COUNT => false,
_ => panic!("take_click: unwired gallery slot {idx}"),
}
}
@@ -675,10 +724,78 @@ fn load_bevel_ramp() -> (Vec<RampKey>, String) {
)
}
+/// Every further style of a widget the named slots show once: the toolkit's own
+/// 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.
+fn variant_exhibits() -> Vec<Exhibit> {
+ const W: f32 = 190.0;
+ let bh = cce_ui::layout::button_height();
+ let tgh = cce_ui::layout::toggle_height();
+ let slh = cce_ui::layout::slider_height();
+ let tbh = cce_ui::layout::textbox_height();
+ let ddh = cce_ui::layout::dropdown_height();
+ // A StatusDot is 12px square; its exhibit is as wide as its label.
+ const DOT_W: f32 = 150.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()];
+ vec![
+ // Button: the five kinds, and the flat (unraised) face.
+ 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.
+ 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: the band style (config `style.control.slider.style`), flat, with a readout.
+ Exhibit::new(Slider::new().with_label("Slider (band)").with_band(true), W, slh),
+ Exhibit::new(Slider::new().with_label("Slider (flat)").with_recessed(false), W, slh),
+ Exhibit::new(Slider::new().with_label("Slider (readout)").with_readout(true), W, slh),
+ // TextBox: multiline, chromeless, password.
+ Exhibit::sized(
+ TextBox::new("TextBox (multiline)\nA second line of text.".to_string()).with_multiline(true),
+ W,
+ 3.0 * tbh,
+ ),
+ 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))
+ .with_label("ButtonStrip (vertical)"),
+ W,
+ TABS_H,
+ )
+ .with_content_width(TAB_COLUMN_W),
+ Exhibit::sized(Paginator::new(three()).with_label("Paginator"), 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),
+ ]
+}
+
/// 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 {
- matches!(i, 2..=14 | 16..=31)
+ matches!(i, 2..=14 | 16..=31) || i >= GALLERY_COUNT
}
impl State {
@@ -699,7 +816,7 @@ impl State {
self.positions = if self.is_child {
child_positions(self.width, self.height, self.use_menubar, self.use_statusbar, self.child_kind.is_some_and(ChildKind::is_editor))
} else {
- demo_positions(self.width, self.height)
+ demo_positions(self.width, self.height, self.roster.len())
};
}
@@ -727,6 +844,9 @@ impl State {
// or drag in, sized here and only here.
const CANVAS_H: f32 = 120.0;
const W: f32 = 190.0;
+ // A StatusDot is 12px square; its exhibit is as wide as its label
+ // (`content_width` hands the dot its own size back after layout).
+ const DOT_W: f32 = 150.0;
let raw: [(usize, f32, f32); 29] = [
(2, W, bh), // Button
(19, W, bh), // ButtonStrip
@@ -743,7 +863,7 @@ impl State {
(17, W, fsh), // FontSelector
(5, W, pbh), // ProgressBar
(22, W, pbh), // UsageBar
- (23, StatusDot::SIZE, StatusDot::SIZE), // StatusDot
+ (23, DOT_W, StatusDot::SIZE), // StatusDot (its label needs the width)
(30, W, 1.0), // Separator (a rule)
(31, 6.0, CANVAS_H), // Splitter (a vertical grip)
(24, W, 3.0 * ddh), // InfoBox (title + two lines)
@@ -758,7 +878,10 @@ impl State {
(12, W, bh), // Color Ramp...
(14, W, bh), // Ramp...
];
- raw.to_vec()
+ let mut sizes = raw.to_vec();
+ let extra = &self.roster.gallery().extra;
+ sizes.extend(extra.iter().enumerate().map(|(k, e)| (GALLERY_COUNT + k, e.w, e.h)));
+ sizes
}
/// Lay the Controls exhibits out with the toolkit strategy the Layout dropdown selects
@@ -785,6 +908,7 @@ impl State {
let (x, y, w, h) = self.exhibit_viewport();
self.exhibit_scroll.set_rect(x, y, w, h);
let mut children: Vec<*mut (dyn WidgetHost + 'static)> = Vec::new();
+ let mut indices: Vec<usize> = Vec::new();
for (idx, cw, ch) in self.exhibit_sizes() {
if self.roster.is_dragging(idx) {
continue;
@@ -792,12 +916,13 @@ impl State {
let widget = self.roster.get_dyn_mut(idx);
widget.set_rect(0.0, 0.0, cw, ch);
children.push(widget as *mut (dyn WidgetHost + 'static));
+ indices.push(idx);
}
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 in &children {
+ 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
@@ -812,10 +937,20 @@ impl State {
if widget.preferred_height().is_none() {
content -= l;
}
+ let cw = self.content_width(idx).unwrap_or(cw);
widget.set_rect(cx, cy - scroll_y, cw, content);
}
}
+ /// An exhibit's content width where it is narrower than the width it is laid out at
+ /// (`Exhibit::content_w`; the named StatusDot slot likewise).
+ fn content_width(&self, idx: usize) -> Option<f32> {
+ if idx == 23 {
+ return Some(StatusDot::SIZE);
+ }
+ idx.checked_sub(GALLERY_COUNT).and_then(|k| self.roster.gallery().extra[k].content_w)
+ }
+
/// Register every roster widget in the ui_context (idempotent — `register` is
/// id-keyed and the boxed slots keep pointers stable). The id-rooted router
/// resolves roots through this registry; the gallery's own paint loop never goes
@@ -840,6 +975,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);
+ // The Layout dropdown takes the height the toolkit says a labeled
+ // dropdown needs — its default height plus the label strip.
+ let h = if i == 15 { widget.preferred_height().unwrap_or(h) } else { h };
if visible {
widget.set_rect(x, y, w, h);
} else {
@@ -993,6 +1131,7 @@ impl cce_ui::engine::Application for State {
ramp_preview_demo: RampPreview::new().with_label("RampPreview"),
separator_demo: Separator::new(0.0, 0.0, 200.0, 1.0, [0.5, 0.5, 0.6, 1.0]).with_label("Separator"),
splitter_demo: Splitter::new(200.0).with_label("Splitter"),
+ extra: variant_exhibits(),
}))
};
@@ -1623,14 +1762,14 @@ const DEFAULT_LAYOUT: usize = 4;
/// The fixed positions: the chrome and the Layout dropdown. The exhibits are laid out
/// by `layout_exhibits` instead.
-fn demo_positions(sw: f32, sh: f32) -> Vec<(f32, f32, f32, f32)> {
- let ddh = cce_ui::layout::dropdown_height();
- let mut vec = vec![(0.0, 0.0, 0.0, 0.0); GALLERY_COUNT];
+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.
- vec[15] = (20.0, 60.0, 190.0, ddh);
+ // with the strategy it selects. Its height is the widget's own preferred
+ // height (`apply_layout`); the one here is a placeholder.
+ vec[15] = (20.0, 60.0, 190.0, 0.0);
vec
}