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