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 }