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

commit3e0aca9e17668e9ecc38bcf8e902bbda7200ef6b
parent0c229a7617
authorLucas Galante <[email protected]>
date2026-09-06 13:01
feat!: the gallery reflects the cce that exists

Windows page: the child kinds are the surfaces a toolkit client can really
be under cce, each mapped the way the compositor sees it — Floating (a plain
toplevel), Fullscreen (the toplevel's fullscreen flag), Utility (the
`utility` hook: set_utility on the cce protocol), Layer: Top / Overlay /
Background (the `layer` hook with a LayerSettings: top anchored
left-top-right with an exclusive zone, overlay unanchored, background on
every edge), and Status segment (the cce-status-* app_id convention, which
the compositor docks into the bar). A `ChildKind` enum carries the `--type`
spelling, label, title, default size, app_id, layer settings, the line in
the child and the plate description; the descriptions now talk about
Floating / Tiled / Fullscreen / Utility and mode_rules, not "cascade, split,
grid" and "layer shell top" fictions. Gone: "Popup" (a client cannot make a
bare xdg_popup) and the Shape dropdown (circular rendering is a compositor
mode_rule; the toolkit cannot request it). Two kinds document compositor
gaps found while verifying: a Fullscreen child maps Floating because cce
acts only on the request_fullscreen signal, not on a state set before the
first commit, and a Background-layer child is covered by the natively drawn
desktop grid.

Controls page: sixteen more exhibits, one per exported widget that stands on
its own — ButtonStrip, Slider2D, Float3, KeybindRecorder, ColorSelector,
FontSelector, UsageBar, StatusDot, Separator, Splitter, InfoBox,
InteractiveListItem, Breadcrumb, TreeList, BevelPreview, RampPreview. The
Layout dropdown now runs the toolkit's own ContainerLayout strategies over
the exhibits (Vertical, Columns, Grid, Adaptive Grid, Mosaic, Reverse
Mosaic, Overlay) instead of a private packer with four strategies cce-ui
never had (Flex, Splitter, Radial, Circular Pane); the dropdown sits above
the exhibits so Columns and Overlay leave it usable. layout_exhibits resets
each exhibit to its content size, runs the strategy, and takes back the
second label inflation the adapter adds when a strategy feeds an inflated
rect through set_rect. The main window defaults to 1000x680 and honours
--width/--height.

serde_json joins the dependencies for the TreeList exhibit's values.

Verified in shadow sessions: every kind spawns from argv and from the page
(a Layer: Top child through Create Window), all seven layouts select and
lay out, no panics.

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

 Cargo.toml  |    1 +
 README.md   |   72 ++--
 src/main.rs | 1385 +++++++++++++++++++++++++++++++----------------------------
 3 files changed, 771 insertions(+), 687 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml
index 33467b2..1ad7984 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,4 +7,5 @@ edition = "2021"
 cce-ui = { path = "../cce-ui" }
 calloop = "0.13.0"
 wayland-client = { version = "0.31", features = ["system"] }
+serde_json = "1"
 
diff --git a/README.md b/README.md
index 17b336b..c7867eb 100644
--- a/README.md
+++ b/README.md
@@ -15,19 +15,26 @@ compositor does with the windows it spawns. It has no automated test suite.
 The dropdown at the right of the status bar switches between three pages; the
 keyboard does the same (see Keybindings).
 
-**Controls** — the widget gallery. One of each of the toolkit's standard
-widgets: Button, Checkbox, Toggle, ProgressBar, Slider, RangeSlider, Spinbox,
-TextBox, Trackpad, Plate, plus the MenuBar and StatusBar that frame every
-page. A `Layout` dropdown re-packs the gallery under each of the toolkit's
-layout strategies (Vertical, Columns, Grid, Adaptive Grid, Overlay, Flex,
-Splitter, Radial, Circular Pane, Mosaic, Reverse Mosaic). `Color Ramp...` and
-`Ramp...` open the ColorRamp and Ramp editors in their own child windows.
-
-**Windows** — spawns *simulated client windows* so the compositor's handling
-of each surface kind can be observed. The control panel on the right chooses:
-
-- window type: Toplevel, Popup, Layer Top, Layer Overlay, Layer Background
-- shape: rectangular or circular
+**Controls** — the widget gallery: one of each widget `cce_ui::widget`
+exports that stands on its own. Inputs: Button, ButtonStrip, Checkbox, Toggle,
+Slider, RangeSlider, Slider2D, Spinbox, Float3, TextBox, KeybindRecorder,
+ColorSelector, FontSelector, Trackpad. Display: ProgressBar, UsageBar,
+StatusDot, Separator, Splitter, InfoBox, InteractiveListItem, Breadcrumb,
+TreeList, Plate, BevelPreview, RampPreview, Ramp. The MenuBar and StatusBar
+frame every page. 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. `Color Ramp...` and `Ramp...` open the ColorRamp and
+Ramp editors in their own child windows.
+
+**Windows** — spawns child windows of each kind a toolkit client can be under
+cce, so the compositor's handling of each can be observed. The control panel
+on the right chooses:
+
+- window kind: Floating (a plain toplevel), Fullscreen, Utility (declared
+  over the cce window-management protocol), the Top, Overlay and Background
+  wlr-layer-shell layers, and a Status segment (the `cce-status-*` app_id
+  convention the compositor docks into the bar)
 - size (width/height spinboxes), opacity on/off with a transparency slider
 - window elements: backplate, menu bar, status bar
 - border: enabled/disabled, width, bevel on/off, bevel depth, and the bevel
@@ -35,8 +42,16 @@ of each surface kind can be observed. The control panel on the right chooses:
 
 `Create Window` re-executes this binary with `--child` and the matching flags
 (see below). A panel in the main window previews the border and bevel that
-the child will be drawn with, and a description plate explains what the
-selected surface type is and how it is expected to be laid out.
+the child will be drawn with, and a description plate says what cce does with
+the selected kind. Tiled is not on the list because a client cannot ask for
+it: the compositor's toggle action tiles a floating window. A `mode_rule` in
+`config.kdl` can force a mode, or circular rendering, for an app_id.
+
+Two kinds show a gap in the current compositor rather than a feature: a
+Fullscreen child maps Floating, because cce acts only on the
+`request_fullscreen` signal and not on a fullscreen state set before the
+first commit, and a Background-layer child is never visible, because cce
+draws its desktop grid natively above that layer.
 
 **XDG** — two buttons, `Open File` and `Save File`, that call the toolkit's
 `file_dialog` module (the XDG Desktop Portal file chooser, via `rfd`) on a
@@ -49,20 +64,27 @@ The child windows spawned from the Windows page are the same binary run with
 `--child`. The flags mirror the control panel:
 
 ```
-cce-gallery --child --type <Toplevel|Popup|LayerTop|LayerOverlay|LayerBackground|Ramp|ColorRamp>
-                   [--shape rectangular|circular] [--width N --height N]
+cce-gallery --child --type <Floating|Fullscreen|Utility|LayerTop|LayerOverlay|LayerBackground|Status|Ramp|ColorRamp>
+                   [--width N --height N]
                    [--opacity --transparency 0.0-1.0]
                    [--no-border | --border-width N [--border-bevel]]
                    [--backplate] [--menubar] [--statusbar]
 ```
 
-A simulated window shows a description label, a `Close` button, and the
-optional menu bar and status bar. `--border-width` and `--border-bevel` are
-accepted so the argv mirrors the panel, but the child does not draw a bevel
-yet; the bevel preview lives in the main window's panel. Its `app_id` encodes what it is, so
-compositor rules can target it: `cce-gallery-child-<type>` with `-circular`
-and/or `-noborder` appended when those apply. The main window's `app_id` is
-`cce-gallery` (also with `-noborder` when the border is disabled).
+The kind decides how the child asks to be mapped: Fullscreen sets the
+toplevel's fullscreen flag, Utility answers the toolkit's `utility` hook, the
+three Layer kinds answer its `layer` hook with a `LayerSettings` (Top is
+anchored left-top-right with an exclusive zone of its height, Overlay is
+unanchored, Background is anchored on every edge), and Status uses the app_id
+`cce-status-right-gallery`. Every other kind's app_id is
+`cce-gallery-child-<type>`, with `-noborder` appended when the border is
+disabled, so `mode_rule`s can target it. The main window's `app_id` is
+`cce-gallery`.
+
+A child shows a one-line description, a `Close` button, and the optional menu
+bar and status bar. `--border-width` and `--border-bevel` are accepted so the
+argv mirrors the panel, but the child does not draw a bevel yet; the bevel
+preview lives in the main window's panel.
 
 `--type Ramp` and `--type ColorRamp` are editor windows rather than simulated
 clients: they host the toolkit's `Ramp` and `ColorRamp` widgets on a
@@ -106,7 +128,7 @@ for example).
 ## Layout of the source
 
 - `src/main.rs` — the `Application` impl. The gallery is a concretely typed
-  roster of 44 named slots (`GallerySlots`) addressed by numeric index in the
+  roster of 59 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.
diff --git a/src/main.rs b/src/main.rs
index 48877ef..76e61ae 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,12 +1,16 @@
 use cce_ui::widget::{
     Button, Checkbox, ContentBg, Dropdown, Label, Panel, ProgressBar, RangeSlider, Slider, Spinbox, StatusBar,
     Toggle, WidgetHost, Trackpad, hover_animation, TextBox, CornerRadii, MenuBar,
-    Ramp, RampKey, ColorRamp, MouseButton, ElementState, Key, NamedKey, KeyEvent, MouseScrollDelta
+    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,
+    VerticalLayout, ColumnsLayout, GridLayout, AdaptiveGridLayout, MosaicLayout, ReverseMosaicLayout, OverlayLayout,
 };
 mod gallery_widgets;
 use gallery_widgets::{Backplate, ControlPanel, Plate, SectionContainer};
 use cce_ui::widget::Adapted;
-use cce_ui::engine::{Vertex, quad_vertices, LogicalSize, LogicalPosition};
+use cce_ui::widget::input::Slider2D;
+use cce_ui::engine::{Vertex, quad_vertices, LogicalSize, LogicalPosition, LayerAnchor, LayerKeyboardInteractivity, LayerKind, LayerSettings};
 use wayland_client::QueueHandle;
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -34,6 +38,216 @@ impl Page {
     }
 }
 
+/// What a child window is, from `--type`. The first seven are the kinds of surface a
+/// toolkit client can be under cce; the last two are the gallery's editor windows.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+enum ChildKind {
+    Floating,
+    Fullscreen,
+    Utility,
+    LayerTop,
+    LayerOverlay,
+    LayerBackground,
+    Status,
+    Ramp,
+    ColorRamp,
+}
+
+impl ChildKind {
+    /// The Windows page's dropdown, in order.
+    const WINDOW_KINDS: [ChildKind; 7] = [
+        ChildKind::Floating, ChildKind::Fullscreen, ChildKind::Utility,
+        ChildKind::LayerTop, ChildKind::LayerOverlay, ChildKind::LayerBackground, ChildKind::Status,
+    ];
+
+    fn from_arg(s: &str) -> Option<ChildKind> {
+        Some(match s {
+            "Floating" => ChildKind::Floating,
+            "Fullscreen" => ChildKind::Fullscreen,
+            "Utility" => ChildKind::Utility,
+            "LayerTop" => ChildKind::LayerTop,
+            "LayerOverlay" => ChildKind::LayerOverlay,
+            "LayerBackground" => ChildKind::LayerBackground,
+            "Status" => ChildKind::Status,
+            "Ramp" => ChildKind::Ramp,
+            "ColorRamp" => ChildKind::ColorRamp,
+            _ => return None,
+        })
+    }
+
+    fn from_dropdown(i: i32) -> ChildKind {
+        Self::WINDOW_KINDS.get(i.max(0) as usize).copied().unwrap_or(ChildKind::Floating)
+    }
+
+    /// The `--type` spelling.
+    fn arg(self) -> &'static str {
+        match self {
+            ChildKind::Floating => "Floating",
+            ChildKind::Fullscreen => "Fullscreen",
+            ChildKind::Utility => "Utility",
+            ChildKind::LayerTop => "LayerTop",
+            ChildKind::LayerOverlay => "LayerOverlay",
+            ChildKind::LayerBackground => "LayerBackground",
+            ChildKind::Status => "Status",
+            ChildKind::Ramp => "Ramp",
+            ChildKind::ColorRamp => "ColorRamp",
+        }
+    }
+
+    /// The dropdown entry.
+    fn label(self) -> &'static str {
+        match self {
+            ChildKind::Floating => "Floating",
+            ChildKind::Fullscreen => "Fullscreen",
+            ChildKind::Utility => "Utility",
+            ChildKind::LayerTop => "Layer: Top",
+            ChildKind::LayerOverlay => "Layer: Overlay",
+            ChildKind::LayerBackground => "Layer: Background",
+            ChildKind::Status => "Status segment",
+            ChildKind::Ramp => "Ramp",
+            ChildKind::ColorRamp => "ColorRamp",
+        }
+    }
+
+    fn title(self) -> &'static str {
+        match self {
+            ChildKind::Floating => "Floating Window",
+            ChildKind::Fullscreen => "Fullscreen Window",
+            ChildKind::Utility => "Utility Window",
+            ChildKind::LayerTop => "Layer Shell (Top) Surface",
+            ChildKind::LayerOverlay => "Layer Shell (Overlay) Surface",
+            ChildKind::LayerBackground => "Layer Shell (Background) Surface",
+            ChildKind::Status => "Status Segment",
+            ChildKind::Ramp => "Ramp Editor",
+            ChildKind::ColorRamp => "Color Ramp Editor",
+        }
+    }
+
+    fn is_editor(self) -> bool {
+        matches!(self, ChildKind::Ramp | ChildKind::ColorRamp)
+    }
+
+    fn default_size(self) -> (f32, f32) {
+        match self {
+            ChildKind::Floating | ChildKind::Fullscreen => (400.0, 250.0),
+            ChildKind::Utility | ChildKind::LayerOverlay => (300.0, 180.0),
+            ChildKind::LayerTop => (800.0, 40.0),
+            ChildKind::LayerBackground => (800.0, 600.0),
+            ChildKind::Status => (140.0, 24.0),
+            ChildKind::Ramp | ChildKind::ColorRamp => (450.0, 350.0),
+        }
+    }
+
+    /// `cce-status-*` is the compositor's status-bar convention (`WindowRole::from_app_id`),
+    /// with the edge read from the `-left-` / `-right-` infix; everything else is ours.
+    fn app_id(self) -> String {
+        match self {
+            ChildKind::Status => "cce-status-right-gallery".to_string(),
+            kind => format!("cce-gallery-child-{}", kind.arg().to_lowercase()),
+        }
+    }
+
+    /// The wlr-layer-shell configuration for the three layer kinds.
+    fn layer(self, height: i32) -> Option<LayerSettings> {
+        let (layer, anchor, exclusive_zone) = match self {
+            ChildKind::LayerTop => (LayerKind::Top, LayerAnchor::TOP | LayerAnchor::LEFT | LayerAnchor::RIGHT, height),
+            ChildKind::LayerOverlay => (LayerKind::Overlay, LayerAnchor::empty(), 0),
+            ChildKind::LayerBackground => (
+                LayerKind::Background,
+                LayerAnchor::TOP | LayerAnchor::BOTTOM | LayerAnchor::LEFT | LayerAnchor::RIGHT,
+                -1,
+            ),
+            _ => return None,
+        };
+        Some(LayerSettings {
+            layer,
+            anchor,
+            exclusive_zone,
+            keyboard_interactivity: LayerKeyboardInteractivity::None,
+            margin: (0, 0, 0, 0),
+            namespace: "cce-gallery".to_string(),
+        })
+    }
+
+    /// The line inside the child window.
+    fn child_text(self) -> &'static str {
+        match self {
+            ChildKind::Floating => "A plain xdg_toplevel, mapped Floating by cce.",
+            ChildKind::Fullscreen => "An xdg_toplevel that asked for fullscreen at map.",
+            ChildKind::Utility => "A toplevel declared UTILITY over the cce protocol.",
+            ChildKind::LayerTop => "A Top-layer wlr-layer-shell surface with an exclusive zone.",
+            ChildKind::LayerOverlay => "An Overlay-layer wlr-layer-shell surface, unanchored.",
+            ChildKind::LayerBackground => "A Background-layer wlr-layer-shell surface.",
+            ChildKind::Status => "A cce-status-* toplevel, docked into the status bar.",
+            ChildKind::Ramp | ChildKind::ColorRamp => "",
+        }
+    }
+
+    /// The Windows page's description plate (about 28 characters per line).
+    fn description(self) -> &'static str {
+        match self {
+            ChildKind::Floating => "A plain xdg_toplevel. cce\n\
+maps it Floating: moved and\n\
+resized freely, its edges\n\
+snapping to the desktop\n\
+grid's cell edges. The\n\
+toggle action tiles it: every\n\
+edge on a cell edge, reported\n\
+as maximized. A mode_rule in\n\
+config.kdl can force a mode.",
+            ChildKind::Fullscreen => "An xdg_toplevel that sets\n\
+fullscreen before its first\n\
+commit. cce acts only on the\n\
+later request_fullscreen\n\
+signal, so today it maps\n\
+Floating; the fullscreen\n\
+action still toggles it, and\n\
+Fullscreen mode then covers\n\
+the output.",
+            ChildKind::Utility => "A toplevel declaring\n\
+set_utility on the cce\n\
+window-management protocol:\n\
+a tool window whose contents\n\
+decide its size. It floats\n\
+and moves like any window\n\
+but gets no resize handle\n\
+and no saved geometry.",
+            ChildKind::LayerTop => "A wlr-layer-shell surface\n\
+on the Top layer, anchored\n\
+left-top-right with an\n\
+exclusive zone.\n\n\
+cce reserves that strip and\n\
+lays normal windows out\n\
+below it, like a panel.",
+            ChildKind::LayerOverlay => "A wlr-layer-shell surface\n\
+on the Overlay layer,\n\
+unanchored: centred above\n\
+every window, keyboard focus\n\
+left with the windows below.\n\n\
+cce-notifier's toasts are\n\
+this kind of surface.",
+            ChildKind::LayerBackground => "A wlr-layer-shell surface\n\
+on the Background layer,\n\
+anchored to all four edges,\n\
+behind every window.\n\n\
+cce draws its desktop grid\n\
+natively above this layer,\n\
+so today the surface is\n\
+covered and never seen.",
+            ChildKind::Status => "An xdg_toplevel whose app_id\n\
+starts with cce-status: the\n\
+WindowRole::StatusBar\n\
+convention. cce docks it\n\
+into the status bar at\n\
+bar_height, spaced among the\n\
+other segments, the way\n\
+cce-status-interface's\n\
+modules are.",
+            ChildKind::Ramp | ChildKind::ColorRamp => "",
+        }
+    }
+}
+
 /// What the roster's visibility filter needs, copied out of `State` so the dispatch
 /// loops can hold `&mut self.roster` while asking.
 #[derive(Clone, Copy)]
@@ -55,10 +269,10 @@ impl Visibility {
             };
         }
         match index {
-            0..=1 | 37 => true,
-            2..=7 | 21 | 22 | 27 | 28 | 38 | 40 | 41 | 43 => self.page == Page::Controls,
-            8..=20 | 29..=36 | 39 | 42 => self.page == Page::Windows,
-            23..=26 => self.page == Page::Xdg,
+            0..=1 | 36 => true,
+            2..=7 | 20 | 21 | 26 | 27 | 37 | 39 | 40 | 42..=58 => self.page == Page::Controls,
+            8..=19 | 28..=35 | 38 | 41 => self.page == Page::Windows,
+            22..=25 => self.page == Page::Xdg,
             _ => false,
         }
     }
@@ -71,7 +285,7 @@ struct PreviewStyle {
     bg_color: [f32; 4],
 }
 
-/// The gallery roster: 44 named slots. The numeric indexes used by the positions
+/// The gallery roster: 59 named slots. The numeric indexes used by the positions
 /// table, the visibility filter and the dispatch loops address these slots through
 /// `get_dyn`/`get_dyn_mut`.
 pub struct GallerySlots {
@@ -89,7 +303,6 @@ pub struct GallerySlots {
     pub transparency_slider: Adapted<Slider>,
     pub transparency_label: Adapted<Label>,
     pub window_type_dd: Adapted<Dropdown>,
-    pub window_shape_dd: Adapted<Dropdown>,
     pub enable_toggle: Adapted<Toggle>,
     pub width_spin: Adapted<Spinbox>,
     pub height_spin: Adapted<Spinbox>,
@@ -119,9 +332,25 @@ pub struct GallerySlots {
     pub ramp_btn: Adapted<Button>,
     pub control_panel: Adapted<ControlPanel>,
     pub layout_dd: Adapted<Dropdown>,
+    pub color_selector_demo: Adapted<ColorSelector>,
+    pub font_selector_demo: Adapted<FontSelector>,
+    pub keybind_demo: Adapted<KeybindRecorder>,
+    pub button_strip_demo: Adapted<ButtonStrip>,
+    pub slider2d_demo: Adapted<Slider2D>,
+    pub float3_demo: Adapted<Float3>,
+    pub usage_bar_demo: Adapted<UsageBar>,
+    pub status_dot_demo: Adapted<StatusDot>,
+    pub info_box_demo: Adapted<InfoBox>,
+    pub list_item_demo: Adapted<InteractiveListItem>,
+    pub breadcrumb_demo: Adapted<Breadcrumb>,
+    pub tree_list_demo: Adapted<TreeList>,
+    pub bevel_preview_demo: Adapted<BevelPreview>,
+    pub ramp_preview_demo: Adapted<RampPreview>,
+    pub separator_demo: Adapted<Separator>,
+    pub splitter_demo: Adapted<Splitter>,
 }
 
-pub const GALLERY_COUNT: usize = 44;
+pub const GALLERY_COUNT: usize = 59;
 
 impl GallerySlots {
 
@@ -142,36 +371,51 @@ impl GallerySlots {
             11 => self.transparency_slider.draggable(),
             12 => self.transparency_label.draggable(),
             13 => self.window_type_dd.draggable(),
-            14 => self.window_shape_dd.draggable(),
-            15 => self.enable_toggle.draggable(),
-            16 => self.width_spin.draggable(),
-            17 => self.height_spin.draggable(),
-            18 => self.surface_plate.draggable(),
-            19 => self.surface_info_label.draggable(),
-            20 => self.surface_desc_label.draggable(),
-            21 => self.range_slider_demo.draggable(),
-            22 => self.trackpad_demo.draggable(),
-            23 => self.portal_panel.draggable(),
-            24 => self.portal_label.draggable(),
-            25 => self.open_dialog_btn.draggable(),
-            26 => self.save_dialog_btn.draggable(),
-            27 => self.textbox_demo.draggable(),
-            28 => self.plate_demo.draggable(),
-            29 => self.backplate_toggle.draggable(),
-            30 => self.menubar_toggle.draggable(),
-            31 => self.statusbar_toggle.draggable(),
-            32 => self.border_section.draggable(),
-            33 => self.bevel_toggle.draggable(),
-            34 => self.border_width_spin.draggable(),
-            35 => self.bevel_depth_spin.draggable(),
-            36 => self.elements_section.draggable(),
-            37 => self.page_selector.draggable(),
-            38 => self.color_ramp_btn.draggable(),
-            39 => self.bevel_shape_btn.draggable(),
-            40 => self.bevel_ramp.draggable(),
-            41 => self.ramp_btn.draggable(),
-            42 => self.control_panel.draggable(),
-            43 => self.layout_dd.draggable(),
+            14 => self.enable_toggle.draggable(),
+            15 => self.width_spin.draggable(),
+            16 => self.height_spin.draggable(),
+            17 => self.surface_plate.draggable(),
+            18 => self.surface_info_label.draggable(),
+            19 => self.surface_desc_label.draggable(),
+            20 => self.range_slider_demo.draggable(),
+            21 => self.trackpad_demo.draggable(),
+            22 => self.portal_panel.draggable(),
+            23 => self.portal_label.draggable(),
+            24 => self.open_dialog_btn.draggable(),
+            25 => self.save_dialog_btn.draggable(),
+            26 => self.textbox_demo.draggable(),
+            27 => self.plate_demo.draggable(),
+            28 => self.backplate_toggle.draggable(),
+            29 => self.menubar_toggle.draggable(),
+            30 => self.statusbar_toggle.draggable(),
+            31 => self.border_section.draggable(),
+            32 => self.bevel_toggle.draggable(),
+            33 => self.border_width_spin.draggable(),
+            34 => self.bevel_depth_spin.draggable(),
+            35 => self.elements_section.draggable(),
+            36 => self.page_selector.draggable(),
+            37 => self.color_ramp_btn.draggable(),
+            38 => self.bevel_shape_btn.draggable(),
+            39 => self.bevel_ramp.draggable(),
+            40 => self.ramp_btn.draggable(),
+            41 => self.control_panel.draggable(),
+            42 => self.layout_dd.draggable(),
+            43 => self.color_selector_demo.draggable(),
+            44 => self.font_selector_demo.draggable(),
+            45 => self.keybind_demo.draggable(),
+            46 => self.button_strip_demo.draggable(),
+            47 => self.slider2d_demo.draggable(),
+            48 => self.float3_demo.draggable(),
+            49 => self.usage_bar_demo.draggable(),
+            50 => self.status_dot_demo.draggable(),
+            51 => self.info_box_demo.draggable(),
+            52 => self.list_item_demo.draggable(),
+            53 => self.breadcrumb_demo.draggable(),
+            54 => self.tree_list_demo.draggable(),
+            55 => self.bevel_preview_demo.draggable(),
+            56 => self.ramp_preview_demo.draggable(),
+            57 => self.separator_demo.draggable(),
+            58 => self.splitter_demo.draggable(),
             _ => panic!("gallery slot index out of range: {idx}"),
         }
     }
@@ -192,36 +436,51 @@ impl GallerySlots {
             11 => self.transparency_slider.is_dragging(),
             12 => self.transparency_label.is_dragging(),
             13 => self.window_type_dd.is_dragging(),
-            14 => self.window_shape_dd.is_dragging(),
-            15 => self.enable_toggle.is_dragging(),
-            16 => self.width_spin.is_dragging(),
-            17 => self.height_spin.is_dragging(),
-            18 => self.surface_plate.is_dragging(),
-            19 => self.surface_info_label.is_dragging(),
-            20 => self.surface_desc_label.is_dragging(),
-            21 => self.range_slider_demo.is_dragging(),
-            22 => self.trackpad_demo.is_dragging(),
-            23 => self.portal_panel.is_dragging(),
-            24 => self.portal_label.is_dragging(),
-            25 => self.open_dialog_btn.is_dragging(),
-            26 => self.save_dialog_btn.is_dragging(),
-            27 => self.textbox_demo.is_dragging(),
-            28 => self.plate_demo.is_dragging(),
-            29 => self.backplate_toggle.is_dragging(),
-            30 => self.menubar_toggle.is_dragging(),
-            31 => self.statusbar_toggle.is_dragging(),
-            32 => self.border_section.is_dragging(),
-            33 => self.bevel_toggle.is_dragging(),
-            34 => self.border_width_spin.is_dragging(),
-            35 => self.bevel_depth_spin.is_dragging(),
-            36 => self.elements_section.is_dragging(),
-            37 => self.page_selector.is_dragging(),
-            38 => self.color_ramp_btn.is_dragging(),
-            39 => self.bevel_shape_btn.is_dragging(),
-            40 => self.bevel_ramp.is_dragging(),
-            41 => self.ramp_btn.is_dragging(),
-            42 => self.control_panel.is_dragging(),
-            43 => self.layout_dd.is_dragging(),
+            14 => self.enable_toggle.is_dragging(),
+            15 => self.width_spin.is_dragging(),
+            16 => self.height_spin.is_dragging(),
+            17 => self.surface_plate.is_dragging(),
+            18 => self.surface_info_label.is_dragging(),
+            19 => self.surface_desc_label.is_dragging(),
+            20 => self.range_slider_demo.is_dragging(),
+            21 => self.trackpad_demo.is_dragging(),
+            22 => self.portal_panel.is_dragging(),
+            23 => self.portal_label.is_dragging(),
+            24 => self.open_dialog_btn.is_dragging(),
+            25 => self.save_dialog_btn.is_dragging(),
+            26 => self.textbox_demo.is_dragging(),
+            27 => self.plate_demo.is_dragging(),
+            28 => self.backplate_toggle.is_dragging(),
+            29 => self.menubar_toggle.is_dragging(),
+            30 => self.statusbar_toggle.is_dragging(),
+            31 => self.border_section.is_dragging(),
+            32 => self.bevel_toggle.is_dragging(),
+            33 => self.border_width_spin.is_dragging(),
+            34 => self.bevel_depth_spin.is_dragging(),
+            35 => self.elements_section.is_dragging(),
+            36 => self.page_selector.is_dragging(),
+            37 => self.color_ramp_btn.is_dragging(),
+            38 => self.bevel_shape_btn.is_dragging(),
+            39 => self.bevel_ramp.is_dragging(),
+            40 => self.ramp_btn.is_dragging(),
+            41 => self.control_panel.is_dragging(),
+            42 => self.layout_dd.is_dragging(),
+            43 => self.color_selector_demo.is_dragging(),
+            44 => self.font_selector_demo.is_dragging(),
+            45 => self.keybind_demo.is_dragging(),
+            46 => self.button_strip_demo.is_dragging(),
+            47 => self.slider2d_demo.is_dragging(),
+            48 => self.float3_demo.is_dragging(),
+            49 => self.usage_bar_demo.is_dragging(),
+            50 => self.status_dot_demo.is_dragging(),
+            51 => self.info_box_demo.is_dragging(),
+            52 => self.list_item_demo.is_dragging(),
+            53 => self.breadcrumb_demo.is_dragging(),
+            54 => self.tree_list_demo.is_dragging(),
+            55 => self.bevel_preview_demo.is_dragging(),
+            56 => self.ramp_preview_demo.is_dragging(),
+            57 => self.separator_demo.is_dragging(),
+            58 => self.splitter_demo.is_dragging(),
             _ => panic!("gallery slot index out of range: {idx}"),
         }
     }
@@ -242,36 +501,51 @@ impl GallerySlots {
             11 => &self.transparency_slider,
             12 => &self.transparency_label,
             13 => &self.window_type_dd,
-            14 => &self.window_shape_dd,
-            15 => &self.enable_toggle,
-            16 => &self.width_spin,
-            17 => &self.height_spin,
-            18 => &self.surface_plate,
-            19 => &self.surface_info_label,
-            20 => &self.surface_desc_label,
-            21 => &self.range_slider_demo,
-            22 => &self.trackpad_demo,
-            23 => &self.portal_panel,
-            24 => &self.portal_label,
-            25 => &self.open_dialog_btn,
-            26 => &self.save_dialog_btn,
-            27 => &self.textbox_demo,
-            28 => &self.plate_demo,
-            29 => &self.backplate_toggle,
-            30 => &self.menubar_toggle,
-            31 => &self.statusbar_toggle,
-            32 => &self.border_section,
-            33 => &self.bevel_toggle,
-            34 => &self.border_width_spin,
-            35 => &self.bevel_depth_spin,
-            36 => &self.elements_section,
-            37 => &self.page_selector,
-            38 => &self.color_ramp_btn,
-            39 => &self.bevel_shape_btn,
-            40 => &self.bevel_ramp,
-            41 => &self.ramp_btn,
-            42 => &self.control_panel,
-            43 => &self.layout_dd,
+            14 => &self.enable_toggle,
+            15 => &self.width_spin,
+            16 => &self.height_spin,
+            17 => &self.surface_plate,
+            18 => &self.surface_info_label,
+            19 => &self.surface_desc_label,
+            20 => &self.range_slider_demo,
+            21 => &self.trackpad_demo,
+            22 => &self.portal_panel,
+            23 => &self.portal_label,
+            24 => &self.open_dialog_btn,
+            25 => &self.save_dialog_btn,
+            26 => &self.textbox_demo,
+            27 => &self.plate_demo,
+            28 => &self.backplate_toggle,
+            29 => &self.menubar_toggle,
+            30 => &self.statusbar_toggle,
+            31 => &self.border_section,
+            32 => &self.bevel_toggle,
+            33 => &self.border_width_spin,
+            34 => &self.bevel_depth_spin,
+            35 => &self.elements_section,
+            36 => &self.page_selector,
+            37 => &self.color_ramp_btn,
+            38 => &self.bevel_shape_btn,
+            39 => &self.bevel_ramp,
+            40 => &self.ramp_btn,
+            41 => &self.control_panel,
+            42 => &self.layout_dd,
+            43 => &self.color_selector_demo,
+            44 => &self.font_selector_demo,
+            45 => &self.keybind_demo,
+            46 => &self.button_strip_demo,
+            47 => &self.slider2d_demo,
+            48 => &self.float3_demo,
+            49 => &self.usage_bar_demo,
+            50 => &self.status_dot_demo,
+            51 => &self.info_box_demo,
+            52 => &self.list_item_demo,
+            53 => &self.breadcrumb_demo,
+            54 => &self.tree_list_demo,
+            55 => &self.bevel_preview_demo,
+            56 => &self.ramp_preview_demo,
+            57 => &self.separator_demo,
+            58 => &self.splitter_demo,
             _ => panic!("gallery slot index out of range: {idx}"),
         }
     }
@@ -292,36 +566,51 @@ impl GallerySlots {
             11 => &mut self.transparency_slider,
             12 => &mut self.transparency_label,
             13 => &mut self.window_type_dd,
-            14 => &mut self.window_shape_dd,
-            15 => &mut self.enable_toggle,
-            16 => &mut self.width_spin,
-            17 => &mut self.height_spin,
-            18 => &mut self.surface_plate,
-            19 => &mut self.surface_info_label,
-            20 => &mut self.surface_desc_label,
-            21 => &mut self.range_slider_demo,
-            22 => &mut self.trackpad_demo,
-            23 => &mut self.portal_panel,
-            24 => &mut self.portal_label,
-            25 => &mut self.open_dialog_btn,
-            26 => &mut self.save_dialog_btn,
-            27 => &mut self.textbox_demo,
-            28 => &mut self.plate_demo,
-            29 => &mut self.backplate_toggle,
-            30 => &mut self.menubar_toggle,
-            31 => &mut self.statusbar_toggle,
-            32 => &mut self.border_section,
-            33 => &mut self.bevel_toggle,
-            34 => &mut self.border_width_spin,
-            35 => &mut self.bevel_depth_spin,
-            36 => &mut self.elements_section,
-            37 => &mut self.page_selector,
-            38 => &mut self.color_ramp_btn,
-            39 => &mut self.bevel_shape_btn,
-            40 => &mut self.bevel_ramp,
-            41 => &mut self.ramp_btn,
-            42 => &mut self.control_panel,
-            43 => &mut self.layout_dd,
+            14 => &mut self.enable_toggle,
+            15 => &mut self.width_spin,
+            16 => &mut self.height_spin,
+            17 => &mut self.surface_plate,
+            18 => &mut self.surface_info_label,
+            19 => &mut self.surface_desc_label,
+            20 => &mut self.range_slider_demo,
+            21 => &mut self.trackpad_demo,
+            22 => &mut self.portal_panel,
+            23 => &mut self.portal_label,
+            24 => &mut self.open_dialog_btn,
+            25 => &mut self.save_dialog_btn,
+            26 => &mut self.textbox_demo,
+            27 => &mut self.plate_demo,
+            28 => &mut self.backplate_toggle,
+            29 => &mut self.menubar_toggle,
+            30 => &mut self.statusbar_toggle,
+            31 => &mut self.border_section,
+            32 => &mut self.bevel_toggle,
+            33 => &mut self.border_width_spin,
+            34 => &mut self.bevel_depth_spin,
+            35 => &mut self.elements_section,
+            36 => &mut self.page_selector,
+            37 => &mut self.color_ramp_btn,
+            38 => &mut self.bevel_shape_btn,
+            39 => &mut self.bevel_ramp,
+            40 => &mut self.ramp_btn,
+            41 => &mut self.control_panel,
+            42 => &mut self.layout_dd,
+            43 => &mut self.color_selector_demo,
+            44 => &mut self.font_selector_demo,
+            45 => &mut self.keybind_demo,
+            46 => &mut self.button_strip_demo,
+            47 => &mut self.slider2d_demo,
+            48 => &mut self.float3_demo,
+            49 => &mut self.usage_bar_demo,
+            50 => &mut self.status_dot_demo,
+            51 => &mut self.info_box_demo,
+            52 => &mut self.list_item_demo,
+            53 => &mut self.breadcrumb_demo,
+            54 => &mut self.tree_list_demo,
+            55 => &mut self.bevel_preview_demo,
+            56 => &mut self.ramp_preview_demo,
+            57 => &mut self.separator_demo,
+            58 => &mut self.splitter_demo,
             _ => panic!("gallery slot index out of range: {idx}"),
         }
     }
@@ -537,27 +826,42 @@ impl Roster {
             10 => s.opacity_toggle.take_click(),
             11 => s.transparency_slider.take_click(),
             13 => s.window_type_dd.take_click(),
-            14 => s.window_shape_dd.take_click(),
-            15 => s.enable_toggle.take_click(),
-            16 => s.width_spin.take_click(),
-            17 => s.height_spin.take_click(),
-            21 => s.range_slider_demo.take_click(),
-            22 => s.trackpad_demo.take_click(),
-            25 => s.open_dialog_btn.take_click(),
-            26 => s.save_dialog_btn.take_click(),
-            27 => s.textbox_demo.take_click(),
-            28 => s.plate_demo.take_click(),
-            29 => s.backplate_toggle.take_click(),
-            30 => s.menubar_toggle.take_click(),
-            31 => s.statusbar_toggle.take_click(),
-            33 => s.bevel_toggle.take_click(),
-            34 => s.border_width_spin.take_click(),
-            35 => s.bevel_depth_spin.take_click(),
-            37 => s.page_selector.take_click(),
-            38 => s.color_ramp_btn.take_click(),
-            39 => s.bevel_shape_btn.take_click(),
-            41 => s.ramp_btn.take_click(),
-            43 => s.layout_dd.take_click(),
+            14 => s.enable_toggle.take_click(),
+            15 => s.width_spin.take_click(),
+            16 => s.height_spin.take_click(),
+            20 => s.range_slider_demo.take_click(),
+            21 => s.trackpad_demo.take_click(),
+            24 => s.open_dialog_btn.take_click(),
+            25 => s.save_dialog_btn.take_click(),
+            26 => s.textbox_demo.take_click(),
+            27 => s.plate_demo.take_click(),
+            28 => s.backplate_toggle.take_click(),
+            29 => s.menubar_toggle.take_click(),
+            30 => s.statusbar_toggle.take_click(),
+            32 => s.bevel_toggle.take_click(),
+            33 => s.border_width_spin.take_click(),
+            34 => s.bevel_depth_spin.take_click(),
+            36 => s.page_selector.take_click(),
+            37 => s.color_ramp_btn.take_click(),
+            38 => s.bevel_shape_btn.take_click(),
+            40 => s.ramp_btn.take_click(),
+            42 => s.layout_dd.take_click(),
+            43 => s.color_selector_demo.take_click(),
+            44 => s.font_selector_demo.take_click(),
+            45 => s.keybind_demo.take_click(),
+            46 => s.button_strip_demo.take_click(),
+            47 => s.slider2d_demo.take_click(),
+            48 => s.float3_demo.take_click(),
+            49 => s.usage_bar_demo.take_click(),
+            50 => s.status_dot_demo.take_click(),
+            51 => s.info_box_demo.take_click(),
+            52 => s.list_item_demo.take_click(),
+            53 => s.breadcrumb_demo.take_click(),
+            54 => s.tree_list_demo.take_click(),
+            55 => s.bevel_preview_demo.take_click(),
+            56 => s.ramp_preview_demo.take_click(),
+            57 => s.separator_demo.take_click(),
+            58 => s.splitter_demo.take_click(),
             _ => panic!("take_click: unwired gallery slot {idx}"),
         }
     }
@@ -567,13 +871,12 @@ impl Roster {
         match idx {
             11 => s.transparency_slider.value(),
             13 => s.window_type_dd.value(),
-            14 => s.window_shape_dd.value(),
-            16 => s.width_spin.value(),
-            17 => s.height_spin.value(),
-            34 => s.border_width_spin.value(),
-            35 => s.bevel_depth_spin.value(),
-            37 => s.page_selector.value(),
-            43 => s.layout_dd.value(),
+            15 => s.width_spin.value(),
+            16 => s.height_spin.value(),
+            33 => s.border_width_spin.value(),
+            34 => s.bevel_depth_spin.value(),
+            36 => s.page_selector.value(),
+            42 => s.layout_dd.value(),
             _ => panic!("value: unwired gallery slot {idx}"),
         }
     }
@@ -582,11 +885,11 @@ impl Roster {
         let s = self.gallery();
         match idx {
             10 => s.opacity_toggle.get_value_string(),
-            15 => s.enable_toggle.get_value_string(),
-            29 => s.backplate_toggle.get_value_string(),
-            30 => s.menubar_toggle.get_value_string(),
-            31 => s.statusbar_toggle.get_value_string(),
-            33 => s.bevel_toggle.get_value_string(),
+            14 => s.enable_toggle.get_value_string(),
+            28 => s.backplate_toggle.get_value_string(),
+            29 => s.menubar_toggle.get_value_string(),
+            30 => s.statusbar_toggle.get_value_string(),
+            32 => s.bevel_toggle.get_value_string(),
             _ => panic!("get_value_string: unwired gallery slot {idx}"),
         }
     }
@@ -594,7 +897,7 @@ impl Roster {
     pub fn set_text(&mut self, idx: usize, text: &str) {
         let s = self.gallery_mut();
         match idx {
-            20 => s.surface_desc_label.set_text(text),
+            19 => s.surface_desc_label.set_text(text),
             _ => panic!("set_text: unwired gallery slot {idx}"),
         }
     }
@@ -620,12 +923,11 @@ struct State {
     use_menubar: bool,
     use_statusbar: bool,
     border_enabled: bool,
-    child_type: Option<String>,
+    child_kind: Option<ChildKind>,
     ui_context: cce_ui::context::UiContext,
     bevel_ramp: Vec<RampKey>,
     bevel_ramp_line_type: String,
     last_ramp_mod: Option<std::time::SystemTime>,
-    child_shape: Option<String>,
 
     sender: calloop::channel::Sender<String>,
     layout_idx: usize,
@@ -766,12 +1068,12 @@ fn push_bevel_slice_corners(
 }
 
 fn is_control_panel_child(i: usize) -> bool {
-    matches!(i, 9..=17 | 29..=36 | 39)
+    matches!(i, 9..=16 | 28..=35 | 38)
 }
 
 /// The control panel's child slots, in arrangement order: the app lays them out, paints
 /// them clamped to the panel viewport, and dispatches them as ordinary roots.
-const CP_CHILDREN: [usize; 18] = [9, 13, 14, 10, 11, 12, 15, 16, 17, 29, 30, 31, 32, 33, 34, 35, 36, 39];
+const CP_CHILDREN: [usize; 17] = [9, 13, 10, 11, 12, 14, 15, 16, 28, 29, 30, 31, 32, 33, 34, 35, 38];
 
 impl State {
     /// The dissolved panel's hit gate: pointer events reach the panel's children only
@@ -837,14 +1139,94 @@ impl State {
     /// Recompute the positions table for the current size, mode and layout.
     fn relayout(&mut self) {
         self.positions = if self.is_child {
-            child_positions(self.width, self.height, self.use_menubar, self.use_statusbar, self.child_type.as_deref())
+            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, self.layout_idx, self.roster.gallery())
+            demo_positions(self.width, self.height)
         };
     }
 
+    /// The Controls exhibits in layout order, each with the content size it is reset to
+    /// before a strategy runs: the toolkit layouts read a child's own rect for its width
+    /// and preferred height, and a previous strategy may have stretched it.
+    fn exhibit_sizes(&self) -> Vec<(usize, f32, f32)> {
+        let bh = cce_ui::layout::button_height();
+        let tgh = cce_ui::layout::toggle_height();
+        let slh = cce_ui::layout::slider_height();
+        let sph = cce_ui::layout::spinbox_height();
+        let ddh = cce_ui::layout::dropdown_height();
+        let raw: [(usize, f32, f32); 29] = [
+            (2, 190.0, bh),                                     // Button
+            (46, 190.0, 28.0),                                  // ButtonStrip
+            (3, 190.0, tgh),                                    // Checkbox
+            (4, 190.0, tgh),                                    // Toggle
+            (6, 190.0, slh),                                    // Slider
+            (20, 190.0, cce_ui::layout::rangeslider_height()),  // RangeSlider
+            (47, 190.0, 100.0),                                 // Slider2D
+            (7, 190.0, sph),                                    // Spinbox
+            (48, 190.0, Float3::preferred_height(false)),       // Float3
+            (26, 190.0, ddh),                                   // TextBox
+            (45, 190.0, 44.0),                                   // KeybindRecorder
+            (43, 190.0, 44.0),                                   // ColorSelector
+            (44, 190.0, 44.0),                                   // FontSelector
+            (5, 190.0, cce_ui::layout::progressbar_height()),   // ProgressBar
+            (49, 190.0, 12.0),                                  // UsageBar
+            (50, 16.0, 16.0),                                   // StatusDot
+            (57, 190.0, 2.0),                                   // Separator
+            (58, 6.0, 80.0),                                    // Splitter
+            (51, 190.0, 70.0),                                  // InfoBox
+            (52, 190.0, 44.0),                                  // InteractiveListItem
+            (53, 190.0, 28.0),                                  // Breadcrumb
+            (54, 190.0, 150.0),                                 // TreeList
+            (21, 190.0, 100.0),                                 // Trackpad
+            (27, 190.0, 120.0),                                 // Plate
+            (55, 190.0, 100.0),                                 // BevelPreview
+            (56, 190.0, 60.0),                                  // RampPreview
+            (39, 190.0, 150.0),                                 // Ramp
+            (37, 190.0, bh),                                    // Color Ramp...
+            (40, 190.0, bh),                                    // Ramp...
+        ];
+        raw.to_vec()
+    }
+
+    /// Lay the Controls exhibits out with the toolkit strategy the Layout dropdown selects
+    /// (`LAYOUTS`), then clip whatever runs past the status bar.
+    fn layout_exhibits(&mut self) {
+        // Below the Layout dropdown, which stays put so every strategy leaves it usable.
+        let (dx, dy, _, dh) = self.roster.get_dyn(42).rect();
+        let (x, y) = (dx, dy + dh + 24.0);
+        let w = (self.width - 2.0 * x).max(300.0);
+        let limit_y = self.height - 24.0;
+        let h = (limit_y - y).max(100.0);
+        let mut children: Vec<*mut (dyn WidgetHost + 'static)> = Vec::new();
+        for (idx, cw, ch) in self.exhibit_sizes() {
+            if self.roster.is_dragging(idx) {
+                continue;
+            }
+            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));
+        }
+        let strategy = (LAYOUTS.get(self.layout_idx).unwrap_or(&LAYOUTS[DEFAULT_LAYOUT]).1)();
+        strategy.layout(x, y, w, h, &children, &mut self.ui_context);
+        for &child in &children {
+            let widget = unsafe { &mut *child };
+            let (cx, cy, cw, mut ch) = widget.rect();
+            // The adapter inflates a detached-label widget's rect by its label on every
+            // set_rect. A strategy sizes a child without an intrinsic height from that
+            // inflated rect and assigns it back through set_rect, so the label lands twice;
+            // take it back out (a widget with an intrinsic height is sized from that).
+            if widget.preferred_height().is_none() {
+                ch -= 2.0 * cce_ui::widget::label_offset(widget);
+            }
+            if cy + ch > limit_y {
+                ch = (limit_y - cy).max(0.0);
+            }
+            widget.set_rect(cx, cy, cw, ch);
+        }
+    }
+
     fn preview_style(&self) -> PreviewStyle {
-        let backplate = self.toggled(29);
+        let backplate = self.toggled(28);
         let transparency = if self.toggled(10) { self.roster.value(11) as f32 / 100.0 } else { 1.0 };
         let bg_color = if backplate {
             let mut col = cce_ui::color::page_low_color();
@@ -880,9 +1262,9 @@ impl State {
             if self.roster.is_dragging(i) {
                 continue;
             }
-            // The page selector (37) is placed inside the status bar below; the panel's
+            // The page selector (36) is placed inside the status bar below; the panel's
             // children are laid out by arrange_control_panel at real screen coordinates.
-            if !self.is_child && (i == 37 || is_control_panel_child(i)) {
+            if !self.is_child && (i == 36 || is_control_panel_child(i)) {
                 continue;
             }
             let visible = self.is_widget_visible(i);
@@ -890,7 +1272,7 @@ impl State {
             let widget = self.roster.get_dyn_mut(i);
             if visible {
                 // Everything but the chrome and the panel clips at the status bar.
-                let clipped = i != 0 && i != 1 && i != 42 && y + h > limit_y;
+                let clipped = i != 0 && i != 1 && i != 41 && y + h > limit_y;
                 let final_h = if clipped { (limit_y - y).max(0.0) } else { h };
                 widget.set_rect(x, y, w, final_h);
             } else {
@@ -899,8 +1281,8 @@ impl State {
         }
 
         if !self.is_child {
-            let (x, y, w, h) = self.positions[42];
-            self.roster.get_dyn_mut(42).set_rect(x, y, w, h);
+            let (x, y, w, h) = self.positions[41];
+            self.roster.get_dyn_mut(41).set_rect(x, y, w, h);
 
             let sb_rect = self.roster.get_dyn_mut(1).rect();
             let ddh = cce_ui::layout::dropdown_height();
@@ -909,8 +1291,11 @@ impl State {
             let dw = 120.0;
             let dx = sb_rect.0 + sb_rect.2 - dw - pad_x;
             let dy = sb_rect.1 + pad_y;
-            self.roster.get_dyn_mut(37).set_rect(dx, dy, dw, ddh);
+            self.roster.get_dyn_mut(36).set_rect(dx, dy, dw, ddh);
 
+            if self.current_page == Page::Controls {
+                self.layout_exhibits();
+            }
             self.arrange_control_panel();
         }
     }
@@ -931,7 +1316,6 @@ impl State {
         let height_p: *mut (dyn WidgetHost + 'static) = &mut s.height_spin;
         col.add_row(&[width_p, height_p], 42.0, 12.0);
         col.add_widget(&mut s.window_type_dd, 44.0);
-        col.add_widget(&mut s.window_shape_dd, 44.0);
         col.add_widget(&mut s.opacity_toggle, 28.0);
         col.add_widget(&mut s.enable_toggle, 28.0);
         col.add_widget(&mut s.transparency_label, 12.0);
@@ -980,63 +1364,39 @@ impl cce_ui::engine::Application for State {
         let use_backplate = if is_child { flag("--backplate") } else { !flag("--no-backplate") };
         let use_menubar = flag("--menubar");
         let use_statusbar = flag("--statusbar");
-        let child_type = value("--type");
-        let child_shape = value("--shape");
+        let child_kind = if is_child {
+            Some(value("--type").and_then(|t| ChildKind::from_arg(&t)).unwrap_or(ChildKind::Floating))
+        } else {
+            None
+        };
         let opacity = flag("--opacity");
         let transparency = number("--transparency").unwrap_or(1.0);
         let border_enabled = !flag("--no-border");
         let custom_width = number("--width");
         let custom_height = number("--height");
 
-        let (mut c_w, mut c_h): (f32, f32) = if is_child {
-            if let (Some(w), Some(h)) = (custom_width, custom_height) {
-                (w, h)
-            } else {
-                match child_type.as_deref() {
-                    Some("Toplevel") => (400.0, 250.0),
-                    Some("Popup") => (250.0, 150.0),
-                    Some("LayerTop") => (800.0, 40.0),
-                    Some("LayerOverlay") => (300.0, 180.0),
-                    Some("LayerBackground") => (800.0, 600.0),
-                    Some("Ramp") | Some("ColorRamp") => (450.0, 350.0),
-                    _ => (400.0, 250.0),
-                }
-            }
-        } else {
-            (800.0, 600.0)
+        let (c_w, c_h): (f32, f32) = match (child_kind, custom_width, custom_height) {
+            (_, Some(w), Some(h)) => (w, h),
+            (Some(kind), _, _) => kind.default_size(),
+            (None, _, _) => (1000.0, 680.0),
         };
-        if is_child && child_shape.as_deref() == Some("circular") {
-            let side = c_w.min(c_h);
-            c_w = side;
-            c_h = side;
-        }
 
-        let is_ramp_child = is_child && matches!(child_type.as_deref(), Some("Ramp") | Some("ColorRamp"));
-        let opacity = opacity || is_ramp_child;
+        let opacity = opacity || child_kind.is_some_and(ChildKind::is_editor);
         let status_text = "Ready.".to_string();
 
-        let roster = if is_child {
-            let desc_label = match child_type.as_deref() {
-                Some("Toplevel") => "This is an active simulated Toplevel window.".to_string(),
-                Some("Popup") => "This is an active simulated Popup window.".to_string(),
-                Some("LayerTop") => "This is an active simulated Layer Top surface.".to_string(),
-                Some("LayerOverlay") => "This is an active simulated Layer Overlay surface.".to_string(),
-                Some("LayerBackground") => "This is an active simulated Layer Background surface.".to_string(),
-                Some(other) => format!("This is an active simulated {} window.", other),
-                None => "This is an active simulated window in the window manager.".to_string(),
-            };
+        let roster = if let Some(kind) = child_kind {
             let bg = if use_backplate {
                 ChildBg::Backplate(Backplate::new(0.0, 0.0, c_w, c_h))
             } else {
                 ChildBg::ContentBg(ContentBg::new())
             };
-            let (main, aux3, aux4) = if child_type.as_deref() == Some("ColorRamp") {
+            let (main, aux3, aux4) = if kind == ChildKind::ColorRamp {
                 (
                     ChildMain::ColorRamp(ColorRamp::new()),
                     ChildAux3::Label(Label::new("").with_font_size(12.0)),
                     ChildAux4::Label(Label::new("").with_font_size(12.0)),
                 )
-            } else if child_type.as_deref() == Some("Ramp") {
+            } else if kind == ChildKind::Ramp {
                 (
                     ChildMain::Ramp({
                         let mut ramp = Ramp::new();
@@ -1057,7 +1417,7 @@ impl cce_ui::engine::Application for State {
                     .with_item("Edit", &["Undo", "Redo", "Cut", "Copy", "Paste"])
                     .with_right_aligned_title(true);
                 (
-                    ChildMain::Desc(Label::new(&desc_label).with_font_size(12.0).with_color([0xcc, 0xcc, 0xd4])),
+                    ChildMain::Desc(Label::new(kind.child_text()).with_font_size(12.0).with_color([0xcc, 0xcc, 0xd4])),
                     ChildAux3::MenuBar(menu_bar),
                     ChildAux4::StatusBar(StatusBar::new()),
                 )
@@ -1089,17 +1449,10 @@ impl cce_ui::engine::Application for State {
                 opacity_toggle: Toggle::new().with_label("Opacity"),
                 transparency_slider: Slider::new(),
                 transparency_label: Label::new("Transparency Level").with_font_size(12.0).with_color([0xcc, 0xcc, 0xd4]),
-                window_type_dd: Dropdown::new(vec![
-                    "Toplevel".to_string(),
-                    "Popup".to_string(),
-                    "Layer: Top".to_string(),
-                    "Layer: Overlay".to_string(),
-                    "Layer: Background".to_string(),
-                ], 0).with_label("Window Type"),
-                window_shape_dd: Dropdown::new(vec![
-                    "Rectangular".to_string(),
-                    "Circular".to_string(),
-                ], 0).with_label("Window Shape"),
+                window_type_dd: Dropdown::new(
+                    ChildKind::WINDOW_KINDS.iter().map(|k| k.label().to_string()).collect(),
+                    0,
+                ).with_label("Window Type"),
                 enable_toggle: {
                     let mut t = Toggle::new().with_label("Enable");
                     t.set_toggled(true);
@@ -1109,7 +1462,7 @@ impl cce_ui::engine::Application for State {
                 height_spin: Spinbox::new(250, 100, 2000, 10).with_label("Height"),
                 surface_plate: Plate::new(0.0, 0.0, 190.0, 250.0, true),
                 surface_info_label: Label::new("Surface Info").with_font_size(12.0),
-                surface_desc_label: Label::new(window_type_description(0)).with_font_size(10.0),
+                surface_desc_label: Label::new(ChildKind::WINDOW_KINDS[0].description()).with_font_size(10.0),
                 range_slider_demo: RangeSlider::new().with_label("RangeSlider"),
                 trackpad_demo: Trackpad::new().with_label("Trackpad"),
                 portal_panel: Panel::new(0.0, 0.0, 450.0, 200.0).with_label("File chooser"),
@@ -1139,19 +1492,43 @@ impl cce_ui::engine::Application for State {
                 bevel_ramp: Ramp::new(),
                 ramp_btn: Button::new(0.0, 0.0, 120.0, 28.0).with_label("Ramp..."),
                 control_panel: ControlPanel::new().with_label("ControlPanel"),
-                layout_dd: Dropdown::new(vec![
-                    "Vertical".to_string(),
-                    "Columns".to_string(),
-                    "Grid".to_string(),
-                    "Adaptive Grid".to_string(),
-                    "Overlay".to_string(),
-                    "Flex".to_string(),
-                    "Splitter".to_string(),
-                    "Radial".to_string(),
-                    "Circular Pane".to_string(),
-                    "Mosaic".to_string(),
-                    "Reverse Mosaic".to_string(),
-                ], 9).with_label("Layout"),
+                layout_dd: Dropdown::new(
+                    LAYOUTS.iter().map(|(name, _)| name.to_string()).collect(),
+                    DEFAULT_LAYOUT,
+                ).with_label("Layout"),
+                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"),
+                button_strip_demo: Adapted::new(
+                    ButtonStrip::new(0.0, 0.0, 200.0, 28.0)
+                        .with_buttons(vec!["One".to_string(), "Two".to_string(), "Three".to_string()])
+                        .with_selected(Some(0)),
+                ).with_label("ButtonStrip"),
+                slider2d_demo: Slider2D::new().with_label("Slider2D"),
+                float3_demo: Float3::new().with_label("Float3"),
+                usage_bar_demo: UsageBar::new(0.62).with_label("UsageBar"),
+                status_dot_demo: StatusDot::new(DotStatus::Active).with_label("StatusDot"),
+                info_box_demo: InfoBox::new("InfoBox", vec!["A titled box of".to_string(), "plain text lines.".to_string()]),
+                list_item_demo: InteractiveListItem::new("InteractiveListItem"),
+                breadcrumb_demo: {
+                    let mut b = Breadcrumb::new();
+                    b.path = vec!["home".to_string(), "lsgalante".to_string(), "projects".to_string()];
+                    b.with_label("Breadcrumb")
+                },
+                tree_list_demo: {
+                    let mut t = TreeList::new();
+                    t.set_flat_keys(vec![
+                        ("layout/bar_height".to_string(), serde_json::json!(24)),
+                        ("layout/gap".to_string(), serde_json::json!(12)),
+                        ("theme/name".to_string(), serde_json::json!("cce")),
+                    ]);
+                    t.rebuild_tree();
+                    t.with_label("TreeList")
+                },
+                bevel_preview_demo: BevelPreview::new().with_label("BevelPreview"),
+                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"),
             }))
         };
 
@@ -1173,20 +1550,19 @@ impl cce_ui::engine::Application for State {
             use_menubar,
             use_statusbar,
             border_enabled,
-            child_type: child_type.clone(),
+            child_kind,
             ui_context: cce_ui::context::UiContext::new(),
             bevel_ramp: loaded_keys,
             bevel_ramp_line_type: loaded_type,
             last_ramp_mod: None,
-            child_shape: child_shape.clone(),
             sender,
-            layout_idx: 9,
+            layout_idx: DEFAULT_LAYOUT,
         };
 
         if !is_child {
-            // Link the page selector (37) under the status bar (1) in the ui tree.
+            // Link the page selector (36) under the status bar (1) in the ui tree.
             let statusbar_ptr = state.roster.get_dyn_mut(1) as *mut (dyn WidgetHost + 'static);
-            let dropdown_ptr = state.roster.get_dyn_mut(37) as *mut (dyn WidgetHost + 'static);
+            let dropdown_ptr = state.roster.get_dyn_mut(36) as *mut (dyn WidgetHost + 'static);
             unsafe {
                 cce_ui::widget::focus::link_parent_child(
                     &mut *statusbar_ptr,
@@ -1217,24 +1593,15 @@ impl cce_ui::engine::Application for State {
     }
 
     fn settings(&self) -> cce_ui::engine::WindowSettings {
-        let title = if self.is_child {
-            child_title(self.child_type.as_deref())
-        } else {
-            "Gallery".to_string()
+        let title = match self.child_kind {
+            Some(kind) => kind.title().to_string(),
+            None => "Gallery".to_string(),
         };
 
-        let mut app_id = if self.is_child {
-            if let Some(ref t) = self.child_type {
-                format!("cce-gallery-child-{}", t.to_lowercase())
-            } else {
-                "cce-gallery-child".to_string()
-            }
-        } else {
-            "cce-gallery".to_string()
+        let mut app_id = match self.child_kind {
+            Some(kind) => kind.app_id(),
+            None => "cce-gallery".to_string(),
         };
-        if self.is_child && self.child_shape.as_deref() == Some("circular") {
-            app_id.push_str("-circular");
-        }
         if !self.border_enabled {
             app_id.push_str("-noborder");
         }
@@ -1244,7 +1611,7 @@ impl cce_ui::engine::Application for State {
             app_id,
             width: self.width as u32,
             height: self.height as u32,
-            fullscreen: false,
+            fullscreen: self.child_kind == Some(ChildKind::Fullscreen),
             min_size: if self.is_child {
                 Some((self.width as u32, self.height as u32))
             } else {
@@ -1253,6 +1620,14 @@ impl cce_ui::engine::Application for State {
         }
     }
 
+    fn layer(&self) -> Option<LayerSettings> {
+        self.child_kind.and_then(|kind| kind.layer(self.height as i32))
+    }
+
+    fn utility(&self) -> bool {
+        self.child_kind == Some(ChildKind::Utility)
+    }
+
     fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool) {
         if msg == "exit" {
             *exit = true;
@@ -1288,7 +1663,7 @@ impl cce_ui::engine::Application for State {
             if is_visible(i) {
                 if w.tick(dt, &mut self.ui_context) {
                     changed = true;
-                    if self.is_child && self.child_type.as_deref() == Some("Ramp") && i == 1 {
+                    if self.child_kind == Some(ChildKind::Ramp) && i == 1 {
                         if let Some(ramp) = w.as_any().downcast_ref::<Ramp>() {
                             let line_type_str = match ramp.line_type_dropdown.selected {
                                 1 => "bezier",
@@ -1359,12 +1734,12 @@ impl cce_ui::engine::Application for State {
             if !self.is_child && i == 8 {
                 let r = cce_ui::color::root_plate_corner_radius();
                 let PreviewStyle { backplate: backplate_enabled, transparency: transparency_val, bg_color } = self.preview_style();
-                let border_bevel = self.toggled(33);
+                let border_bevel = self.toggled(32);
 
                 let (wx, wy, ww, wh) = w.rect();
                 if backplate_enabled {
                     if border_bevel {
-                        let t = self.roster.value(34) as f32;
+                        let t = self.roster.value(33) as f32;
                         let r_inner = (r - t).max(0.0);
                         push_rounded(&mut pc, wx + t, wy + t, ww - 2.0 * t, wh - 2.0 * t, r_inner, bg_color, (true, true, true, true));
                     } else {
@@ -1372,7 +1747,7 @@ impl cce_ui::engine::Application for State {
                     }
                 }
 
-                let menubar_enabled = self.toggled(30);
+                let menubar_enabled = self.toggled(29);
                 if menubar_enabled {
                     let mut menu_color = cce_ui::color::root_plate_menubar_color();
                     menu_color[3] = transparency_val;
@@ -1383,7 +1758,7 @@ impl cce_ui::engine::Application for State {
                     }
                 }
 
-                let statusbar_enabled = self.toggled(31);
+                let statusbar_enabled = self.toggled(30);
                 if statusbar_enabled {
                     let mut status_color = cce_ui::color::root_plate_statusbar_color();
                     status_color[3] = transparency_val;
@@ -1545,10 +1920,9 @@ impl cce_ui::engine::Application for State {
         }
 
         // ── Text ──
-        let label_text = if self.is_child {
-            child_title(self.child_type.as_deref())
-        } else {
-            format!("Gallery - {}", self.current_page.label())
+        let label_text = match self.child_kind {
+            Some(kind) => kind.title().to_string(),
+            None => format!("Gallery - {}", self.current_page.label()),
         };
         let mut has_menu_bar = false;
         for i in 0..self.roster.len() {
@@ -1659,14 +2033,14 @@ impl cce_ui::engine::Application for State {
             }
 
             if !self.is_child && i == 8 {
-                let border_enabled = self.toggled(15);
+                let border_enabled = self.toggled(14);
                 if border_enabled {
                     let PreviewStyle { backplate: backplate_enabled, transparency: transparency_val, bg_color } = self.preview_style();
 
                     let mut border_color = cce_ui::color::plate_border_color().unwrap_or([0.3, 0.3, 0.4, 1.0]);
                     border_color[3] = transparency_val;
-                    let t = self.roster.value(34) as f32;
-                    let border_bevel = self.toggled(33);
+                    let t = self.roster.value(33) as f32;
+                    let border_bevel = self.toggled(32);
                     let r = cce_ui::color::root_plate_corner_radius();
                     let (wx, wy, ww, wh) = w.rect();
 
@@ -1683,7 +2057,7 @@ impl cce_ui::engine::Application for State {
                                 let h_inner = interpolate_ramp_value(&self.bevel_ramp, u_next, &self.bevel_ramp_line_type);
 
                                 let d_h = h_inner - h_outer;
-                                let bevel_depth = self.roster.value(35) as f32 / 100.0;
+                                let bevel_depth = self.roster.value(34) as f32 / 100.0;
                                 let color_offset = d_h * bevel_depth * 2.667;
 
                                  let rad = cce_ui::layout::light_source_position();
@@ -1877,21 +2251,21 @@ impl cce_ui::engine::Application for State {
                         return Some("exit".to_string());
                     }
                 } else {
-                    if self.roster.take_click(37) {
-                        self.go_to_page(Page::from_index(self.roster.value(37) as usize));
+                    if self.roster.take_click(36) {
+                        self.go_to_page(Page::from_index(self.roster.value(36) as usize));
                         changed = true;
-                    } else if self.roster.take_click(43) {
-                        self.layout_idx = self.roster.value(43) as usize;
+                    } else if self.roster.take_click(42) {
+                        self.layout_idx = self.roster.value(42) as usize;
                         self.relayout();
                         self.apply_layout();
                         changed = true;
                     } else if self.current_page == Page::Controls {
-                        if self.roster.take_click(38) {
+                        if self.roster.take_click(37) {
                             spawn_editor("ColorRamp");
-                        } else if self.roster.take_click(41) {
+                        } else if self.roster.take_click(40) {
                             spawn_editor("Ramp");
                         } else {
-                            for i in [2, 3, 4, 6, 7, 21, 22, 27, 28] {
+                            for i in [2, 3, 4, 6, 7, 20, 21, 26, 27, 43, 44, 45, 46, 47, 48, 52, 53, 54, 55, 56] {
                                 if self.roster.take_click(i) {
                                     changed = true;
                                 }
@@ -1902,18 +2276,18 @@ impl cce_ui::engine::Application for State {
 
                         if self.roster.take_click(9) {
                             create_window = true;
-                        } else if self.roster.take_click(39) {
+                        } else if self.roster.take_click(38) {
                             spawn_editor("Ramp");
                         } else {
                             let mut dropdown_clicked = false;
-                            for i in [10, 11, 13, 14, 15, 16, 17, 29, 30, 31, 33, 34, 35] {
+                            for i in [10, 11, 13, 14, 15, 16, 28, 29, 30, 32, 33, 34] {
                                 if self.roster.take_click(i) {
                                     changed = true;
                                     if i == 13 {
                                         dropdown_clicked = true;
                                     }
-                                    if i == 35 {
-                                        let depth = self.roster.value(35) as f32 / 100.0;
+                                    if i == 34 {
+                                        let depth = self.roster.value(34) as f32 / 100.0;
                                         if let Ok(mut registry) = cce_ui::layout::get_style_registry().write() {
                                             registry.set_float("bevel_depth", depth);
                                         }
@@ -1921,47 +2295,34 @@ impl cce_ui::engine::Application for State {
                                 }
                             }
                             if dropdown_clicked {
-                                let desc = window_type_description(self.roster.value(13));
-                                self.roster.set_text(20, desc);
+                                let desc = ChildKind::from_dropdown(self.roster.value(13)).description();
+                                self.roster.set_text(19, desc);
                             }
                         }
 
                         if create_window {
-                            let window_type = match self.roster.value(13) {
-                                0 => "Toplevel",
-                                1 => "Popup",
-                                2 => "LayerTop",
-                                3 => "LayerOverlay",
-                                4 => "LayerBackground",
-                                _ => "Toplevel",
-                            };
-                            let shape = match self.roster.value(14) {
-                                0 => "rectangular",
-                                1 => "circular",
-                                _ => "rectangular",
-                            };
+                            let kind = ChildKind::from_dropdown(self.roster.value(13));
                             let opacity_enabled = self.toggled(10);
-                            let transparency_pct = self.roster.value(11);
-                            let transparency_val = transparency_pct as f32 / 100.0;
-                            let border_enabled = self.toggled(15);
-                            let custom_width = self.roster.value(16);
-                            let custom_height = self.roster.value(17);
+                            let transparency_val = self.roster.value(11) as f32 / 100.0;
+                            let border_enabled = self.toggled(14);
+                            let custom_width = self.roster.value(15);
+                            let custom_height = self.roster.value(16);
 
-                            self.update_status_text(&format!("Spawning simulated {} {} window...", shape, window_type));
+                            self.update_status_text(&format!("Spawning a {} child window...", kind.label()));
                             if let Some(mut cmd) = child_command() {
-                                cmd.args(["--type", window_type, "--shape", shape]);
+                                cmd.args(["--type", kind.arg()]);
                                 if opacity_enabled {
                                     cmd.args(["--opacity", "--transparency", &transparency_val.to_string()]);
                                 }
                                 if !border_enabled {
                                     cmd.arg("--no-border");
                                 } else {
-                                    cmd.args(["--border-width", &self.roster.value(34).to_string()]);
-                                    if self.toggled(33) {
+                                    cmd.args(["--border-width", &self.roster.value(33).to_string()]);
+                                    if self.toggled(32) {
                                         cmd.arg("--border-bevel");
                                     }
                                 }
-                                for (flag, on) in [("--backplate", self.toggled(29)), ("--menubar", self.toggled(30)), ("--statusbar", self.toggled(31))] {
+                                for (flag, on) in [("--backplate", self.toggled(28)), ("--menubar", self.toggled(29)), ("--statusbar", self.toggled(30))] {
                                     if on {
                                         cmd.arg(flag);
                                     }
@@ -1972,9 +2333,9 @@ impl cce_ui::engine::Application for State {
                             changed = true;
                         }
                     } else if self.current_page == Page::Xdg {
-                        let save = if self.roster.take_click(25) {
+                        let save = if self.roster.take_click(24) {
                             Some(false)
-                        } else if self.roster.take_click(26) {
+                        } else if self.roster.take_click(25) {
                             Some(true)
                         } else {
                             None
@@ -2004,8 +2365,8 @@ impl cce_ui::engine::Application for State {
         // The panel's scroll frame gets the wheel first; a consumed wheel never reaches
         // its children.
         let mut cp_took_wheel = false;
-        if !self.is_child && is_visible(42) {
-            let root = self.roster.get_dyn(42).base().id();
+        if !self.is_child && is_visible(41) {
+            let root = self.roster.get_dyn(41).base().id();
             if self.ui_context.propagate_event(&ev, root) {
                 changed = true;
                 cp_took_wheel = true;
@@ -2013,7 +2374,7 @@ impl cce_ui::engine::Application for State {
         }
         let cp_wheel_ok = !self.is_child && self.cp_gate(lx, ly);
         for i in 0..self.roster.len() {
-            if i == 42 {
+            if i == 41 {
                 continue;
             }
             if !is_visible(i) {
@@ -2100,69 +2461,6 @@ impl cce_ui::engine::Application for State {
         None
     }
 
-    fn adjust_size(&self, w: f32, h: f32) -> (f32, f32) {
-        if self.is_child && self.child_shape.as_deref() == Some("circular") {
-            let side = w.min(h);
-            (side, side)
-        } else {
-            (w, h)
-        }
-    }
-}
-
-/// A child window's title, from its `--type`.
-fn child_title(child_type: Option<&str>) -> String {
-    match child_type {
-        Some("Toplevel") => "Simulated Toplevel Window".to_string(),
-        Some("Popup") => "Simulated Popup Window".to_string(),
-        Some("LayerTop") => "Simulated Layer Shell (Top) Surface".to_string(),
-        Some("LayerOverlay") => "Simulated Layer Shell (Overlay) Surface".to_string(),
-        Some("LayerBackground") => "Simulated Layer Shell (Background) Surface".to_string(),
-        Some(other) => format!("Simulated {} Window", other),
-        None => "Simulated Window".to_string(),
-    }
-}
-
-/// The Windows page's description of a window-type dropdown entry.
-fn window_type_description(idx: i32) -> &'static str {
-    match idx {
-        0 => "A standard application\n\
-window (xdg_toplevel).\n\
-It supports tiling (cascade,\n\
-split, grid), fullscreening,\n\
-dragging, and resizing.\n\n\
-Testing layout:\n\
-cascades in cce.",
-        1 => "An anchored sub-surface\n\
-popup (xdg_popup).\n\
-Usually transient context\n\
-menus, tooltips, or dropdown\n\
-lists. Bypasses tiling.\n\n\
-Testing layout:\n\
-maps floating on top.",
-        2 => "A high-level Layer Shell\n\
-surface (anchored to top).\n\
-Typically status bars, menus,\n\
-or docks. Reserves panel\n\
-space, limiting workspace.\n\n\
-Testing layout:\n\
-full width at top.",
-        3 => "A high-priority Layer Shell\n\
-surface (overlay layer).\n\
-Used for screensavers, lock\n\
-screens, or overlay HUDs.\n\
-Sits above tiling windows.\n\n\
-Testing layout:\n\
-center floating window.",
-        4 => "A bottom-level Layer Shell\n\
-surface (background layer).\n\
-Used for desktop wallpaper.\n\
-Renders below all other\n\
-client windows.\n\n\
-Testing layout:\n\
-full screen background.",
-        _ => "",
-    }
 }
 
 /// A `Command` that re-runs this binary as a child window; the caller adds the flags.
@@ -2182,7 +2480,25 @@ fn spawn_editor(kind: &str) {
     }
 }
 
-fn demo_positions(sw: f32, sh: f32, layout_idx: usize, slots: &GallerySlots) -> Vec<(f32, f32, f32, f32)> {
+/// The toolkit container layouts the Layout dropdown offers, by name.
+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 })),
+    ("Overlay", || Box::new(OverlayLayout::default())),
+];
+const DEFAULT_LAYOUT: usize = 4;
+
+/// The fixed positions: the chrome, the Layout dropdown, and the Windows and XDG pages.
+/// The Controls exhibits are laid out by `layout_exhibits` instead.
+fn demo_positions(sw: f32, sh: f32) -> Vec<(f32, f32, f32, f32)> {
     let base_x = 20.0;
     let sph = cce_ui::layout::spinbox_height();
     let tgh = cce_ui::layout::toggle_height();
@@ -2190,12 +2506,16 @@ fn demo_positions(sw: f32, sh: f32, layout_idx: usize, slots: &GallerySlots) ->
     let bh = cce_ui::layout::button_height();
     let ddh = cce_ui::layout::dropdown_height();
 
-    let mut vec = vec![(0.0, 0.0, 0.0, 0.0); 44];
+    let mut vec = vec![(0.0, 0.0, 0.0, 0.0); GALLERY_COUNT];
 
     // Common layout elements
     vec[0] = (0.0, 0.0, sw, 40.0); // 0 MenuBar
     vec[1] = (0.0, sh - 24.0, sw, 24.0); // 1 StatusBar
 
+    // Page 0 (Controls): the Layout dropdown; the exhibits below it are laid out by
+    // `layout_exhibits` with the strategy it selects.
+    vec[42] = (base_x, 60.0, 190.0, ddh); // 42 Dropdown (Layout)
+
     // Page 1 (Windows)
     vec[8] = (base_x, 60.0, 400.0, 250.0); // 8 Panel (Window simulation area)
     vec[9] = (base_x + 420.0, 60.0, 140.0, bh); // 9 Button (Create Window)
@@ -2203,290 +2523,31 @@ fn demo_positions(sw: f32, sh: f32, layout_idx: usize, slots: &GallerySlots) ->
     vec[11] = (base_x + 420.0, 210.0, 140.0, slh); // 11 Slider
     vec[12] = (base_x + 420.0, 250.0, 140.0, 20.0); // 12 Label
     vec[13] = (base_x + 580.0, 60.0, 180.0, ddh); // 13 Dropdown (Window Type)
-    vec[14] = (base_x + 580.0, 115.0, 180.0, ddh); // 14 Dropdown (Window Shape)
-    vec[15] = (base_x + 580.0, 170.0, 120.0, tgh); // 15 Toggle (Enable)
-    vec[16] = (base_x + 580.0, 210.0, 140.0, sph); // 16 Spinbox (Width)
-    vec[17] = (base_x + 580.0, 255.0, 140.0, sph); // 17 Spinbox (Height)
-    vec[18] = (base_x, 320.0, 190.0, 250.0); // 18 Plate
-    vec[19] = (base_x + 10.0, 330.0, 170.0, 20.0); // 19 Label
-    vec[20] = (base_x + 10.0, 360.0, 170.0, 180.0); // 20 Label
+    vec[14] = (base_x + 580.0, 170.0, 120.0, tgh); // 14 Toggle (Enable)
+    vec[15] = (base_x + 580.0, 210.0, 140.0, sph); // 15 Spinbox (Width)
+    vec[16] = (base_x + 580.0, 255.0, 140.0, sph); // 16 Spinbox (Height)
+    vec[17] = (base_x, 320.0, 190.0, 250.0); // 17 Plate
+    vec[18] = (base_x + 10.0, 330.0, 170.0, 20.0); // 18 Label
+    vec[19] = (base_x + 10.0, 360.0, 170.0, 180.0); // 19 Label
 
     // Page 2 (XDG)
-    vec[23] = (base_x, 60.0, 450.0, 200.0); // 23 Panel
-    vec[24] = (base_x + 20.0, 80.0, 410.0, 60.0); // 24 Label
-    vec[25] = (base_x + 20.0, 160.0, 180.0, bh); // 25 Button
-    vec[26] = (base_x + 220.0, 160.0, 180.0, bh); // 26 Button
+    vec[22] = (base_x, 60.0, 450.0, 200.0); // 22 Panel
+    vec[23] = (base_x + 20.0, 80.0, 410.0, 60.0); // 23 Label
+    vec[24] = (base_x + 20.0, 160.0, 180.0, bh); // 24 Button
+    vec[25] = (base_x + 220.0, 160.0, 180.0, bh); // 25 Button
 
     // Window Simulation options (visible when Page::Windows is active)
-    vec[29] = (base_x + 420.0, 300.0, 140.0, tgh); // 29 Toggle: Backplate
-    vec[30] = (base_x + 420.0, 340.0, 140.0, tgh); // 30 Toggle: MenuBar
-    vec[31] = (base_x + 420.0, 380.0, 140.0, tgh); // 31 Toggle: StatusBar
-    vec[32] = (base_x + 420.0, 420.0, 140.0, 20.0); // 32 SectionContainer: Border
-    vec[33] = (base_x + 420.0, 450.0, 140.0, tgh); // 33 Toggle: Bevel
-    vec[34] = (base_x + 420.0, 490.0, 140.0, sph); // 34 Spinbox: Border Width
-    vec[35] = (base_x + 420.0, 535.0, 140.0, sph); // 35 Spinbox: Bevel Depth
-    vec[36] = (base_x + 420.0, 270.0, 140.0, 20.0); // 36 SectionContainer: Window Elements
-    // 37 (page selector) is placed by apply_layout, inside the status bar.
-    vec[39] = (base_x + 420.0, 580.0, 140.0, bh); // 39 Button: Bevel Shape
-    vec[42] = (sw - 270.0, 60.0, 250.0, sh - 100.0); // 42 ControlPanel
-
-    // Dynamically position Page 0 (Controls) elements using the selected layout index
-    let available_w = (sw - base_x - 290.0).max(300.0);
-
-    struct DemoPacker {
-        free_rects: Vec<(f32, f32, f32, f32)>,
-        max_w: f32,
-        max_h: f32,
-        gap: f32,
-    }
-
-    impl DemoPacker {
-        fn new(start_x: f32, start_y: f32, max_width: f32, max_height: f32, gap: f32) -> Self {
-            Self {
-                free_rects: vec![(start_x, start_y, max_width, max_height)],
-                max_w: max_width,
-                max_h: 0.0,
-                gap,
-            }
-        }
-
-        fn pack(&mut self, cw: f32, ch: f32) -> (f32, f32) {
-            let cw_clamped = cw.min(self.max_w);
-
-            let mut best_idx = None;
-            let mut best_y = f32::MAX;
-            let mut best_x = f32::MAX;
-
-            for (idx, &(rx, ry, rw, rh)) in self.free_rects.iter().enumerate() {
-                if rw >= cw_clamped && rh >= ch {
-                    if ry < best_y || (ry == best_y && rx < best_x) {
-                        best_y = ry;
-                        best_x = rx;
-                        best_idx = Some(idx);
-                    }
-                }
-            }
-
-            let chosen_idx = match best_idx {
-                Some(idx) => idx,
-                None => {
-                    let new_y = self.max_h + self.gap;
-                    let new_rect = (self.free_rects[0].0, new_y, self.max_w, 100000.0);
-                    self.free_rects.push(new_rect);
-                    self.free_rects.len() - 1
-                }
-            };
-
-            let (fx, fy, fw, fh) = self.free_rects.remove(chosen_idx);
-            let px = fx;
-            let py = fy;
-
-            let rx = fx + cw_clamped + self.gap;
-            let rw = fw - cw_clamped - self.gap;
-            let by = py + ch + self.gap;
-            let bh = fh - ch - self.gap;
-
-            let split_horizontally = rw * fh > fw * bh;
-
-            if split_horizontally {
-                if rw > 0.0 && fh > 0.0 {
-                    self.free_rects.push((rx, py, rw, fh));
-                }
-                if cw_clamped > 0.0 && bh > 0.0 {
-                    self.free_rects.push((fx, by, cw_clamped, bh));
-                }
-            } else {
-                if rw > 0.0 && ch > 0.0 {
-                    self.free_rects.push((rx, py, rw, ch));
-                }
-                if fw > 0.0 && bh > 0.0 {
-                    self.free_rects.push((fx, by, fw, bh));
-                }
-            }
-
-            self.max_h = self.max_h.max(py + ch);
-
-            (px, py)
-        }
-    }
-
-    let label_off = |idx: usize| -> f32 {
-        cce_ui::widget::label_offset(slots.get_dyn(idx))
-    };
-
-    let mut items: Vec<(usize, f32, f32, f32)> = vec![
-        // Button
-        (2, 200.0, bh + label_off(2), bh),
-        // Progress Bar
-        (5, 415.0, cce_ui::layout::progressbar_height() + label_off(5), cce_ui::layout::progressbar_height()),
-        // Inputs
-        (43, 200.0, ddh + label_off(43), ddh),
-        (3, 200.0, tgh + label_off(3), tgh),
-        (4, 200.0, tgh + label_off(4), tgh),
-        (6, 200.0, slh + label_off(6), slh),
-        (7, 200.0, sph + label_off(7), sph),
-        (27, 200.0, ddh + label_off(27), ddh),
-        (21, 200.0, cce_ui::layout::rangeslider_height() + label_off(21), cce_ui::layout::rangeslider_height()),
-        (22, 200.0, 100.0 + label_off(22), 100.0),
-        (28, 200.0, 120.0 + label_off(28), 120.0),
-        (38, 200.0, bh + label_off(38), bh),
-        (41, 200.0, bh + label_off(41), bh),
-        (40, 200.0, 150.0 + label_off(40), 150.0),
-    ];
-    items.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
-
-    match layout_idx {
-        0 => {
-            // Vertical Layout
-            let mut cur_y = 60.0;
-            for (idx, _w_item, h_item_total, h_item_content) in items {
-                vec[idx] = (base_x, cur_y, available_w, h_item_content);
-                cur_y += h_item_total + 15.0;
-            }
-        }
-        1 => {
-            // Columns Layout
-            let num_cols = 3;
-            let col_w = (available_w - (num_cols - 1) as f32 * 15.0) / num_cols as f32;
-            let mut col_y = vec![60.0; num_cols];
-            for (i, (idx, _w_item, h_item_total, h_item_content)) in items.into_iter().enumerate() {
-                let col = i % num_cols;
-                let cx = base_x + col as f32 * (col_w + 15.0);
-                let cy = col_y[col];
-                vec[idx] = (cx, cy, col_w, h_item_content);
-                col_y[col] += h_item_total + 15.0;
-            }
-        }
-        2 | 3 => {
-            // Grid Layout and Adaptive Grid Layout
-            let num_cols = if layout_idx == 2 {
-                3
-            } else {
-                let min_col_w = 200.0;
-                (((available_w + 15.0) / (min_col_w + 15.0)).floor().max(1.0)) as usize
-            };
-            let col_w = (available_w - (num_cols - 1) as f32 * 15.0) / num_cols as f32;
-            let mut col_y = vec![60.0; num_cols];
-            for (idx, _w_item, h_item_total, h_item_content) in items {
-                let mut shortest_col = 0;
-                let mut min_h = col_y[0];
-                for col in 1..num_cols {
-                    if col_y[col] < min_h {
-                        min_h = col_y[col];
-                        shortest_col = col;
-                    }
-                }
-                let cx = base_x + shortest_col as f32 * (col_w + 15.0);
-                let cy = col_y[shortest_col];
-                vec[idx] = (cx, cy, col_w, h_item_content);
-                col_y[shortest_col] += h_item_total + 15.0;
-            }
-        }
-        4 => {
-            // Overlay Layout
-            for (idx, _w_item, _h_item_total, h_item_content) in items {
-                vec[idx] = (base_x, 60.0, available_w, h_item_content);
-            }
-        }
-        5 => {
-            // Flex Layout
-            let mut cur_x = base_x;
-            let mut cur_y = 60.0;
-            let mut row_h = 0.0f32;
-            for (idx, w_item, h_item_total, h_item_content) in items {
-                let target_w = w_item.min(available_w);
-                if cur_x + target_w > base_x + available_w && cur_x > base_x {
-                    cur_x = base_x;
-                    cur_y += row_h + 15.0;
-                    row_h = 0.0;
-                }
-                vec[idx] = (cur_x, cur_y, target_w, h_item_content);
-                cur_x += target_w + 15.0;
-                row_h = row_h.max(h_item_total);
-            }
-        }
-        6 => {
-            // Splitter Layout
-            let count = items.len();
-            let total_h = (sh - 120.0).max(300.0);
-            let single_h = (total_h - (count - 1) as f32 * 10.0) / count as f32;
-            let mut cur_y = 60.0;
-            for (idx, _w_item, _h_item_total, _h_item_content) in items {
-                let content_h = (single_h - label_off(idx)).max(10.0);
-                vec[idx] = (base_x, cur_y, available_w, content_h);
-                cur_y += single_h + 10.0;
-            }
-        }
-        7 => {
-            // Radial Layout
-            let cx = base_x + available_w / 2.0;
-            let cy = 300.0;
-            let radius = 180.0;
-            let count = items.len();
-            for (i, (idx, w_item, h_item_total, h_item_content)) in items.into_iter().enumerate() {
-                let angle = (i as f32 / count as f32) * 2.0 * std::f32::consts::PI;
-                let px = cx + radius * angle.cos() - w_item / 2.0;
-                let py = cy + radius * angle.sin() - h_item_total / 2.0;
-                vec[idx] = (px, py, w_item, h_item_content);
-            }
-        }
-        8 => {
-            // Circular Pane Layout
-            let cx = base_x + available_w / 2.0;
-            let cy = 300.0;
-            let count = items.len();
-            for (i, (idx, w_item, h_item_total, h_item_content)) in items.into_iter().enumerate() {
-                let radius = 100.0 + (i as f32 * 12.0);
-                let angle = (i as f32 / count as f32) * 2.0 * std::f32::consts::PI;
-                let px = cx + radius * angle.cos() - w_item / 2.0;
-                let py = cy + radius * angle.sin() - h_item_total / 2.0;
-                vec[idx] = (px, py, w_item, h_item_content);
-            }
-        }
-        9 => {
-            // Mosaic Layout (Guillotine Packer)
-            let max_h = (sh - 99.0).max(300.0);
-            let mut packer = DemoPacker::new(base_x, 60.0, available_w, max_h, 15.0);
-            for (idx, w_item, h_item_total, h_item_content) in items {
-                let (px, py) = packer.pack(w_item, h_item_total);
-                vec[idx] = (px, py, w_item.min(available_w), h_item_content);
-            }
-        }
-        _ => {
-            // Reverse Mosaic Layout
-            let max_h = (sh - 99.0).max(300.0);
-            let mut packer = DemoPacker::new(base_x, 60.0, available_w, max_h, 15.0);
-            let mut temp = Vec::with_capacity(items.len());
-            for &(idx, w_item, h_item_total, h_item_content) in &items {
-                let (px, py) = packer.pack(w_item, h_item_total);
-                temp.push((px, py, w_item, h_item_total, h_item_content, idx));
-            }
-            let mut x_min = f32::MAX;
-            let mut x_max = f32::MIN;
-            let mut y_min = f32::MAX;
-            let mut y_max = f32::MIN;
-            for &(px, py, pw, ph_total, _, _) in &temp {
-                x_min = x_min.min(px);
-                x_max = x_max.max(px + pw);
-                y_min = y_min.min(py);
-                y_max = y_max.max(py + ph_total);
-            }
-            let src_w = (x_max - x_min).max(1.0);
-            let src_h = (y_max - y_min).max(1.0);
-            let dst_w = available_w;
-            let dst_h = (sh - 120.0).max(300.0);
-            let scale_x = dst_w / src_w;
-            let scale_y = dst_h / src_h;
-            for (px, py, pw, _ph_total, ph_content, idx) in temp {
-                let new_x = base_x + (px - x_min) * scale_x;
-                let new_y = 60.0 + (py - y_min) * scale_y;
-                let new_w = pw * scale_x;
-                let new_h = ph_content * scale_y;
-                vec[idx] = (new_x, new_y, new_w, new_h);
-            }
-        }
-    }
-
+    vec[28] = (base_x + 420.0, 300.0, 140.0, tgh); // 28 Toggle: Backplate
+    vec[29] = (base_x + 420.0, 340.0, 140.0, tgh); // 29 Toggle: MenuBar
+    vec[30] = (base_x + 420.0, 380.0, 140.0, tgh); // 30 Toggle: StatusBar
+    vec[31] = (base_x + 420.0, 420.0, 140.0, 20.0); // 31 SectionContainer: Border
+    vec[32] = (base_x + 420.0, 450.0, 140.0, tgh); // 32 Toggle: Bevel
+    vec[33] = (base_x + 420.0, 490.0, 140.0, sph); // 33 Spinbox: Border Width
+    vec[34] = (base_x + 420.0, 535.0, 140.0, sph); // 34 Spinbox: Bevel Depth
+    vec[35] = (base_x + 420.0, 270.0, 140.0, 20.0); // 35 SectionContainer: Window Elements
+    // 36 (page selector) is placed by apply_layout, inside the status bar.
+    vec[38] = (base_x + 420.0, 580.0, 140.0, bh); // 38 Button: Bevel Shape
+    vec[41] = (sw - 270.0, 60.0, 250.0, sh - 100.0); // 41 ControlPanel
 
     vec
 }
@@ -2494,7 +2555,7 @@ fn demo_positions(sw: f32, sh: f32, layout_idx: usize, slots: &GallerySlots) ->
 /// The five `ChildSlots` rects for a child window of `sw`x`sh`: 0 background,
 /// 1 main (editor or description), 2 Close, 3 and 4 the MenuBar / StatusBar of a
 /// simulated window or the two aux Labels of a Ramp / ColorRamp editor.
-fn child_positions(sw: f32, sh: f32, use_menubar: bool, use_statusbar: bool, child_type: Option<&str>) -> Vec<(f32, f32, f32, f32)> {
+fn child_positions(sw: f32, sh: f32, use_menubar: bool, use_statusbar: bool, editor: bool) -> Vec<(f32, f32, f32, f32)> {
     let dy = if use_menubar { 40.0 } else { 0.0 };
     let dh = if use_statusbar { 24.0 } else { 0.0 };
     let inner_h = sh - dy - dh;
@@ -2508,7 +2569,7 @@ fn child_positions(sw: f32, sh: f32, use_menubar: bool, use_statusbar: bool, chi
     }
 
     vec[0] = (0.0, dy, sw, inner_h);
-    if matches!(child_type, Some("Ramp") | Some("ColorRamp")) {
+    if editor {
         vec[1] = (20.0, dy + 20.0, sw - 40.0, inner_h - 90.0);
         vec[2] = ((sw - 100.0) / 2.0, dy + inner_h - 55.0, 100.0, 35.0);
         vec[3] = (20.0, dy + inner_h - 90.0, 200.0, 20.0);