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

src/main.rs (83.6K)

   1 use cce_ui::widget::{
   2     Button, Checkbox, ContentBg, Dropdown, Label, ProgressBar, RangeSlider, Slider, Spinbox, StatusBar,
   3     Toggle, WidgetHost, Trackpad, hover_animation, TextBox, MenuBar, Group,
   4     Ramp, RampKey, ColorRamp, MouseButton, ElementState, Key, NamedKey, KeyEvent, MouseScrollDelta,
   5     ColorSelector, FontSelector, KeybindRecorder, ButtonStrip, Float3, UsageBar, StatusDot, DotStatus,
   6     InfoBox, InteractiveListItem, Breadcrumb, TreeList, BevelPreview, RampPreview, Separator, Splitter, Paginator,
   7     VerticalLayout, ColumnsLayout, GridLayout, AdaptiveGridLayout, MosaicLayout, ReverseMosaicLayout, OverlayLayout, ScrollBox,
   8 };
   9 mod gallery_widgets;
  10 use gallery_widgets::{RootPlate, Plate};
  11 use cce_ui::widget::{Adapted, LayoutConstraints, Point};
  12 use cce_ui::widget::input::Slider2D;
  13 use cce_ui::engine::{LogicalSize, LogicalPosition, LayerAnchor, LayerKeyboardInteractivity, LayerKind, LayerSettings};
  14 use cce_ui::scene::paint::Prim;
  15 use wayland_client::QueueHandle;
  16 
  17 /// What a child window is, from `--type`. The first seven are the kinds of surface a
  18 /// toolkit client can be under cce; the last two are the gallery's editor windows.
  19 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
  20 enum ChildKind {
  21     Floating,
  22     Fullscreen,
  23     Utility,
  24     LayerTop,
  25     LayerOverlay,
  26     LayerBackground,
  27     Status,
  28     Ramp,
  29     ColorRamp,
  30 }
  31 
  32 impl ChildKind {
  33     fn from_arg(s: &str) -> Option<ChildKind> {
  34         Some(match s {
  35             "Floating" => ChildKind::Floating,
  36             "Fullscreen" => ChildKind::Fullscreen,
  37             "Utility" => ChildKind::Utility,
  38             "LayerTop" => ChildKind::LayerTop,
  39             "LayerOverlay" => ChildKind::LayerOverlay,
  40             "LayerBackground" => ChildKind::LayerBackground,
  41             "Status" => ChildKind::Status,
  42             "Ramp" => ChildKind::Ramp,
  43             "ColorRamp" => ChildKind::ColorRamp,
  44             _ => return None,
  45         })
  46     }
  47 
  48     /// The `--type` spelling.
  49     fn arg(self) -> &'static str {
  50         match self {
  51             ChildKind::Floating => "Floating",
  52             ChildKind::Fullscreen => "Fullscreen",
  53             ChildKind::Utility => "Utility",
  54             ChildKind::LayerTop => "LayerTop",
  55             ChildKind::LayerOverlay => "LayerOverlay",
  56             ChildKind::LayerBackground => "LayerBackground",
  57             ChildKind::Status => "Status",
  58             ChildKind::Ramp => "Ramp",
  59             ChildKind::ColorRamp => "ColorRamp",
  60         }
  61     }
  62 
  63     fn title(self) -> &'static str {
  64         match self {
  65             ChildKind::Floating => "Floating Window",
  66             ChildKind::Fullscreen => "Fullscreen Window",
  67             ChildKind::Utility => "Utility Window",
  68             ChildKind::LayerTop => "Layer Shell (Top) Surface",
  69             ChildKind::LayerOverlay => "Layer Shell (Overlay) Surface",
  70             ChildKind::LayerBackground => "Layer Shell (Background) Surface",
  71             ChildKind::Status => "Status Segment",
  72             ChildKind::Ramp => "Ramp Editor",
  73             ChildKind::ColorRamp => "Color Ramp Editor",
  74         }
  75     }
  76 
  77     fn is_editor(self) -> bool {
  78         matches!(self, ChildKind::Ramp | ChildKind::ColorRamp)
  79     }
  80 
  81     fn default_size(self) -> (f32, f32) {
  82         match self {
  83             ChildKind::Floating | ChildKind::Fullscreen => (400.0, 250.0),
  84             ChildKind::Utility | ChildKind::LayerOverlay => (300.0, 180.0),
  85             ChildKind::LayerTop => (800.0, 40.0),
  86             ChildKind::LayerBackground => (800.0, 600.0),
  87             ChildKind::Status => (140.0, 24.0),
  88             // 390 tall: the editor keeps the 260px it had at hand-set margins now that
  89             // the title line, one root gap and the root insets stand around it.
  90             ChildKind::Ramp | ChildKind::ColorRamp => (450.0, 390.0),
  91         }
  92     }
  93 
  94     /// `cce-status-*` is the compositor's status-bar convention (`WindowRole::from_app_id`),
  95     /// with the edge read from the `-left-` / `-right-` infix; everything else is ours.
  96     fn app_id(self) -> String {
  97         match self {
  98             ChildKind::Status => "cce-status-right-gallery".to_string(),
  99             kind => format!("cce-gallery-child-{}", kind.arg().to_lowercase()),
 100         }
 101     }
 102 
 103     /// The wlr-layer-shell configuration for the three layer kinds.
 104     fn layer(self, height: i32) -> Option<LayerSettings> {
 105         let (layer, anchor, exclusive_zone) = match self {
 106             ChildKind::LayerTop => (LayerKind::Top, LayerAnchor::TOP | LayerAnchor::LEFT | LayerAnchor::RIGHT, height),
 107             ChildKind::LayerOverlay => (LayerKind::Overlay, LayerAnchor::empty(), 0),
 108             ChildKind::LayerBackground => (
 109                 LayerKind::Background,
 110                 LayerAnchor::TOP | LayerAnchor::BOTTOM | LayerAnchor::LEFT | LayerAnchor::RIGHT,
 111                 -1,
 112             ),
 113             _ => return None,
 114         };
 115         Some(LayerSettings {
 116             layer,
 117             anchor,
 118             exclusive_zone,
 119             keyboard_interactivity: LayerKeyboardInteractivity::None,
 120             margin: (0, 0, 0, 0),
 121             namespace: "cce-gallery".to_string(),
 122         })
 123     }
 124 
 125     /// The line inside the child window.
 126     fn child_text(self) -> &'static str {
 127         match self {
 128             ChildKind::Floating => "A plain xdg_toplevel, mapped Floating by cce.",
 129             ChildKind::Fullscreen => "An xdg_toplevel that asked for fullscreen at map.",
 130             ChildKind::Utility => "A toplevel declared UTILITY over the cce protocol.",
 131             ChildKind::LayerTop => "A Top-layer wlr-layer-shell surface with an exclusive zone.",
 132             ChildKind::LayerOverlay => "An Overlay-layer wlr-layer-shell surface, unanchored.",
 133             ChildKind::LayerBackground => "A Background-layer wlr-layer-shell surface.",
 134             ChildKind::Status => "A cce-status-* toplevel, docked into the status bar.",
 135             ChildKind::Ramp | ChildKind::ColorRamp => "",
 136         }
 137     }
 138 }
 139 
 140 /// What the roster's visibility filter needs, copied out of `State` so the dispatch
 141 /// loops can hold `&mut self.roster` while asking. The gallery shows every slot; a
 142 /// child window shows its menu bar and status bar only when asked to.
 143 #[derive(Clone, Copy)]
 144 struct Visibility {
 145     is_child: bool,
 146     use_menubar: bool,
 147     use_statusbar: bool,
 148 }
 149 
 150 impl Visibility {
 151     fn is_visible(self, index: usize) -> bool {
 152         if self.is_child {
 153             return match index {
 154                 0..=2 => true, // background, main, Close
 155                 3 => self.use_menubar,
 156                 4 => self.use_statusbar,
 157                 _ => false,
 158             };
 159         }
 160         true
 161     }
 162 }
 163 
 164 /// The gallery roster: 32 named slots. The numeric indexes used by the positions
 165 /// table, the visibility filter and the dispatch loops address these slots through
 166 /// `get_dyn`/`get_dyn_mut`.
 167 pub struct GallerySlots {
 168     pub menu_bar: Adapted<MenuBar>,
 169     pub status_bar: Adapted<StatusBar>,
 170     pub button_demo: Adapted<Button>,
 171     pub checkbox_demo: Adapted<Checkbox>,
 172     pub toggle_demo: Adapted<Toggle>,
 173     pub progress_demo: Adapted<ProgressBar>,
 174     pub slider_demo: Adapted<Slider>,
 175     pub spinbox_demo: Adapted<Spinbox>,
 176     pub range_slider_demo: Adapted<RangeSlider>,
 177     pub trackpad_demo: Adapted<Trackpad>,
 178     pub textbox_demo: Adapted<TextBox>,
 179     pub plate_demo: Adapted<Plate>,
 180     pub color_ramp_btn: Adapted<Button>,
 181     pub bevel_ramp: Adapted<Ramp>,
 182     pub ramp_btn: Adapted<Button>,
 183     pub layout_dd: Adapted<Dropdown>,
 184     pub color_selector_demo: Adapted<ColorSelector>,
 185     pub font_selector_demo: Adapted<FontSelector>,
 186     pub keybind_demo: Adapted<KeybindRecorder>,
 187     pub button_strip_demo: Adapted<ButtonStrip>,
 188     pub slider2d_demo: Adapted<Slider2D>,
 189     pub float3_demo: Adapted<Float3>,
 190     pub usage_bar_demo: Adapted<UsageBar>,
 191     pub status_dot_demo: Adapted<StatusDot>,
 192     pub info_box_demo: Adapted<InfoBox>,
 193     pub list_item_demo: Adapted<InteractiveListItem>,
 194     pub breadcrumb_demo: Adapted<Breadcrumb>,
 195     pub tree_list_demo: Adapted<TreeList>,
 196     pub bevel_preview_demo: Adapted<BevelPreview>,
 197     pub ramp_preview_demo: Adapted<RampPreview>,
 198     pub separator_demo: Adapted<Separator>,
 199     pub splitter_demo: Adapted<Splitter>,
 200     /// Two `Group` lassos over other exhibits (slots 32 and 33): overlays, not
 201     /// exhibits — laid out by their members, drawn under the exhibit clip.
 202     pub group_loose: Adapted<Group>,
 203     pub group_fitted: Adapted<Group>,
 204     /// The Style dropdown (slot 34), beside Layout in the header: Relief or Flat
 205     /// for every control at once (`cce_ui::layout::set_control_relief`).
 206     pub style_dd: Adapted<Dropdown>,
 207     /// The variant exhibits (`variant_exhibits`): every further style of a widget one of
 208     /// the named slots already shows, addressed as slots `GALLERY_COUNT..`.
 209     pub extra: Vec<Exhibit>,
 210 }
 211 
 212 /// The number of NAMED gallery slots; the variant exhibits follow them, so the roster's
 213 /// `len` is this plus `GallerySlots::extra.len()`.
 214 pub const GALLERY_COUNT: usize = 35;
 215 
 216 /// A variant exhibit: a widget in a style other than the one its named slot shows, with
 217 /// the content size `layout_exhibits` resets it to.
 218 pub struct Exhibit {
 219     pub widget: Box<dyn WidgetHost + 'static>,
 220     pub w: f32,
 221     pub h: f32,
 222     /// A content width narrower than `w`: the strategy lays the exhibit out at `w` (room
 223     /// for its label) and the widget is then given this width — a StatusDot stays a dot.
 224     pub content_w: Option<f32>,
 225 }
 226 
 227 impl Exhibit {
 228     /// Sized by the widget's own preferred (content) height, `fallback_h` when it declares none.
 229     fn new<W: WidgetHost + 'static>(widget: W, w: f32, fallback_h: f32) -> Self {
 230         let h = widget.preferred_height().unwrap_or(fallback_h);
 231         Exhibit { widget: Box::new(widget), w, h, content_w: None }
 232     }
 233 
 234     /// Sized by hand: for widgets whose declared height is a single row (a multiline
 235     /// TextBox, a vertical ButtonStrip) when the exhibit wants several.
 236     fn sized<W: WidgetHost + 'static>(widget: W, w: f32, h: f32) -> Self {
 237         Exhibit { widget: Box::new(widget), w, h, content_w: None }
 238     }
 239 
 240     fn with_content_width(mut self, w: f32) -> Self {
 241         self.content_w = Some(w);
 242         self
 243     }
 244 }
 245 
 246 impl GallerySlots {
 247 
 248     // Per-slot drag queries.
 249     pub fn draggable(&self, idx: usize) -> bool {
 250         if idx >= GALLERY_COUNT {
 251             return false; // variant exhibits never drag
 252         }
 253         match idx {
 254             0 => self.menu_bar.draggable(),
 255             1 => self.status_bar.draggable(),
 256             2 => self.button_demo.draggable(),
 257             3 => self.checkbox_demo.draggable(),
 258             4 => self.toggle_demo.draggable(),
 259             5 => self.progress_demo.draggable(),
 260             6 => self.slider_demo.draggable(),
 261             7 => self.spinbox_demo.draggable(),
 262             8 => self.range_slider_demo.draggable(),
 263             9 => self.trackpad_demo.draggable(),
 264             10 => self.textbox_demo.draggable(),
 265             11 => self.plate_demo.draggable(),
 266             12 => self.color_ramp_btn.draggable(),
 267             13 => self.bevel_ramp.draggable(),
 268             14 => self.ramp_btn.draggable(),
 269             15 => self.layout_dd.draggable(),
 270             34 => self.style_dd.draggable(),
 271             16 => self.color_selector_demo.draggable(),
 272             17 => self.font_selector_demo.draggable(),
 273             18 => self.keybind_demo.draggable(),
 274             19 => self.button_strip_demo.draggable(),
 275             20 => self.slider2d_demo.draggable(),
 276             21 => self.float3_demo.draggable(),
 277             22 => self.usage_bar_demo.draggable(),
 278             23 => self.status_dot_demo.draggable(),
 279             24 => self.info_box_demo.draggable(),
 280             25 => self.list_item_demo.draggable(),
 281             26 => self.breadcrumb_demo.draggable(),
 282             27 => self.tree_list_demo.draggable(),
 283             28 => self.bevel_preview_demo.draggable(),
 284             29 => self.ramp_preview_demo.draggable(),
 285             30 => self.separator_demo.draggable(),
 286             31 => self.splitter_demo.draggable(),
 287             32 | 33 => false,
 288             _ => panic!("gallery slot index out of range: {idx}"),
 289         }
 290     }
 291 
 292     pub fn is_dragging(&self, idx: usize) -> bool {
 293         if idx >= GALLERY_COUNT {
 294             return false;
 295         }
 296         match idx {
 297             0 => self.menu_bar.is_dragging(),
 298             1 => self.status_bar.is_dragging(),
 299             2 => self.button_demo.is_dragging(),
 300             3 => self.checkbox_demo.is_dragging(),
 301             4 => self.toggle_demo.is_dragging(),
 302             5 => self.progress_demo.is_dragging(),
 303             6 => self.slider_demo.is_dragging(),
 304             7 => self.spinbox_demo.is_dragging(),
 305             8 => self.range_slider_demo.is_dragging(),
 306             9 => self.trackpad_demo.is_dragging(),
 307             10 => self.textbox_demo.is_dragging(),
 308             11 => self.plate_demo.is_dragging(),
 309             12 => self.color_ramp_btn.is_dragging(),
 310             13 => self.bevel_ramp.is_dragging(),
 311             14 => self.ramp_btn.is_dragging(),
 312             15 => self.layout_dd.is_dragging(),
 313             34 => self.style_dd.is_dragging(),
 314             16 => self.color_selector_demo.is_dragging(),
 315             17 => self.font_selector_demo.is_dragging(),
 316             18 => self.keybind_demo.is_dragging(),
 317             19 => self.button_strip_demo.is_dragging(),
 318             20 => self.slider2d_demo.is_dragging(),
 319             21 => self.float3_demo.is_dragging(),
 320             22 => self.usage_bar_demo.is_dragging(),
 321             23 => self.status_dot_demo.is_dragging(),
 322             24 => self.info_box_demo.is_dragging(),
 323             25 => self.list_item_demo.is_dragging(),
 324             26 => self.breadcrumb_demo.is_dragging(),
 325             27 => self.tree_list_demo.is_dragging(),
 326             28 => self.bevel_preview_demo.is_dragging(),
 327             29 => self.ramp_preview_demo.is_dragging(),
 328             30 => self.separator_demo.is_dragging(),
 329             31 => self.splitter_demo.is_dragging(),
 330             32 | 33 => false,
 331             _ => panic!("gallery slot index out of range: {idx}"),
 332         }
 333     }
 334 
 335     pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
 336         if idx >= GALLERY_COUNT {
 337             return &*self.extra[idx - GALLERY_COUNT].widget;
 338         }
 339         match idx {
 340             0 => &self.menu_bar,
 341             1 => &self.status_bar,
 342             2 => &self.button_demo,
 343             3 => &self.checkbox_demo,
 344             4 => &self.toggle_demo,
 345             5 => &self.progress_demo,
 346             6 => &self.slider_demo,
 347             7 => &self.spinbox_demo,
 348             8 => &self.range_slider_demo,
 349             9 => &self.trackpad_demo,
 350             10 => &self.textbox_demo,
 351             11 => &self.plate_demo,
 352             12 => &self.color_ramp_btn,
 353             13 => &self.bevel_ramp,
 354             14 => &self.ramp_btn,
 355             15 => &self.layout_dd,
 356             16 => &self.color_selector_demo,
 357             17 => &self.font_selector_demo,
 358             18 => &self.keybind_demo,
 359             19 => &self.button_strip_demo,
 360             20 => &self.slider2d_demo,
 361             21 => &self.float3_demo,
 362             22 => &self.usage_bar_demo,
 363             23 => &self.status_dot_demo,
 364             24 => &self.info_box_demo,
 365             25 => &self.list_item_demo,
 366             26 => &self.breadcrumb_demo,
 367             27 => &self.tree_list_demo,
 368             28 => &self.bevel_preview_demo,
 369             29 => &self.ramp_preview_demo,
 370             30 => &self.separator_demo,
 371             31 => &self.splitter_demo,
 372             32 => &self.group_loose,
 373             33 => &self.group_fitted,
 374             34 => &self.style_dd,
 375             _ => panic!("gallery slot index out of range: {idx}"),
 376         }
 377     }
 378 
 379     pub fn get_dyn_mut(&mut self, idx: usize) -> &mut (dyn WidgetHost + 'static) {
 380         if idx >= GALLERY_COUNT {
 381             return &mut *self.extra[idx - GALLERY_COUNT].widget;
 382         }
 383         match idx {
 384             0 => &mut self.menu_bar,
 385             1 => &mut self.status_bar,
 386             2 => &mut self.button_demo,
 387             3 => &mut self.checkbox_demo,
 388             4 => &mut self.toggle_demo,
 389             5 => &mut self.progress_demo,
 390             6 => &mut self.slider_demo,
 391             7 => &mut self.spinbox_demo,
 392             8 => &mut self.range_slider_demo,
 393             9 => &mut self.trackpad_demo,
 394             10 => &mut self.textbox_demo,
 395             11 => &mut self.plate_demo,
 396             12 => &mut self.color_ramp_btn,
 397             13 => &mut self.bevel_ramp,
 398             14 => &mut self.ramp_btn,
 399             15 => &mut self.layout_dd,
 400             16 => &mut self.color_selector_demo,
 401             17 => &mut self.font_selector_demo,
 402             18 => &mut self.keybind_demo,
 403             19 => &mut self.button_strip_demo,
 404             20 => &mut self.slider2d_demo,
 405             21 => &mut self.float3_demo,
 406             22 => &mut self.usage_bar_demo,
 407             23 => &mut self.status_dot_demo,
 408             24 => &mut self.info_box_demo,
 409             25 => &mut self.list_item_demo,
 410             26 => &mut self.breadcrumb_demo,
 411             27 => &mut self.tree_list_demo,
 412             28 => &mut self.bevel_preview_demo,
 413             29 => &mut self.ramp_preview_demo,
 414             30 => &mut self.separator_demo,
 415             31 => &mut self.splitter_demo,
 416             32 => &mut self.group_loose,
 417             33 => &mut self.group_fitted,
 418             34 => &mut self.style_dd,
 419             _ => panic!("gallery slot index out of range: {idx}"),
 420         }
 421     }
 422 }
 423 
 424 pub enum ChildBg {
 425     RootPlate(Adapted<RootPlate>),
 426     ContentBg(Adapted<ContentBg>),
 427 }
 428 
 429 pub enum ChildMain {
 430     ColorRamp(Adapted<ColorRamp>),
 431     Ramp(Adapted<Ramp>),
 432     Desc(Adapted<Label>),
 433 }
 434 
 435 pub enum ChildAux3 {
 436     Label(Adapted<Label>),
 437     MenuBar(Adapted<MenuBar>),
 438 }
 439 
 440 pub enum ChildAux4 {
 441     Label(Adapted<Label>),
 442     StatusBar(Adapted<StatusBar>),
 443 }
 444 
 445 pub struct ChildSlots {
 446     pub bg: ChildBg,
 447     pub main: ChildMain,
 448     pub close: Adapted<Button>,
 449     pub aux3: ChildAux3,
 450     pub aux4: ChildAux4,
 451 }
 452 
 453 pub const CHILD_COUNT: usize = 5;
 454 
 455 impl ChildSlots {
 456 
 457     pub fn draggable(&self, idx: usize) -> bool {
 458         match idx {
 459             0 => match &self.bg {
 460                 ChildBg::RootPlate(w) => w.draggable(),
 461                 ChildBg::ContentBg(w) => w.draggable(),
 462             },
 463             1 => match &self.main {
 464                 ChildMain::ColorRamp(w) => w.draggable(),
 465                 ChildMain::Ramp(w) => w.draggable(),
 466                 ChildMain::Desc(w) => w.draggable(),
 467             },
 468             2 => self.close.draggable(),
 469             3 => match &self.aux3 {
 470                 ChildAux3::Label(w) => w.draggable(),
 471                 ChildAux3::MenuBar(w) => w.draggable(),
 472             },
 473             4 => match &self.aux4 {
 474                 ChildAux4::Label(w) => w.draggable(),
 475                 ChildAux4::StatusBar(w) => w.draggable(),
 476             },
 477             _ => panic!("child slot index out of range: {idx}"),
 478         }
 479     }
 480 
 481     pub fn is_dragging(&self, idx: usize) -> bool {
 482         match idx {
 483             0 => match &self.bg {
 484                 ChildBg::RootPlate(w) => w.is_dragging(),
 485                 ChildBg::ContentBg(w) => w.is_dragging(),
 486             },
 487             1 => match &self.main {
 488                 ChildMain::ColorRamp(w) => w.is_dragging(),
 489                 ChildMain::Ramp(w) => w.is_dragging(),
 490                 ChildMain::Desc(w) => w.is_dragging(),
 491             },
 492             2 => self.close.is_dragging(),
 493             3 => match &self.aux3 {
 494                 ChildAux3::Label(w) => w.is_dragging(),
 495                 ChildAux3::MenuBar(w) => w.is_dragging(),
 496             },
 497             4 => match &self.aux4 {
 498                 ChildAux4::Label(w) => w.is_dragging(),
 499                 ChildAux4::StatusBar(w) => w.is_dragging(),
 500             },
 501             _ => panic!("child slot index out of range: {idx}"),
 502         }
 503     }
 504 
 505     pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
 506         match idx {
 507             0 => match &self.bg {
 508                 ChildBg::RootPlate(w) => w,
 509                 ChildBg::ContentBg(w) => w,
 510             },
 511             1 => match &self.main {
 512                 ChildMain::ColorRamp(w) => w,
 513                 ChildMain::Ramp(w) => w,
 514                 ChildMain::Desc(w) => w,
 515             },
 516             2 => &self.close,
 517             3 => match &self.aux3 {
 518                 ChildAux3::Label(w) => w,
 519                 ChildAux3::MenuBar(w) => w,
 520             },
 521             4 => match &self.aux4 {
 522                 ChildAux4::Label(w) => w,
 523                 ChildAux4::StatusBar(w) => w,
 524             },
 525             _ => panic!("child slot index out of range: {idx}"),
 526         }
 527     }
 528 
 529     pub fn get_dyn_mut(&mut self, idx: usize) -> &mut (dyn WidgetHost + 'static) {
 530         match idx {
 531             0 => match &mut self.bg {
 532                 ChildBg::RootPlate(w) => w,
 533                 ChildBg::ContentBg(w) => w,
 534             },
 535             1 => match &mut self.main {
 536                 ChildMain::ColorRamp(w) => w,
 537                 ChildMain::Ramp(w) => w,
 538                 ChildMain::Desc(w) => w,
 539             },
 540             2 => &mut self.close,
 541             3 => match &mut self.aux3 {
 542                 ChildAux3::Label(w) => w,
 543                 ChildAux3::MenuBar(w) => w,
 544             },
 545             4 => match &mut self.aux4 {
 546                 ChildAux4::Label(w) => w,
 547                 ChildAux4::StatusBar(w) => w,
 548             },
 549             _ => panic!("child slot index out of range: {idx}"),
 550         }
 551     }
 552 }
 553 
 554 /// The two roster modes. Boxed slot structs keep registered widget pointers stable while
 555 /// the containing `State` moves.
 556 pub enum Roster {
 557     Gallery(Box<GallerySlots>),
 558     Child(Box<ChildSlots>),
 559 }
 560 
 561 impl Roster {
 562     pub fn len(&self) -> usize {
 563         match self {
 564             Roster::Gallery(s) => GALLERY_COUNT + s.extra.len(),
 565             Roster::Child(_) => CHILD_COUNT,
 566         }
 567     }
 568 
 569     pub fn get_dyn(&self, idx: usize) -> &(dyn WidgetHost + 'static) {
 570         match self {
 571             Roster::Gallery(s) => s.get_dyn(idx),
 572             Roster::Child(s) => s.get_dyn(idx),
 573         }
 574     }
 575 
 576     pub fn draggable(&self, idx: usize) -> bool {
 577         match self {
 578             Roster::Gallery(s) => s.draggable(idx),
 579             Roster::Child(s) => s.draggable(idx),
 580         }
 581     }
 582 
 583     pub fn is_dragging(&self, idx: usize) -> bool {
 584         match self {
 585             Roster::Gallery(s) => s.is_dragging(idx),
 586             Roster::Child(s) => s.is_dragging(idx),
 587         }
 588     }
 589 
 590     pub fn get_dyn_mut(&mut self, idx: usize) -> &mut (dyn WidgetHost + 'static) {
 591         match self {
 592             Roster::Gallery(s) => s.get_dyn_mut(idx),
 593             Roster::Child(s) => s.get_dyn_mut(idx),
 594         }
 595     }
 596 
 597     /// The gallery slots; panics in child mode (gallery-only paths assert their mode).
 598     pub fn gallery(&self) -> &GallerySlots {
 599         match self {
 600             Roster::Gallery(s) => s,
 601             Roster::Child(_) => panic!("gallery slots requested in child mode"),
 602         }
 603     }
 604 
 605     pub fn gallery_mut(&mut self) -> &mut GallerySlots {
 606         match self {
 607             Roster::Gallery(s) => s,
 608             Roster::Child(_) => panic!("gallery slots requested in child mode"),
 609         }
 610     }
 611 
 612     // --- Value drains: route a slot index to its concrete slot's `take_click` /
 613     // `value`. Arms exist for every slot the page logic drains.
 614 
 615     pub fn take_click(&mut self, idx: usize) -> bool {
 616         let s = match self {
 617             Roster::Gallery(s) => s,
 618             // The child roster drains one slot: its Close button.
 619             Roster::Child(c) => return idx == 2 && c.close.take_click(),
 620         };
 621         match idx {
 622             2 => s.button_demo.take_click(),
 623             3 => s.checkbox_demo.take_click(),
 624             4 => s.toggle_demo.take_click(),
 625             5 => s.progress_demo.take_click(),
 626             6 => s.slider_demo.take_click(),
 627             7 => s.spinbox_demo.take_click(),
 628             8 => s.range_slider_demo.take_click(),
 629             9 => s.trackpad_demo.take_click(),
 630             10 => s.textbox_demo.take_click(),
 631             11 => s.plate_demo.take_click(),
 632             12 => s.color_ramp_btn.take_click(),
 633             13 => s.bevel_ramp.take_click(),
 634             14 => s.ramp_btn.take_click(),
 635             15 => s.layout_dd.take_click(),
 636             34 => s.style_dd.take_click(),
 637             16 => s.color_selector_demo.take_click(),
 638             17 => s.font_selector_demo.take_click(),
 639             18 => s.keybind_demo.take_click(),
 640             19 => s.button_strip_demo.take_click(),
 641             20 => s.slider2d_demo.take_click(),
 642             21 => s.float3_demo.take_click(),
 643             22 => s.usage_bar_demo.take_click(),
 644             23 => s.status_dot_demo.take_click(),
 645             24 => s.info_box_demo.take_click(),
 646             25 => s.list_item_demo.take_click(),
 647             26 => s.breadcrumb_demo.take_click(),
 648             27 => s.tree_list_demo.take_click(),
 649             28 => s.bevel_preview_demo.take_click(),
 650             29 => s.ramp_preview_demo.take_click(),
 651             30 => s.separator_demo.take_click(),
 652             31 => s.splitter_demo.take_click(),
 653             32 | 33 => false,
 654             // The variant exhibits are looked at, not drained.
 655             i if i >= GALLERY_COUNT => false,
 656             _ => panic!("take_click: unwired gallery slot {idx}"),
 657         }
 658     }
 659 
 660     pub fn value(&self, idx: usize) -> i32 {
 661         let s = self.gallery();
 662         match idx {
 663             15 => s.layout_dd.value(),
 664             34 => s.style_dd.value(),
 665             _ => panic!("value: unwired gallery slot {idx}"),
 666         }
 667     }
 668 }
 669 
 670 struct State {
 671     roster: Roster,
 672     positions: Vec<(f32, f32, f32, f32)>,
 673 
 674     status_text: String,
 675 
 676     focused_widget: Option<usize>,
 677 
 678     width: f32,
 679     height: f32,
 680     scale: f64,
 681 
 682     is_child: bool,
 683     opacity: bool,
 684     transparency: f32,
 685     use_root_plate: bool,
 686     use_menubar: bool,
 687     use_statusbar: bool,
 688     border_enabled: bool,
 689     child_kind: Option<ChildKind>,
 690     ui_context: cce_ui::context::UiContext,
 691 
 692     layout_idx: usize,
 693     /// The scroll frame the exhibits below the Layout dropdown scroll inside.
 694     exhibit_scroll: ScrollBox,
 695 }
 696 
 697 fn save_bevel_ramp(keys: &[RampKey], line_type: &str) {
 698     let mut s = String::new();
 699     s.push_str("keys {\n");
 700     for k in keys {
 701         s.push_str(&format!("    key pos={} val={}\n", k.pos, k.value));
 702     }
 703     s.push_str("}\n");
 704     s.push_str(&format!("line_type \"{}\"\n", line_type));
 705     let path = cce_ui::config::get_config_path().parent().unwrap().join("bevel_ramp.kdl");
 706     let _ = std::fs::write(path, s);
 707 }
 708 
 709 fn load_bevel_ramp() -> (Vec<RampKey>, String) {
 710     let path = cce_ui::config::get_config_path().parent().unwrap().join("bevel_ramp.kdl");
 711     let mut line_type = "linear".to_string();
 712     let mut keys = Vec::new();
 713     if let Ok(content) = std::fs::read_to_string(path) {
 714         for line in content.lines() {
 715             let line = line.trim();
 716             if line.starts_with("key ") {
 717                 let mut pos = 0.0;
 718                 let mut val = 0.5;
 719                 for part in line.split_whitespace() {
 720                     if let Some(rest) = part.strip_prefix("pos=") {
 721                         pos = rest.parse().unwrap_or(0.0);
 722                     } else if let Some(rest) = part.strip_prefix("val=") {
 723                         val = rest.parse().unwrap_or(0.5);
 724                     }
 725                 }
 726                 keys.push(RampKey { pos, value: val });
 727             } else if line.starts_with("line_type ") {
 728                 if let Some(val_str) = line.split_whitespace().nth(1) {
 729                     line_type = val_str.trim_matches('"').to_string();
 730                 }
 731             }
 732         }
 733         if !keys.is_empty() {
 734             keys.sort_by(|a, b| a.pos.partial_cmp(&b.pos).unwrap());
 735             return (keys, line_type);
 736         }
 737     }
 738     (
 739         vec![
 740             RampKey { pos: 0.0, value: 0.5 },
 741             RampKey { pos: 0.2, value: 1.0 },
 742             RampKey { pos: 0.8, value: 1.0 },
 743             RampKey { pos: 1.0, value: 0.5 },
 744         ],
 745         "linear".to_string()
 746     )
 747 }
 748 
 749 /// Every further style of a widget the named slots show once: the toolkit's own
 750 /// variants — a constructor (`Button::new_reset`) or a builder (`with_band`) —
 751 /// so the page shows each look a widget can take, in the toolkit's default size
 752 /// for it.
 753 ///
 754 /// The relief-off look is NOT a variant here: the header's Style dropdown switches
 755 /// every control between Relief and Flat at once (`set_control_relief`), so each
 756 /// exhibit shows both, and no widget appears twice for its style alone.
 757 fn variant_exhibits() -> Vec<Exhibit> {
 758     const W: f32 = 190.0;
 759     let bh = cce_ui::layout::button_height();
 760     let slh = cce_ui::layout::slider_height();
 761     let tbh = cce_ui::layout::textbox_height();
 762     let ddh = cce_ui::layout::dropdown_height();
 763     let csh = cce_ui::layout::color_selector_height();
 764     // A StatusDot is 12px square; its exhibit is as wide as its label.
 765     const DOT_W: f32 = 200.0;
 766     // A column of three rotated tabs, and the sidebar width a vertical strip is drawn for.
 767     const TABS_H: f32 = 120.0;
 768     const TAB_COLUMN_W: f32 = 40.0;
 769     let three = || vec!["One".to_string(), "Two".to_string(), "Three".to_string()];
 770     vec![
 771         // Button: the other four kinds.
 772         Exhibit::new(Button::new_reset(0.0, 0.0, W, bh).with_label("Button (reset)"), W, bh),
 773         Exhibit::new(Button::new_list_row(0.0, 0.0, W, bh).with_label("Button (list row)"), W, bh),
 774         Exhibit::new(Button::new_menu_item(0.0, 0.0, W, bh).with_label("Button (menu item)"), W, bh),
 775         Exhibit::new(Button::new_copy_icon(0.0, 0.0, bh, bh), bh, bh),
 776         // Slider: with a readout.
 777         Exhibit::new(Slider::new().with_label("Slider (readout)").with_readout(true), W, slh),
 778         // TextBox: multiline, chromeless, password.
 779         Exhibit::sized(
 780             TextBox::new("TextBox (multiline)\nA second line of text.".to_string()).with_multiline(true),
 781             W,
 782             3.0 * tbh,
 783         ),
 784         Exhibit::new(TextBox::new("TextBox (chromeless)".to_string()).with_draw_bg_border(false), W, tbh),
 785         Exhibit::new(TextBox::new("hunter2".to_string()).with_password(true).with_label("TextBox (password)"), W, tbh),
 786         // ButtonStrip: the vertical column (rotated tabs), and the Paginator sidebar built on it.
 787         Exhibit::sized(
 788             Adapted::new(ButtonStrip::new(0.0, 0.0, W, TABS_H).with_buttons(three()).with_selected(Some(0)).with_vertical(true))
 789                 .with_label("ButtonStrip (vertical)"),
 790             W,
 791             TABS_H,
 792         )
 793         .with_content_width(TAB_COLUMN_W),
 794         Exhibit::sized(Paginator::new(three()).with_label("Paginator"), W, TABS_H),
 795         // ColorSelector: the alpha swatch.
 796         Exhibit::new(ColorSelector::new_rgba([64, 128, 255, 128]).with_label("ColorSelector (alpha)"), W, csh),
 797         // Label: the plain text widget.
 798         Exhibit::new(Label::new("Label"), W, ddh),
 799         // StatusDot: the other three statuses.
 800         Exhibit::new(StatusDot::new(DotStatus::Inactive).with_label("StatusDot (inactive)"), DOT_W, StatusDot::SIZE)
 801             .with_content_width(StatusDot::SIZE),
 802         Exhibit::new(StatusDot::new(DotStatus::Warning).with_label("StatusDot (warning)"), DOT_W, StatusDot::SIZE)
 803             .with_content_width(StatusDot::SIZE),
 804         Exhibit::new(StatusDot::new(DotStatus::Error).with_label("StatusDot (error)"), DOT_W, StatusDot::SIZE)
 805             .with_content_width(StatusDot::SIZE),
 806     ]
 807 }
 808 
 809 /// The exhibits: every slot but the chrome (0, 1), the lassos (32, 33) and the
 810 /// header dropdowns (15 Layout, 34 Style), which `layout_exhibits` lays out under
 811 /// the header row.
 812 fn is_exhibit(i: usize) -> bool {
 813     matches!(i, 2..=14 | 16..=31) || i >= GALLERY_COUNT
 814 }
 815 
 816 /// The Group lassos: drawn under the exhibit clip like exhibits, laid out by
 817 /// their members rather than by the strategy.
 818 fn is_overlay(i: usize) -> bool {
 819     matches!(i, 32 | 33)
 820 }
 821 
 822 impl State {
 823     fn visibility(&self) -> Visibility {
 824         Visibility {
 825             is_child: self.is_child,
 826             use_menubar: self.use_menubar,
 827             use_statusbar: self.use_statusbar,
 828         }
 829     }
 830 
 831     fn is_widget_visible(&self, index: usize) -> bool {
 832         self.visibility().is_visible(index)
 833     }
 834 
 835     /// Recompute the positions table for the current size, mode and layout.
 836     fn relayout(&mut self) {
 837         self.positions = if self.is_child {
 838             child_positions(self.width, self.height, self.use_menubar, self.use_statusbar, self.child_kind.is_some_and(ChildKind::is_editor))
 839         } else {
 840             demo_positions(self.width, self.height, self.roster.len())
 841         };
 842     }
 843 
 844     /// The Controls exhibits in layout order, each with the content size it is reset to
 845     /// before a strategy runs: the toolkit layouts read a child's own rect for its width
 846     /// and preferred height, and a previous strategy may have stretched it.
 847     fn exhibit_sizes(&self) -> Vec<(usize, f32, f32)> {
 848         // Every height below is the toolkit's default for that control — its
 849         // configured `style.control.<name>.height`, or the intrinsic size the
 850         // widget declares — so the page is a record of the defaults, not of
 851         // numbers chosen here. The strategies read a widget's own intrinsic
 852         // height anyway; the table only has to agree with it.
 853         let bh = cce_ui::layout::button_height();
 854         let tgh = cce_ui::layout::toggle_height();
 855         let slh = cce_ui::layout::slider_height();
 856         let rsh = cce_ui::layout::rangeslider_height();
 857         let pbh = cce_ui::layout::progressbar_height();
 858         let sph = cce_ui::layout::spinbox_height();
 859         let ddh = cce_ui::layout::dropdown_height();
 860         let tbh = cce_ui::layout::textbox_height();
 861         let csh = cce_ui::layout::color_selector_height();
 862         let fsh = cce_ui::layout::font_selector_height();
 863         let rmh = cce_ui::layout::ramp_height();
 864         // The few exhibits with no toolkit default are canvases: an area to draw
 865         // or drag in, sized here and only here.
 866         const CANVAS_H: f32 = 120.0;
 867         const W: f32 = 190.0;
 868         let s2d_w = self
 869             .roster
 870             .get_dyn(20)
 871             .measure(LayoutConstraints::new(0.0, f32::MAX, 0.0, f32::MAX), &self.ui_context)
 872             .width;
 873         // A StatusDot is 12px square; its exhibit is as wide as its label
 874         // (`content_width` hands the dot its own size back after layout).
 875         const DOT_W: f32 = 200.0;
 876         let raw: [(usize, f32, f32); 29] = [
 877             (2, W, bh),                                   // Button
 878             (19, W, bh),                                  // ButtonStrip
 879             (3, W, tgh),                                  // Checkbox (a toggle row)
 880             (4, W, tgh),                                  // Toggle
 881             (6, W, slh),                                  // Slider
 882             (8, W, rsh),                                 // RangeSlider
 883             (20, s2d_w, 64.0),                            // Slider2D (a 64px pad, wide enough for its label)
 884             (7, W, sph),                                  // Spinbox
 885             (21, W, Float3::preferred_height(false)),     // Float3 (three slider rows)
 886             (10, W, tbh),                                 // TextBox
 887             (18, W, tbh),                                 // KeybindRecorder (a textbox)
 888             (16, W, csh),                                 // ColorSelector
 889             (17, W, fsh),                                 // FontSelector
 890             (5, W, pbh),                                  // ProgressBar
 891             (22, W, pbh),                                 // UsageBar
 892             (23, DOT_W, StatusDot::SIZE),                 // StatusDot (its label needs the width)
 893             (30, W, 1.0),                                 // Separator (a rule)
 894             (31, 6.0, CANVAS_H),                          // Splitter (a vertical grip)
 895             (24, W, 3.0 * ddh),                           // InfoBox (title + two lines)
 896             (25, W, 2.0 * ddh),                           // InteractiveListItem (title + subtitle)
 897             (26, W, bh),                                  // Breadcrumb (button plates)
 898             (27, W, CANVAS_H),                            // TreeList
 899             (9, W, CANVAS_H),                            // Trackpad
 900             (11, W, CANVAS_H),                            // Plate
 901             (28, W, CANVAS_H),                            // BevelPreview
 902             (29, W, 2.0 * rmh),                           // RampPreview (a ramp's curve)
 903             (13, W, CANVAS_H),                            // Ramp (its editor declares 150)
 904             (12, W, bh),                                  // Color Ramp...
 905             (14, W, bh),                                  // Ramp...
 906         ];
 907         let mut sizes = raw.to_vec();
 908         let extra = &self.roster.gallery().extra;
 909         sizes.extend(extra.iter().enumerate().map(|(k, e)| (GALLERY_COUNT + k, e.w, e.h)));
 910         sizes
 911     }
 912 
 913     /// Lay the Controls exhibits out with the toolkit strategy the Layout dropdown selects
 914     /// (`LAYOUTS`), then clip whatever runs past the status bar.
 915     /// The exhibits' viewport: below the Layout dropdown, above the status bar.
 916     /// The exhibit area: a well sunk into the root plate, standing on it like the
 917     /// header dropdowns — their inset in from the window's sides (`x` is the Layout
 918     /// dropdown's), one root gap below them and one root gap above the status band.
 919     fn exhibit_viewport(&self) -> (f32, f32, f32, f32) {
 920         let gap = cce_ui::layout::root_plate_gap();
 921         let (dx, dy, _, dh) = self.roster.get_dyn(15).rect();
 922         let (x, y) = (dx, dy + dh + gap);
 923         let w = (self.width - 2.0 * x).max(300.0);
 924         let h = ((self.height - 24.0 - gap) - y).max(100.0);
 925         (x, y, w, h)
 926     }
 927 
 928     /// The well's rect, outer corner radius and wall depth. The wall is every
 929     /// well's rule (`well_rim`: the DE bevel width capped at a fifth of the
 930     /// height), taken in both styles so the page lays out the same whichever the
 931     /// Style dropdown selects — flat, it is the margin inside the hairline frame.
 932     fn exhibit_well(&self) -> (cce_ui::scene::layout::Rect, f32, f32) {
 933         let (x, y, w, h) = self.exhibit_viewport();
 934         let rect = cce_ui::scene::layout::Rect { x, y, width: w, height: h };
 935         let radius = cce_ui::layout::plate_corner_radius();
 936         let wall = cce_ui::layout::bevel_width().min(h * 0.2);
 937         (rect, radius, wall)
 938     }
 939 
 940     /// The well's floor inside its wall — the plate the exhibits sit on and the
 941     /// fitted lasso snaps to — with the floor's corner radius.
 942     fn exhibit_floor(&self) -> (cce_ui::scene::layout::Rect, f32) {
 943         let (rect, radius, wall) = self.exhibit_well();
 944         let (floor, radii) = cce_ui::layout::carve_inside(rect, (radius, radius, radius, radius), wall);
 945         (floor, radii.0)
 946     }
 947 
 948     fn in_exhibit_viewport(&self, px: f32, py: f32) -> bool {
 949         let (x, y, w, h) = self.exhibit_viewport();
 950         px >= x && px <= x + w && py >= y && py <= y + h
 951     }
 952 
 953     /// Lay the Controls exhibits out with the toolkit strategy the Layout dropdown selects
 954     /// (`LAYOUTS`), then shift them by the scroll frame's offset; painting clips them to
 955     /// the viewport. Runs from apply_layout and from display_list, which is what
 956     /// re-arranges after a scroll.
 957     fn layout_exhibits(&mut self) {
 958         let (x, y, w, h) = self.exhibit_viewport();
 959         self.exhibit_scroll.set_rect(x, y, w, h);
 960         // The exhibits sit on the well's floor, inside its wall; the fitted lasso's
 961         // plate is that floor, so its sides snap to the foot of the wall.
 962         let (floor, floor_r) = self.exhibit_floor();
 963         if let Roster::Gallery(s) = &mut self.roster {
 964             s.group_fitted.inner_mut().set_plate(floor, floor_r);
 965         }
 966         let mut children: Vec<*mut (dyn WidgetHost + 'static)> = Vec::new();
 967         let mut indices: Vec<usize> = Vec::new();
 968         for (idx, cw, ch) in self.exhibit_sizes() {
 969             if self.roster.is_dragging(idx) {
 970                 continue;
 971             }
 972             let widget = self.roster.get_dyn_mut(idx);
 973             // Seed the block: the content height plus the label strip above it.
 974             let strip = widget.label_strip();
 975             widget.set_rect(0.0, 0.0, cw, ch + strip);
 976             children.push(widget as *mut (dyn WidgetHost + 'static));
 977             indices.push(idx);
 978         }
 979         // The exhibits start at the fitted lasso's seat: one padding in from the
 980         // area's sides for the frame plus one for the members inside it, and the
 981         // lasso's headroom (padding + title tab) plus a padding down. A fitted group
 982         // snaps to the area's edges one padding in, and its tab needs room inside
 983         // the area above its members — laid out flush with the corner, the row had
 984         // the frame's wall on its left edge and the tab over its labels.
 985         let (inset_x, inset_top) = match &self.roster {
 986             Roster::Gallery(s) => {
 987                 let g = s.group_fitted.inner();
 988                 (2.0 * g.padding(), g.padding() + g.headroom())
 989             }
 990             _ => (0.0, 0.0),
 991         };
 992         let strategy = (LAYOUTS.get(self.layout_idx).unwrap_or(&LAYOUTS[DEFAULT_LAYOUT]).1)();
 993         let (lx, ly) = (floor.x + inset_x, floor.y + inset_top);
 994         let content_h = strategy.layout(lx, ly, floor.width - 2.0 * inset_x, floor.y + floor.height - ly, &children, &mut self.ui_context);
 995         self.exhibit_scroll.update_bounds(content_h + (ly - y), y, h);
 996         let scroll_y = self.exhibit_scroll.scroll_y;
 997         for (&child, &idx) in children.iter().zip(&indices) {
 998             let widget = unsafe { &mut *child };
 999             // The strategy placed each exhibit's content box (its label hanging in the
1000             // strip above); re-land it through `layout` shifted by the scroll — the
1001             // landed rect is the occupied one, so the content origin is `strip` below
1002             // its top and the content height is the rest.
1003             let (cx, cy, cw, ch) = widget.rect();
1004             let strip = widget.label_strip();
1005             let content = ch - strip;
1006             let cw = self.content_width(idx).unwrap_or(cw);
1007             widget.layout(
1008                 Point { x: cx, y: cy + strip - scroll_y },
1009                 LayoutConstraints::new(cw, cw, content, content),
1010                 &mut self.ui_context,
1011             );
1012         }
1013     }
1014 
1015     /// An exhibit's content width where it is narrower than the width it is laid out at
1016     /// (`Exhibit::content_w`; the named StatusDot slot likewise).
1017     fn content_width(&self, idx: usize) -> Option<f32> {
1018         if idx == 23 {
1019             return Some(StatusDot::SIZE);
1020         }
1021         idx.checked_sub(GALLERY_COUNT).and_then(|k| self.roster.gallery().extra[k].content_w)
1022     }
1023 
1024     /// Register every roster widget in the ui_context (idempotent — `register` is
1025     /// id-keyed and the boxed slots keep pointers stable). The id-rooted router
1026     /// resolves roots through this registry; the gallery's own paint loop never goes
1027     /// through `render_widget`, where other apps pick registration up as a side effect.
1028     fn register_roster(&mut self) {
1029         for i in 0..self.roster.len() {
1030             let w = self.roster.get_dyn_mut(i);
1031             let (id, ptr) = (w.base().id(), w as *mut (dyn WidgetHost + 'static));
1032             self.ui_context.register_widget(id, ptr);
1033         }
1034     }
1035 
1036     /// Drain the header dropdowns' selections and apply them: Layout (15) picks the
1037     /// strategy, Style (34) switches every control between Relief and Flat
1038     /// (`set_control_relief` — the toolkit reads it live at paint, so a rebuild is
1039     /// all it takes). Called after mouse AND key input: a Dropdown selects from the
1040     /// keyboard too (Down, Enter), and a selection must not wait for the next click.
1041     fn apply_header_dropdowns(&mut self) -> bool {
1042         let mut applied = false;
1043         if self.roster.take_click(15) {
1044             self.layout_idx = self.roster.value(15) as usize;
1045             applied = true;
1046         }
1047         if self.roster.take_click(34) {
1048             cce_ui::layout::set_control_relief(self.roster.value(34) == 0);
1049             applied = true;
1050         }
1051         if applied {
1052             self.relayout();
1053             self.apply_layout();
1054         }
1055         applied
1056     }
1057 
1058     fn apply_layout(&mut self) {
1059         for i in 0..self.roster.len() {
1060             if self.roster.is_dragging(i) {
1061                 continue;
1062             }
1063             // The exhibits are laid out by layout_exhibits below.
1064             if !self.is_child && is_exhibit(i) {
1065                 continue;
1066             }
1067             let visible = self.is_widget_visible(i);
1068             let (x, y, w, h) = self.positions[i];
1069             let widget = self.roster.get_dyn_mut(i);
1070             if visible && (i == 15 || i == 34) {
1071                 // The header dropdowns land through `layout` at their own preferred
1072                 // height: `y` is where the label goes, the content sits a strip below.
1073                 let h = widget.preferred_height().unwrap_or(h);
1074                 let strip = widget.label_strip();
1075                 widget.layout(Point { x, y: y + strip }, LayoutConstraints::new(w, w, h, h), &mut self.ui_context);
1076                 continue;
1077             }
1078             if visible {
1079                 widget.set_rect(x, y, w, h);
1080             } else {
1081                 widget.set_rect(-1000.0, -1000.0, 0.0, 0.0);
1082             }
1083         }
1084         if !self.is_child {
1085             self.layout_exhibits();
1086         }
1087     }
1088 
1089     fn update_status_text(&mut self, text: &str) {
1090         self.status_text = text.to_string();
1091     }
1092 }
1093 
1094 impl cce_ui::engine::Application for State {
1095     type Message = String;
1096 
1097     fn new(_qh: &QueueHandle<cce_ui::engine::EngineState<Self>>, _sender: calloop::channel::Sender<Self::Message>) -> Self {
1098         let args: Vec<String> = std::env::args().collect();
1099         let flag = |name: &str| args.iter().any(|a| a == name);
1100         let value = |name: &str| args.iter().position(|a| a == name).and_then(|i| args.get(i + 1)).cloned();
1101         let number = |name: &str| value(name).and_then(|s| s.parse::<f32>().ok());
1102 
1103         let is_child = flag("--child");
1104         let use_root_plate = if is_child { flag("--root-plate") } else { !flag("--no-root-plate") };
1105         let use_menubar = flag("--menubar");
1106         let use_statusbar = flag("--statusbar");
1107         let child_kind = if is_child {
1108             Some(value("--type").and_then(|t| ChildKind::from_arg(&t)).unwrap_or(ChildKind::Floating))
1109         } else {
1110             None
1111         };
1112         let opacity = flag("--opacity");
1113         let transparency = number("--transparency").unwrap_or(1.0);
1114         let border_enabled = !flag("--no-border");
1115         let custom_width = number("--width");
1116         let custom_height = number("--height");
1117 
1118         let (c_w, c_h): (f32, f32) = match (child_kind, custom_width, custom_height) {
1119             (_, Some(w), Some(h)) => (w, h),
1120             (Some(kind), _, _) => kind.default_size(),
1121             (None, _, _) => (1000.0, 680.0),
1122         };
1123 
1124         let opacity = opacity || child_kind.is_some_and(ChildKind::is_editor);
1125         let status_text = "Ready.".to_string();
1126 
1127         let roster = if let Some(kind) = child_kind {
1128             let bg = if use_root_plate {
1129                 ChildBg::RootPlate(RootPlate::new(0.0, 0.0, c_w, c_h))
1130             } else {
1131                 ChildBg::ContentBg(ContentBg::new())
1132             };
1133             let (main, aux3, aux4) = if kind == ChildKind::ColorRamp {
1134                 (
1135                     ChildMain::ColorRamp(ColorRamp::new()),
1136                     ChildAux3::Label(Label::new("").with_font_size(12.0)),
1137                     ChildAux4::Label(Label::new("").with_font_size(12.0)),
1138                 )
1139             } else if kind == ChildKind::Ramp {
1140                 (
1141                     ChildMain::Ramp({
1142                         let mut ramp = Ramp::new();
1143                         let (loaded_keys, loaded_type) = load_bevel_ramp();
1144                         ramp.keys = loaded_keys;
1145                         ramp.line_type_dropdown.selected = match loaded_type.as_str() {
1146                             "bezier" => 1,
1147                             _ => 0,
1148                         };
1149                         ramp
1150                     }),
1151                     ChildAux3::Label(Label::new("").with_font_size(12.0)),
1152                     ChildAux4::Label(Label::new("").with_font_size(12.0)),
1153                 )
1154             } else {
1155                 let menu_bar = MenuBar::new(0.0, 0.0, c_w, 40.0)
1156                     .with_item("File", &["New", "Open", "Save", "Exit"])
1157                     .with_item("Edit", &["Undo", "Redo", "Cut", "Copy", "Paste"])
1158                     .with_right_aligned_title(true);
1159                 (
1160                     ChildMain::Desc(Label::new(kind.child_text()).with_font_size(12.0).with_color([0xcc, 0xcc, 0xd4])),
1161                     ChildAux3::MenuBar(menu_bar),
1162                     ChildAux4::StatusBar(StatusBar::new()),
1163                 )
1164             };
1165             Roster::Child(Box::new(ChildSlots {
1166                 bg,
1167                 main,
1168                 close: Button::new(0.0, 0.0, 100.0, 35.0).with_label("Close"),
1169                 aux3,
1170                 aux4,
1171             }))
1172         } else {
1173             let menu_bar = MenuBar::new(0.0, 0.0, c_w, 40.0)
1174                 .with_item("File", &["Exit"])
1175                 .with_item("Edit", &["Settings"])
1176                 .with_item("Help", &["About"])
1177                 .with_right_aligned_title(true);
1178             Roster::Gallery(Box::new(GallerySlots {
1179                 menu_bar,
1180                 status_bar: StatusBar::new(),
1181                 button_demo: Button::new(0.0, 0.0, 140.0, 40.0).with_label("Button"),
1182                 checkbox_demo: Checkbox::new().with_label("Checkbox"),
1183                 toggle_demo: Toggle::new().with_label("Toggle"),
1184                 progress_demo: ProgressBar::new(0.43).with_label("ProgressBar"),
1185                 slider_demo: Slider::new().with_label("Slider"),
1186                 spinbox_demo: Spinbox::new(10, 1, 100, 5).with_label("Spinbox"),
1187                 range_slider_demo: RangeSlider::new().with_label("RangeSlider"),
1188                 trackpad_demo: Trackpad::new().with_label("Trackpad"),
1189                 textbox_demo: TextBox::new("Interactive TextBox".to_string()),
1190                 plate_demo: Plate::new(0.0, 0.0, 120.0, 120.0, true).with_label("Plate"),
1191                 color_ramp_btn: Button::new(0.0, 0.0, 120.0, 28.0).with_label("Color Ramp..."),
1192                 bevel_ramp: Ramp::new(),
1193                 ramp_btn: Button::new(0.0, 0.0, 120.0, 28.0).with_label("Ramp..."),
1194                 layout_dd: Dropdown::new(
1195                     LAYOUTS.iter().map(|(name, _)| name.to_string()).collect(),
1196                     DEFAULT_LAYOUT,
1197                 ).with_label("Layout"),
1198                 style_dd: Dropdown::new(
1199                     vec!["Relief".to_string(), "Flat".to_string()],
1200                     if cce_ui::layout::control_relief() { 0 } else { 1 },
1201                 ).with_label("Style"),
1202                 color_selector_demo: ColorSelector::new([64, 128, 255]).with_label("ColorSelector"),
1203                 font_selector_demo: FontSelector::new("Sans".to_string()).with_label("FontSelector"),
1204                 keybind_demo: KeybindRecorder::new("ctrl+1".to_string()).with_label("KeybindRecorder"),
1205                 button_strip_demo: Adapted::new(
1206                     ButtonStrip::new(0.0, 0.0, 200.0, 28.0)
1207                         .with_buttons(vec!["One".to_string(), "Two".to_string(), "Three".to_string()])
1208                         .with_selected(Some(0)),
1209                 ).with_label("ButtonStrip"),
1210                 slider2d_demo: Slider2D::new().with_label("Slider2D"),
1211                 float3_demo: Float3::new().with_label("Float3"),
1212                 usage_bar_demo: UsageBar::new(0.62).with_label("UsageBar"),
1213                 status_dot_demo: StatusDot::new(DotStatus::Active).with_label("StatusDot"),
1214                 info_box_demo: InfoBox::new("InfoBox", vec!["A titled box of".to_string(), "plain text lines.".to_string()]),
1215                 list_item_demo: InteractiveListItem::new("InteractiveListItem"),
1216                 breadcrumb_demo: {
1217                     let mut b = Breadcrumb::new();
1218                     b.path = vec!["home".to_string(), "lsgalante".to_string(), "projects".to_string()];
1219                     b.with_label("Breadcrumb")
1220                 },
1221                 tree_list_demo: {
1222                     let mut t = TreeList::new();
1223                     t.set_flat_keys(vec![
1224                         ("layout/bar_height".to_string(), serde_json::json!(24)),
1225                         ("layout/gap".to_string(), serde_json::json!(12)),
1226                         ("theme/name".to_string(), serde_json::json!("cce")),
1227                     ]);
1228                     t.rebuild_tree();
1229                     t.with_label("TreeList")
1230                 },
1231                 bevel_preview_demo: BevelPreview::new().with_label("BevelPreview"),
1232                 ramp_preview_demo: RampPreview::new().with_label("RampPreview"),
1233                 separator_demo: Separator::new(0.0, 0.0, 200.0, 1.0, [0.5, 0.5, 0.6, 1.0]).with_label("Separator"),
1234                 splitter_demo: Splitter::new(200.0).with_label("Splitter"),
1235                 // Members are wired below, once the slots have ids.
1236                 // style: deliberate — tight padding: the gallery packs its rows closer than a
1237                 // settings page, and the fitted lasso's padding is what insets the exhibits.
1238                 group_loose: Group::new(Vec::new()).with_label("Group").with_padding(6.0),
1239                 group_fitted: Group::new(Vec::new()).with_label("Group (fitted)").with_fit(true).with_padding(6.0),
1240                 extra: variant_exhibits(),
1241             }))
1242         };
1243 
1244         let roster = {
1245             let mut roster = roster;
1246             if let Roster::Gallery(s) = &mut roster {
1247                 // The lassos: a loose one around the FontSelector and the StatusDot, and
1248                 // one around the top row (Button, ButtonStrip, Checkbox, Toggle) that
1249                 // fits the exhibit area's edges — its top snaps to the area's top with
1250                 // the title tab kept inside, its left to the area's left edge.
1251                 let ids = |s: &GallerySlots, idx: &[usize]| idx.iter().map(|&i| s.get_dyn(i).base().id()).collect::<Vec<_>>();
1252                 let loose = ids(s, &[17, 23]);
1253                 let fitted = ids(s, &[2, 19, 3, 4]);
1254                 s.group_loose.inner_mut().set_members(loose);
1255                 s.group_fitted.inner_mut().set_members(fitted);
1256             }
1257             roster
1258         };
1259         let mut state = Self {
1260             roster,
1261             positions: Vec::new(),
1262             status_text,
1263             focused_widget: None,
1264             width: c_w,
1265             height: c_h,
1266             scale: 1.0,
1267             is_child,
1268             opacity,
1269             transparency,
1270             use_root_plate,
1271             use_menubar,
1272             use_statusbar,
1273             border_enabled,
1274             child_kind,
1275             ui_context: cce_ui::context::UiContext::new(),
1276             layout_idx: DEFAULT_LAYOUT,
1277             exhibit_scroll: {
1278                 let mut sb = ScrollBox::new();
1279                 sb.show_border = false;
1280                 sb.show_background = false;
1281                 sb
1282             },
1283         };
1284 
1285         if let Some(ramp) = state.roster.get_dyn_mut(1).as_any_mut().downcast_mut::<Ramp>().filter(|_| is_child) {
1286             // Only a `--type Ramp` child hosts a Ramp in slot 1: it opens with the
1287             // preset dropdown focused so the keyboard drives it at once. Every other
1288             // child kind (ColorRamp, or the description Label of the Toplevel /
1289             // Popup / Layer* windows) starts with nothing focused — the key sweep
1290             // in handle_key reaches every visible child slot anyway, and a click
1291             // focuses whatever it lands on. This used to downcast unconditionally
1292             // and panic for those kinds ("child ramp widget"), which is why Create
1293             // Window on the Windows page spawned children that died at startup.
1294             let preset_ptr = ramp.preset_dropdown.as_ptr_mut();
1295             state.focused_widget = Some(1);
1296             state.ui_context.set_focused_ptr(preset_ptr);
1297             unsafe {
1298                 (*preset_ptr).focus();
1299             }
1300         }
1301 
1302         state.relayout();
1303         state.apply_layout();
1304         state
1305     }
1306 
1307     /// The gallery navigates in plate terms: Tab walks the exhibits.
1308     fn plate_navigation(&self) -> bool {
1309         true
1310     }
1311 
1312     fn settings(&self) -> cce_ui::engine::WindowSettings {
1313         let title = match self.child_kind {
1314             Some(kind) => kind.title().to_string(),
1315             None => "Gallery".to_string(),
1316         };
1317 
1318         let mut app_id = match self.child_kind {
1319             Some(kind) => kind.app_id(),
1320             None => "cce-gallery".to_string(),
1321         };
1322         if !self.border_enabled {
1323             app_id.push_str("-noborder");
1324         }
1325 
1326         cce_ui::engine::WindowSettings {
1327             title,
1328             app_id,
1329             width: self.width as u32,
1330             height: self.height as u32,
1331             fullscreen: self.child_kind == Some(ChildKind::Fullscreen),
1332             min_size: if self.is_child {
1333                 Some((self.width as u32, self.height as u32))
1334             } else {
1335                 Some((100, 100))
1336             },
1337         }
1338     }
1339 
1340     fn layer(&self) -> Option<LayerSettings> {
1341         self.child_kind.and_then(|kind| kind.layer(self.height as i32))
1342     }
1343 
1344     fn utility(&self) -> bool {
1345         self.child_kind == Some(ChildKind::Utility)
1346     }
1347 
1348     fn update(&mut self, msg: Self::Message, needs_rebuild: &mut bool, exit: &mut bool) {
1349         if msg == "exit" {
1350             *exit = true;
1351         } else {
1352             self.update_status_text(&msg);
1353             *needs_rebuild = true;
1354         }
1355     }
1356 
1357     fn tick(&mut self, dt: f32, needs_rebuild: &mut bool) {
1358         let mut changed = false;
1359         if hover_animation::tick(dt) {
1360             changed = true;
1361         }
1362         if !self.is_child && self.exhibit_scroll.tick(dt, &mut self.ui_context) {
1363             changed = true;
1364         }
1365         let vis = self.visibility();
1366         let is_visible = move |index: usize| vis.is_visible(index);
1367         for i in 0..self.roster.len() {
1368             let w = self.roster.get_dyn_mut(i);
1369             if is_visible(i) {
1370                 if w.tick(dt, &mut self.ui_context) {
1371                     changed = true;
1372                     if self.child_kind == Some(ChildKind::Ramp) && i == 1 {
1373                         if let Some(ramp) = w.as_any().downcast_ref::<Ramp>() {
1374                             let line_type_str = match ramp.line_type_dropdown.selected {
1375                                 1 => "bezier",
1376                                 _ => "linear",
1377                             };
1378                             save_bevel_ramp(&ramp.keys, line_type_str);
1379                         }
1380                     }
1381                 }
1382             }
1383         }
1384         if changed {
1385             *needs_rebuild = true;
1386         }
1387     }
1388 
1389     fn display_list(&mut self, size: LogicalSize, scale: f64) -> Option<cce_ui::scene::paint::DisplayList> {
1390         // The whole frame — rounded geometry, plain geometry, popovers, then text — is
1391         // one display list, rebuilt each frame.
1392         use cce_ui::scene::layout::Rect;
1393         self.register_roster();
1394         // Panel children follow the scroll offset at layout time: re-arrange every frame
1395         // so a wheel scroll moves the content on the frame it repaints. Idempotent and
1396         // cheap (~20 set_rects).
1397         if !self.is_child {
1398             self.layout_exhibits();
1399         }
1400         if (self.width - size.width as f32).abs() > 0.001 || (self.height - size.height as f32).abs() > 0.001 || (self.scale - scale).abs() > 0.001 {
1401             self.width = size.width as f32;
1402             self.height = size.height as f32;
1403             self.scale = scale;
1404             cce_ui::scale::set_scale_factor(scale as f32);
1405 
1406             self.relayout();
1407             self.apply_layout();
1408             let text = self.status_text.clone();
1409             self.update_status_text(&text);
1410         }
1411 
1412         let sw = self.width;
1413         let sh = self.height;
1414         let mut pc = cce_ui::scene::paint::PaintCtx::new();
1415 
1416         // ── Rounded geometry ──
1417         if !self.is_child && self.use_root_plate {
1418             // The standard root plate (cce-ui PlateSpec::window).
1419             pc.root_plate(sw, sh);
1420         }
1421         // The exhibit area is a well in the root plate: its floor under the
1422         // exhibits here, its rim over them below (`well_rim`), so an exhibit
1423         // scrolled to the edge slides under the wall rather than sitting on it.
1424         if !self.is_child {
1425             let (well, radius, _) = self.exhibit_well();
1426             pc.well_floor(well, radius, &cce_ui::scene::Material::pane(), false);
1427         }
1428 
1429         let push_rounded = |pc: &mut cce_ui::scene::paint::PaintCtx, qx: f32, qy: f32, qw: f32, qh: f32, qr: f32, qc: [f32; 4], qcorners: (bool, bool, bool, bool)| {
1430             let rect = Rect { x: qx, y: qy, width: qw, height: qh };
1431             if qr > 0.1 {
1432                 pc.rounded_rect(rect, qr, qcorners, qc);
1433             } else {
1434                 pc.quad(rect, qc);
1435             }
1436         };
1437 
1438         for i in 0..self.roster.len() {
1439             let w = self.roster.get_dyn(i);
1440             if !self.is_widget_visible(i) {
1441                 continue;
1442             }
1443             {
1444                 let clip = if !self.is_child && (is_exhibit(i) || is_overlay(i)) { Some(self.exhibit_viewport()) } else { None };
1445                 if let Some((vx, vy, vw, vh)) = clip {
1446                     pc.push_clip(Rect { x: vx, y: vy, width: vw, height: vh });
1447                 }
1448                 for (qx, qy, qw, qh, qr, qc, qcorners) in w.all_rounded_quads(&self.ui_context) {
1449                     push_rounded(&mut pc, qx, qy, qw, qh, qr, qc, qcorners);
1450                 }
1451                 if clip.is_some() {
1452                     pc.pop_clip();
1453                 }
1454             }
1455         }
1456 
1457         // ── Plain geometry ──
1458         for i in 0..self.roster.len() {
1459             let w = self.roster.get_dyn(i);
1460             if !self.is_widget_visible(i) {
1461                 continue;
1462             }
1463             {
1464                 let clip = if !self.is_child && (is_exhibit(i) || is_overlay(i)) { Some(self.exhibit_viewport()) } else { None };
1465                 if let Some((vx, vy, vw, vh)) = clip {
1466                     pc.push_clip(Rect { x: vx, y: vy, width: vw, height: vh });
1467                 }
1468                 for (qx, qy, qw, qh, qc) in w.all_quads(&self.ui_context) {
1469                     pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
1470                 }
1471                 // The quad bridges above carry only quads. A widget that paints
1472                 // borders, circles, arcs or vectors (the round Checkbox's ring and
1473                 // dot, a slider's knob, a button's border) would lose them, so
1474                 // replay every other own prim; text stays with the text pass.
1475                 replay_non_quad_prims(w, &self.ui_context, &mut pc);
1476                 if clip.is_some() {
1477                     pc.pop_clip();
1478                 }
1479             }
1480         }
1481         // The well's rim over the exhibits, then the area's scrollbar over the
1482         // rim: the toolkit's relief scrollbar (a groove track, a raised thumb —
1483         // the TreeList's), the flat quads only when relief is off.
1484         if !self.is_child {
1485             let (well, radius, _) = self.exhibit_well();
1486             pc.well_rim(well, radius, cce_ui::layout::control_relief());
1487             if cce_ui::layout::control_relief() {
1488                 self.exhibit_scroll.paint_scrollbar_relief(&mut pc);
1489             } else {
1490                 for (sx, sy, sw, sh, sc) in self.exhibit_scroll.extra_quads() {
1491                     pc.quad(Rect { x: sx, y: sy, width: sw, height: sh }, sc);
1492                 }
1493             }
1494         }
1495 
1496         // ── Popovers: in-frame, on top of everything. PaintCtx is a
1497         // RenderTarget — real prims (the dropdown's expanded inset-plate
1498         // surface) with per-label bounds; glyphs render in the later text pass
1499         // regardless of emission order. ──
1500         for i in 0..self.roster.len() {
1501             let w = self.roster.get_dyn(i);
1502             if !self.is_widget_visible(i) {
1503                 continue;
1504             }
1505             if w.popover_rect().is_some() {
1506                 w.render_popover(&mut pc);
1507             }
1508         }
1509 
1510         // ── Text ──
1511         let label_text = match self.child_kind {
1512             Some(kind) => kind.title().to_string(),
1513             None => "Gallery".to_string(),
1514         };
1515         let mut has_menu_bar = false;
1516         for i in 0..self.roster.len() {
1517             let w = self.roster.get_dyn_mut(i);
1518             if let Some(menu_bar) = w.as_any_mut().downcast_mut::<MenuBar>() {
1519                 menu_bar.title = label_text.clone();
1520                 has_menu_bar = true;
1521             }
1522         }
1523         if !has_menu_bar {
1524             // The title stands on the root plate: one inset in from the window's corner.
1525             let inset = cce_ui::layout::root_plate_inset();
1526             pc.text_with(label_text, inset, inset, CHILD_TITLE_SIZE, [255, 255, 255], Some(cce_ui::layout::statusbar_font()), None);
1527         }
1528 
1529         let mut popover_rects = Vec::new();
1530         for i in 0..self.roster.len() {
1531             let w = self.roster.get_dyn(i);
1532             if !self.is_widget_visible(i) {
1533                 continue;
1534             }
1535             if let Some(rect) = w.popover_rect() {
1536                 popover_rects.push(rect);
1537             }
1538         }
1539         let in_any_popover = |lx: f32, ly: f32| -> bool {
1540             for &(px, py, pw, ph) in &popover_rects {
1541                 if lx >= px - 5.0 && lx <= px + pw + 5.0 && ly >= py - 5.0 && ly <= py + ph + 5.0 {
1542                     return true;
1543                 }
1544             }
1545             false
1546         };
1547 
1548         for i in 0..self.roster.len() {
1549             let w = self.roster.get_dyn(i);
1550             if !self.is_widget_visible(i) {
1551                 continue;
1552             }
1553             // A widget with a ui-tree parent (the page selector under the status bar) is
1554             // covered by that parent's walk — emitting it here too would draw its text
1555             // twice. Text inside a popover is culled; panel children clip to the panel.
1556             if self.ui_context.tree.parent_ptr(w.base().id()).is_some() {
1557                 continue;
1558             }
1559             // Text is cut at the foot of the wall: geometry slides under the rim,
1560             // words do not sit on it (the TreeList's rule).
1561             let cp_clip = if !self.is_child && (is_exhibit(i) || is_overlay(i)) {
1562                 let (f, _) = self.exhibit_floor();
1563                 let (vx, vy, vw, vh) = (f.x, f.y, f.width, f.height);
1564                 Some([vx, vy, vx + vw, vy + vh])
1565             } else {
1566                 None
1567             };
1568             let mut scratch = cce_ui::scene::paint::PaintCtx::new();
1569             cce_ui::scene::painter::append_widget_text(&self.ui_context, w, &mut scratch);
1570             for item in scratch.finish().items {
1571                 if let cce_ui::scene::paint::Prim::Text { text, x, y, font_size, color, font, bounds, .. } = item.prim {
1572                     if in_any_popover(x, y) {
1573                         continue;
1574                     }
1575                     let bounds = match (bounds, cp_clip) {
1576                         (Some([l, t, r, b]), Some([pl, pt, pr, pb])) => {
1577                             Some([l.max(pl), t.max(pt), r.min(pr), b.min(pb)])
1578                         }
1579                         (None, Some(clip)) => Some(clip),
1580                         (b, None) => b,
1581                     };
1582                     pc.text_with(text, x, y, font_size, color, font, bounds);
1583                 }
1584             }
1585         }
1586 
1587 
1588         // The status line.
1589         if !self.is_child {
1590             let (_, status_font_size) = cce_ui::layout::statusbar_font_parsed();
1591             let status_size = if status_font_size > 0.0 { status_font_size } else { 12.0 };
1592             let scol = cce_ui::color::root_plate_statusbar_text_color();
1593             // style: deliberate — 12px in from the bar's edge is the toolkit StatusBar's
1594             // own default text offset, so the line sits where a StatusBar's text would.
1595             pc.text_with(
1596                 self.status_text.clone(),
1597                 12.0,
1598                 self.height - 24.0,
1599                 status_size,
1600                 [
1601                     (scol[0] * 255.0) as u8,
1602                     (scol[1] * 255.0) as u8,
1603                     (scol[2] * 255.0) as u8,
1604                 ],
1605                 Some(cce_ui::layout::statusbar_font()),
1606                 None,
1607             );
1608         }
1609 
1610         Some(pc.finish())
1611     }
1612 
1613     fn display_list_text(&self) -> bool {
1614         true
1615     }
1616 
1617     fn clear_color(&self) -> [f32; 4] {
1618         let clear_alpha = if self.opacity || self.use_root_plate {
1619             if self.use_root_plate {
1620                 0.0
1621             } else {
1622                 self.transparency
1623             }
1624         } else {
1625             1.0
1626         };
1627         [0.05 * clear_alpha, 0.05 * clear_alpha, 0.08 * clear_alpha, clear_alpha]
1628     }
1629 
1630     fn desired_size(&self) -> Option<(u32, u32)> {
1631         Some((self.width as u32, self.height as u32))
1632     }
1633 
1634     fn ui_context(&self) -> Option<&cce_ui::context::UiContext> {
1635         Some(&self.ui_context)
1636     }
1637 
1638     fn ui_context_mut(&mut self) -> Option<&mut cce_ui::context::UiContext> {
1639         Some(&mut self.ui_context)
1640     }
1641 
1642     fn handle_pointer_move(&mut self, pos: LogicalPosition, needs_rebuild: &mut bool) {
1643         let (lx, ly) = (pos.x as f32, pos.y as f32);
1644 
1645         let mut changed = false;
1646         if !self.is_child && self.exhibit_scroll.cursor_moved(lx, ly, &mut self.ui_context)
1647         {
1648             changed = true;
1649         }
1650         // The router owns the drag lifecycle — one
1651         // PointerMove per visible root forwards DragUpdate to a live drag target and
1652         // runs hover bookkeeping otherwise.
1653         let mv = cce_ui::widget::Event::PointerMove { x: lx, y: ly, local_x: lx, local_y: ly };
1654         {
1655             let vis = self.visibility();
1656             let is_visible = move |index: usize| vis.is_visible(index);
1657             for i in 0..self.roster.len() {
1658                 if !is_visible(i) {
1659                     continue;
1660                 }
1661                 let root = self.roster.get_dyn(i).base().id();
1662                 if self.ui_context.propagate_event(&mv, root) {
1663                     changed = true;
1664                 }
1665             }
1666             if self.ui_context.is_dragging {
1667                 changed = true;
1668             }
1669         }
1670         if changed {
1671             *needs_rebuild = true;
1672         }
1673     }
1674 
1675     fn handle_mouse_input(&mut self, button: MouseButton, state: ElementState, pos: LogicalPosition, needs_rebuild: &mut bool) -> Option<Self::Message> {
1676         let (lx, ly) = (pos.x as f32, pos.y as f32);
1677 
1678         let mut changed = false;
1679         let vis = self.visibility();
1680         let is_visible = move |index: usize| vis.is_visible(index);
1681 
1682         if state == ElementState::Pressed {
1683             let mut clicked_idx = None;
1684             // The exhibit area's scrollbar takes a press on its track before any exhibit.
1685             let exhibit_bar = !self.is_child && self.exhibit_scroll.mouse_input(button, state, lx, ly, &mut self.ui_context);
1686             if exhibit_bar {
1687                 changed = true;
1688             }
1689             if clicked_idx.is_none() && !exhibit_bar {
1690                 for i in (0..self.roster.len()).rev() {
1691                     if !is_visible(i) {
1692                         continue;
1693                     }
1694                     // An exhibit scrolled out of the viewport is not there to click.
1695                     if !self.is_child && is_exhibit(i) && !self.in_exhibit_viewport(lx, ly) {
1696                         continue;
1697                     }
1698                     if self.roster.get_dyn_mut(i).hit_test(lx, ly, &self.ui_context) {
1699                         clicked_idx = Some(i);
1700                         break;
1701                     }
1702                 }
1703             }
1704             if button == MouseButton::Left {
1705                 if let Some(old) = self.focused_widget {
1706                     if Some(old) != clicked_idx {
1707                         self.roster.get_dyn_mut(old).unfocus();
1708                         self.focused_widget = None;
1709                     }
1710                 }
1711             }
1712             if let Some(i) = clicked_idx {
1713                 // The router records the drag target on a handled press and synthesizes
1714                 // DragStart past its threshold; a draggable slot whose press handler
1715                 // returned false is armed explicitly.
1716                 let ev = cce_ui::widget::Event::MouseButton { button, state, x: lx, y: ly, local_x: lx, local_y: ly };
1717                 let root = self.roster.get_dyn(i).base().id();
1718                 let press_handled = self.ui_context.propagate_event(&ev, root);
1719                 if press_handled {
1720                     changed = true;
1721                 }
1722                 if button == MouseButton::Left && !press_handled && self.roster.draggable(i) {
1723                     let id = self.roster.get_dyn(i).base().id();
1724                     self.ui_context.drag_target = Some(id);
1725                 }
1726                 if button == MouseButton::Left {
1727                     self.roster.get_dyn_mut(i).focus();
1728                     self.focused_widget = Some(i);
1729                 }
1730             }
1731         } else {
1732             // The router delivers DragEnd to the drag target on the first propagate call
1733             // of a release; every visible root then sees the release (commit contract).
1734             if self.ui_context.is_dragging {
1735                 changed = true;
1736             }
1737             let ev = cce_ui::widget::Event::MouseButton { button, state, x: lx, y: ly, local_x: lx, local_y: ly };
1738             if !self.is_child {
1739                 self.exhibit_scroll.mouse_input(button, state, lx, ly, &mut self.ui_context);
1740             }
1741             for i in 0..self.roster.len() {
1742                 if !is_visible(i) {
1743                     continue;
1744                 }
1745                 let root = self.roster.get_dyn(i).base().id();
1746                 if self.ui_context.propagate_event(&ev, root) {
1747                     changed = true;
1748                 }
1749             }
1750 
1751             if button == MouseButton::Left {
1752                 if self.is_child {
1753                     if self.roster.take_click(2) {
1754                         return Some("exit".to_string());
1755                     }
1756                 } else {
1757                     if self.apply_header_dropdowns() {
1758                         changed = true;
1759                     } else if self.roster.take_click(12) {
1760                         spawn_editor("ColorRamp");
1761                     } else if self.roster.take_click(14) {
1762                         spawn_editor("Ramp");
1763                     } else {
1764                         for i in [2, 3, 4, 6, 7, 8, 9, 10, 11, 16, 17, 18, 19, 20, 21, 25, 26, 27, 28, 29] {
1765                             if self.roster.take_click(i) {
1766                                 changed = true;
1767                             }
1768                         }
1769                     }
1770                 }
1771             }
1772         }
1773         if changed {
1774             *needs_rebuild = true;
1775         }
1776         None
1777     }
1778 
1779     fn handle_mouse_wheel(&mut self, delta: &MouseScrollDelta, pos: LogicalPosition, needs_rebuild: &mut bool) {
1780         let (lx, ly) = (pos.x as f32, pos.y as f32);
1781         let mut changed = false;
1782         let vis = self.visibility();
1783         let is_visible = move |index: usize| vis.is_visible(index);
1784         let ev = cce_ui::widget::Event::MouseWheel { delta: *delta, x: lx, y: ly, local_x: lx, local_y: ly };
1785         // The control under the pointer gets the wheel first (wheel events are
1786         // hit-gated in the toolkit, so only it can take one): a slider, spinbox
1787         // or scrolling list adjusts itself and the page stays put. Only an
1788         // unclaimed wheel scrolls the exhibit area.
1789         let in_exhibits = !self.is_child && self.in_exhibit_viewport(lx, ly);
1790         let mut widget_took_wheel = false;
1791         for i in 0..self.roster.len() {
1792             if !is_visible(i) {
1793                 continue;
1794             }
1795             if !self.is_child && is_exhibit(i) && !in_exhibits {
1796                 continue;
1797             }
1798             let root = self.roster.get_dyn(i).base().id();
1799             if self.ui_context.propagate_event(&ev, root) {
1800                 widget_took_wheel = true;
1801                 changed = true;
1802             }
1803         }
1804         if !widget_took_wheel
1805             && !self.is_child
1806             && self.exhibit_scroll.mouse_wheel(delta, lx, ly, &mut self.ui_context)
1807         {
1808             changed = true;
1809         }
1810         if changed {
1811             *needs_rebuild = true;
1812         }
1813     }
1814 
1815     fn handle_key_input(&mut self, event: &KeyEvent, needs_rebuild: &mut bool) -> Option<Self::Message> {
1816         let mut changed = false;
1817         let vis = self.visibility();
1818         let is_visible = move |index: usize| vis.is_visible(index);
1819         let mut handled = false;
1820         let key_ev = cce_ui::widget::Event::KeyInput(event.clone());
1821         if let Some(focused) = self.focused_widget {
1822             let root = self.roster.get_dyn(focused).base().id();
1823             if self.ui_context.propagate_event(&key_ev, root) {
1824                 changed = true;
1825                 handled = true;
1826             }
1827         }
1828 
1829         if !handled {
1830             for i in 0..self.roster.len() {
1831                 if Some(i) == self.focused_widget {
1832                     continue;
1833                 }
1834                 if !is_visible(i) {
1835                     continue;
1836                 }
1837                 // Panel children take keys only through the focused path above.
1838                 let root = self.roster.get_dyn(i).base().id();
1839                 if self.ui_context.propagate_event(&key_ev, root) {
1840                     changed = true;
1841                     // A consumed key is HANDLED, not just repaint-worthy: the
1842                     // Escape-quits-app fallback below is gated on !handled,
1843                     // and without this a dropdown that took Escape through
1844                     // this sweep closed its menu AND exited the app.
1845                     handled = true;
1846                     // And delivered ONCE: the context routes a key to the
1847                     // focused widget from ANY root, so without this break a
1848                     // Tab-focused slider took one Right press 58 times over —
1849                     // once per roster root.
1850                     break;
1851                 }
1852             }
1853         }
1854 
1855         if !handled && event.state == ElementState::Pressed && event.logical_key == Key::Named(NamedKey::Escape) {
1856             return Some("exit".to_string());
1857         }
1858 
1859         // A header dropdown driven by the keyboard selects on Enter: apply it now.
1860         if !self.is_child && self.apply_header_dropdowns() {
1861             changed = true;
1862         }
1863 
1864         if changed {
1865             *needs_rebuild = true;
1866         }
1867         None
1868     }
1869 
1870 }
1871 
1872 /// Re-emit a widget's own prims other than quads, rounded rects and text: the
1873 /// gallery's paint loop bridges quads through `all_quads`/`all_rounded_quads` and
1874 /// text through the text pass, and would otherwise drop a widget's borders,
1875 /// circles, arcs and vectors.
1876 fn replay_non_quad_prims(w: &(dyn WidgetHost + 'static), ui: &cce_ui::context::UiContext, pc: &mut cce_ui::scene::paint::PaintCtx) {
1877     let mut scratch = cce_ui::scene::paint::PaintCtx::new();
1878     w.paint_self(ui, &mut scratch);
1879     for item in scratch.finish().items {
1880         match item.prim {
1881             Prim::Quad { .. } | Prim::RoundedRect { .. } | Prim::Text { .. } => {}
1882             prim => {
1883                 let _ = pc.replay(prim);
1884             }
1885         }
1886     }
1887 }
1888 
1889 /// A `Command` that re-runs this binary as a child window; the caller adds the flags.
1890 fn child_command() -> Option<std::process::Command> {
1891     let exe = std::env::current_exe().ok()?;
1892     let mut cmd = std::process::Command::new(exe);
1893     cmd.arg("--child");
1894     Some(cmd)
1895 }
1896 
1897 /// Opens a Ramp or ColorRamp editor window. Untracked on purpose: an editor outlives
1898 /// the gallery, unlike the simulated windows Create Window spawns.
1899 fn spawn_editor(kind: &str) {
1900     if let Some(mut cmd) = child_command() {
1901         // The editor kinds' `default_size`, spelled out for the child's argv.
1902         cmd.args(["--type", kind, "--width", "450", "--height", "390", "--root-plate"]);
1903         let _ = cmd.spawn();
1904     }
1905 }
1906 
1907 /// The toolkit container layouts the Layout dropdown offers, by name.
1908 /// The strategies at the toolkit's own spacing (`layout::control_gap()`, every strategy's
1909 /// default gap), no padding — the exhibit viewport is already inset — so the page shows
1910 /// the rhythm a container gets by default.
1911 const LAYOUTS: [(&str, fn() -> Box<dyn cce_ui::layout::LayoutStrategy>); 7] = [
1912     ("Vertical", || Box::new(VerticalLayout::default())),
1913     ("Columns", || Box::new(ColumnsLayout { padding_x: 0.0, padding_y: 0.0, ..ColumnsLayout::default() })),
1914     ("Grid", || Box::new(GridLayout { columns: 3, gap: cce_ui::layout::control_gap(), padding_x: 0.0, padding_y: 0.0, grid: None })),
1915     ("Adaptive Grid", || Box::new(AdaptiveGridLayout { min_col_width: 190.0, gap: cce_ui::layout::control_gap(), padding_x: 0.0, padding_y: 0.0, grid: None })),
1916     ("Mosaic", || Box::new(MosaicLayout { padding_x: 0.0, padding_y: 0.0, ..MosaicLayout::default() })),
1917     ("Reverse Mosaic", || Box::new(ReverseMosaicLayout { padding_x: 0.0, padding_y: 0.0, ..ReverseMosaicLayout::default() })),
1918     ("Overlay", || Box::new(OverlayLayout::default())),
1919 ];
1920 const DEFAULT_LAYOUT: usize = 4;
1921 
1922 /// The fixed positions: the chrome and the Layout dropdown. The exhibits are laid out
1923 /// by `layout_exhibits` instead.
1924 fn demo_positions(sw: f32, sh: f32, count: usize) -> Vec<(f32, f32, f32, f32)> {
1925     let mut vec = vec![(0.0, 0.0, 0.0, 0.0); count];
1926     vec[0] = (0.0, 0.0, sw, 40.0); // MenuBar
1927     vec[1] = (0.0, sh - 24.0, sw, 24.0); // StatusBar
1928     // The header dropdowns; the exhibits below them are laid out by
1929     // `layout_exhibits` with the strategy Layout selects. Their heights are the
1930     // widgets' own preferred heights (`apply_layout`); the ones here are placeholders.
1931     // Both stand on the root plate: one inset in from the window's side, one root
1932     // gap below the menu bar's band.
1933     let inset = cce_ui::layout::root_plate_inset();
1934     let gap = cce_ui::layout::root_plate_gap();
1935     let header_y = 40.0 + gap;
1936     vec[15] = (inset, header_y, 190.0, 0.0);
1937     // The Style dropdown, one root gap to its right.
1938     vec[34] = (inset + 190.0 + gap, header_y, 190.0, 0.0);
1939     vec
1940 }
1941 
1942 /// A child window's title, painted by `display_list` when it has no MenuBar to
1943 /// carry it; the description below leaves this line of headroom.
1944 const CHILD_TITLE_SIZE: f32 = 16.0;
1945 
1946 /// The five `ChildSlots` rects for a child window of `sw`x`sh`: 0 background,
1947 /// 1 main (editor or description), 2 Close, 3 and 4 the MenuBar / StatusBar of a
1948 /// simulated window or the two aux Labels of a Ramp / ColorRamp editor.
1949 /// Everything stands on the child's root plate: one root inset in from the
1950 /// window's edges, one root gap between the main slot and Close.
1951 fn child_positions(sw: f32, sh: f32, use_menubar: bool, use_statusbar: bool, editor: bool) -> Vec<(f32, f32, f32, f32)> {
1952     let dy = if use_menubar { 40.0 } else { 0.0 };
1953     let dh = if use_statusbar { 24.0 } else { 0.0 };
1954     let inner_h = sh - dy - dh;
1955     let inset = cce_ui::layout::root_plate_inset();
1956     let gap = cce_ui::layout::root_plate_gap();
1957     let (close_w, close_h) = (100.0, 35.0);
1958     let mut vec = vec![(-1000.0, -1000.0, 0.0, 0.0); CHILD_COUNT];
1959 
1960     if use_menubar {
1961         vec[3] = (0.0, 0.0, sw, 40.0);
1962     }
1963     if use_statusbar {
1964         vec[4] = (0.0, sh - 24.0, sw, 24.0);
1965     }
1966 
1967     vec[0] = (0.0, dy, sw, inner_h);
1968     // Close is centred, one inset up from the bottom edge; the main slot fills the
1969     // room above it, one root gap off, under the title line's headroom (kept whether
1970     // the title is painted or a MenuBar carries it).
1971     let close_y = dy + inner_h - inset - close_h;
1972     vec[2] = ((sw - close_w) / 2.0, close_y, close_w, close_h);
1973     let main_y = dy + inset + CHILD_TITLE_SIZE + gap;
1974     let main_h = close_y - gap - main_y;
1975     vec[1] = (inset, main_y, sw - 2.0 * inset, main_h);
1976     if editor {
1977         // TODO(style): the two aux labels' 20px row lies over the editor's bottom
1978         // strip, as it always has; that overlap is the editor's layout, not a rung.
1979         let label_y = main_y + main_h - 20.0;
1980         vec[3] = (inset, label_y, 200.0, 20.0);
1981         vec[4] = (sw - inset - 200.0, label_y, 200.0, 20.0);
1982     }
1983 
1984     vec
1985 }
1986 
1987 fn main() {
1988     cce_ui::engine::run::<State>();
1989 }