1use operad::platform::{DragPayload, PixelSize, UiLayer};
2use operad::widgets::{CalendarDate, TextInputLayoutMetrics, TextInputOptions, TextInputState};
3use operad::{
4 root_style, widgets, AccessibilityMeta, AccessibilityRole, AlignedStroke, BuiltInIcon,
5 CanvasContent, CanvasRenderOutput, ClipBehavior, ColorRgba, CornerRadii, DragDropSurfaceKind,
6 DropPayloadFilter, DynamicLabelMeta, FocusRestoreTarget, FontFamily, FontWeight, FormState,
7 FormValidationResult, ImageContent, LayoutFlexWrap, LayoutStyle, LocaleId, LocalizationPolicy,
8 NativeWgpuCanvasRenderContext, NativeWgpuCanvasRenderRegistry, NativeWindowOptions,
9 NativeWindowResult, PaintEffect, PaintRect, PaintText, RenderError, ScenePrimitive, ScrollAxes,
10 StrokeStyle, TextHorizontalAlign, TextStyle, TextVerticalAlign, TextWrap, TooltipContent,
11 UiDocument, UiNode, UiNodeId, UiNodeStyle, UiPoint, UiRect, UiSize, UiVisual,
12 ValidationMessage, WgpuCanvasContext, WgpuCanvasRenderPass, WidgetAction, WidgetActionBinding,
13 WidgetActionKind, WidgetDrag, WidgetDragPhase, WidgetTextEdit,
14};
15
16const RIGHT_PANEL_WIDTH: f32 = 300.0;
17const SHOWCASE_WINDOW_Z_BASE: i16 = 64;
18const SHOWCASE_WINDOW_Z_STRIDE: i16 = 32;
19const SHOWCASE_WINDOW_Z_MAX: i16 = 960;
20const SHOWCASE_TICK_RATE_HZ: f32 = 60.0;
21const SHOWCASE_PROGRESS_RADIANS_PER_SECOND: f32 = 1.08;
22const TEXT_CARET_BLINK_HZ: f32 = 1.1;
23const CONTROLS_WIDGET_ROW_HEIGHT: f32 = 28.0;
24const CONTROLS_WIDGET_ROW_GAP: f32 = 4.0;
25const SHOWCASE_WIDGET_WINDOW_IDS: [&str; 28] = [
26 "labels",
27 "buttons",
28 "checkbox",
29 "toggles",
30 "slider",
31 "numeric",
32 "text_input",
33 "selection",
34 "menus",
35 "command_palette",
36 "date_picker",
37 "color_picker",
38 "color_buttons",
39 "progress",
40 "lists_tables",
41 "property_inspector",
42 "trees",
43 "layout_widgets",
44 "containers",
45 "forms",
46 "overlays",
47 "drag_drop",
48 "media",
49 "timeline",
50 "toasts",
51 "popup_panel",
52 "canvas",
53 "styling",
54];
55
56fn main() -> NativeWindowResult {
57 let mut canvas_renderers = NativeWgpuCanvasRenderRegistry::new();
58 canvas_renderers.register("canvas.shader", render_showcase_canvas);
59 operad::run_app_with_canvas_renderers(
60 NativeWindowOptions::new("showcase")
61 .with_size(900.0, 760.0)
62 .with_min_size(720.0, 560.0)
63 .with_tick_action("runtime.tick")
64 .with_tick_rate_hz(SHOWCASE_TICK_RATE_HZ),
65 ShowcaseState::default(),
66 ShowcaseState::update,
67 ShowcaseState::view,
68 canvas_renderers,
69 )
70}
71
72struct ShowcaseState {
73 checked: bool,
74 slider: f32,
75 slider_left: f32,
76 slider_right: f32,
77 slider_value_text: TextInputState,
78 slider_left_text: TextInputState,
79 slider_right_text: TextInputState,
80 slider_step_value: f32,
81 slider_step_text: TextInputState,
82 slider_trailing_color: bool,
83 slider_trailing_picker: widgets::ColorPickerState,
84 slider_trailing_picker_open: bool,
85 slider_thumb_shape: SliderThumbChoice,
86 slider_use_steps: bool,
87 slider_logarithmic: bool,
88 slider_clamping: widgets::SliderClamping,
89 slider_smart_aim: bool,
90 label_locale: widgets::SelectMenuState,
91 label_link_visited: bool,
92 label_hyperlink_visited: bool,
93 label_link_status: &'static str,
94 color: widgets::ColorPickerState,
95 date: widgets::DatePickerModel,
96 radio_choice: &'static str,
97 switch_enabled: bool,
98 mixed_switch: widgets::ToggleValue,
99 theme_preference: widgets::ThemePreference,
100 numeric_value: f32,
101 numeric_angle: f32,
102 numeric_tau: f32,
103 combo_open: bool,
104 combo_label: String,
105 dropdown: widgets::SelectMenuState,
106 select_menu: widgets::SelectMenuState,
107 text: TextInputState,
108 selectable_text: TextInputState,
109 singleline_text: TextInputState,
110 multiline_text: TextInputState,
111 text_area_text: TextInputState,
112 code_editor_text: TextInputState,
113 search_text: TextInputState,
114 password_text: TextInputState,
115 focused_text: Option<FocusedTextInput>,
116 clipboard_text: String,
117 system_clipboard: Option<arboard::Clipboard>,
118 last_button: &'static str,
119 toggle_button: bool,
120 table_selection: widgets::DataTableSelection,
121 tree: widgets::TreeViewState,
122 outliner: widgets::TreeViewState,
123 toast_visible: bool,
124 toast_action_status: &'static str,
125 popup_open: bool,
126 progress_phase: f32,
127 caret_phase: f32,
128 command_palette: widgets::CommandPaletteState,
129 last_command: String,
130 list_scroll: f32,
131 virtual_scroll: f32,
132 table_scroll: f32,
133 layout_preview_scroll: f32,
134 layout_left_scroll: f32,
135 layout_right_scroll: f32,
136 layout_inspector_scroll: f32,
137 layout_document_scroll: f32,
138 layout_assets_scroll: f32,
139 scrollbars: widgets::ScrollbarControllerState,
140 layout_tab: usize,
141 styling: StylingState,
142 cube: CanvasCubeState,
143 menu_bar: widgets::MenuBarState,
144 menu_button: widgets::MenuButtonState,
145 image_text_menu_button: widgets::MenuButtonState,
146 image_menu_button: widgets::MenuButtonState,
147 context_menu: widgets::ContextMenuState,
148 menu_autosave: bool,
149 menu_grid: bool,
150 form: FormState,
151 form_status: String,
152 overlay_expanded: bool,
153 overlay_popup_open: bool,
154 overlay_modal_open: bool,
155 color_button_status: &'static str,
156 drag_drop_status: &'static str,
157 layout_split: widgets::SplitPaneState,
158 containers_scroll: operad::ScrollState,
159 controls_scroll: f32,
160 color_copied_hex: Option<String>,
161 windows: ShowcaseWindows,
162 desktop: widgets::FloatingDesktopState,
163}
164
165#[derive(Clone, Copy)]
166struct StylingState {
167 inner_same: bool,
168 inner_margin: f32,
169 inner_right: f32,
170 inner_top: f32,
171 inner_bottom: f32,
172 outer_same: bool,
173 outer_margin: f32,
174 outer_right: f32,
175 outer_top: f32,
176 outer_bottom: f32,
177 radius_same: bool,
178 corner_radius: f32,
179 corner_ne: f32,
180 corner_sw: f32,
181 corner_se: f32,
182 shadow_x: f32,
183 shadow_y: f32,
184 shadow_blur: f32,
185 shadow_spread: f32,
186 shadow_alpha: f32,
187 stroke_width: f32,
188 stroke_tint: f32,
189 fill_tint: f32,
190 fill: ColorRgba,
191}
192
193impl Default for StylingState {
194 fn default() -> Self {
195 Self {
196 inner_same: true,
197 inner_margin: 12.0,
198 inner_right: 12.0,
199 inner_top: 12.0,
200 inner_bottom: 12.0,
201 outer_same: true,
202 outer_margin: 24.0,
203 outer_right: 24.0,
204 outer_top: 24.0,
205 outer_bottom: 24.0,
206 radius_same: true,
207 corner_radius: 12.0,
208 corner_ne: 12.0,
209 corner_sw: 12.0,
210 corner_se: 12.0,
211 shadow_x: 8.0,
212 shadow_y: 12.0,
213 shadow_blur: 16.0,
214 shadow_spread: 0.0,
215 shadow_alpha: 140.0,
216 stroke_width: 1.0,
217 stroke_tint: 0.68,
218 fill_tint: 0.54,
219 fill: ColorRgba::new(79, 45, 191, 255),
220 }
221 }
222}
223
224impl StylingState {
225 fn inner_edges(self) -> [f32; 4] {
226 if self.inner_same {
227 [self.inner_margin; 4]
228 } else {
229 [
230 self.inner_margin,
231 self.inner_right,
232 self.inner_top,
233 self.inner_bottom,
234 ]
235 }
236 }
237
238 fn outer_edges(self) -> [f32; 4] {
239 if self.outer_same {
240 [self.outer_margin; 4]
241 } else {
242 [
243 self.outer_margin,
244 self.outer_right,
245 self.outer_top,
246 self.outer_bottom,
247 ]
248 }
249 }
250
251 fn radii(self) -> CornerRadii {
252 if self.radius_same {
253 CornerRadii::uniform(self.corner_radius)
254 } else {
255 CornerRadii::new(
256 self.corner_radius,
257 self.corner_ne,
258 self.corner_se,
259 self.corner_sw,
260 )
261 }
262 }
263
264 fn stroke_color(self) -> ColorRgba {
265 let t = unit(self.stroke_tint);
266 ColorRgba::new(
267 (140.0 + t * 85.0) as u8,
268 (140.0 + t * 85.0) as u8,
269 (150.0 + t * 75.0) as u8,
270 255,
271 )
272 }
273
274 fn fill_color(self) -> ColorRgba {
275 let t = unit(self.fill_tint);
276 ColorRgba::new(
277 (58.0 + t * 80.0) as u8,
278 (30.0 + t * 44.0) as u8,
279 (150.0 + t * 95.0) as u8,
280 255,
281 )
282 }
283
284 fn shadow_color(self) -> ColorRgba {
285 ColorRgba::new(0, 0, 0, self.shadow_alpha.clamp(0.0, 255.0) as u8)
286 }
287}
288
289#[derive(Clone, Copy, Debug, PartialEq, Eq)]
290enum FocusedTextInput {
291 Editable,
292 Selectable,
293 Singleline,
294 Multiline,
295 TextArea,
296 CodeEditor,
297 Search,
298 Password,
299 SelectableHelper,
300 SliderValue,
301 SliderRangeLeft,
302 SliderRangeRight,
303 SliderStep,
304}
305
306impl FocusedTextInput {
307 const fn is_read_only(self) -> bool {
308 matches!(self, Self::Selectable | Self::SelectableHelper)
309 }
310}
311
312#[derive(Clone, Copy, Debug, PartialEq, Eq)]
313enum SliderThumbChoice {
314 Circle,
315 Square,
316 Rectangle,
317}
318
319#[derive(Clone, Copy)]
320struct CanvasCubeState {
321 yaw: f32,
322 pitch: f32,
323 drag_origin_yaw: f32,
324 drag_origin_pitch: f32,
325 rendered: Option<CanvasCubeRenderKey>,
326}
327
328#[derive(Clone, Copy, PartialEq, Eq)]
329struct CanvasCubeRenderKey {
330 yaw_bits: u32,
331 pitch_bits: u32,
332 size: PixelSize,
333}
334
335impl Default for CanvasCubeState {
336 fn default() -> Self {
337 Self {
338 yaw: 0.82,
339 pitch: 0.52,
340 drag_origin_yaw: 0.82,
341 drag_origin_pitch: 0.52,
342 rendered: None,
343 }
344 }
345}
346
347impl CanvasCubeState {
348 fn apply_drag(&mut self, drag: WidgetDrag) {
349 match drag.phase {
350 WidgetDragPhase::Begin => {
351 self.drag_origin_yaw = self.yaw;
352 self.drag_origin_pitch = self.pitch;
353 self.apply_drag_delta(drag.total_delta);
354 }
355 WidgetDragPhase::Update | WidgetDragPhase::Commit => {
356 self.apply_drag_delta(drag.total_delta);
357 }
358 WidgetDragPhase::Cancel => {
359 self.yaw = self.drag_origin_yaw;
360 self.pitch = self.drag_origin_pitch;
361 }
362 }
363 }
364
365 fn apply_drag_delta(&mut self, total_delta: UiPoint) {
366 self.yaw = self.drag_origin_yaw + total_delta.x * 0.012;
367 self.pitch = (self.drag_origin_pitch + total_delta.y * 0.012).clamp(-1.25, 1.25);
368 }
369
370 fn render_key(self, size: PixelSize) -> CanvasCubeRenderKey {
371 CanvasCubeRenderKey {
372 yaw_bits: self.yaw.to_bits(),
373 pitch_bits: self.pitch.to_bits(),
374 size,
375 }
376 }
377
378 fn needs_render(self, size: PixelSize) -> bool {
379 self.rendered != Some(self.render_key(size))
380 }
381
382 fn mark_rendered(&mut self, size: PixelSize) {
383 self.rendered = Some(self.render_key(size));
384 }
385}
386
387impl Default for ShowcaseState {
388 fn default() -> Self {
389 let text = TextInputState::new("Editable text");
390 let mut selectable_text = TextInputState::new("Selectable read-only text");
391 selectable_text.selection_anchor = Some(0);
392 selectable_text.caret = "Selectable".len();
393 let windows = ShowcaseWindows::default();
394 let desktop = widgets::FloatingDesktopState::with_visible_order(
395 SHOWCASE_WIDGET_WINDOW_IDS
396 .into_iter()
397 .filter(|id| windows.is_visible(id))
398 .map(str::to_string),
399 showcase_window_z_policy(),
400 );
401
402 Self {
403 checked: true,
404 slider: 10.0,
405 slider_left: 1.0,
406 slider_right: 10000.0,
407 slider_value_text: TextInputState::new("10"),
408 slider_left_text: TextInputState::new("1"),
409 slider_right_text: TextInputState::new("10000"),
410 slider_step_value: 10.0,
411 slider_step_text: TextInputState::new("10"),
412 slider_trailing_color: true,
413 slider_trailing_picker: widgets::ColorPickerState::new(color(120, 170, 230)),
414 slider_trailing_picker_open: false,
415 slider_thumb_shape: SliderThumbChoice::Circle,
416 slider_use_steps: false,
417 slider_logarithmic: true,
418 slider_clamping: widgets::SliderClamping::Always,
419 slider_smart_aim: true,
420 label_locale: widgets::SelectMenuState::with_selected(1),
421 label_link_visited: false,
422 label_hyperlink_visited: false,
423 label_link_status: "No link action yet",
424 color: widgets::ColorPickerState::new(color(118, 183, 255)),
425 date: widgets::DatePickerModel::builder()
426 .selected(CalendarDate::new(2026, 5, 12))
427 .today(CalendarDate::new(2026, 5, 12))
428 .build(),
429 radio_choice: "compact",
430 switch_enabled: true,
431 mixed_switch: widgets::ToggleValue::Mixed,
432 theme_preference: widgets::ThemePreference::Dark,
433 numeric_value: 42.0,
434 numeric_angle: 0.75,
435 numeric_tau: 0.75,
436 combo_open: false,
437 combo_label: "Compact".to_string(),
438 dropdown: widgets::SelectMenuState::with_selected(1),
439 select_menu: widgets::SelectMenuState {
440 open: true,
441 selected: Some(0),
442 active: Some(2),
443 },
444 text,
445 selectable_text,
446 singleline_text: TextInputState::new("Single line"),
447 multiline_text: TextInputState::new("First line\nSecond line"),
448 text_area_text: TextInputState::new("Text area content"),
449 code_editor_text: TextInputState::new("fn main() {\n println!(\"showcase\");\n}"),
450 search_text: TextInputState::new("widgets"),
451 password_text: TextInputState::new("correct horse"),
452 focused_text: None,
453 clipboard_text: String::new(),
454 system_clipboard: create_system_clipboard(),
455 last_button: "None",
456 toggle_button: false,
457 table_selection: widgets::DataTableSelection::single_row(2)
458 .with_active_cell(widgets::DataTableCellIndex::new(2, 1)),
459 tree: widgets::TreeViewState::expanded(["root"]),
460 outliner: widgets::TreeViewState::expanded(["root", "assets"]),
461 toast_visible: false,
462 toast_action_status: "No toast action",
463 popup_open: false,
464 progress_phase: 0.0,
465 caret_phase: 0.0,
466 command_palette: widgets::CommandPaletteState {
467 query: String::new(),
468 active_match: Some(0),
469 max_results: 24,
470 },
471 last_command: "None".to_string(),
472 list_scroll: 0.0,
473 virtual_scroll: 0.0,
474 table_scroll: 0.0,
475 layout_preview_scroll: 0.0,
476 layout_left_scroll: 0.0,
477 layout_right_scroll: 0.0,
478 layout_inspector_scroll: 0.0,
479 layout_document_scroll: 0.0,
480 layout_assets_scroll: 0.0,
481 scrollbars: widgets::ScrollbarControllerState::new(),
482 layout_tab: 0,
483 styling: StylingState::default(),
484 cube: CanvasCubeState::default(),
485 menu_bar: widgets::MenuBarState {
486 open_menu: Some(0),
487 active_item: Some(0),
488 },
489 menu_button: widgets::MenuButtonState::new(),
490 image_text_menu_button: widgets::MenuButtonState::new(),
491 image_menu_button: widgets::MenuButtonState::new(),
492 context_menu: widgets::ContextMenuState::closed(),
493 menu_autosave: true,
494 menu_grid: true,
495 form: profile_form_state(),
496 form_status: "Unsaved profile changes".to_string(),
497 overlay_expanded: true,
498 overlay_popup_open: false,
499 overlay_modal_open: false,
500 color_button_status: "None",
501 drag_drop_status: "Idle",
502 layout_split: widgets::SplitPaneState::new(0.44).with_min_sizes(80.0, 80.0),
503 containers_scroll: operad::ScrollState {
504 axes: ScrollAxes::BOTH,
505 offset: UiPoint::new(24.0, 18.0),
506 viewport_size: UiSize::new(260.0, 82.0),
507 content_size: UiSize::new(440.0, 180.0),
508 },
509 controls_scroll: 0.0,
510 color_copied_hex: None,
511 windows,
512 desktop,
513 }
514 }
515}
516
517struct ShowcaseWindows {
518 labels: bool,
519 buttons: bool,
520 checkbox: bool,
521 toggles: bool,
522 slider: bool,
523 numeric: bool,
524 text_input: bool,
525 selection: bool,
526 menus: bool,
527 command_palette: bool,
528 date_picker: bool,
529 color_picker: bool,
530 color_buttons: bool,
531 progress: bool,
532 lists_tables: bool,
533 property_inspector: bool,
534 trees: bool,
535 layout_widgets: bool,
536 containers: bool,
537 forms: bool,
538 overlays: bool,
539 drag_drop: bool,
540 media: bool,
541 timeline: bool,
542 toasts: bool,
543 popup_panel: bool,
544 canvas: bool,
545 styling: bool,
546}
547
548impl Default for ShowcaseWindows {
549 fn default() -> Self {
550 Self {
551 labels: true,
552 buttons: true,
553 checkbox: false,
554 toggles: false,
555 slider: false,
556 numeric: false,
557 text_input: false,
558 selection: false,
559 menus: false,
560 command_palette: false,
561 date_picker: false,
562 color_picker: true,
563 color_buttons: false,
564 progress: false,
565 lists_tables: false,
566 property_inspector: false,
567 trees: false,
568 layout_widgets: false,
569 containers: false,
570 forms: false,
571 overlays: false,
572 drag_drop: false,
573 media: false,
574 timeline: false,
575 toasts: false,
576 popup_panel: false,
577 canvas: true,
578 styling: false,
579 }
580 }
581}
582
583impl ShowcaseWindows {
584 fn is_visible(&self, id: &str) -> bool {
585 match id {
586 "labels" => self.labels,
587 "buttons" => self.buttons,
588 "checkbox" => self.checkbox,
589 "toggles" => self.toggles,
590 "slider" => self.slider,
591 "numeric" => self.numeric,
592 "text_input" => self.text_input,
593 "selection" => self.selection,
594 "menus" => self.menus,
595 "command_palette" => self.command_palette,
596 "date_picker" => self.date_picker,
597 "color_picker" => self.color_picker,
598 "color_buttons" => self.color_buttons,
599 "progress" => self.progress,
600 "lists_tables" => self.lists_tables,
601 "property_inspector" => self.property_inspector,
602 "trees" => self.trees,
603 "layout_widgets" => self.layout_widgets,
604 "containers" => self.containers,
605 "forms" => self.forms,
606 "overlays" => self.overlays,
607 "drag_drop" => self.drag_drop,
608 "media" => self.media,
609 "timeline" => self.timeline,
610 "toasts" => self.toasts,
611 "popup_panel" => self.popup_panel,
612 "canvas" => self.canvas,
613 "styling" => self.styling,
614 _ => false,
615 }
616 }
617
618 fn slot_mut(&mut self, id: &str) -> Option<&mut bool> {
619 match id {
620 "labels" => Some(&mut self.labels),
621 "buttons" => Some(&mut self.buttons),
622 "checkbox" => Some(&mut self.checkbox),
623 "toggles" => Some(&mut self.toggles),
624 "slider" => Some(&mut self.slider),
625 "numeric" => Some(&mut self.numeric),
626 "text_input" => Some(&mut self.text_input),
627 "selection" => Some(&mut self.selection),
628 "menus" => Some(&mut self.menus),
629 "command_palette" => Some(&mut self.command_palette),
630 "date_picker" => Some(&mut self.date_picker),
631 "color_picker" => Some(&mut self.color_picker),
632 "color_buttons" => Some(&mut self.color_buttons),
633 "progress" => Some(&mut self.progress),
634 "lists_tables" => Some(&mut self.lists_tables),
635 "property_inspector" => Some(&mut self.property_inspector),
636 "trees" => Some(&mut self.trees),
637 "layout_widgets" => Some(&mut self.layout_widgets),
638 "containers" => Some(&mut self.containers),
639 "forms" => Some(&mut self.forms),
640 "overlays" => Some(&mut self.overlays),
641 "drag_drop" => Some(&mut self.drag_drop),
642 "media" => Some(&mut self.media),
643 "timeline" => Some(&mut self.timeline),
644 "toasts" => Some(&mut self.toasts),
645 "popup_panel" => Some(&mut self.popup_panel),
646 "canvas" => Some(&mut self.canvas),
647 "styling" => Some(&mut self.styling),
648 _ => None,
649 }
650 }
651
652 fn toggle(&mut self, id: &str) -> Option<bool> {
653 if let Some(visible) = self.slot_mut(id) {
654 *visible = !*visible;
655 return Some(*visible);
656 }
657 None
658 }
659
660 fn close(&mut self, id: &str) {
661 if let Some(visible) = self.slot_mut(id) {
662 *visible = false;
663 }
664 }
665
666 fn clear_all(&mut self) {
667 for id in SHOWCASE_WIDGET_WINDOW_IDS {
668 if let Some(visible) = self.slot_mut(id) {
669 *visible = false;
670 }
671 }
672 }
673}
674
675fn showcase_window_z_policy() -> widgets::FloatingDesktopZPolicy {
676 widgets::FloatingDesktopZPolicy::new(
677 SHOWCASE_WINDOW_Z_BASE,
678 SHOWCASE_WINDOW_Z_STRIDE,
679 SHOWCASE_WINDOW_Z_MAX,
680 )
681}
682
683fn window_defaults(id: &str) -> widgets::FloatingWindowDefaults {
684 widgets::FloatingWindowDefaults::new(
685 default_window_position(id),
686 default_window_size(id),
687 default_window_state_min_size(id),
688 )
689}
690
691impl ShowcaseState {
692 fn update(&mut self, action: WidgetAction) {
693 let WidgetAction { binding, kind, .. } = action;
694 let WidgetActionBinding::Action(action_id) = binding else {
695 return;
696 };
697 let action_id = action_id.as_str();
698
699 let color_outcome = self.color.apply_action(
700 action_id,
701 kind.clone(),
702 widgets::ColorPickerActionOptions::new("color").copy_hex("color.copy_hex"),
703 );
704 if color_outcome.update.is_some()
705 || color_outcome.effect.is_some()
706 || color_outcome.mode_changed
707 {
708 if let Some(widgets::ColorPickerEffect::CopyHex(hex)) = color_outcome.effect {
709 self.copy_text_to_system_clipboard(&hex);
710 self.clipboard_text = hex.clone();
711 self.color_copied_hex = Some(hex);
712 }
713 return;
714 }
715 let color_buttons_outcome = self.color.apply_action(
716 action_id,
717 kind.clone(),
718 widgets::ColorPickerActionOptions::new("color_buttons.hsva_2d"),
719 );
720 if color_buttons_outcome.update.is_some() || color_buttons_outcome.mode_changed {
721 self.color_button_status = "HSVA field";
722 return;
723 }
724 let slider_color_outcome = self.slider_trailing_picker.apply_action(
725 action_id,
726 kind.clone(),
727 widgets::ColorPickerActionOptions::new("slider.trailing_picker"),
728 );
729 if slider_color_outcome.update.is_some() || slider_color_outcome.mode_changed {
730 return;
731 }
732
733 if action_id == "window.clear_all" {
734 self.windows.clear_all();
735 return;
736 }
737 if let Some(id) = action_id.strip_prefix("window.toggle.") {
738 if self.windows.toggle(id).unwrap_or(false) {
739 self.desktop.ensure_window(id, window_defaults(id));
740 self.desktop.bring_to_front(id);
741 }
742 return;
743 }
744 if let Some(id) = action_id.strip_prefix("window.close.") {
745 self.windows.close(id);
746 self.desktop.close(id);
747 return;
748 }
749 if let Some(id) = action_id.strip_prefix("window.activate.") {
750 self.desktop.bring_to_front(id);
751 return;
752 }
753 if let Some(id) = action_id.strip_prefix("window.drag.") {
754 if let WidgetActionKind::PointerEdit(edit) = kind {
755 self.desktop
756 .apply_drag(id, edit, default_window_position(id));
757 }
758 return;
759 }
760 if let Some(id) = action_id.strip_prefix("window.resize.") {
761 if let WidgetActionKind::PointerEdit(edit) = kind {
762 self.desktop.apply_resize(id, edit, window_defaults(id));
763 }
764 return;
765 }
766 if let Some(id) = action_id.strip_prefix("window.collapse.") {
767 self.desktop.toggle_collapsed(id);
768 return;
769 }
770 if let Some(id) = window_for_action(action_id) {
771 self.desktop.bring_to_front(id);
772 }
773 if action_id == "runtime.tick" {
774 self.progress_phase += SHOWCASE_PROGRESS_RADIANS_PER_SECOND / SHOWCASE_TICK_RATE_HZ;
775 self.caret_phase = (self.caret_phase
776 + std::f32::consts::TAU * TEXT_CARET_BLINK_HZ / SHOWCASE_TICK_RATE_HZ)
777 % std::f32::consts::TAU;
778 return;
779 }
780 if action_id == "command_palette.search" {
781 if let WidgetActionKind::TextEdit(edit) = kind {
782 self.apply_command_palette_event(edit.event);
783 }
784 return;
785 }
786 if let Some(id) = action_id.strip_prefix("command_palette.item.") {
787 self.select_command_palette_item(id);
788 return;
789 }
790 if let Some(input) = focused_text_for_action(action_id) {
791 if let WidgetActionKind::TextEdit(edit) = kind {
792 self.apply_text_edit(input, edit);
793 }
794 return;
795 }
796
797 match action_id {
798 "labels.link" => {
799 self.label_link_visited = true;
800 self.label_link_status = "Internal link activated";
801 return;
802 }
803 "labels.hyperlink" => {
804 self.label_hyperlink_visited = true;
805 self.label_link_status = "Opened docs.rs/operad";
806 open_url("https://docs.rs/operad");
807 return;
808 }
809 "button.default" => self.last_button = "Default",
810 "button.primary" => self.last_button = "Primary",
811 "button.secondary" => self.last_button = "Secondary",
812 "button.destructive" => self.last_button = "Destructive",
813 "button.small" => self.last_button = "Small",
814 "button.icon" => self.last_button = "Settings",
815 "button.image" => self.last_button = "Folder",
816 "button.reset" => {
817 self.toggle_button = false;
818 self.last_button = "Reset";
819 }
820 "button.toggle" => {
821 self.toggle_button = !self.toggle_button;
822 self.last_button = "Toggle";
823 }
824 "checkbox.enabled" => self.checked = !self.checked,
825 "labels.locale.toggle" => {
826 self.label_locale.toggle(&label_locale_options());
827 return;
828 }
829 "toggles.switch" => self.switch_enabled = !self.switch_enabled,
830 "toggles.mixed" => self.mixed_switch = self.mixed_switch.toggled(),
831 "toggles.radio.compact" => self.radio_choice = "compact",
832 "toggles.radio.comfortable" => self.radio_choice = "comfortable",
833 "toggles.radio.spacious" => self.radio_choice = "spacious",
834 "toggles.theme.system" => {
835 self.theme_preference = widgets::ThemePreference::System;
836 return;
837 }
838 "toggles.theme.light" => {
839 self.theme_preference = widgets::ThemePreference::Light;
840 return;
841 }
842 "toggles.theme.dark" => {
843 self.theme_preference = widgets::ThemePreference::Dark;
844 return;
845 }
846 "theme.preference.dark" => {
847 self.theme_preference = if self.theme_preference.is_dark() {
848 widgets::ThemePreference::Light
849 } else {
850 widgets::ThemePreference::Dark
851 };
852 return;
853 }
854 "combo.toggle" => self.combo_open = !self.combo_open,
855 "selection.dropdown.toggle" => {
856 self.dropdown.toggle(&select_options());
857 return;
858 }
859 "menus.menu_button" => {
860 let button_items = menu_items(self.menu_autosave);
861 let outcome = self.menu_button.toggle(&button_items);
862 if outcome.opened {
863 self.image_text_menu_button.close();
864 self.image_menu_button.close();
865 self.context_menu.close();
866 }
867 return;
868 }
869 "menus.image_text_menu_button" => {
870 let button_items = menu_items(self.menu_autosave);
871 let outcome = self.image_text_menu_button.toggle(&button_items);
872 if outcome.opened {
873 self.menu_button.close();
874 self.image_menu_button.close();
875 self.context_menu.close();
876 }
877 return;
878 }
879 "menus.image_menu_button" => {
880 let button_items = menu_items(self.menu_autosave);
881 let outcome = self.image_menu_button.toggle(&button_items);
882 if outcome.opened {
883 self.menu_button.close();
884 self.image_text_menu_button.close();
885 self.context_menu.close();
886 }
887 return;
888 }
889 "menus.context.open" => {
890 self.context_menu
891 .open_with_items(UiPoint::new(0.0, 0.0), &menu_items(self.menu_autosave));
892 self.menu_button.close();
893 self.image_text_menu_button.close();
894 self.image_menu_button.close();
895 return;
896 }
897 "menus.context.close" => {
898 self.context_menu.close();
899 return;
900 }
901 "menus.bar.file" => {
902 self.menu_bar
903 .open(&menu_bar_menus(self.menu_autosave, self.menu_grid), 0);
904 return;
905 }
906 "menus.bar.edit" => {
907 self.menu_bar
908 .open(&menu_bar_menus(self.menu_autosave, self.menu_grid), 1);
909 return;
910 }
911 "menus.bar.view" => {
912 self.menu_bar
913 .open(&menu_bar_menus(self.menu_autosave, self.menu_grid), 2);
914 return;
915 }
916 "date.previous" => self.date.show_previous_month(),
917 "date.next" => self.date.show_next_month(),
918 "date.week.sunday" => {
919 self.date.first_weekday = widgets::Weekday::Sunday;
920 return;
921 }
922 "date.week.monday" => {
923 self.date.first_weekday = widgets::Weekday::Monday;
924 return;
925 }
926 "date.range.toggle" => {
927 if self.date.min.is_some() || self.date.max.is_some() {
928 self.date.min = None;
929 self.date.max = None;
930 } else {
931 self.date.min = CalendarDate::new(2026, 5, 4);
932 self.date.max = CalendarDate::new(2026, 5, 29);
933 }
934 return;
935 }
936 "toast.show" => {
937 self.toast_visible = true;
938 return;
939 }
940 "toast.hide" => {
941 self.toast_visible = false;
942 return;
943 }
944 id if id.starts_with("toast.dismiss.") => {
945 self.toast_visible = false;
946 return;
947 }
948 "toast.action.1.undo" => {
949 self.toast_action_status = "Undo requested";
950 return;
951 }
952 "popup.toggle" => {
953 self.popup_open = !self.popup_open;
954 return;
955 }
956 "popup.close" => {
957 self.popup_open = false;
958 return;
959 }
960 "layout.tab.preview" => {
961 self.layout_tab = 0;
962 return;
963 }
964 "layout.tab.settings" => {
965 self.layout_tab = 1;
966 return;
967 }
968 "forms.profile.submit" => {
969 self.form.submit();
970 self.form_status = "Submit requested".to_string();
971 return;
972 }
973 "forms.profile.apply" => {
974 self.form.apply();
975 self.form_status = "Applied".to_string();
976 return;
977 }
978 "forms.profile.cancel" => {
979 self.form.cancel();
980 self.form_status = "Cancelled".to_string();
981 return;
982 }
983 "forms.profile.reset" => {
984 self.form = profile_form_state();
985 self.form_status = "Reset".to_string();
986 return;
987 }
988 "overlays.collapsing.toggle" => {
989 self.overlay_expanded = !self.overlay_expanded;
990 return;
991 }
992 "overlays.popup.toggle" => {
993 self.overlay_popup_open = !self.overlay_popup_open;
994 return;
995 }
996 "overlays.popup.close" => {
997 self.overlay_popup_open = false;
998 return;
999 }
1000 "overlays.modal.open" => {
1001 self.overlay_modal_open = true;
1002 return;
1003 }
1004 "overlays.modal.close" => {
1005 self.overlay_modal_open = false;
1006 return;
1007 }
1008 "drag_drop.text_source" => {
1009 self.drag_drop_status = "Text drag started";
1010 return;
1011 }
1012 "drag_drop.accept_text" => {
1013 self.drag_drop_status = "Text payload accepted";
1014 return;
1015 }
1016 "drag_drop.files_only" => {
1017 self.drag_drop_status = "File payload rejected";
1018 return;
1019 }
1020 "slider.trailing" => {
1021 self.slider_trailing_color = !self.slider_trailing_color;
1022 return;
1023 }
1024 "slider.trailing_color_button" => {
1025 self.slider_trailing_picker_open = !self.slider_trailing_picker_open;
1026 return;
1027 }
1028 "slider.thumb.circle" => {
1029 self.slider_thumb_shape = SliderThumbChoice::Circle;
1030 return;
1031 }
1032 "slider.thumb.square" => {
1033 self.slider_thumb_shape = SliderThumbChoice::Square;
1034 return;
1035 }
1036 "slider.thumb.rectangle" => {
1037 self.slider_thumb_shape = SliderThumbChoice::Rectangle;
1038 return;
1039 }
1040 "slider.steps" => {
1041 self.slider_use_steps = !self.slider_use_steps;
1042 if self.slider_use_steps {
1043 self.set_slider_value(widgets::round_slider_to_step(
1044 self.slider,
1045 self.slider_step(),
1046 ));
1047 }
1048 return;
1049 }
1050 "slider.logarithmic" => {
1051 self.slider_logarithmic = !self.slider_logarithmic;
1052 return;
1053 }
1054 "slider.clamping.never" => {
1055 self.slider_clamping = widgets::SliderClamping::Never;
1056 return;
1057 }
1058 "slider.clamping.edits" => {
1059 self.slider_clamping = widgets::SliderClamping::Edits;
1060 return;
1061 }
1062 "slider.clamping.always" => {
1063 self.slider_clamping = widgets::SliderClamping::Always;
1064 self.clamp_slider_to_range();
1065 return;
1066 }
1067 "slider.smart_aim" => {
1068 self.slider_smart_aim = !self.slider_smart_aim;
1069 return;
1070 }
1071 "styling.inner_same" => {
1072 self.styling.inner_same = !self.styling.inner_same;
1073 return;
1074 }
1075 "styling.outer_same" => {
1076 self.styling.outer_same = !self.styling.outer_same;
1077 return;
1078 }
1079 "styling.radius_same" => {
1080 self.styling.radius_same = !self.styling.radius_same;
1081 return;
1082 }
1083 _ => {}
1084 }
1085
1086 if action_id == "canvas.rotate" {
1087 if let WidgetActionKind::Drag(drag) = kind {
1088 self.cube.apply_drag(drag);
1089 }
1090 return;
1091 }
1092 if let WidgetActionKind::Scroll(scroll) = &kind {
1093 match action_id {
1094 "lists_tables.scroll_area.scroll" => self.list_scroll = scroll.offset.y,
1095 "lists_tables.virtual_list.scroll" => self.virtual_scroll = scroll.offset.y,
1096 "lists_tables.data_table.scroll" => self.table_scroll = scroll.offset.y,
1097 "layout.preview.scroll" => self.layout_preview_scroll = scroll.offset.y,
1098 "layout.left.scroll" => self.layout_left_scroll = scroll.offset.y,
1099 "layout.right.scroll" => self.layout_right_scroll = scroll.offset.y,
1100 "layout.inspector.scroll" => self.layout_inspector_scroll = scroll.offset.y,
1101 "layout.document.scroll" => self.layout_document_scroll = scroll.offset.y,
1102 "layout.assets.scroll" => self.layout_assets_scroll = scroll.offset.y,
1103 "containers.scroll_area_with_bars.scroll" => {
1104 self.containers_scroll.offset =
1105 self.containers_scroll.clamp_offset(scroll.offset);
1106 }
1107 "controls.widget_list.scroll" => {
1108 self.controls_scroll =
1109 controls_scroll_state(scroll.offset.y, scroll.viewport_size.height)
1110 .offset
1111 .y;
1112 }
1113 _ => {}
1114 }
1115 return;
1116 }
1117
1118 if let Some(date) = action_id
1119 .strip_prefix("date.day.")
1120 .and_then(parse_calendar_date)
1121 {
1122 self.date.select(date);
1123 return;
1124 }
1125
1126 if let Some(option_id) = action_id.strip_prefix("labels.locale.option.") {
1127 self.label_locale
1128 .select_id_and_close(&label_locale_options(), option_id);
1129 return;
1130 }
1131 if let Some(option_id) = action_id.strip_prefix("selection.dropdown.option.") {
1132 self.dropdown
1133 .select_id_and_close(&select_options(), option_id);
1134 return;
1135 }
1136 if let Some(option_id) = action_id.strip_prefix("selection.combo.option.") {
1137 if let Some(option) = select_options()
1138 .into_iter()
1139 .find(|option| option.id == option_id && option.enabled)
1140 {
1141 self.combo_label = option.label;
1142 self.combo_open = false;
1143 }
1144 return;
1145 }
1146 if let Some(option_id) = action_id.strip_prefix("selection.menu.option.") {
1147 self.select_menu.select_id(&select_options(), option_id);
1148 return;
1149 }
1150 if let Some(menu_id) = action_id.strip_prefix("menus.item.") {
1151 self.apply_menu_item(menu_id);
1152 return;
1153 }
1154 if let Some(menu_id) = action_id.strip_prefix("menus.context.") {
1155 self.apply_menu_item(menu_id);
1156 self.context_menu.close();
1157 return;
1158 }
1159 if let Some(kind) = action_id.strip_prefix("color_buttons.") {
1160 self.color_button_status = match kind {
1161 "compact" => "Compact",
1162 "swatch" => "Swatch",
1163 "rgb" => "RGB",
1164 "rgba" => "RGBA",
1165 "srgb" => "SRGB",
1166 "srgba" => "SRGBA",
1167 "hsva" => "HSVA",
1168 "oklch" => "OKLCH",
1169 "color32" => "Color32",
1170 "rgba_premultiplied" => "RGBA premultiplied",
1171 "rgba_unmultiplied" => "RGBA unmultiplied",
1172 "srgba_premultiplied" => "SRGBA premultiplied",
1173 "srgba_unmultiplied" => "SRGBA unmultiplied",
1174 _ => self.color_button_status,
1175 };
1176 return;
1177 }
1178 if let Some(row) = action_id
1179 .strip_prefix("lists_tables.data_table.row.")
1180 .and_then(|row| row.parse::<usize>().ok())
1181 {
1182 self.table_selection = widgets::DataTableSelection::single_row(row)
1183 .with_active_cell(widgets::DataTableCellIndex::new(row, 0));
1184 return;
1185 }
1186 if let Some(cell) = action_id
1187 .strip_prefix("lists_tables.data_table.cell.")
1188 .and_then(parse_table_cell)
1189 {
1190 self.table_selection =
1191 widgets::DataTableSelection::single_row(cell.row).with_active_cell(cell);
1192 return;
1193 }
1194 if let Some(id) = action_id.strip_prefix("trees.tree.row.") {
1195 self.apply_tree_row(id, false);
1196 return;
1197 }
1198 if let Some(id) = action_id.strip_prefix("trees.outliner.row.") {
1199 self.apply_tree_row(id, true);
1200 return;
1201 }
1202
1203 let WidgetActionKind::PointerEdit(edit) = kind else {
1204 return;
1205 };
1206 match action_id {
1207 "numeric.drag_value" => {
1208 self.numeric_value = scaled_slider(edit.target_rect, edit.position, 0.0, 100.0);
1209 }
1210 "numeric.drag_angle" => {
1211 self.numeric_angle =
1212 scaled_slider(edit.target_rect, edit.position, 0.0, 360.0).to_radians();
1213 }
1214 "numeric.drag_angle_tau" => {
1215 self.numeric_tau = scaled_slider(edit.target_rect, edit.position, 0.0, 1.0)
1216 * std::f32::consts::TAU;
1217 }
1218 "layout_widgets.split_pane.handle" => {
1219 let total_extent = self
1220 .desktop
1221 .size("layout_widgets", default_window_size("layout_widgets"))
1222 .width
1223 - 48.0;
1224 let total_extent = total_extent.max(1.0);
1225 let handle_center = edit.target_rect.x + edit.target_rect.width * 0.5;
1226 self.layout_split
1227 .resize_by(edit.position.x - handle_center, total_extent, 6.0);
1228 }
1229 "slider.value" => {
1230 self.set_slider_value(
1231 self.slider_value_spec()
1232 .value_from_control_point(edit.target_rect, edit.position),
1233 );
1234 }
1235 "slider.range_left" => {
1236 let value = widgets::SliderValueSpec::new(0.0, self.slider_right.max(1.0))
1237 .value_from_control_point(edit.target_rect, edit.position);
1238 self.set_slider_left(value.min(self.slider_right - 1.0));
1239 }
1240 "slider.range_right" => {
1241 let value = widgets::SliderValueSpec::new(self.slider_left + 1.0, 10000.0)
1242 .value_from_control_point(edit.target_rect, edit.position);
1243 self.set_slider_right(value.max(self.slider_left + 1.0));
1244 }
1245 "lists_tables.scroll_area.scrollbar" => {
1246 let scroll = scroll_state(self.list_scroll, 92.0, 6.0 * 26.0);
1247 self.list_scroll = self
1248 .scrollbars
1249 .apply_drag_for_target_rect("list", scroll, widgets::ScrollAxis::Vertical, edit)
1250 .y;
1251 }
1252 "lists_tables.virtual_list.scrollbar" => {
1253 let scroll = scroll_state(self.virtual_scroll, 112.0, 24.0 * 28.0);
1254 self.virtual_scroll = self
1255 .scrollbars
1256 .apply_drag_for_target_rect(
1257 "virtual",
1258 scroll,
1259 widgets::ScrollAxis::Vertical,
1260 edit,
1261 )
1262 .y;
1263 }
1264 "lists_tables.data_table.scrollbar" => {
1265 let scroll = scroll_state(self.table_scroll, 128.0, 16.0 * 28.0);
1266 self.table_scroll = self
1267 .scrollbars
1268 .apply_drag_for_target_rect(
1269 "table",
1270 scroll,
1271 widgets::ScrollAxis::Vertical,
1272 edit,
1273 )
1274 .y;
1275 }
1276 "containers.scroll_area_with_bars.vertical-scrollbar" => {
1277 self.containers_scroll.offset = self.scrollbars.apply_drag_for_target_rect(
1278 "containers.vertical",
1279 self.containers_scroll,
1280 widgets::ScrollAxis::Vertical,
1281 edit,
1282 );
1283 }
1284 "containers.scroll_area_with_bars.horizontal-scrollbar" => {
1285 self.containers_scroll.offset = self.scrollbars.apply_drag_for_target_rect(
1286 "containers.horizontal",
1287 self.containers_scroll,
1288 widgets::ScrollAxis::Horizontal,
1289 edit,
1290 );
1291 }
1292 "controls.widget_list.scrollbar" => {
1293 self.controls_scroll = self
1294 .scrollbars
1295 .apply_drag_for_target_rect(
1296 "controls.widget_list",
1297 controls_scroll_state(self.controls_scroll, edit.target_rect.height),
1298 widgets::ScrollAxis::Vertical,
1299 edit,
1300 )
1301 .y;
1302 }
1303 "styling.inner" => {
1304 self.styling.inner_margin =
1305 scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1306 if self.styling.inner_same {
1307 self.styling.inner_right = self.styling.inner_margin;
1308 self.styling.inner_top = self.styling.inner_margin;
1309 self.styling.inner_bottom = self.styling.inner_margin;
1310 }
1311 }
1312 "styling.inner_right" => {
1313 self.styling.inner_right =
1314 scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1315 }
1316 "styling.inner_top" => {
1317 self.styling.inner_top = scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1318 }
1319 "styling.inner_bottom" => {
1320 self.styling.inner_bottom =
1321 scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1322 }
1323 "styling.outer" => {
1324 self.styling.outer_margin =
1325 scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
1326 if self.styling.outer_same {
1327 self.styling.outer_right = self.styling.outer_margin;
1328 self.styling.outer_top = self.styling.outer_margin;
1329 self.styling.outer_bottom = self.styling.outer_margin;
1330 }
1331 }
1332 "styling.outer_right" => {
1333 self.styling.outer_right =
1334 scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
1335 }
1336 "styling.outer_top" => {
1337 self.styling.outer_top = scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
1338 }
1339 "styling.outer_bottom" => {
1340 self.styling.outer_bottom =
1341 scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
1342 }
1343 "styling.radius" => {
1344 self.styling.corner_radius =
1345 scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
1346 if self.styling.radius_same {
1347 self.styling.corner_ne = self.styling.corner_radius;
1348 self.styling.corner_sw = self.styling.corner_radius;
1349 self.styling.corner_se = self.styling.corner_radius;
1350 }
1351 }
1352 "styling.radius_ne" => {
1353 self.styling.corner_ne = scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
1354 }
1355 "styling.radius_sw" => {
1356 self.styling.corner_sw = scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
1357 }
1358 "styling.radius_se" => {
1359 self.styling.corner_se = scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
1360 }
1361 "styling.shadow_x" => {
1362 self.styling.shadow_x = scaled_slider(edit.target_rect, edit.position, -24.0, 24.0);
1363 }
1364 "styling.shadow_y" => {
1365 self.styling.shadow_y = scaled_slider(edit.target_rect, edit.position, -24.0, 24.0);
1366 }
1367 "styling.shadow" => {
1368 self.styling.shadow_blur =
1369 scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1370 }
1371 "styling.shadow_spread" => {
1372 self.styling.shadow_spread =
1373 scaled_slider(edit.target_rect, edit.position, 0.0, 16.0);
1374 }
1375 "styling.shadow_alpha" => {
1376 self.styling.shadow_alpha =
1377 scaled_slider(edit.target_rect, edit.position, 0.0, 220.0);
1378 }
1379 "styling.stroke" => {
1380 self.styling.stroke_width =
1381 scaled_slider(edit.target_rect, edit.position, 0.0, 4.0);
1382 }
1383 "styling.fill" => {
1384 self.styling.fill_tint = widgets::slider_value_from_control_point(
1385 edit.target_rect,
1386 edit.position,
1387 0.0..1.0,
1388 );
1389 self.styling.fill = self.styling.fill_color();
1390 }
1391 "styling.stroke_color" => {
1392 self.styling.stroke_tint = widgets::slider_value_from_control_point(
1393 edit.target_rect,
1394 edit.position,
1395 0.0..1.0,
1396 );
1397 }
1398 _ => {}
1399 }
1400 }
1401
1402 fn apply_command_palette_event(&mut self, event: operad::UiInputEvent) {
1403 let outcome = self
1404 .command_palette
1405 .handle_event(&command_palette_items(), &event);
1406 if let Some(selection) = outcome.selected {
1407 self.select_command_palette_item(&selection.id);
1408 }
1409 }
1410
1411 fn select_command_palette_item(&mut self, id: &str) {
1412 if let Some(item) = command_palette_items()
1413 .into_iter()
1414 .find(|item| item.id == id && item.enabled)
1415 {
1416 self.last_command = item.title;
1417 self.command_palette.set_query("", &command_palette_items());
1418 }
1419 }
1420
1421 fn apply_text_edit(&mut self, input: FocusedTextInput, edit: WidgetTextEdit) {
1422 self.focused_text = Some(input);
1423 if let Some(point) = edit.local_position {
1424 let style = text(13.0, color(230, 236, 246));
1425 let target_rect = edit
1426 .target_rect
1427 .unwrap_or_else(|| UiRect::new(0.0, 0.0, 320.0, 36.0));
1428 let metrics = TextInputLayoutMetrics::from_style(
1429 UiRect::new(
1430 6.0,
1431 6.0,
1432 (target_rect.width - 12.0).max(1.0),
1433 (target_rect.height - 12.0).max(1.0),
1434 ),
1435 &style,
1436 );
1437 if let Some(state) = self.text_state_mut(input) {
1438 state.move_caret_to_point(metrics, point, edit.selecting);
1439 }
1440 return;
1441 }
1442
1443 let outcome = if input.is_read_only() {
1444 self.text_state_mut(input).map(|state| {
1445 state.handle_event_with_policy(
1446 &edit.event,
1447 widgets::TextInputInteractionPolicy::read_only(),
1448 )
1449 })
1450 } else {
1451 self.text_state_mut(input)
1452 .map(|state| state.handle_event(&edit.event))
1453 };
1454 if let Some(outcome) = outcome {
1455 self.apply_text_clipboard_outcome(input, outcome);
1456 self.sync_text_input_value(input);
1457 }
1458 }
1459
1460 fn apply_text_clipboard_outcome(
1461 &mut self,
1462 input: FocusedTextInput,
1463 outcome: widgets::TextInputOutcome,
1464 ) {
1465 match outcome.clipboard {
1466 Some(widgets::TextInputClipboardAction::Copy(text))
1467 | Some(widgets::TextInputClipboardAction::Cut(text)) => {
1468 self.copy_text_to_system_clipboard(&text);
1469 self.clipboard_text = text;
1470 }
1471 Some(widgets::TextInputClipboardAction::Paste) => {
1472 let pasted = self
1473 .read_text_from_system_clipboard()
1474 .unwrap_or_else(|| self.clipboard_text.clone());
1475 if !input.is_read_only() {
1476 if let Some(state) = self.text_state_mut(input) {
1477 state.paste_text(&pasted);
1478 }
1479 }
1480 }
1481 None => {}
1482 }
1483 }
1484
1485 fn text_state_mut(&mut self, input: FocusedTextInput) -> Option<&mut TextInputState> {
1486 match input {
1487 FocusedTextInput::Editable => Some(&mut self.text),
1488 FocusedTextInput::Selectable | FocusedTextInput::SelectableHelper => {
1489 Some(&mut self.selectable_text)
1490 }
1491 FocusedTextInput::Singleline => Some(&mut self.singleline_text),
1492 FocusedTextInput::Multiline => Some(&mut self.multiline_text),
1493 FocusedTextInput::TextArea => Some(&mut self.text_area_text),
1494 FocusedTextInput::CodeEditor => Some(&mut self.code_editor_text),
1495 FocusedTextInput::Search => Some(&mut self.search_text),
1496 FocusedTextInput::Password => Some(&mut self.password_text),
1497 FocusedTextInput::SliderValue => Some(&mut self.slider_value_text),
1498 FocusedTextInput::SliderRangeLeft => Some(&mut self.slider_left_text),
1499 FocusedTextInput::SliderRangeRight => Some(&mut self.slider_right_text),
1500 FocusedTextInput::SliderStep => Some(&mut self.slider_step_text),
1501 }
1502 }
1503
1504 fn sync_text_input_value(&mut self, input: FocusedTextInput) {
1505 match input {
1506 FocusedTextInput::SliderValue => {
1507 if let Ok(value) = self.slider_value_text.text.parse::<f32>() {
1508 self.apply_slider_value_from_text(value);
1509 }
1510 }
1511 FocusedTextInput::SliderRangeLeft => {
1512 if let Ok(value) = self.slider_left_text.text.parse::<f32>() {
1513 self.apply_slider_left_from_text(value);
1514 }
1515 }
1516 FocusedTextInput::SliderRangeRight => {
1517 if let Ok(value) = self.slider_right_text.text.parse::<f32>() {
1518 self.apply_slider_right_from_text(value);
1519 }
1520 }
1521 FocusedTextInput::SliderStep => {
1522 if let Ok(value) = self.slider_step_text.text.parse::<f32>() {
1523 self.slider_step_value = value.abs().max(0.0001);
1524 if self.slider_use_steps {
1525 self.set_slider_value(widgets::round_slider_to_step(
1526 self.slider,
1527 self.slider_step(),
1528 ));
1529 }
1530 }
1531 }
1532 _ => {}
1533 }
1534 }
1535
1536 fn copy_text_to_system_clipboard(&mut self, text: &str) {
1537 if self.system_clipboard.is_none() {
1538 self.system_clipboard = create_system_clipboard();
1539 }
1540 if let Some(clipboard) = self.system_clipboard.as_mut() {
1541 if clipboard.set_text(text.to_string()).is_err() {
1542 self.system_clipboard = None;
1543 }
1544 }
1545 }
1546
1547 fn read_text_from_system_clipboard(&mut self) -> Option<String> {
1548 if self.system_clipboard.is_none() {
1549 self.system_clipboard = create_system_clipboard();
1550 }
1551 self.system_clipboard
1552 .as_mut()
1553 .and_then(|clipboard| clipboard.get_text().ok())
1554 }
1555
1556 fn apply_menu_item(&mut self, id: &str) {
1557 let menus = menu_bar_menus(self.menu_autosave, self.menu_grid);
1558 self.menu_bar.set_active_item_by_id(&menus, id);
1559 if id == "autosave" {
1560 self.menu_autosave = !self.menu_autosave;
1561 } else if id == "grid" {
1562 self.menu_grid = !self.menu_grid;
1563 }
1564 self.menu_button.close();
1565 self.image_text_menu_button.close();
1566 self.image_menu_button.close();
1567 }
1568
1569 fn apply_tree_row(&mut self, id: &str, outliner: bool) {
1570 let roots = tree_items();
1571 let state = if outliner {
1572 &mut self.outliner
1573 } else {
1574 &mut self.tree
1575 };
1576 state.activate_visible_item_id(&roots, id);
1577 }
1578
1579 fn slider_value_spec(&self) -> widgets::SliderValueSpec {
1580 let mut spec = widgets::SliderValueSpec::new(self.slider_left, self.slider_right)
1581 .logarithmic(self.slider_logarithmic)
1582 .clamping(self.slider_clamping)
1583 .smart_aim(self.slider_smart_aim);
1584 if self.slider_use_steps {
1585 spec = spec.step(self.slider_step());
1586 }
1587 spec
1588 }
1589
1590 fn set_slider_value(&mut self, value: f32) {
1591 let value = self.slider_value_spec().adjust_value(value);
1592 self.slider = value;
1593 self.slider_value_text.text = widgets::format_slider_value(value);
1594 self.slider_value_text.caret = self.slider_value_text.text.len();
1595 self.slider_value_text.selection_anchor = None;
1596 }
1597
1598 fn apply_slider_value_from_text(&mut self, value: f32) {
1599 self.slider = if self.slider_clamping == widgets::SliderClamping::Always {
1600 self.slider_value_spec().clamp(value)
1601 } else {
1602 value
1603 };
1604 }
1605
1606 fn set_slider_left(&mut self, value: f32) {
1607 self.slider_left = value.min(self.slider_right - 1.0).max(0.0);
1608 self.slider_left_text.text = widgets::format_slider_value(self.slider_left);
1609 self.slider_left_text.caret = self.slider_left_text.text.len();
1610 if self.slider_clamping == widgets::SliderClamping::Always {
1611 self.clamp_slider_to_range();
1612 }
1613 }
1614
1615 fn apply_slider_left_from_text(&mut self, value: f32) {
1616 if value < self.slider_right {
1617 self.slider_left = value.max(0.0);
1618 if self.slider_clamping == widgets::SliderClamping::Always {
1619 self.slider = self.slider.clamp(self.slider_left, self.slider_right);
1620 }
1621 }
1622 }
1623
1624 fn set_slider_right(&mut self, value: f32) {
1625 self.slider_right = value.max(self.slider_left + 1.0).min(10000.0);
1626 self.slider_right_text.text = widgets::format_slider_value(self.slider_right);
1627 self.slider_right_text.caret = self.slider_right_text.text.len();
1628 if self.slider_clamping == widgets::SliderClamping::Always {
1629 self.clamp_slider_to_range();
1630 }
1631 }
1632
1633 fn apply_slider_right_from_text(&mut self, value: f32) {
1634 if value > self.slider_left {
1635 self.slider_right = value.min(10000.0);
1636 if self.slider_clamping == widgets::SliderClamping::Always {
1637 self.slider = self.slider.clamp(self.slider_left, self.slider_right);
1638 }
1639 }
1640 }
1641
1642 fn clamp_slider_to_range(&mut self) {
1643 self.set_slider_value(self.slider.clamp(self.slider_left, self.slider_right));
1644 }
1645
1646 fn slider_step(&self) -> f32 {
1647 self.slider_step_value.abs().max(0.0001)
1648 }
1649
1650 fn view(&self, viewport: UiSize) -> UiDocument {
1651 let mut ui = UiDocument::new(root_style(viewport.width, viewport.height));
1652 ui.node_mut(ui.root).visual = UiVisual::panel(color(16, 20, 26), None, 0.0);
1653
1654 let root = ui.root;
1655 let shell = ui.add_child(
1656 root,
1657 UiNode::container(
1658 "showcase.shell",
1659 LayoutStyle::row().with_size(viewport.width, viewport.height),
1660 ),
1661 );
1662 let desktop_width = (viewport.width - RIGHT_PANEL_WIDTH).max(360.0);
1663 let desktop = ui.add_child(
1664 shell,
1665 UiNode::container(
1666 "showcase.desktop",
1667 LayoutStyle::new()
1668 .with_width(desktop_width)
1669 .with_height(viewport.height)
1670 .with_flex_shrink(1.0),
1671 )
1672 .with_visual(UiVisual::panel(color(15, 19, 25), None, 0.0)),
1673 );
1674 let controls = ui.add_child(
1675 shell,
1676 UiNode::container(
1677 "showcase.controls",
1678 LayoutStyle::column()
1679 .with_width(RIGHT_PANEL_WIDTH)
1680 .with_height(viewport.height)
1681 .with_flex_shrink(0.0)
1682 .padding(12.0)
1683 .gap(4.0),
1684 )
1685 .with_visual(UiVisual::panel(
1686 color(21, 26, 33),
1687 Some(StrokeStyle::new(color(46, 56, 70), 1.0)),
1688 0.0,
1689 )),
1690 );
1691
1692 showcase_windows(
1693 &mut ui,
1694 desktop,
1695 self,
1696 UiSize::new(desktop_width, viewport.height),
1697 );
1698 control_panel(&mut ui, controls, self, viewport.height);
1699
1700 ui
1701 }
1702}
1703
1704fn showcase_windows(
1705 ui: &mut UiDocument,
1706 desktop: UiNodeId,
1707 state: &ShowcaseState,
1708 desktop_size: UiSize,
1709) {
1710 let windows = showcase_window_descriptors(state, desktop_size);
1711 let mut options = widgets::FloatingDesktopOptions::new(desktop_size).with_layout(
1712 LayoutStyle::new()
1713 .with_width_percent(1.0)
1714 .with_height_percent(1.0),
1715 );
1716 options.base_z_index = SHOWCASE_WINDOW_Z_BASE;
1717 options.window_z_stride = SHOWCASE_WINDOW_Z_STRIDE;
1718 options.margin = 18.0;
1719 options.gap = 14.0;
1720 widgets::floating_desktop(
1721 ui,
1722 desktop,
1723 "showcase.windows",
1724 &windows,
1725 options,
1726 |ui, window, descriptor| match descriptor.id.as_str() {
1727 "labels" => labels(ui, window, state),
1728 "buttons" => buttons(ui, window, state),
1729 "checkbox" => checkbox(ui, window, state),
1730 "toggles" => toggles(ui, window, state),
1731 "slider" => slider(ui, window, state),
1732 "numeric" => numeric_inputs(ui, window, state),
1733 "text_input" => text_input(ui, window, state),
1734 "selection" => selection_widgets(ui, window, state),
1735 "menus" => menu_widgets(ui, window, state),
1736 "command_palette" => command_palette(ui, window, state),
1737 "date_picker" => date_picker(ui, window, state),
1738 "color_picker" => color_picker(ui, window, state),
1739 "color_buttons" => color_buttons(ui, window, state),
1740 "progress" => progress_indicator(ui, window, state),
1741 "lists_tables" => list_and_table_widgets(ui, window, state),
1742 "property_inspector" => property_inspector(ui, window, state),
1743 "trees" => tree_widgets(ui, window, state),
1744 "layout_widgets" => tab_split_dock_widgets(ui, window, state),
1745 "containers" => container_widgets(ui, window, state),
1746 "forms" => form_widgets(ui, window, state),
1747 "overlays" => overlay_widgets(ui, window, state),
1748 "drag_drop" => drag_drop_widgets(ui, window, state),
1749 "media" => media_widgets(ui, window),
1750 "timeline" => timeline_ruler(ui, window),
1751 "toasts" => toast_controls(ui, window, state),
1752 "popup_panel" => popup_controls(ui, window, state),
1753 "canvas" => canvas(ui, window),
1754 "styling" => styling_widgets(ui, window, state),
1755 _ => {}
1756 },
1757 );
1758 showcase_overlays(ui, desktop, state, desktop_size);
1759}
1760
1761fn showcase_overlays(
1762 ui: &mut UiDocument,
1763 desktop: UiNodeId,
1764 state: &ShowcaseState,
1765 desktop_size: UiSize,
1766) {
1767 if state.toast_visible {
1768 let overlay_width = 320.0;
1769 let overlay = ui.add_child(
1770 desktop,
1771 UiNode::container(
1772 "showcase.toast_overlay",
1773 UiNodeStyle {
1774 layout: LayoutStyle::absolute_rect(UiRect::new(
1775 (desktop_size.width - overlay_width - 18.0).max(18.0),
1776 18.0,
1777 overlay_width,
1778 180.0,
1779 ))
1780 .as_taffy_style()
1781 .clone(),
1782 clip: ClipBehavior::None,
1783 z_index: 6000,
1784 ..Default::default()
1785 },
1786 ),
1787 );
1788 let mut stack = widgets::ToastStack::new(3);
1789 stack.push_toast(
1790 widgets::Toast::new(
1791 widgets::ToastId(1),
1792 widgets::ToastSeverity::Success,
1793 "Saved",
1794 Some("All changes are written".to_string()),
1795 None,
1796 )
1797 .with_action(widgets::ToastAction::new("undo", "Undo")),
1798 );
1799 stack.push(
1800 widgets::ToastSeverity::Warning,
1801 "Autosave paused",
1802 Some("Changes are kept locally".to_string()),
1803 None,
1804 );
1805 let mut options = widgets::ToastStackOptions::default();
1806 options.z_index = 6100;
1807 widgets::toast_stack(ui, overlay, "showcase.toast_overlay.stack", &stack, options);
1808 }
1809
1810 if state.popup_open {
1811 let popup_width = 280.0;
1812 let popup_height = 110.0;
1813 let popup = widgets::popup_panel(
1814 ui,
1815 desktop,
1816 "showcase.popup_overlay",
1817 UiRect::new(
1818 (desktop_size.width - popup_width - 36.0).max(18.0),
1819 220.0_f32.min((desktop_size.height - popup_height - 18.0).max(18.0)),
1820 popup_width,
1821 popup_height,
1822 ),
1823 widgets::PopupOptions {
1824 z_index: 6100,
1825 accessibility: Some(
1826 AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup panel"),
1827 ),
1828 ..Default::default()
1829 },
1830 );
1831 let body = ui.add_child(
1832 popup,
1833 UiNode::container(
1834 "showcase.popup_overlay.body",
1835 LayoutStyle::column()
1836 .with_width_percent(1.0)
1837 .with_height_percent(1.0)
1838 .padding(12.0)
1839 .gap(8.0),
1840 ),
1841 );
1842 let header = row(ui, body, "showcase.popup_overlay.header", 8.0);
1843 widgets::label(
1844 ui,
1845 header,
1846 "showcase.popup_overlay.title",
1847 "Popup panel",
1848 text(13.0, color(240, 244, 250)),
1849 LayoutStyle::new().with_width_percent(1.0),
1850 );
1851 let mut close =
1852 widgets::ButtonOptions::new(LayoutStyle::size(28.0, 24.0)).with_action("popup.close");
1853 close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
1854 close.hovered_visual = Some(button_visual(54, 70, 92));
1855 close.text_style = text(13.0, color(220, 228, 238));
1856 widgets::button(ui, header, "showcase.popup_overlay.close", "x", close);
1857 widgets::label(
1858 ui,
1859 body,
1860 "showcase.popup_overlay.body_text",
1861 "This surface is rendered as an overlay.",
1862 text(12.0, color(196, 210, 230)),
1863 LayoutStyle::new().with_width_percent(1.0),
1864 );
1865 }
1866}
1867
1868fn showcase_window_descriptors(
1869 state: &ShowcaseState,
1870 desktop_size: UiSize,
1871) -> Vec<widgets::FloatingWindowDescriptor> {
1872 let wide = (desktop_size.width - 36.0).min(720.0).max(320.0);
1873 let medium = (desktop_size.width - 36.0).min(604.0).max(300.0);
1874 let buttons_width = medium.min(620.0);
1875 let mut windows = Vec::new();
1876 push_window(
1877 &mut windows,
1878 state.windows.labels,
1879 "labels",
1880 "Labels",
1881 UiSize::new(380.0, 460.0),
1882 );
1883 push_window(
1884 &mut windows,
1885 state.windows.buttons,
1886 "buttons",
1887 "Buttons",
1888 UiSize::new(buttons_width, 220.0),
1889 );
1890 push_window(
1891 &mut windows,
1892 state.windows.checkbox,
1893 "checkbox",
1894 "Checkbox",
1895 UiSize::new(250.0, 72.0),
1896 );
1897 push_window(
1898 &mut windows,
1899 state.windows.toggles,
1900 "toggles",
1901 "Radio and toggles",
1902 UiSize::new(360.0, 320.0),
1903 );
1904 push_window(
1905 &mut windows,
1906 state.windows.slider,
1907 "slider",
1908 "Slider",
1909 UiSize::new(430.0, 560.0),
1910 );
1911 push_window(
1912 &mut windows,
1913 state.windows.numeric,
1914 "numeric",
1915 "Numeric input",
1916 UiSize::new(360.0, 180.0),
1917 );
1918 push_window(
1919 &mut windows,
1920 state.windows.text_input,
1921 "text_input",
1922 "Text input",
1923 UiSize::new(520.0, 560.0),
1924 );
1925 push_window(
1926 &mut windows,
1927 state.windows.selection,
1928 "selection",
1929 "Select controls",
1930 UiSize::new(360.0, 360.0),
1931 );
1932 push_window(
1933 &mut windows,
1934 state.windows.menus,
1935 "menus",
1936 "Menus",
1937 UiSize::new(wide, 520.0),
1938 );
1939 push_window(
1940 &mut windows,
1941 state.windows.command_palette,
1942 "command_palette",
1943 "Command palette",
1944 UiSize::new(520.0, 320.0),
1945 );
1946 push_window(
1947 &mut windows,
1948 state.windows.date_picker,
1949 "date_picker",
1950 "Date picker",
1951 UiSize::new(430.0, 390.0),
1952 );
1953 push_window(
1954 &mut windows,
1955 state.windows.color_picker,
1956 "color_picker",
1957 "Color picker",
1958 UiSize::new(340.0, 390.0),
1959 );
1960 push_window(
1961 &mut windows,
1962 state.windows.color_buttons,
1963 "color_buttons",
1964 "Color buttons",
1965 UiSize::new(430.0, 360.0),
1966 );
1967 push_window(
1968 &mut windows,
1969 state.windows.progress,
1970 "progress",
1971 "Progress indicator",
1972 UiSize::new(500.0, 168.0),
1973 );
1974 push_window(
1975 &mut windows,
1976 state.windows.lists_tables,
1977 "lists_tables",
1978 "Lists and tables",
1979 UiSize::new(wide, 620.0),
1980 );
1981 push_window(
1982 &mut windows,
1983 state.windows.property_inspector,
1984 "property_inspector",
1985 "Property inspector",
1986 UiSize::new(330.0, 250.0),
1987 );
1988 push_window(
1989 &mut windows,
1990 state.windows.trees,
1991 "trees",
1992 "Trees",
1993 UiSize::new(430.0, 390.0),
1994 );
1995 push_window(
1996 &mut windows,
1997 state.windows.layout_widgets,
1998 "layout_widgets",
1999 "Layout widgets",
2000 UiSize::new(wide.min(560.0), 400.0),
2001 );
2002 push_window(
2003 &mut windows,
2004 state.windows.containers,
2005 "containers",
2006 "Containers",
2007 UiSize::new(560.0, 640.0),
2008 );
2009 push_window(
2010 &mut windows,
2011 state.windows.forms,
2012 "forms",
2013 "Forms",
2014 UiSize::new(460.0, 520.0),
2015 );
2016 push_window(
2017 &mut windows,
2018 state.windows.overlays,
2019 "overlays",
2020 "Overlays",
2021 UiSize::new(560.0, 500.0),
2022 );
2023 push_window(
2024 &mut windows,
2025 state.windows.drag_drop,
2026 "drag_drop",
2027 "Drag and drop",
2028 UiSize::new(390.0, 340.0),
2029 );
2030 push_window(
2031 &mut windows,
2032 state.windows.media,
2033 "media",
2034 "Media",
2035 UiSize::new(360.0, 280.0),
2036 );
2037 push_window(
2038 &mut windows,
2039 state.windows.timeline,
2040 "timeline",
2041 "Timeline",
2042 UiSize::new(600.0, 120.0),
2043 );
2044 push_window(
2045 &mut windows,
2046 state.windows.toasts,
2047 "toasts",
2048 "Toasts",
2049 UiSize::new(320.0, 270.0),
2050 );
2051 push_window(
2052 &mut windows,
2053 state.windows.popup_panel,
2054 "popup_panel",
2055 "Popup panel",
2056 UiSize::new(360.0, 200.0),
2057 );
2058 push_window(
2059 &mut windows,
2060 state.windows.canvas,
2061 "canvas",
2062 "Canvas",
2063 UiSize::new(420.0, 292.0),
2064 );
2065 push_window(
2066 &mut windows,
2067 state.windows.styling,
2068 "styling",
2069 "Styling",
2070 UiSize::new(640.0, 560.0),
2071 );
2072 for window in &mut windows {
2073 window.drag_action = Some(WidgetActionBinding::action(format!(
2074 "window.drag.{}",
2075 window.id
2076 )));
2077 window.collapse_action = Some(WidgetActionBinding::action(format!(
2078 "window.collapse.{}",
2079 window.id
2080 )));
2081 window.resize_action = Some(WidgetActionBinding::action(format!(
2082 "window.resize.{}",
2083 window.id
2084 )));
2085 state
2086 .desktop
2087 .apply_to_descriptor(window, window_defaults(window.id.as_str()));
2088 }
2089 windows
2090}
2091
2092fn push_window(
2093 windows: &mut Vec<widgets::FloatingWindowDescriptor>,
2094 visible: bool,
2095 id: &'static str,
2096 title: &'static str,
2097 preferred_size: UiSize,
2098) {
2099 if visible {
2100 windows.push(
2101 widgets::FloatingWindowDescriptor::new(id, title, preferred_size)
2102 .with_min_size(default_window_state_min_size(id))
2103 .with_auto_size_to_content(true)
2104 .with_activate_action(format!("window.activate.{id}"))
2105 .with_close_action(format!("window.close.{id}")),
2106 );
2107 }
2108}
2109
2110fn default_window_size(id: &str) -> UiSize {
2111 match id {
2112 "labels" => UiSize::new(380.0, 460.0),
2113 "buttons" => UiSize::new(604.0, 220.0),
2114 "checkbox" => UiSize::new(250.0, 72.0),
2115 "toggles" => UiSize::new(360.0, 380.0),
2116 "slider" => UiSize::new(430.0, 560.0),
2117 "numeric" => UiSize::new(430.0, 180.0),
2118 "text_input" => UiSize::new(520.0, 640.0),
2119 "selection" => UiSize::new(360.0, 360.0),
2120 "menus" => UiSize::new(640.0, 640.0),
2121 "command_palette" => UiSize::new(520.0, 320.0),
2122 "date_picker" => UiSize::new(430.0, 390.0),
2123 "color_picker" => UiSize::new(340.0, 390.0),
2124 "color_buttons" => UiSize::new(430.0, 360.0),
2125 "progress" => UiSize::new(500.0, 168.0),
2126 "lists_tables" => UiSize::new(600.0, 700.0),
2127 "property_inspector" => UiSize::new(330.0, 250.0),
2128 "trees" => UiSize::new(430.0, 450.0),
2129 "layout_widgets" => UiSize::new(560.0, 400.0),
2130 "containers" => UiSize::new(560.0, 640.0),
2131 "forms" => UiSize::new(460.0, 520.0),
2132 "overlays" => UiSize::new(560.0, 500.0),
2133 "drag_drop" => UiSize::new(390.0, 340.0),
2134 "media" => UiSize::new(360.0, 280.0),
2135 "timeline" => UiSize::new(600.0, 120.0),
2136 "toasts" => UiSize::new(320.0, 270.0),
2137 "popup_panel" => UiSize::new(360.0, 200.0),
2138 "canvas" => UiSize::new(420.0, 292.0),
2139 "styling" => UiSize::new(640.0, 560.0),
2140 _ => UiSize::new(300.0, 180.0),
2141 }
2142}
2143
2144fn default_window_state_min_size(_id: &str) -> UiSize {
2145 UiSize::new(160.0, 96.0)
2146}
2147
2148fn default_window_position(id: &str) -> UiPoint {
2149 match id {
2150 "labels" => UiPoint::new(18.0, 18.0),
2151 "buttons" => UiPoint::new(420.0, 18.0),
2152 "checkbox" => UiPoint::new(360.0, 18.0),
2153 "toggles" => UiPoint::new(360.0, 110.0),
2154 "slider" => UiPoint::new(360.0, 110.0),
2155 "numeric" => UiPoint::new(360.0, 260.0),
2156 "text_input" => UiPoint::new(360.0, 18.0),
2157 "selection" => UiPoint::new(360.0, 404.0),
2158 "menus" => UiPoint::new(18.0, 18.0),
2159 "command_palette" => UiPoint::new(68.0, 88.0),
2160 "date_picker" => UiPoint::new(300.0, 170.0),
2161 "color_picker" => UiPoint::new(18.0, 560.0),
2162 "color_buttons" => UiPoint::new(380.0, 500.0),
2163 "progress" => UiPoint::new(72.0, 540.0),
2164 "lists_tables" => UiPoint::new(18.0, 90.0),
2165 "property_inspector" => UiPoint::new(300.0, 420.0),
2166 "trees" => UiPoint::new(36.0, 220.0),
2167 "layout_widgets" => UiPoint::new(18.0, 18.0),
2168 "containers" => UiPoint::new(48.0, 120.0),
2169 "forms" => UiPoint::new(120.0, 160.0),
2170 "overlays" => UiPoint::new(80.0, 110.0),
2171 "drag_drop" => UiPoint::new(210.0, 250.0),
2172 "media" => UiPoint::new(120.0, 360.0),
2173 "timeline" => UiPoint::new(18.0, 620.0),
2174 "toasts" => UiPoint::new(320.0, 70.0),
2175 "popup_panel" => UiPoint::new(320.0, 370.0),
2176 "canvas" => UiPoint::new(380.0, 560.0),
2177 "styling" => UiPoint::new(86.0, 118.0),
2178 _ => UiPoint::new(18.0, 18.0),
2179 }
2180}
2181
2182fn window_for_action(action_id: &str) -> Option<&'static str> {
2183 match action_id {
2184 id if id.starts_with("labels.") => Some("labels"),
2185 id if id.starts_with("button.") => Some("buttons"),
2186 id if id.starts_with("checkbox.") => Some("checkbox"),
2187 id if id.starts_with("toggles.") => Some("toggles"),
2188 id if id.starts_with("theme.preference.") => Some("toggles"),
2189 id if id.starts_with("slider.") => Some("slider"),
2190 id if id.starts_with("numeric.") => Some("numeric"),
2191 id if id.starts_with("text.") => Some("text_input"),
2192 id if id.starts_with("combo.")
2193 || id.starts_with("selection.dropdown.")
2194 || id.starts_with("selection.menu.") =>
2195 {
2196 Some("selection")
2197 }
2198 id if id.starts_with("menus.") => Some("menus"),
2199 id if id.starts_with("command_palette.") => Some("command_palette"),
2200 id if id.starts_with("date.") => Some("date_picker"),
2201 id if id.starts_with("color.") => Some("color_picker"),
2202 id if id.starts_with("color_buttons.") => Some("color_buttons"),
2203 id if id.starts_with("progress.") => Some("progress"),
2204 id if id.starts_with("lists_tables.") => Some("lists_tables"),
2205 id if id.starts_with("property_inspector.") => Some("property_inspector"),
2206 id if id.starts_with("trees.") => Some("trees"),
2207 id if id.starts_with("layout.") || id.starts_with("layout_widgets.") => {
2208 Some("layout_widgets")
2209 }
2210 id if id.starts_with("containers.") => Some("containers"),
2211 id if id.starts_with("forms.") => Some("forms"),
2212 id if id.starts_with("overlays.") => Some("overlays"),
2213 id if id.starts_with("drag_drop.") => Some("drag_drop"),
2214 id if id.starts_with("media.") => Some("media"),
2215 id if id.starts_with("toast.") => Some("toasts"),
2216 id if id.starts_with("popup.") => Some("popup_panel"),
2217 id if id.starts_with("canvas.") => Some("canvas"),
2218 id if id.starts_with("styling.") => Some("styling"),
2219 _ => None,
2220 }
2221}
2222
2223fn focused_text_for_action(action_id: &str) -> Option<FocusedTextInput> {
2224 Some(match action_id {
2225 "text.input.edit" => FocusedTextInput::Editable,
2226 "text.selectable.edit" => FocusedTextInput::Selectable,
2227 "text.singleline.edit" => FocusedTextInput::Singleline,
2228 "text.multiline.edit" => FocusedTextInput::Multiline,
2229 "text.area.edit" => FocusedTextInput::TextArea,
2230 "text.code_editor.edit" => FocusedTextInput::CodeEditor,
2231 "text.search.edit" => FocusedTextInput::Search,
2232 "text.password.edit" => FocusedTextInput::Password,
2233 "text.selectable_helper.edit" => FocusedTextInput::SelectableHelper,
2234 "slider.value_text.edit" => FocusedTextInput::SliderValue,
2235 "slider.left_text.edit" => FocusedTextInput::SliderRangeLeft,
2236 "slider.right_text.edit" => FocusedTextInput::SliderRangeRight,
2237 "slider.step_text.edit" => FocusedTextInput::SliderStep,
2238 _ => return None,
2239 })
2240}
2241
2242fn control_panel(
2243 ui: &mut UiDocument,
2244 parent: UiNodeId,
2245 state: &ShowcaseState,
2246 viewport_height: f32,
2247) {
2248 widgets::label(
2249 ui,
2250 parent,
2251 "controls.title",
2252 "Widgets",
2253 text(16.0, color(244, 248, 252)),
2254 LayoutStyle::new().with_width_percent(1.0),
2255 );
2256 let list_viewport_height = controls_list_viewport_height(viewport_height);
2257 let list_row = ui.add_child(
2258 parent,
2259 UiNode::container(
2260 "controls.widget_list.row",
2261 LayoutStyle::row()
2262 .with_width_percent(1.0)
2263 .with_height(list_viewport_height)
2264 .with_flex_grow(1.0)
2265 .with_flex_shrink(1.0)
2266 .gap(6.0),
2267 ),
2268 );
2269 let list = widgets::scroll_area(
2270 ui,
2271 list_row,
2272 "controls.widget_list",
2273 ScrollAxes::VERTICAL,
2274 LayoutStyle::column()
2275 .with_width(0.0)
2276 .with_height_percent(1.0)
2277 .with_flex_grow(1.0)
2278 .gap(CONTROLS_WIDGET_ROW_GAP),
2279 );
2280 ui.node_mut(list).action = Some("controls.widget_list.scroll".into());
2281 if let Some(scroll) = ui.node_mut(list).scroll.as_mut() {
2282 scroll.offset.y = controls_scroll_state(state.controls_scroll, list_viewport_height)
2283 .offset
2284 .y;
2285 }
2286 widgets::scrollbar(
2287 ui,
2288 list_row,
2289 "controls.widget_list.scrollbar",
2290 controls_scroll_state(state.controls_scroll, list_viewport_height),
2291 widgets::ScrollAxis::Vertical,
2292 widgets::ScrollbarOptions::default()
2293 .with_layout(LayoutStyle::new().with_width(8.0).with_height_percent(1.0))
2294 .with_track_size(UiSize::new(8.0, list_viewport_height))
2295 .with_action("controls.widget_list.scrollbar"),
2296 );
2297
2298 window_toggle(ui, list, "labels", "Labels", state.windows.labels);
2299 window_toggle(ui, list, "buttons", "Buttons", state.windows.buttons);
2300 window_toggle(ui, list, "checkbox", "Checkbox", state.windows.checkbox);
2301 window_toggle(
2302 ui,
2303 list,
2304 "toggles",
2305 "Radio and toggles",
2306 state.windows.toggles,
2307 );
2308 window_toggle(ui, list, "slider", "Slider", state.windows.slider);
2309 window_toggle(ui, list, "numeric", "Numeric input", state.windows.numeric);
2310 window_toggle(
2311 ui,
2312 list,
2313 "text_input",
2314 "Text input",
2315 state.windows.text_input,
2316 );
2317 window_toggle(
2318 ui,
2319 list,
2320 "selection",
2321 "Select controls",
2322 state.windows.selection,
2323 );
2324 window_toggle(ui, list, "menus", "Menus", state.windows.menus);
2325 window_toggle(
2326 ui,
2327 list,
2328 "command_palette",
2329 "Command palette",
2330 state.windows.command_palette,
2331 );
2332 window_toggle(
2333 ui,
2334 list,
2335 "date_picker",
2336 "Date picker",
2337 state.windows.date_picker,
2338 );
2339 window_toggle(
2340 ui,
2341 list,
2342 "color_picker",
2343 "Color picker",
2344 state.windows.color_picker,
2345 );
2346 window_toggle(
2347 ui,
2348 list,
2349 "color_buttons",
2350 "Color buttons",
2351 state.windows.color_buttons,
2352 );
2353 window_toggle(
2354 ui,
2355 list,
2356 "progress",
2357 "Progress indicator",
2358 state.windows.progress,
2359 );
2360 window_toggle(
2361 ui,
2362 list,
2363 "lists_tables",
2364 "Lists and tables",
2365 state.windows.lists_tables,
2366 );
2367 window_toggle(
2368 ui,
2369 list,
2370 "property_inspector",
2371 "Property inspector",
2372 state.windows.property_inspector,
2373 );
2374 window_toggle(ui, list, "trees", "Trees", state.windows.trees);
2375 window_toggle(
2376 ui,
2377 list,
2378 "layout_widgets",
2379 "Layout widgets",
2380 state.windows.layout_widgets,
2381 );
2382 window_toggle(
2383 ui,
2384 list,
2385 "containers",
2386 "Containers",
2387 state.windows.containers,
2388 );
2389 window_toggle(ui, list, "forms", "Forms", state.windows.forms);
2390 window_toggle(ui, list, "overlays", "Overlays", state.windows.overlays);
2391 window_toggle(
2392 ui,
2393 list,
2394 "drag_drop",
2395 "Drag and drop",
2396 state.windows.drag_drop,
2397 );
2398 window_toggle(ui, list, "media", "Media", state.windows.media);
2399 window_toggle(ui, list, "timeline", "Timeline", state.windows.timeline);
2400 window_toggle(ui, list, "toasts", "Toasts", state.windows.toasts);
2401 window_toggle(
2402 ui,
2403 list,
2404 "popup_panel",
2405 "Popup panel",
2406 state.windows.popup_panel,
2407 );
2408 window_toggle(ui, list, "canvas", "Canvas", state.windows.canvas);
2409 window_toggle(ui, list, "styling", "Styling", state.windows.styling);
2410
2411 ui.add_child(
2412 parent,
2413 UiNode::container(
2414 "controls.clear_all.spacer",
2415 LayoutStyle::new()
2416 .with_width_percent(1.0)
2417 .with_height(1.0)
2418 .with_flex_grow(1.0)
2419 .with_flex_shrink(1.0),
2420 ),
2421 );
2422 let mut clear =
2423 widgets::ButtonOptions::new(LayoutStyle::new().with_width_percent(1.0).with_height(30.0))
2424 .with_action("window.clear_all");
2425 clear.visual = UiVisual::panel(
2426 color(31, 38, 48),
2427 Some(StrokeStyle::new(color(76, 88, 106), 1.0)),
2428 4.0,
2429 );
2430 clear.hovered_visual = Some(UiVisual::panel(
2431 color(45, 56, 70),
2432 Some(StrokeStyle::new(color(118, 144, 174), 1.0)),
2433 4.0,
2434 ));
2435 clear.pressed_visual = Some(UiVisual::panel(
2436 color(20, 27, 36),
2437 Some(StrokeStyle::new(color(82, 104, 132), 1.0)),
2438 4.0,
2439 ));
2440 clear.pressed_hovered_visual = Some(UiVisual::panel(
2441 color(36, 48, 62),
2442 Some(StrokeStyle::new(color(138, 170, 206), 1.0)),
2443 4.0,
2444 ));
2445 clear.text_style = text(12.0, color(230, 236, 246));
2446 clear.accessibility_label = Some("Clear all widgets".to_string());
2447 widgets::button(ui, parent, "controls.clear_all", "Clear all", clear);
2448}
2449
2450fn window_toggle(
2451 ui: &mut UiDocument,
2452 parent: UiNodeId,
2453 id: &'static str,
2454 label: &'static str,
2455 checked: bool,
2456) {
2457 let mut options =
2458 widgets::CheckboxOptions::default().with_action(format!("window.toggle.{id}"));
2459 options.layout = LayoutStyle::new()
2460 .with_width_percent(1.0)
2461 .with_height(CONTROLS_WIDGET_ROW_HEIGHT);
2462 options.text_style = text(12.0, color(220, 228, 238));
2463 widgets::checkbox(
2464 ui,
2465 parent,
2466 format!("controls.{id}"),
2467 label,
2468 checked,
2469 options,
2470 );
2471}
2472
2473fn labels(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
2474 let body = section(ui, parent, "labels", "Labels");
2475 ui.node_mut(body).style.layout = LayoutStyle::column()
2476 .with_width_percent(1.0)
2477 .with_height_percent(1.0)
2478 .with_flex_grow(1.0)
2479 .gap(10.0)
2480 .as_taffy_style()
2481 .clone();
2482 widgets::label(
2483 ui,
2484 body,
2485 "labels.plain",
2486 "Plain label",
2487 text(13.0, color(226, 232, 242)),
2488 LayoutStyle::new().with_width_percent(1.0),
2489 );
2490 let locale_items = label_locale_options();
2491 let locale_id = state
2492 .label_locale
2493 .selected_id(&locale_items)
2494 .unwrap_or("es-MX");
2495 let localization = LocalizationPolicy::new(LocaleId::new(locale_id).expect("valid locale"));
2496 let locale_row = ui.add_child(
2497 body,
2498 UiNode::container(
2499 "labels.locale.row",
2500 LayoutStyle::row()
2501 .with_width_percent(1.0)
2502 .with_align_items(taffy::prelude::AlignItems::Center)
2503 .gap(10.0),
2504 ),
2505 );
2506 let locale_label_width = 270.0;
2507 let locale_dropdown_width = 148.0;
2508 let locale_gap = 10.0;
2509 widgets::localized_label(
2510 ui,
2511 locale_row,
2512 "labels.localized",
2513 DynamicLabelMeta::keyed("showcase.localized.greeting", localized_label(locale_id)),
2514 Some(&localization),
2515 text(13.0, color(170, 202, 255)),
2516 LayoutStyle::new().with_width(locale_label_width),
2517 );
2518 let mut locale_options = widgets::DropdownSelectOptions::default();
2519 locale_options.trigger_layout = LayoutStyle::row()
2520 .with_width(locale_dropdown_width)
2521 .with_height(30.0)
2522 .with_align_items(taffy::prelude::AlignItems::Center)
2523 .with_justify_content(taffy::prelude::JustifyContent::Center)
2524 .padding(6.0);
2525 locale_options.text_style = text(13.0, color(226, 232, 242));
2526 locale_options.accessibility_label = Some("Locale".to_string());
2527 locale_options.menu = widgets::SelectMenuOptions::default().with_action_prefix("labels.locale");
2528 locale_options.menu.width = locale_dropdown_width;
2529 locale_options.menu.row_height = 30.0;
2530 locale_options.menu.max_visible_rows = locale_items.len();
2531 locale_options.menu.text_style = text(13.0, color(226, 232, 242));
2532 let locale_nodes = widgets::dropdown_select(
2533 ui,
2534 locale_row,
2535 "labels.locale",
2536 &locale_items,
2537 &state.label_locale,
2538 Some(widgets::AnchoredPopup::new(
2539 UiRect::new(
2540 locale_label_width + locale_gap,
2541 0.0,
2542 locale_dropdown_width,
2543 30.0,
2544 ),
2545 UiRect::new(0.0, 0.0, 460.0, 260.0),
2546 widgets::PopupPlacement::default(),
2547 )),
2548 locale_options,
2549 );
2550 ui.node_mut(locale_nodes.trigger).action = Some("labels.locale.toggle".into());
2551 widgets::label(
2552 ui,
2553 body,
2554 "labels.muted",
2555 "Muted helper label",
2556 text(12.0, color(154, 166, 184)),
2557 LayoutStyle::new().with_width_percent(1.0),
2558 );
2559
2560 let sizes = ui.add_child(
2561 body,
2562 UiNode::container(
2563 "labels.sizes",
2564 LayoutStyle::row()
2565 .with_width_percent(1.0)
2566 .with_align_items(taffy::prelude::AlignItems::FlexEnd)
2567 .gap(12.0),
2568 ),
2569 );
2570 widgets::label(
2571 ui,
2572 sizes,
2573 "labels.size.small",
2574 "12px",
2575 text(12.0, color(226, 232, 242)),
2576 LayoutStyle::new(),
2577 );
2578 widgets::label(
2579 ui,
2580 sizes,
2581 "labels.size.default",
2582 "13px",
2583 text(13.0, color(226, 232, 242)),
2584 LayoutStyle::new(),
2585 );
2586 widgets::label(
2587 ui,
2588 sizes,
2589 "labels.size.large",
2590 "18px",
2591 text(18.0, color(246, 249, 252)),
2592 LayoutStyle::new(),
2593 );
2594 widgets::label(
2595 ui,
2596 sizes,
2597 "labels.size.display",
2598 "24px",
2599 text(24.0, color(246, 249, 252)),
2600 LayoutStyle::new(),
2601 );
2602
2603 let style_row = row(ui, body, "labels.styles", 12.0);
2604 let mut bold = text(13.0, color(246, 249, 252));
2605 bold.weight = FontWeight::BOLD;
2606 widgets::label(
2607 ui,
2608 style_row,
2609 "labels.style.bold",
2610 "Bold",
2611 bold,
2612 LayoutStyle::new(),
2613 );
2614 widgets::label(
2615 ui,
2616 style_row,
2617 "labels.style.weak",
2618 "Muted",
2619 text(13.0, color(154, 166, 184)),
2620 LayoutStyle::new(),
2621 );
2622
2623 let font_row = row(ui, body, "labels.fonts", 12.0);
2624 let mut serif = text(13.0, color(226, 232, 242));
2625 serif.family = FontFamily::Serif;
2626 widgets::label(
2627 ui,
2628 font_row,
2629 "labels.font.serif",
2630 "Serif",
2631 serif,
2632 LayoutStyle::new(),
2633 );
2634 let mut mono = text(13.0, color(226, 232, 242));
2635 mono.family = FontFamily::Monospace;
2636 widgets::label(
2637 ui,
2638 font_row,
2639 "labels.font.mono",
2640 "Monospace",
2641 mono,
2642 LayoutStyle::new(),
2643 );
2644
2645 let code_panel = ui.add_child(
2646 body,
2647 UiNode::container(
2648 "labels.code.panel",
2649 LayoutStyle::new()
2650 .with_width_percent(1.0)
2651 .padding(8.0)
2652 .with_height(36.0),
2653 )
2654 .with_visual(UiVisual::panel(
2655 color(10, 14, 20),
2656 Some(StrokeStyle::new(color(47, 59, 74), 1.0)),
2657 4.0,
2658 )),
2659 );
2660 widgets::code_label(
2661 ui,
2662 code_panel,
2663 "labels.code",
2664 "let label = widgets::label(...);",
2665 LayoutStyle::new().with_width_percent(1.0),
2666 );
2667
2668 let colors = row(ui, body, "labels.colors", 14.0);
2669 widgets::colored_label(
2670 ui,
2671 colors,
2672 "labels.color.green",
2673 "Green",
2674 color(111, 203, 159),
2675 LayoutStyle::new(),
2676 );
2677 widgets::colored_label(
2678 ui,
2679 colors,
2680 "labels.color.yellow",
2681 "Yellow",
2682 color(232, 196, 101),
2683 LayoutStyle::new(),
2684 );
2685 widgets::colored_label(
2686 ui,
2687 colors,
2688 "labels.color.red",
2689 "Red",
2690 color(244, 118, 118),
2691 LayoutStyle::new(),
2692 );
2693
2694 let wrap_row = wrapping_row(ui, body, "labels.wrap.row", 10.0);
2695 let wrap_word = ui.add_child(
2696 wrap_row,
2697 UiNode::container(
2698 "labels.wrap.word.panel",
2699 LayoutStyle::column().with_width(172.0).padding(8.0),
2700 )
2701 .with_visual(UiVisual::panel(
2702 color(18, 23, 31),
2703 Some(StrokeStyle::new(color(47, 59, 74), 1.0)),
2704 4.0,
2705 )),
2706 );
2707 widgets::wrapped_label(
2708 ui,
2709 wrap_word,
2710 "labels.wrap.word",
2711 "Word wrapping keeps this sentence readable in a narrow box.",
2712 TextWrap::Word,
2713 LayoutStyle::new().with_width_percent(1.0),
2714 );
2715 let wrap_glyph = ui.add_child(
2716 wrap_row,
2717 UiNode::container(
2718 "labels.wrap.glyph.panel",
2719 LayoutStyle::column().with_width(172.0).padding(8.0),
2720 )
2721 .with_visual(UiVisual::panel(
2722 color(18, 23, 31),
2723 Some(StrokeStyle::new(color(47, 59, 74), 1.0)),
2724 4.0,
2725 )),
2726 );
2727 widgets::wrapped_label(
2728 ui,
2729 wrap_glyph,
2730 "labels.wrap.glyph",
2731 "LongIdentifierWithoutSpaces",
2732 TextWrap::Glyph,
2733 LayoutStyle::new().with_width_percent(1.0),
2734 );
2735
2736 let links = wrapping_row(ui, body, "labels.links", 12.0);
2737 widgets::link(
2738 ui,
2739 links,
2740 "labels.link",
2741 "Internal action",
2742 widgets::LinkOptions::default()
2743 .visited(state.label_link_visited)
2744 .with_action("labels.link"),
2745 );
2746 widgets::hyperlink(
2747 ui,
2748 links,
2749 "labels.hyperlink",
2750 "Open docs.rs",
2751 "https://docs.rs/operad",
2752 widgets::LinkOptions::default()
2753 .visited(state.label_hyperlink_visited)
2754 .with_action("labels.hyperlink"),
2755 );
2756 if state.label_link_status != "No link action yet" {
2757 widgets::label(
2758 ui,
2759 body,
2760 "labels.status",
2761 format!("Last action: {}", state.label_link_status),
2762 text(12.0, color(154, 166, 184)),
2763 LayoutStyle::new().with_width_percent(1.0),
2764 );
2765 }
2766}
2767
2768fn buttons(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
2769 let body = section(ui, parent, "buttons", "Buttons");
2770 let primary_row = wrapping_row(ui, body, "buttons.row", 10.0);
2771 button(
2772 ui,
2773 primary_row,
2774 "button.default",
2775 "Default",
2776 "button.default",
2777 button_visual(38, 46, 58),
2778 );
2779 button(
2780 ui,
2781 primary_row,
2782 "button.primary",
2783 "Primary",
2784 "button.primary",
2785 button_visual(48, 112, 184),
2786 );
2787 button(
2788 ui,
2789 primary_row,
2790 "button.secondary",
2791 "Secondary",
2792 "button.secondary",
2793 button_visual(58, 78, 96),
2794 );
2795 button(
2796 ui,
2797 primary_row,
2798 "button.destructive",
2799 "Destructive",
2800 "button.destructive",
2801 button_visual(157, 65, 73),
2802 );
2803 let mut disabled = widgets::ButtonOptions::new(LayoutStyle::size(92.0, 32.0));
2804 disabled.enabled = false;
2805 disabled.visual = button_visual(40, 44, 52);
2806 disabled.text_style = text(13.0, color(138, 146, 158));
2807 widgets::button(ui, primary_row, "button.disabled", "Disabled", disabled);
2808 let second_row = wrapping_row(ui, body, "buttons.row.options", 10.0);
2809 button(
2810 ui,
2811 second_row,
2812 "button.momentary",
2813 "Press only",
2814 "button.default",
2815 button_visual(42, 50, 62),
2816 );
2817 let mut toggle =
2818 widgets::ButtonOptions::new(LayoutStyle::size(112.0, 32.0)).with_action("button.toggle");
2819 toggle.pressed = state.toggle_button;
2820 toggle.visual = button_visual(42, 50, 62);
2821 toggle.hovered_visual = Some(button_visual(62, 74, 92));
2822 toggle.pressed_visual = Some(button_visual(86, 64, 156));
2823 toggle.pressed_hovered_visual = Some(button_visual(126, 94, 218));
2824 toggle.text_style = text(13.0, color(246, 249, 252));
2825 widgets::button(
2826 ui,
2827 second_row,
2828 "button.toggle",
2829 if state.toggle_button {
2830 "Toggle on"
2831 } else {
2832 "Toggle off"
2833 },
2834 toggle,
2835 );
2836 let mut forced_pressed = widgets::ButtonOptions::new(LayoutStyle::size(112.0, 32.0));
2837 forced_pressed.pressed = true;
2838 forced_pressed.visual = button_visual(42, 50, 62);
2839 forced_pressed.hovered_visual = Some(button_visual(62, 74, 92));
2840 forced_pressed.pressed_visual = Some(button_visual(38, 82, 136));
2841 forced_pressed.pressed_hovered_visual = Some(button_visual(62, 126, 196));
2842 forced_pressed.text_style = text(13.0, color(246, 249, 252));
2843 widgets::button(
2844 ui,
2845 second_row,
2846 "button.state.pressed",
2847 "Pressed",
2848 forced_pressed,
2849 );
2850 let helper_row = wrapping_row(ui, body, "buttons.row.helpers", 10.0);
2851 widgets::small_button(
2852 ui,
2853 helper_row,
2854 "button.small",
2855 "Small",
2856 widgets::ButtonOptions::default().with_action("button.small"),
2857 );
2858 widgets::icon_button(
2859 ui,
2860 helper_row,
2861 "button.icon",
2862 icon_image(BuiltInIcon::Settings),
2863 "Settings",
2864 widgets::ButtonOptions::default().with_action("button.icon"),
2865 );
2866 widgets::image_button(
2867 ui,
2868 helper_row,
2869 "button.image",
2870 icon_image(BuiltInIcon::Folder),
2871 "Folder",
2872 widgets::ButtonOptions::default().with_action("button.image"),
2873 );
2874 widgets::reset_button(
2875 ui,
2876 helper_row,
2877 "button.reset",
2878 state.toggle_button,
2879 widgets::ButtonOptions::default().with_action("button.reset"),
2880 );
2881 widgets::toggle_button(
2882 ui,
2883 helper_row,
2884 "button.toggle_helper",
2885 "Toggle helper",
2886 state.toggle_button,
2887 widgets::ButtonOptions::default().with_action("button.toggle"),
2888 );
2889 widgets::label(
2890 ui,
2891 body,
2892 "buttons.last",
2893 format!("Last pressed: {}", state.last_button),
2894 text(12.0, color(154, 166, 184)),
2895 LayoutStyle::new().with_width_percent(1.0),
2896 );
2897}
2898
2899fn checkbox(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
2900 let body = section(ui, parent, "checkbox", "Checkbox");
2901 let mut options = widgets::CheckboxOptions::default().with_action("checkbox.enabled");
2902 options.text_style = text(13.0, color(222, 228, 238));
2903 widgets::checkbox(
2904 ui,
2905 body,
2906 "checkbox.enabled",
2907 if state.checked { "Enabled" } else { "Disabled" },
2908 state.checked,
2909 options,
2910 );
2911}
2912
2913fn toggles(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
2914 let body = section(ui, parent, "toggles", "Radio and toggles");
2915 let radio_options = [
2916 widgets::RadioOption::new("compact", "Compact").with_action("toggles.radio.compact"),
2917 widgets::RadioOption::new("comfortable", "Comfortable")
2918 .with_action("toggles.radio.comfortable"),
2919 widgets::RadioOption::new("spacious", "Spacious").with_action("toggles.radio.spacious"),
2920 widgets::RadioOption::new("disabled", "Disabled").enabled(false),
2921 ];
2922 widgets::radio_group(
2923 ui,
2924 body,
2925 "toggles.radio_group",
2926 &radio_options,
2927 Some(state.radio_choice),
2928 widgets::RadioGroupOptions::default(),
2929 );
2930 widgets::radio_button(
2931 ui,
2932 body,
2933 "toggles.radio_single",
2934 "Standalone radio button",
2935 true,
2936 widgets::RadioButtonOptions::default().with_action("toggles.radio.compact"),
2937 );
2938 widgets::toggle_switch(
2939 ui,
2940 body,
2941 "toggles.switch",
2942 if state.switch_enabled {
2943 "Switch on"
2944 } else {
2945 "Switch off"
2946 },
2947 widgets::ToggleValue::from(state.switch_enabled),
2948 widgets::ToggleSwitchOptions::default().with_action("toggles.switch"),
2949 );
2950 widgets::toggle_switch(
2951 ui,
2952 body,
2953 "toggles.mixed",
2954 match state.mixed_switch {
2955 widgets::ToggleValue::Mixed => "Mixed switch",
2956 widgets::ToggleValue::On => "Mixed switch on",
2957 widgets::ToggleValue::Off => "Mixed switch off",
2958 },
2959 state.mixed_switch,
2960 widgets::ToggleSwitchOptions::default().with_action("toggles.mixed"),
2961 );
2962 widgets::theme_preference_buttons(
2963 ui,
2964 body,
2965 "toggles.theme_buttons",
2966 state.theme_preference,
2967 widgets::ThemePreferenceButtonsOptions::default().with_action_prefix("toggles.theme"),
2968 );
2969 widgets::theme_preference_switch(
2970 ui,
2971 body,
2972 "toggles.theme_switch",
2973 state.theme_preference,
2974 widgets::ThemePreferenceSwitchOptions::default().with_action("theme.preference.dark"),
2975 );
2976}
2977
2978fn slider(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
2979 let body = section(ui, parent, "slider", "Slider");
2980 widgets::label(
2981 ui,
2982 body,
2983 "slider.note",
2984 "Click a slider value to edit it with the keyboard.",
2985 text(12.0, color(166, 176, 190)),
2986 LayoutStyle::new().with_width_percent(1.0),
2987 );
2988
2989 let value_row = row(ui, body, "slider.value.row", 10.0);
2990 let options = slider_options(state, 180.0).with_value_edit_action("slider.value");
2991 let slider_unit = state.slider_value_spec().normalize(state.slider);
2992 widgets::slider(
2993 ui,
2994 value_row,
2995 "slider.value",
2996 slider_unit,
2997 0.0..1.0,
2998 options.clone(),
2999 );
3000 slider_number_input(
3001 ui,
3002 value_row,
3003 "slider.value_text",
3004 &state.slider_value_text,
3005 FocusedTextInput::SliderValue,
3006 state,
3007 86.0,
3008 );
3009 widgets::label(
3010 ui,
3011 value_row,
3012 "slider.value.label",
3013 "f64 demo slider",
3014 text(12.0, color(186, 198, 216)),
3015 LayoutStyle::new().with_width_percent(1.0),
3016 );
3017
3018 widgets::label(
3019 ui,
3020 body,
3021 "slider.precision",
3022 format!(
3023 "Displayed value: {} Full precision: {:.6}",
3024 widgets::format_slider_value(state.slider),
3025 state.slider
3026 ),
3027 text(11.0, color(154, 166, 184)),
3028 LayoutStyle::new().with_width_percent(1.0),
3029 );
3030
3031 divider(ui, body, "slider.divider.range");
3032 widgets::label(
3033 ui,
3034 body,
3035 "slider.range.label",
3036 "Slider range",
3037 text(12.0, color(220, 228, 238)),
3038 LayoutStyle::new().with_width_percent(1.0),
3039 );
3040 let left_row = row(ui, body, "slider.range.left.row", 10.0);
3041 let left_options = widgets::SliderOptions::default()
3042 .with_layout(
3043 LayoutStyle::new()
3044 .with_width(180.0)
3045 .with_height(24.0)
3046 .with_flex_shrink(0.0),
3047 )
3048 .with_value_edit_action("slider.range_left");
3049 widgets::slider(
3050 ui,
3051 left_row,
3052 "slider.range_left",
3053 state.slider_left,
3054 0.0..state.slider_right.max(1.0),
3055 left_options,
3056 );
3057 slider_number_input(
3058 ui,
3059 left_row,
3060 "slider.left_text",
3061 &state.slider_left_text,
3062 FocusedTextInput::SliderRangeLeft,
3063 state,
3064 96.0,
3065 );
3066 widgets::label(
3067 ui,
3068 left_row,
3069 "slider.range.left.label",
3070 "left",
3071 text(12.0, color(186, 198, 216)),
3072 LayoutStyle::new().with_width(46.0),
3073 );
3074 let right_row = row(ui, body, "slider.range.right.row", 10.0);
3075 let right_options = widgets::SliderOptions::default()
3076 .with_layout(
3077 LayoutStyle::new()
3078 .with_width(180.0)
3079 .with_height(24.0)
3080 .with_flex_shrink(0.0),
3081 )
3082 .with_value_edit_action("slider.range_right");
3083 widgets::slider(
3084 ui,
3085 right_row,
3086 "slider.range_right",
3087 state.slider_right,
3088 (state.slider_left + 1.0)..10000.0,
3089 right_options,
3090 );
3091 slider_number_input(
3092 ui,
3093 right_row,
3094 "slider.right_text",
3095 &state.slider_right_text,
3096 FocusedTextInput::SliderRangeRight,
3097 state,
3098 96.0,
3099 );
3100 widgets::label(
3101 ui,
3102 right_row,
3103 "slider.range.right.label",
3104 "right",
3105 text(12.0, color(186, 198, 216)),
3106 LayoutStyle::new().with_width(46.0),
3107 );
3108
3109 divider(ui, body, "slider.divider.trailing");
3110 let trailing_row = row(ui, body, "slider.trailing.row", 8.0);
3111 slider_checkbox_with_layout(
3112 ui,
3113 trailing_row,
3114 "slider.trailing",
3115 "Trailing color",
3116 state.slider_trailing_color,
3117 LayoutStyle::new()
3118 .with_width(142.0)
3119 .with_height(30.0)
3120 .with_flex_shrink(0.0),
3121 );
3122 widgets::color_edit_button_rgb(
3123 ui,
3124 trailing_row,
3125 "slider.trailing_color_button",
3126 state.slider_trailing_picker.value,
3127 color_square_button_options("slider.trailing_color_button")
3128 .accessibility_label("Pick trailing slider color"),
3129 );
3130 if state.slider_trailing_picker_open {
3131 widgets::color_picker(
3132 ui,
3133 body,
3134 "slider.trailing_picker",
3135 &state.slider_trailing_picker,
3136 widgets::ColorPickerOptions::default()
3137 .with_label("Trailing slider color")
3138 .with_action_prefix("slider.trailing_picker"),
3139 );
3140 }
3141 let thumb_row = row(ui, body, "slider.thumb.row", 8.0);
3142 widgets::label(
3143 ui,
3144 thumb_row,
3145 "slider.thumb.label",
3146 "Thumb",
3147 text(12.0, color(166, 176, 190)),
3148 LayoutStyle::new().with_width(64.0),
3149 );
3150 choice_button(
3151 ui,
3152 thumb_row,
3153 "slider.thumb.circle",
3154 "Circle",
3155 state.slider_thumb_shape == SliderThumbChoice::Circle,
3156 );
3157 choice_button(
3158 ui,
3159 thumb_row,
3160 "slider.thumb.square",
3161 "Square",
3162 state.slider_thumb_shape == SliderThumbChoice::Square,
3163 );
3164 choice_button(
3165 ui,
3166 thumb_row,
3167 "slider.thumb.rectangle",
3168 "Rectangle",
3169 state.slider_thumb_shape == SliderThumbChoice::Rectangle,
3170 );
3171 slider_checkbox(
3172 ui,
3173 body,
3174 "slider.steps",
3175 "Use steps",
3176 state.slider_use_steps,
3177 );
3178 let step_row = row(ui, body, "slider.step.row", 10.0);
3179 widgets::label(
3180 ui,
3181 step_row,
3182 "slider.step.label",
3183 "Step value",
3184 text(12.0, color(166, 176, 190)),
3185 LayoutStyle::new().with_width(74.0),
3186 );
3187 slider_number_input(
3188 ui,
3189 step_row,
3190 "slider.step_text",
3191 &state.slider_step_text,
3192 FocusedTextInput::SliderStep,
3193 state,
3194 86.0,
3195 );
3196 slider_checkbox(
3197 ui,
3198 body,
3199 "slider.logarithmic",
3200 "Logarithmic",
3201 state.slider_logarithmic,
3202 );
3203 let clamp_row = row(ui, body, "slider.clamping.row", 8.0);
3204 widgets::label(
3205 ui,
3206 clamp_row,
3207 "slider.clamping.label",
3208 "Clamping",
3209 text(12.0, color(166, 176, 190)),
3210 LayoutStyle::new().with_width(74.0),
3211 );
3212 choice_button(
3213 ui,
3214 clamp_row,
3215 "slider.clamping.never",
3216 "Never",
3217 state.slider_clamping == widgets::SliderClamping::Never,
3218 );
3219 choice_button(
3220 ui,
3221 clamp_row,
3222 "slider.clamping.edits",
3223 "Edits",
3224 state.slider_clamping == widgets::SliderClamping::Edits,
3225 );
3226 choice_button(
3227 ui,
3228 clamp_row,
3229 "slider.clamping.always",
3230 "Always",
3231 state.slider_clamping == widgets::SliderClamping::Always,
3232 );
3233 slider_checkbox(
3234 ui,
3235 body,
3236 "slider.smart_aim",
3237 "Smart aim",
3238 state.slider_smart_aim,
3239 );
3240}
3241
3242fn numeric_inputs(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3243 let body = section(ui, parent, "numeric", "Numeric input");
3244 let row_one = row(ui, body, "numeric.row.values", 10.0);
3245 widgets::drag_value_input(
3246 ui,
3247 row_one,
3248 "numeric.drag_value",
3249 state.numeric_value as f64,
3250 widgets::DragValueOptions::default()
3251 .with_range(widgets::NumericRange::new(0.0, 100.0))
3252 .with_precision(widgets::NumericPrecision::decimals(1))
3253 .with_unit(widgets::NumericUnitFormat::default().suffix(" px"))
3254 .with_action("numeric.drag_value"),
3255 );
3256 widgets::drag_angle(
3257 ui,
3258 row_one,
3259 "numeric.drag_angle",
3260 state.numeric_angle as f64,
3261 widgets::DragValueOptions::default().with_action("numeric.drag_angle"),
3262 );
3263 widgets::drag_angle_tau(
3264 ui,
3265 row_one,
3266 "numeric.drag_angle_tau",
3267 state.numeric_tau as f64,
3268 widgets::DragValueOptions::default().with_action("numeric.drag_angle_tau"),
3269 );
3270 widgets::label(
3271 ui,
3272 body,
3273 "numeric.note",
3274 "Drag values expose spinbutton semantics and unit-aware formatting.",
3275 text(12.0, color(166, 176, 190)),
3276 LayoutStyle::new().with_width_percent(1.0),
3277 );
3278}
3279
3280fn selection_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3281 let body = section(ui, parent, "selection", "Select controls");
3282 let select_width = 180.0;
3283
3284 widgets::label(
3285 ui,
3286 body,
3287 "selection.combo.label",
3288 "Combo box",
3289 text(12.0, color(166, 176, 190)),
3290 LayoutStyle::new().with_width_percent(1.0),
3291 );
3292
3293 let mut options = widgets::ComboBoxOptions::default();
3294 options.accessibility_label = Some("Display density".to_string());
3295 options.text_style = text(13.0, color(230, 236, 246));
3296 options.layout = LayoutStyle::new()
3297 .with_width(select_width)
3298 .with_height(30.0);
3299 let combo = widgets::combo_box(
3300 ui,
3301 body,
3302 "combo.toggle",
3303 state.combo_label.clone(),
3304 state.combo_open,
3305 options,
3306 );
3307 ui.node_mut(combo).action = Some("combo.toggle".into());
3308 let select_options = select_options();
3309 if state.combo_open {
3310 widgets::select_menu_popup(
3311 ui,
3312 body,
3313 "selection.combo_menu",
3314 widgets::AnchoredPopup::new(
3315 UiRect::new(0.0, 27.0, select_width, 30.0),
3316 UiRect::new(0.0, 0.0, 320.0, 308.0),
3317 widgets::PopupPlacement::default(),
3318 ),
3319 &select_options,
3320 &widgets::SelectMenuState {
3321 open: true,
3322 selected: select_options
3323 .iter()
3324 .position(|option| option.label == state.combo_label),
3325 active: select_options
3326 .iter()
3327 .position(|option| option.label == state.combo_label),
3328 },
3329 select_menu_options(select_width).with_action_prefix("selection.combo"),
3330 );
3331 }
3332
3333 widgets::label(
3334 ui,
3335 body,
3336 "selection.menu.label",
3337 "Select menu",
3338 text(12.0, color(166, 176, 190)),
3339 LayoutStyle::new().with_width_percent(1.0),
3340 );
3341 widgets::select_menu(
3342 ui,
3343 body,
3344 "selection.select_menu",
3345 &select_options,
3346 &state.select_menu,
3347 widgets::SelectMenuOptions::default().with_action_prefix("selection.menu"),
3348 );
3349 widgets::label(
3350 ui,
3351 body,
3352 "selection.dropdown.label",
3353 "Dropdown select",
3354 text(12.0, color(166, 176, 190)),
3355 LayoutStyle::new().with_width_percent(1.0),
3356 );
3357 let mut dropdown_options = widgets::DropdownSelectOptions::default();
3358 dropdown_options.menu =
3359 select_menu_options(select_width).with_action_prefix("selection.dropdown");
3360 let dropdown_nodes = widgets::dropdown_select(
3361 ui,
3362 body,
3363 "selection.dropdown",
3364 &select_options,
3365 &state.dropdown,
3366 Some(widgets::AnchoredPopup::new(
3367 UiRect::new(0.0, 0.0, select_width, 30.0),
3368 UiRect::new(0.0, 0.0, 320.0, 308.0),
3369 widgets::PopupPlacement::default(),
3370 )),
3371 dropdown_options,
3372 );
3373 ui.node_mut(dropdown_nodes.trigger).action = Some("selection.dropdown.toggle".into());
3374}
3375
3376fn select_menu_options(width: f32) -> widgets::SelectMenuOptions {
3377 let mut options = widgets::SelectMenuOptions::default();
3378 options.width = width;
3379 options
3380}
3381
3382fn text_input(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3383 let body = section(ui, parent, "text_input", "Text input");
3384 let mut options = TextInputOptions::default();
3385 options.placeholder = "Type here".to_string();
3386 options.layout = LayoutStyle::new().with_width(300.0).with_height(36.0);
3387 options.text_style = text(13.0, color(230, 236, 246));
3388 options.placeholder_style = text(13.0, color(144, 156, 174));
3389 options.edit_action = Some("text.input.edit".into());
3390 options.focused = state.focused_text == Some(FocusedTextInput::Editable);
3391 options.caret_visible = caret_visible(state.caret_phase);
3392 widgets::text_input(ui, body, "text.input", &state.text, options);
3393
3394 let mut selectable_options = TextInputOptions::default();
3395 selectable_options.layout = LayoutStyle::new().with_width(360.0).with_height(36.0);
3396 selectable_options.text_style = text(13.0, color(196, 210, 230));
3397 selectable_options.read_only = true;
3398 selectable_options.selectable = true;
3399 selectable_options.focused = state.focused_text == Some(FocusedTextInput::Selectable);
3400 selectable_options.edit_action = Some("text.selectable.edit".into());
3401 selectable_options.caret_visible = caret_visible(state.caret_phase);
3402 widgets::text_input(
3403 ui,
3404 body,
3405 "text.selectable",
3406 &state.selectable_text,
3407 selectable_options,
3408 );
3409
3410 let mut singleline = TextInputOptions::default();
3411 singleline.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
3412 singleline.text_style = text(13.0, color(230, 236, 246));
3413 singleline.placeholder = "Single line".to_string();
3414 singleline.edit_action = Some("text.singleline.edit".into());
3415 singleline.focused = state.focused_text == Some(FocusedTextInput::Singleline);
3416 singleline.caret_visible = caret_visible(state.caret_phase);
3417 widgets::singleline_text_input(
3418 ui,
3419 body,
3420 "text.singleline",
3421 &state.singleline_text,
3422 singleline,
3423 );
3424
3425 let mut multiline = TextInputOptions::default();
3426 multiline.layout = LayoutStyle::new().with_width(360.0).with_height(72.0);
3427 multiline.text_style = text(13.0, color(230, 236, 246));
3428 multiline.edit_action = Some("text.multiline.edit".into());
3429 multiline.focused = state.focused_text == Some(FocusedTextInput::Multiline);
3430 multiline.caret_visible = caret_visible(state.caret_phase);
3431 widgets::multiline_text_input(ui, body, "text.multiline", &state.multiline_text, multiline);
3432
3433 let mut area = TextInputOptions::default();
3434 area.layout = LayoutStyle::new().with_width(360.0).with_height(66.0);
3435 area.text_style = text(13.0, color(230, 236, 246));
3436 area.edit_action = Some("text.area.edit".into());
3437 area.focused = state.focused_text == Some(FocusedTextInput::TextArea);
3438 area.caret_visible = caret_visible(state.caret_phase);
3439 widgets::text_area(ui, body, "text.area", &state.text_area_text, area);
3440
3441 let mut code = TextInputOptions::default();
3442 code.layout = LayoutStyle::new().with_width(360.0).with_height(88.0);
3443 code.edit_action = Some("text.code_editor.edit".into());
3444 code.focused = state.focused_text == Some(FocusedTextInput::CodeEditor);
3445 code.caret_visible = caret_visible(state.caret_phase);
3446 widgets::code_editor(ui, body, "text.code_editor", &state.code_editor_text, code);
3447
3448 let mut search = TextInputOptions::default();
3449 search.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
3450 search.text_style = text(13.0, color(230, 236, 246));
3451 search.edit_action = Some("text.search.edit".into());
3452 search.focused = state.focused_text == Some(FocusedTextInput::Search);
3453 search.caret_visible = caret_visible(state.caret_phase);
3454 widgets::search_input(ui, body, "text.search", &state.search_text, search);
3455
3456 let mut password = TextInputOptions::default();
3457 password.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
3458 password.text_style = text(13.0, color(230, 236, 246));
3459 password.edit_action = Some("text.password.edit".into());
3460 password.focused = state.focused_text == Some(FocusedTextInput::Password);
3461 password.caret_visible = caret_visible(state.caret_phase);
3462 widgets::password_input(ui, body, "text.password", &state.password_text, password);
3463
3464 let mut helper = TextInputOptions::default();
3465 helper.read_only = true;
3466 helper.selectable = true;
3467 helper.layout = LayoutStyle::new().with_width(360.0).with_height(36.0);
3468 helper.text_style = text(13.0, color(196, 210, 230));
3469 helper.edit_action = Some("text.selectable_helper.edit".into());
3470 helper.focused = state.focused_text == Some(FocusedTextInput::SelectableHelper);
3471 helper.caret_visible = caret_visible(state.caret_phase);
3472 widgets::selectable_text(
3473 ui,
3474 body,
3475 "text.selectable_helper",
3476 &state.selectable_text,
3477 helper,
3478 );
3479}
3480
3481fn date_picker(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3482 let body = section(ui, parent, "date", "Date picker");
3483 let controls = row(ui, body, "date.options", 8.0);
3484 choice_button(
3485 ui,
3486 controls,
3487 "date.week.sunday",
3488 "Sun first",
3489 state.date.first_weekday == widgets::Weekday::Sunday,
3490 );
3491 choice_button(
3492 ui,
3493 controls,
3494 "date.week.monday",
3495 "Mon first",
3496 state.date.first_weekday == widgets::Weekday::Monday,
3497 );
3498 let mut range_button =
3499 widgets::ButtonOptions::new(LayoutStyle::new().with_width(92.0).with_height(28.0))
3500 .with_action("date.range.toggle");
3501 range_button.visual = if state.date.min.is_some() || state.date.max.is_some() {
3502 button_visual(48, 112, 184)
3503 } else {
3504 button_visual(38, 46, 58)
3505 };
3506 range_button.hovered_visual = Some(button_visual(65, 86, 106));
3507 range_button.text_style = text(12.0, color(238, 244, 252));
3508 widgets::button(
3509 ui,
3510 controls,
3511 "date.range.toggle",
3512 "Limit range",
3513 range_button,
3514 );
3515 widgets::date_picker(
3516 ui,
3517 body,
3518 "date.picker",
3519 &state.date,
3520 widgets::DatePickerOptions::default().with_action_prefix("date"),
3521 );
3522 widgets::label(
3523 ui,
3524 body,
3525 "date.selected",
3526 format!(
3527 "Selected: {}",
3528 state
3529 .date
3530 .selected
3531 .map_or_else(|| "None".to_string(), CalendarDate::iso_string)
3532 ),
3533 text(11.0, color(154, 166, 184)),
3534 LayoutStyle::new().with_width_percent(1.0),
3535 );
3536}
3537
3538fn color_picker(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3539 let body = section(ui, parent, "color", "Color picker");
3540 widgets::color_picker(
3541 ui,
3542 body,
3543 "color.picker",
3544 &state.color,
3545 widgets::ColorPickerOptions::default()
3546 .with_action_prefix("color")
3547 .with_copy_hex_action("color.copy_hex")
3548 .with_copy_hex_label("Copy"),
3549 );
3550 if let Some(hex) = &state.color_copied_hex {
3551 widgets::label(
3552 ui,
3553 body,
3554 "color.copied",
3555 format!("Copied {hex}"),
3556 text(11.0, color(154, 166, 184)),
3557 LayoutStyle::new().with_width_percent(1.0),
3558 );
3559 }
3560}
3561
3562fn color_buttons(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3563 let body = section(ui, parent, "color_buttons", "Color buttons");
3564 let current_color = state.color.value;
3565
3566 widgets::label(
3567 ui,
3568 body,
3569 "color_buttons.edit_label",
3570 "Color edit button",
3571 text(12.0, color(166, 176, 190)),
3572 LayoutStyle::new().with_width_percent(1.0),
3573 );
3574 let edit_row = row(ui, body, "color_buttons.edit_row", 8.0);
3575 widgets::color_edit_button_rgb(
3576 ui,
3577 edit_row,
3578 "color_buttons.compact",
3579 current_color,
3580 color_square_button_options("color_buttons.compact").accessibility_label("Edit RGB color"),
3581 );
3582 widgets::label(
3583 ui,
3584 edit_row,
3585 "color_buttons.hex_value",
3586 widgets::format_hex_color(current_color, false),
3587 text(12.0, color(220, 228, 238)),
3588 LayoutStyle::new().with_width(92.0),
3589 );
3590
3591 widgets::label(
3592 ui,
3593 body,
3594 "color_buttons.format_label",
3595 "Value formats",
3596 text(12.0, color(166, 176, 190)),
3597 LayoutStyle::new().with_width_percent(1.0),
3598 );
3599 let rgb_row = row(ui, body, "color_buttons.rgb_row", 8.0);
3600 widgets::label(
3601 ui,
3602 rgb_row,
3603 "color_buttons.rgb_label",
3604 "RGB",
3605 text(12.0, color(186, 198, 216)),
3606 LayoutStyle::new().with_width(48.0),
3607 );
3608 widgets::color_edit_button_rgb(
3609 ui,
3610 rgb_row,
3611 "color_buttons.rgb",
3612 current_color,
3613 color_value_button_options("color_buttons.rgb", 180.0),
3614 );
3615 let rgba_row = row(ui, body, "color_buttons.rgba_row", 8.0);
3616 widgets::label(
3617 ui,
3618 rgba_row,
3619 "color_buttons.rgba_label",
3620 "RGBA",
3621 text(12.0, color(186, 198, 216)),
3622 LayoutStyle::new().with_width(48.0),
3623 );
3624 widgets::color_edit_button_rgba(
3625 ui,
3626 rgba_row,
3627 "color_buttons.rgba",
3628 current_color,
3629 color_value_button_options("color_buttons.rgba", 230.0),
3630 );
3631 let hsva_row = row(ui, body, "color_buttons.hsva_row", 8.0);
3632 widgets::label(
3633 ui,
3634 hsva_row,
3635 "color_buttons.hsva_label",
3636 "HSVA",
3637 text(12.0, color(186, 198, 216)),
3638 LayoutStyle::new().with_width(48.0),
3639 );
3640 widgets::color_edit_button_hsva(
3641 ui,
3642 hsva_row,
3643 "color_buttons.hsva",
3644 current_color,
3645 color_value_button_options("color_buttons.hsva", 260.0),
3646 );
3647
3648 widgets::label(
3649 ui,
3650 body,
3651 "color_buttons.field_label",
3652 "2D color field",
3653 text(12.0, color(166, 176, 190)),
3654 LayoutStyle::new().with_width_percent(1.0),
3655 );
3656 widgets::color_picker_hsva_2d(
3657 ui,
3658 body,
3659 "color_buttons.hsva_2d",
3660 state.color.hsv(),
3661 widgets::ColorHsva2dOptions::default()
3662 .with_layout(LayoutStyle::new().with_width(204.0).with_height(112.0))
3663 .with_action_prefix("color_buttons.hsva_2d"),
3664 );
3665 widgets::label(
3666 ui,
3667 body,
3668 "color_buttons.status",
3669 format!("Last activated: {}", state.color_button_status),
3670 text(11.0, color(154, 166, 184)),
3671 LayoutStyle::new().with_width_percent(1.0),
3672 );
3673}
3674
3675fn menu_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3676 let body = section(ui, parent, "menus", "Menus");
3677 let menus = menu_bar_menus(state.menu_autosave, state.menu_grid);
3678 let active_items = state
3679 .menu_bar
3680 .open_menu
3681 .and_then(|index| menus.get(index))
3682 .map(|menu| menu.items.clone())
3683 .unwrap_or_default();
3684 widgets::menu_bar(
3685 ui,
3686 body,
3687 "menus.menu_bar",
3688 &menus,
3689 &state.menu_bar,
3690 None,
3691 widgets::MenuBarOptions::default().with_action_prefix("menus.bar"),
3692 );
3693
3694 if !active_items.is_empty() {
3695 widgets::menu_list(
3696 ui,
3697 body,
3698 "menus.menu_list",
3699 &active_items,
3700 state.menu_bar.active_item,
3701 widgets::MenuListOptions::default().with_action_prefix("menus.item"),
3702 );
3703 if let Some(active_item) = state.menu_bar.active_item {
3704 if let Some(children) = active_items
3705 .get(active_item)
3706 .and_then(|item| item.children())
3707 {
3708 widgets::menu_list_popup(
3709 ui,
3710 body,
3711 "menus.submenu",
3712 widgets::AnchoredPopup::new(
3713 UiRect::new(
3714 0.0,
3715 40.0 + menu_item_top_offset(&active_items, active_item),
3716 240.0,
3717 menu_item_height(active_items.get(active_item)),
3718 ),
3719 UiRect::new(0.0, 0.0, 680.0, 468.0),
3720 widgets::PopupPlacement::new(
3721 widgets::PopupSide::Right,
3722 widgets::PopupAlign::Start,
3723 )
3724 .with_offset(4.0),
3725 ),
3726 children,
3727 Some(0),
3728 widgets::MenuListOptions::default().with_action_prefix("menus.item"),
3729 );
3730 }
3731 }
3732 }
3733 divider(ui, body, "menus.divider.buttons");
3734 let button_row = row(ui, body, "menus.buttons", 8.0);
3735 let button_items = menu_items(state.menu_autosave);
3736 widgets::menu_button(
3737 ui,
3738 button_row,
3739 "menus.menu_button",
3740 "Menu button",
3741 &button_items,
3742 &state.menu_button,
3743 None,
3744 widgets::MenuButtonOptions::default().with_action("menus.menu_button"),
3745 );
3746 widgets::image_text_menu_button(
3747 ui,
3748 button_row,
3749 "menus.image_text_menu_button",
3750 "Image text",
3751 icon_image(BuiltInIcon::Folder),
3752 &button_items,
3753 &state.image_text_menu_button,
3754 None,
3755 widgets::MenuButtonOptions::default().with_action("menus.image_text_menu_button"),
3756 );
3757 widgets::image_menu_button(
3758 ui,
3759 button_row,
3760 "menus.image_menu_button",
3761 icon_image(BuiltInIcon::Settings),
3762 &button_items,
3763 &state.image_menu_button,
3764 None,
3765 widgets::MenuButtonOptions::default().with_action("menus.image_menu_button"),
3766 );
3767 if state.menu_button.open || state.image_text_menu_button.open || state.image_menu_button.open {
3768 let active = state
3769 .menu_button
3770 .navigation
3771 .active_path
3772 .first()
3773 .copied()
3774 .or_else(|| {
3775 state
3776 .image_text_menu_button
3777 .navigation
3778 .active_path
3779 .first()
3780 .copied()
3781 })
3782 .or_else(|| {
3783 state
3784 .image_menu_button
3785 .navigation
3786 .active_path
3787 .first()
3788 .copied()
3789 });
3790 widgets::menu_list(
3791 ui,
3792 body,
3793 "menus.button_menu",
3794 &button_items,
3795 active,
3796 widgets::MenuListOptions::default().with_action_prefix("menus.item"),
3797 );
3798 }
3799
3800 let context_row = row(ui, body, "menus.context.controls", 8.0);
3801 button(
3802 ui,
3803 context_row,
3804 "menus.context.open",
3805 "Open context",
3806 "menus.context.open",
3807 button_visual(48, 112, 184),
3808 );
3809 button(
3810 ui,
3811 context_row,
3812 "menus.context.close",
3813 "Close",
3814 "menus.context.close",
3815 button_visual(58, 78, 96),
3816 );
3817 let mut context_options =
3818 widgets::MenuListOptions::default().with_action_prefix("menus.context");
3819 context_options.width = 180.0;
3820 context_options.max_visible_rows = 4;
3821 let _ = widgets::context_menu(
3822 ui,
3823 parent,
3824 "menus.context_menu",
3825 &button_items,
3826 &state.context_menu,
3827 UiRect::new(0.0, 0.0, 180.0, 120.0),
3828 widgets::PopupPlacement::default(),
3829 context_options,
3830 );
3831}
3832
3833fn command_palette(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3834 let body = section(ui, parent, "command_palette", "Command palette");
3835 let items = command_palette_items();
3836 let mut options =
3837 widgets::CommandPaletteOptions::default().with_action_prefix("command_palette");
3838 options.width = 480.0;
3839 options.row_height = 44.0;
3840 options.max_visible_rows = 5;
3841 options.text_style = text(13.0, color(238, 244, 252));
3842 options.muted_text_style = text(11.0, color(166, 178, 196));
3843 widgets::command_palette(
3844 ui,
3845 body,
3846 "command_palette.panel",
3847 &items,
3848 &state.command_palette,
3849 None,
3850 options,
3851 );
3852 widgets::label(
3853 ui,
3854 body,
3855 "command_palette.last",
3856 format!("Last command: {}", state.last_command),
3857 text(12.0, color(154, 166, 184)),
3858 LayoutStyle::new().with_width_percent(1.0),
3859 );
3860}
3861
3862fn progress_indicator(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3863 let body = section(ui, parent, "progress", "Progress indicator");
3864 let animated = smooth_loop(state.progress_phase * 0.85, 0.0) * 100.0;
3865 let mut progress = widgets::ProgressIndicatorOptions::default();
3866 progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(10.0);
3867 progress.accessibility_label = Some("Progress".to_string());
3868 widgets::progress_indicator(
3869 ui,
3870 body,
3871 "progress.primary",
3872 widgets::ProgressIndicatorValue::percent(animated),
3873 progress,
3874 );
3875 let compact_value = smooth_loop(state.progress_phase * 1.15, 0.7) * 100.0;
3876 let mut compact = widgets::ProgressIndicatorOptions::default();
3877 compact.layout = LayoutStyle::new().with_width_percent(1.0).with_height(6.0);
3878 compact.fill_visual = UiVisual::panel(color(111, 203, 159), None, 3.0);
3879 widgets::progress_indicator(
3880 ui,
3881 body,
3882 "progress.compact",
3883 widgets::ProgressIndicatorValue::percent(compact_value),
3884 compact,
3885 );
3886 let warning_value = smooth_loop(state.progress_phase * 0.65, 1.4) * 100.0;
3887 let mut warning = widgets::ProgressIndicatorOptions::default();
3888 warning.layout = LayoutStyle::new().with_width_percent(1.0).with_height(14.0);
3889 warning.fill_visual = UiVisual::panel(color(232, 186, 88), None, 4.0);
3890 widgets::progress_indicator(
3891 ui,
3892 body,
3893 "progress.warning",
3894 widgets::ProgressIndicatorValue::percent(warning_value),
3895 warning,
3896 );
3897 let spinner_row = row(ui, body, "progress.spinner.row", 8.0);
3898 widgets::spinner(
3899 ui,
3900 spinner_row,
3901 "progress.spinner",
3902 widgets::SpinnerOptions::default()
3903 .with_phase(state.progress_phase)
3904 .with_accessibility_label("Loading spinner"),
3905 );
3906 widgets::label(
3907 ui,
3908 spinner_row,
3909 "progress.spinner.label",
3910 "Spinner",
3911 text(12.0, color(196, 210, 230)),
3912 LayoutStyle::new().with_width_percent(1.0),
3913 );
3914}
3915
3916fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3917 let body = section(ui, parent, "lists_tables", "Lists and tables");
3918
3919 let scroll_shell = row(ui, body, "lists_tables.scroll_area.shell", 8.0);
3920 let nested_scroll = widgets::scroll_area(
3921 ui,
3922 scroll_shell,
3923 "lists_tables.scroll_area",
3924 ScrollAxes::VERTICAL,
3925 LayoutStyle::column()
3926 .with_width(0.0)
3927 .with_flex_grow(1.0)
3928 .with_height(92.0),
3929 );
3930 ui.node_mut(nested_scroll).action = Some("lists_tables.scroll_area.scroll".into());
3931 if let Some(scroll) = ui.node_mut(nested_scroll).scroll.as_mut() {
3932 scroll.offset.y = state.list_scroll;
3933 }
3934 for index in 0..6 {
3935 widgets::label(
3936 ui,
3937 nested_scroll,
3938 format!("lists_tables.scroll_area.row.{index}"),
3939 format!("Scroll row {}", index + 1),
3940 text(12.0, color(200, 212, 228)),
3941 LayoutStyle::new()
3942 .with_width_percent(1.0)
3943 .with_height(26.0)
3944 .with_flex_shrink(0.0),
3945 );
3946 }
3947 widgets::scrollbar(
3948 ui,
3949 scroll_shell,
3950 "lists_tables.scroll_area.scrollbar",
3951 scroll_state(state.list_scroll, 92.0, 6.0 * 26.0),
3952 widgets::ScrollAxis::Vertical,
3953 widgets::ScrollbarOptions::default()
3954 .with_layout(LayoutStyle::size(8.0, 92.0))
3955 .with_track_size(UiSize::new(8.0, 92.0))
3956 .with_action("lists_tables.scroll_area.scrollbar"),
3957 );
3958
3959 widgets::table_header(ui, body, "lists_tables.table_header", &table_columns());
3960
3961 let virtual_shell = row(ui, body, "lists_tables.virtual_list.shell", 8.0);
3962 let virtual_list = widgets::virtual_list(
3963 ui,
3964 virtual_shell,
3965 "lists_tables.virtual_list",
3966 widgets::VirtualListSpec {
3967 row_count: 24,
3968 row_height: 28.0,
3969 viewport_height: 112.0,
3970 scroll_offset: state.virtual_scroll,
3971 overscan: 1,
3972 },
3973 |ui, row_parent, row| {
3974 widgets::label(
3975 ui,
3976 row_parent,
3977 format!("lists_tables.virtual_list.row.{row}"),
3978 format!("Virtual row {}", row + 1),
3979 text(12.0, color(214, 224, 238)),
3980 LayoutStyle::new()
3981 .with_width_percent(1.0)
3982 .with_height(28.0)
3983 .with_flex_shrink(0.0),
3984 );
3985 },
3986 );
3987 ui.node_mut(virtual_list).action = Some("lists_tables.virtual_list.scroll".into());
3988 widgets::scrollbar(
3989 ui,
3990 virtual_shell,
3991 "lists_tables.virtual_list.scrollbar",
3992 scroll_state(state.virtual_scroll, 112.0, 24.0 * 28.0),
3993 widgets::ScrollAxis::Vertical,
3994 widgets::ScrollbarOptions::default()
3995 .with_layout(LayoutStyle::size(8.0, 112.0))
3996 .with_track_size(UiSize::new(8.0, 112.0))
3997 .with_action("lists_tables.virtual_list.scrollbar"),
3998 );
3999
4000 let table_shell = row(ui, body, "lists_tables.data_table.shell", 8.0);
4001 let table_scroll = widgets::scroll_area(
4002 ui,
4003 table_shell,
4004 "lists_tables.data_table",
4005 ScrollAxes::VERTICAL,
4006 LayoutStyle::column()
4007 .with_width(0.0)
4008 .with_flex_grow(1.0)
4009 .with_height(128.0),
4010 );
4011 ui.node_mut(table_scroll).action = Some("lists_tables.data_table.scroll".into());
4012 if let Some(scroll) = ui.node_mut(table_scroll).scroll.as_mut() {
4013 scroll.offset.y = state.table_scroll;
4014 }
4015 for row_index in 0..16 {
4016 data_table_row(ui, table_scroll, row_index, state);
4017 }
4018 widgets::scrollbar(
4019 ui,
4020 table_shell,
4021 "lists_tables.data_table.scrollbar",
4022 scroll_state(state.table_scroll, 128.0, 16.0 * 28.0),
4023 widgets::ScrollAxis::Vertical,
4024 widgets::ScrollbarOptions::default()
4025 .with_layout(LayoutStyle::size(8.0, 128.0))
4026 .with_track_size(UiSize::new(8.0, 128.0))
4027 .with_action("lists_tables.data_table.scrollbar"),
4028 );
4029
4030 let columns = vec![
4031 widgets::DataTableColumn::new("name", "Virtualized", 160.0),
4032 widgets::DataTableColumn::new("status", "Status", 110.0),
4033 widgets::DataTableColumn::new("value", "Value", 70.0)
4034 .with_alignment(widgets::DataCellAlignment::End),
4035 ];
4036 let mut table_options = widgets::DataTableOptions::default()
4037 .with_row_action_prefix("lists_tables.virtualized_table.row")
4038 .with_cell_action_prefix("lists_tables.virtualized_table.cell");
4039 table_options.selection = state.table_selection.clone();
4040 widgets::virtualized_data_table(
4041 ui,
4042 body,
4043 "lists_tables.virtualized_table",
4044 &columns,
4045 widgets::VirtualDataTableSpec {
4046 row_count: 32,
4047 row_height: 28.0,
4048 viewport_width: 420.0,
4049 viewport_height: 128.0,
4050 scroll_offset: UiPoint::new(0.0, state.table_scroll),
4051 overscan_rows: 1,
4052 },
4053 table_options,
4054 |ui, cell_parent, cell| {
4055 let value = match cell.column {
4056 0 => format!("Virtual row {}", cell.row + 1),
4057 1 if cell.row % 2 == 0 => "Ready".to_string(),
4058 1 => "Pending".to_string(),
4059 _ => format!("{}%", 30 + cell.row * 2),
4060 };
4061 widgets::label(
4062 ui,
4063 cell_parent,
4064 format!(
4065 "lists_tables.virtualized_table.cell.{}.{}.label",
4066 cell.row, cell.column
4067 ),
4068 value,
4069 text(12.0, color(220, 228, 238)),
4070 LayoutStyle::new().with_width_percent(1.0),
4071 );
4072 },
4073 );
4074}
4075
4076fn data_table_row(ui: &mut UiDocument, parent: UiNodeId, row_index: usize, state: &ShowcaseState) {
4077 let selected = state.table_selection.contains_row(row_index);
4078 let row = ui.add_child(
4079 parent,
4080 UiNode::container(
4081 format!("lists_tables.data_table.row.{row_index}"),
4082 LayoutStyle::row()
4083 .with_width_percent(1.0)
4084 .with_height(28.0)
4085 .with_flex_shrink(0.0),
4086 )
4087 .with_input(operad::InputBehavior::BUTTON)
4088 .with_action(format!("lists_tables.data_table.row.{row_index}"))
4089 .with_visual(if selected {
4090 UiVisual::panel(color(45, 73, 109), None, 0.0)
4091 } else {
4092 UiVisual::TRANSPARENT
4093 }),
4094 );
4095 let values = [
4096 format!("Item {}", row_index + 1),
4097 if row_index % 2 == 0 {
4098 "Ready".to_string()
4099 } else {
4100 "Pending".to_string()
4101 },
4102 format!("{}%", 40 + row_index * 3),
4103 ];
4104 let widths = [0.42, 0.33, 0.25];
4105 for (column, value) in values.into_iter().enumerate() {
4106 let cell = ui.add_child(
4107 row,
4108 UiNode::container(
4109 format!("lists_tables.data_table.cell.{row_index}.{column}"),
4110 LayoutStyle::new()
4111 .with_width_percent(widths[column])
4112 .with_height_percent(1.0)
4113 .padding(6.0),
4114 )
4115 .with_input(operad::InputBehavior::BUTTON)
4116 .with_action(format!("lists_tables.data_table.cell.{row_index}.{column}")),
4117 );
4118 widgets::label(
4119 ui,
4120 cell,
4121 format!("lists_tables.data_table.cell.{row_index}.{column}.label"),
4122 value,
4123 text(12.0, color(222, 230, 240)),
4124 LayoutStyle::new().with_width_percent(1.0),
4125 );
4126 }
4127}
4128
4129fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4130 let body = section(ui, parent, "property_inspector", "Property inspector");
4131 widgets::label(
4132 ui,
4133 body,
4134 "property_inspector.target",
4135 "Inspecting: Styling preview",
4136 text(12.0, color(196, 210, 230)),
4137 LayoutStyle::new().with_width_percent(1.0),
4138 );
4139 let mut options = widgets::PropertyInspectorOptions::default();
4140 options.selected_index = Some(0);
4141 options.label_width = 120.0;
4142 options.row_height = 30.0;
4143 widgets::property_inspector_grid(
4144 ui,
4145 body,
4146 "property_inspector.grid",
4147 &[
4148 widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
4149 widgets::PropertyGridRow::new(
4150 "inner",
4151 "Inner margin",
4152 format!("{:.0}px", state.styling.inner_margin),
4153 )
4154 .with_kind(widgets::PropertyValueKind::Number),
4155 widgets::PropertyGridRow::new(
4156 "outer",
4157 "Outer margin",
4158 format!("{:.0}px", state.styling.outer_margin),
4159 )
4160 .with_kind(widgets::PropertyValueKind::Number),
4161 widgets::PropertyGridRow::new(
4162 "radius",
4163 "Corner radius",
4164 format!("{:.0}px", state.styling.corner_radius),
4165 )
4166 .with_kind(widgets::PropertyValueKind::Number),
4167 widgets::PropertyGridRow::new(
4168 "stroke",
4169 "Stroke",
4170 format!("{:.1}px", state.styling.stroke_width),
4171 )
4172 .with_kind(widgets::PropertyValueKind::Number)
4173 .changed(),
4174 widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
4175 ],
4176 options,
4177 );
4178}
4179
4180fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4181 let body = section(ui, parent, "trees", "Tree view");
4182 widgets::tree_view(
4183 ui,
4184 body,
4185 "trees.tree_view",
4186 &tree_items(),
4187 &state.tree,
4188 widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
4189 );
4190 widgets::outliner(
4191 ui,
4192 body,
4193 "trees.outliner",
4194 &tree_items(),
4195 &state.outliner,
4196 widgets::TreeViewOptions::default().with_row_action_prefix("trees.outliner"),
4197 );
4198}
4199
4200fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4201 let body = section(ui, parent, "layout_widgets", "Layout panels");
4202 let shell = ui.add_child(
4203 body,
4204 UiNode::container(
4205 "layout_widgets.egui_shell",
4206 LayoutStyle::column()
4207 .with_width_percent(1.0)
4208 .with_height(340.0)
4209 .with_flex_shrink(0.0),
4210 )
4211 .with_visual(UiVisual::panel(
4212 color(13, 17, 23),
4213 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
4214 0.0,
4215 )),
4216 );
4217 let panels = row(ui, shell, "layout_widgets.egui_panels", 0.0);
4218 ui.node_mut(panels).style.layout.size.height = operad::length(340.0);
4219 let inspector = ui.add_child(
4220 panels,
4221 UiNode::container(
4222 "layout_widgets.inspector_panel",
4223 LayoutStyle::column()
4224 .with_width(230.0)
4225 .with_height_percent(1.0)
4226 .with_flex_shrink(0.0),
4227 )
4228 .with_visual(UiVisual::panel(
4229 color(18, 22, 29),
4230 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
4231 0.0,
4232 )),
4233 );
4234 egui_panel_contents(
4235 ui,
4236 inspector,
4237 "layout.inspector",
4238 "Inspector",
4239 state.layout_inspector_scroll,
4240 );
4241
4242 let assets = ui.add_child(
4243 panels,
4244 UiNode::container(
4245 "layout_widgets.assets_panel",
4246 LayoutStyle::column()
4247 .with_width(0.0)
4248 .with_height_percent(1.0)
4249 .with_flex_grow(1.0),
4250 )
4251 .with_visual(UiVisual::panel(
4252 color(15, 19, 25),
4253 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
4254 0.0,
4255 )),
4256 );
4257 egui_panel_contents(
4258 ui,
4259 assets,
4260 "layout.assets",
4261 "Assets",
4262 state.layout_assets_scroll,
4263 );
4264}
4265
4266fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4267 let body = section(ui, parent, "containers", "Containers");
4268
4269 let frame = widgets::frame(
4270 ui,
4271 body,
4272 "containers.frame",
4273 widgets::FrameOptions::default().with_layout(
4274 LayoutStyle::column()
4275 .with_width_percent(1.0)
4276 .with_height(64.0)
4277 .with_padding(8.0)
4278 .with_gap(6.0),
4279 ),
4280 );
4281 widgets::strong_label(
4282 ui,
4283 frame,
4284 "containers.frame.title",
4285 "Frame",
4286 LayoutStyle::new().with_width_percent(1.0),
4287 );
4288 widgets::weak_label(
4289 ui,
4290 frame,
4291 "containers.frame.body",
4292 "Default framed surface with padding, stroke, and clipping.",
4293 LayoutStyle::new().with_width_percent(1.0),
4294 );
4295
4296 let group = widgets::group(ui, body, "containers.group");
4297 widgets::label(
4298 ui,
4299 group,
4300 "containers.group.label",
4301 "Group helper",
4302 text(12.0, color(220, 228, 238)),
4303 LayoutStyle::new().with_width_percent(1.0),
4304 );
4305 let generic_panel = widgets::panel(
4306 ui,
4307 body,
4308 "containers.panel",
4309 widgets::PanelOptions::group().with_layout(
4310 LayoutStyle::column()
4311 .with_width_percent(1.0)
4312 .with_height(44.0)
4313 .with_padding(8.0),
4314 ),
4315 );
4316 widgets::label(
4317 ui,
4318 generic_panel,
4319 "containers.panel.label",
4320 "Generic panel",
4321 text(12.0, color(220, 228, 238)),
4322 LayoutStyle::new().with_width_percent(1.0),
4323 );
4324 let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
4325 widgets::label(
4326 ui,
4327 group_panel,
4328 "containers.group_panel.label",
4329 "Group panel",
4330 text(12.0, color(220, 228, 238)),
4331 LayoutStyle::new().with_width_percent(1.0),
4332 );
4333
4334 widgets::separator(
4335 ui,
4336 body,
4337 "containers.separator",
4338 widgets::SeparatorOptions::default(),
4339 );
4340 widgets::spacer(
4341 ui,
4342 body,
4343 "containers.spacer",
4344 LayoutStyle::new()
4345 .with_width_percent(1.0)
4346 .with_height(8.0)
4347 .with_flex_shrink(0.0),
4348 );
4349
4350 let grid = widgets::grid(
4351 ui,
4352 body,
4353 "containers.grid",
4354 widgets::GridOptions::default().with_layout(
4355 LayoutStyle::column()
4356 .with_width_percent(1.0)
4357 .with_height(78.0)
4358 .with_gap(4.0),
4359 ),
4360 );
4361 for row_index in 0..2 {
4362 let row = widgets::grid_row(
4363 ui,
4364 grid,
4365 format!("containers.grid.row.{row_index}"),
4366 widgets::GridRowOptions::default(),
4367 );
4368 for column_index in 0..3 {
4369 widgets::grid_text_cell(
4370 ui,
4371 row,
4372 format!("containers.grid.row.{row_index}.cell.{column_index}"),
4373 format!("R{} C{}", row_index + 1, column_index + 1),
4374 widgets::GridCellOptions {
4375 text_style: text(12.0, color(214, 224, 238)),
4376 ..Default::default()
4377 },
4378 );
4379 }
4380 }
4381
4382 widgets::sides(
4383 ui,
4384 body,
4385 "containers.sides",
4386 widgets::SidesOptions::default()
4387 .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
4388 .with_gap(8.0)
4389 .with_visual(UiVisual::panel(
4390 color(20, 25, 32),
4391 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
4392 4.0,
4393 )),
4394 |ui, left| {
4395 widgets::label(
4396 ui,
4397 left,
4398 "containers.sides.left.label",
4399 "Left side",
4400 text(12.0, color(220, 228, 238)),
4401 LayoutStyle::new().with_width_percent(1.0),
4402 );
4403 },
4404 |ui, right| {
4405 widgets::label(
4406 ui,
4407 right,
4408 "containers.sides.right.label",
4409 "Right side",
4410 text(12.0, color(220, 228, 238)),
4411 LayoutStyle::new().with_width_percent(1.0),
4412 );
4413 },
4414 );
4415
4416 widgets::columns(
4417 ui,
4418 body,
4419 "containers.columns",
4420 3,
4421 widgets::ColumnsOptions::default()
4422 .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
4423 .with_gap(8.0),
4424 |ui, column, index| {
4425 widgets::label(
4426 ui,
4427 column,
4428 format!("containers.columns.{index}.label"),
4429 format!("Column {}", index + 1),
4430 text(12.0, color(220, 228, 238)),
4431 LayoutStyle::new().with_width_percent(1.0),
4432 );
4433 },
4434 );
4435
4436 let indented = widgets::indented_section(
4437 ui,
4438 body,
4439 "containers.indented",
4440 widgets::IndentOptions::default().with_amount(24.0),
4441 );
4442 widgets::label(
4443 ui,
4444 indented,
4445 "containers.indented.label",
4446 "Indented section",
4447 text(12.0, color(196, 210, 230)),
4448 LayoutStyle::new().with_width_percent(1.0),
4449 );
4450
4451 widgets::resize_container(
4452 ui,
4453 body,
4454 "containers.resize_container",
4455 widgets::ResizeContainerOptions::default().with_layout(
4456 LayoutStyle::column()
4457 .with_width_percent(1.0)
4458 .with_height(92.0)
4459 .with_flex_shrink(0.0),
4460 ),
4461 |ui, content| {
4462 widgets::label(
4463 ui,
4464 content,
4465 "containers.resize_container.label",
4466 "Resize container",
4467 text(12.0, color(220, 228, 238)),
4468 LayoutStyle::new().with_width_percent(1.0),
4469 );
4470 },
4471 );
4472 widgets::resize_handle(
4473 ui,
4474 body,
4475 "containers.resize_handle",
4476 widgets::ResizeHandleOptions::default()
4477 .with_layout(LayoutStyle::size(20.0, 20.0))
4478 .accessibility_label("Inline resize handle"),
4479 );
4480
4481 widgets::scene(
4482 ui,
4483 body,
4484 "containers.scene",
4485 vec![
4486 ScenePrimitive::Rect(
4487 PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
4488 .stroke(AlignedStroke::inside(StrokeStyle::new(
4489 color(132, 174, 222),
4490 1.0,
4491 )))
4492 .corner_radii(CornerRadii::uniform(6.0)),
4493 ),
4494 ScenePrimitive::Circle {
4495 center: UiPoint::new(150.0, 35.0),
4496 radius: 22.0,
4497 fill: color(111, 203, 159),
4498 stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
4499 },
4500 ScenePrimitive::Line {
4501 from: UiPoint::new(188.0, 18.0),
4502 to: UiPoint::new(238.0, 52.0),
4503 stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
4504 },
4505 ],
4506 widgets::SceneOptions::default()
4507 .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
4508 .accessibility_label("Scene primitives"),
4509 );
4510
4511 let panel_shell = widgets::frame(
4512 ui,
4513 body,
4514 "containers.panels",
4515 widgets::FrameOptions::default().with_layout(
4516 LayoutStyle::column()
4517 .with_width_percent(1.0)
4518 .with_height(160.0)
4519 .with_padding(0.0)
4520 .with_gap(0.0),
4521 ),
4522 );
4523 let top = widgets::top_panel(ui, panel_shell, "containers.panels.top", 28.0);
4524 widgets::label(
4525 ui,
4526 top,
4527 "containers.panels.top.label",
4528 "Top panel",
4529 text(12.0, color(220, 228, 238)),
4530 LayoutStyle::new().with_width_percent(1.0),
4531 );
4532 let middle = row(ui, panel_shell, "containers.panels.middle", 0.0);
4533 let left = widgets::side_panel(
4534 ui,
4535 middle,
4536 "containers.panels.side",
4537 widgets::SidePanelSide::Left,
4538 90.0,
4539 );
4540 widgets::label(
4541 ui,
4542 left,
4543 "containers.panels.side.label",
4544 "Side",
4545 text(12.0, color(220, 228, 238)),
4546 LayoutStyle::new().with_width_percent(1.0),
4547 );
4548 let left = widgets::left_panel(ui, middle, "containers.panels.left", 90.0);
4549 widgets::label(
4550 ui,
4551 left,
4552 "containers.panels.left.label",
4553 "Left",
4554 text(12.0, color(220, 228, 238)),
4555 LayoutStyle::new().with_width_percent(1.0),
4556 );
4557 let center = widgets::central_panel(ui, middle, "containers.panels.center");
4558 widgets::label(
4559 ui,
4560 center,
4561 "containers.panels.center.label",
4562 "Central panel",
4563 text(12.0, color(220, 228, 238)),
4564 LayoutStyle::new().with_width_percent(1.0),
4565 );
4566 let right = widgets::right_panel(ui, middle, "containers.panels.right", 110.0);
4567 widgets::label(
4568 ui,
4569 right,
4570 "containers.panels.right.label",
4571 "Right",
4572 text(12.0, color(220, 228, 238)),
4573 LayoutStyle::new().with_width_percent(1.0),
4574 );
4575 let bottom = widgets::bottom_panel(ui, panel_shell, "containers.panels.bottom", 28.0);
4576 widgets::label(
4577 ui,
4578 bottom,
4579 "containers.panels.bottom.label",
4580 "Bottom panel",
4581 text(12.0, color(220, 228, 238)),
4582 LayoutStyle::new().with_width_percent(1.0),
4583 );
4584
4585 let scroll = widgets::scroll_area_with_bars(
4586 ui,
4587 body,
4588 "containers.scroll_area_with_bars",
4589 state.containers_scroll,
4590 widgets::ScrollAreaWithBarsOptions::default()
4591 .with_axes(ScrollAxes::BOTH)
4592 .with_vertical_scrollbar(
4593 widgets::ScrollbarOptions::default()
4594 .with_action("containers.scroll_area_with_bars.vertical-scrollbar"),
4595 )
4596 .with_horizontal_scrollbar(
4597 widgets::ScrollbarOptions::default()
4598 .with_action("containers.scroll_area_with_bars.horizontal-scrollbar"),
4599 )
4600 .with_layout(LayoutStyle::column().with_width(300.0).with_height(116.0)),
4601 );
4602 ui.node_mut(scroll.viewport).action = Some("containers.scroll_area_with_bars.scroll".into());
4603 for index in 0..5 {
4604 widgets::label(
4605 ui,
4606 scroll.viewport,
4607 format!("containers.scroll_area_with_bars.row.{index}"),
4608 format!("Scrollable row {}", index + 1),
4609 text(12.0, color(200, 212, 228)),
4610 LayoutStyle::new()
4611 .with_width(420.0)
4612 .with_height(28.0)
4613 .with_flex_shrink(0.0),
4614 );
4615 }
4616
4617 let area_host = ui.add_child(
4618 body,
4619 UiNode::container(
4620 "containers.area.host",
4621 LayoutStyle::new()
4622 .with_width_percent(1.0)
4623 .with_height(82.0)
4624 .with_flex_shrink(0.0),
4625 )
4626 .with_visual(UiVisual::panel(
4627 color(17, 20, 25),
4628 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
4629 4.0,
4630 )),
4631 );
4632 widgets::area(
4633 ui,
4634 area_host,
4635 "containers.area",
4636 widgets::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
4637 .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
4638 .accessibility_label("Absolute positioned area"),
4639 |ui, area| {
4640 widgets::label(
4641 ui,
4642 area,
4643 "containers.area.label",
4644 "Area",
4645 text(12.0, color(238, 244, 252)),
4646 LayoutStyle::new().with_width_percent(1.0),
4647 );
4648 },
4649 );
4650}
4651
4652fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4653 let body = section(ui, parent, "forms", "Forms");
4654 let section = widgets::form_section(
4655 ui,
4656 body,
4657 "forms.profile",
4658 Some("Profile".to_string()),
4659 widgets::FormSectionOptions::default().with_layout(
4660 LayoutStyle::column()
4661 .with_width_percent(1.0)
4662 .with_padding(12.0)
4663 .with_gap(10.0),
4664 ),
4665 );
4666 let name = widgets::form_row(
4667 ui,
4668 section.root,
4669 "forms.profile.name",
4670 widgets::FormRowOptions::default(),
4671 );
4672 widgets::field_label(
4673 ui,
4674 name,
4675 "forms.profile.name.label",
4676 "Name",
4677 widgets::FieldLabelOptions::default().required(),
4678 );
4679 widgets::field_help_text(
4680 ui,
4681 name,
4682 "forms.profile.name.help",
4683 "Shown in window titles and project lists.",
4684 widgets::FieldHelpOptions::default(),
4685 );
4686 form_text_field(ui, name, "forms.profile.name.input", "Ada Lovelace");
4687
4688 let email = widgets::form_row(
4689 ui,
4690 section.root,
4691 "forms.profile.email",
4692 widgets::FormRowOptions::default()
4693 .required()
4694 .invalid("Use a complete email address"),
4695 );
4696 widgets::field_label(
4697 ui,
4698 email,
4699 "forms.profile.email.label",
4700 "Email",
4701 widgets::FieldLabelOptions::default().required(),
4702 );
4703 widgets::field_validation_message(
4704 ui,
4705 email,
4706 "forms.profile.email.validation",
4707 ValidationMessage::error("Use a complete email address"),
4708 widgets::ValidationMessageOptions::default(),
4709 );
4710 form_text_field(ui, email, "forms.profile.email.input", "ada@");
4711
4712 let role = widgets::form_row(
4713 ui,
4714 section.root,
4715 "forms.profile.role",
4716 widgets::FormRowOptions::default(),
4717 );
4718 widgets::field_label(
4719 ui,
4720 role,
4721 "forms.profile.role.label",
4722 "Role",
4723 widgets::FieldLabelOptions::default(),
4724 );
4725 widgets::field_help_text(
4726 ui,
4727 role,
4728 "forms.profile.role.help",
4729 "Form rows compose labels, controls, help, and validation text.",
4730 widgets::FieldHelpOptions::default(),
4731 );
4732 form_text_field(ui, role, "forms.profile.role.input", "Maintainer");
4733
4734 widgets::form_error_summary(
4735 ui,
4736 section.root,
4737 "forms.profile.errors",
4738 &state.form,
4739 widgets::FormErrorSummaryOptions::default(),
4740 );
4741 widgets::form_action_buttons(
4742 ui,
4743 section.root,
4744 "forms.profile.actions",
4745 &state.form,
4746 widgets::FormActionButtonsOptions::default()
4747 .include_reset(true)
4748 .with_action_prefix("forms.profile"),
4749 );
4750 widgets::label(
4751 ui,
4752 section.root,
4753 "forms.profile.status",
4754 format!("Status: {}", state.form_status),
4755 text(11.0, color(154, 166, 184)),
4756 LayoutStyle::new().with_width_percent(1.0),
4757 );
4758}
4759
4760fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4761 let body = section(ui, parent, "overlays", "Overlays");
4762 let header = widgets::collapsing_header(
4763 ui,
4764 body,
4765 "overlays.collapsing",
4766 "Collapsing header",
4767 widgets::CollapsingHeaderOptions::default()
4768 .expanded(state.overlay_expanded)
4769 .with_toggle_action("overlays.collapsing.toggle"),
4770 );
4771 if let Some(panel) = header.body {
4772 widgets::label(
4773 ui,
4774 panel,
4775 "overlays.collapsing.body",
4776 "Expanded content lives under the header.",
4777 text(12.0, color(196, 210, 230)),
4778 LayoutStyle::new().with_width_percent(1.0),
4779 );
4780 }
4781
4782 let controls = row(ui, body, "overlays.controls", 8.0);
4783 button(
4784 ui,
4785 controls,
4786 "overlays.popup.toggle",
4787 if state.overlay_popup_open {
4788 "Close popup"
4789 } else {
4790 "Open popup"
4791 },
4792 "overlays.popup.toggle",
4793 button_visual(48, 112, 184),
4794 );
4795 button(
4796 ui,
4797 controls,
4798 "overlays.modal.open",
4799 "Open modal",
4800 "overlays.modal.open",
4801 button_visual(58, 78, 96),
4802 );
4803
4804 let tooltip = TooltipContent::new("Tooltip")
4805 .body("Tooltip boxes are overlay surfaces with title, body, and shortcut text.")
4806 .shortcut_label("Ctrl+K");
4807 let mut tooltip_options = widgets::TooltipBoxOptions::default()
4808 .with_layout(
4809 LayoutStyle::column()
4810 .with_width(280.0)
4811 .with_padding(8.0)
4812 .with_gap(4.0),
4813 )
4814 .with_animation(None);
4815 tooltip_options.layer = UiLayer::AppContent;
4816 tooltip_options.z_index = 0;
4817 widgets::tooltip_box(ui, body, "overlays.tooltip", tooltip, tooltip_options);
4818
4819 if state.overlay_popup_open {
4820 let popup = widgets::popup_panel(
4821 ui,
4822 parent,
4823 "overlays.popup_panel",
4824 UiRect::new(0.0, 20.0, 160.0, 96.0),
4825 widgets::PopupOptions {
4826 z_index: 20,
4827 accessibility: Some(
4828 AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup"),
4829 ),
4830 ..Default::default()
4831 },
4832 );
4833 let popup_body = ui.add_child(
4834 popup,
4835 UiNode::container(
4836 "overlays.popup_panel.body",
4837 LayoutStyle::column()
4838 .with_width_percent(1.0)
4839 .with_height_percent(1.0)
4840 .with_padding(10.0)
4841 .with_gap(6.0),
4842 ),
4843 );
4844 let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
4845 widgets::label(
4846 ui,
4847 popup_header,
4848 "overlays.popup_panel.label",
4849 "Popup panel",
4850 text(12.0, color(220, 228, 238)),
4851 LayoutStyle::new().with_width_percent(1.0),
4852 );
4853 let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
4854 .with_action("overlays.popup.close");
4855 close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
4856 close.hovered_visual = Some(button_visual(54, 70, 92));
4857 close.text_style = text(12.0, color(220, 228, 238));
4858 widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
4859 widgets::label(
4860 ui,
4861 popup_body,
4862 "overlays.popup_panel.body_text",
4863 "Popup content is conditionally rendered.",
4864 text(11.0, color(196, 210, 230)),
4865 LayoutStyle::new().with_width_percent(1.0),
4866 );
4867 }
4868
4869 if state.overlay_modal_open {
4870 let modal = widgets::modal_dialog(
4871 ui,
4872 body,
4873 "overlays.modal",
4874 "Modal dialog",
4875 widgets::ModalDialogOptions::default()
4876 .with_size(320.0, 180.0)
4877 .with_close_action("overlays.modal.close")
4878 .with_dismissal(widgets::DialogDismissal::STANDARD)
4879 .with_focus_restore(FocusRestoreTarget::Previous)
4880 .modeless(),
4881 );
4882 widgets::label(
4883 ui,
4884 modal.body,
4885 "overlays.modal.body.text",
4886 "Dialog body",
4887 text(12.0, color(220, 228, 238)),
4888 LayoutStyle::new().with_width_percent(1.0),
4889 );
4890 }
4891}
4892
4893fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4894 let body = section(ui, parent, "drag_drop", "Drag and drop");
4895 widgets::dnd_drag_source(
4896 ui,
4897 body,
4898 "drag_drop.text_source",
4899 "Drag text payload",
4900 DragPayload::text("Operad payload"),
4901 widgets::DragSourceOptions::default()
4902 .with_kind(DragDropSurfaceKind::ListRow)
4903 .with_action("drag_drop.text_source")
4904 .with_accessibility_hint("Start a text drag operation"),
4905 );
4906
4907 let accepted_options = widgets::DropZoneOptions::default()
4908 .with_kind(DragDropSurfaceKind::EditorSurface)
4909 .with_accepted_payload(DropPayloadFilter::empty().text())
4910 .with_action("drag_drop.accept_text")
4911 .with_accessibility_hint("Accepts text payloads");
4912 let accepted = widgets::dnd_drop_zone(
4913 ui,
4914 body,
4915 "drag_drop.accept_text",
4916 "Drop text here",
4917 accepted_options.clone(),
4918 );
4919 widgets::dnd_apply_drop_zone_preview(
4920 ui,
4921 accepted.root,
4922 &accepted_options,
4923 widgets::DropZonePreviewState::Accepted,
4924 );
4925
4926 let rejected_options = widgets::DropZoneOptions::default()
4927 .with_layout(
4928 LayoutStyle::column()
4929 .with_width(240.0)
4930 .with_height(82.0)
4931 .with_padding(12.0),
4932 )
4933 .with_kind(DragDropSurfaceKind::Asset)
4934 .with_accepted_payload(DropPayloadFilter::empty().files())
4935 .with_action("drag_drop.files_only");
4936 let rejected = widgets::dnd_drop_zone(
4937 ui,
4938 body,
4939 "drag_drop.files_only",
4940 "Files only",
4941 rejected_options.clone(),
4942 );
4943 widgets::dnd_apply_drop_zone_preview(
4944 ui,
4945 rejected.root,
4946 &rejected_options,
4947 widgets::DropZonePreviewState::Rejected,
4948 );
4949 widgets::label(
4950 ui,
4951 body,
4952 "drag_drop.status",
4953 format!("Status: {}", state.drag_drop_status),
4954 text(11.0, color(154, 166, 184)),
4955 LayoutStyle::new().with_width_percent(1.0),
4956 );
4957}
4958
4959fn media_widgets(ui: &mut UiDocument, parent: UiNodeId) {
4960 let body = section(ui, parent, "media", "Media");
4961 let icons = row(ui, body, "media.icons", 10.0);
4962 widgets::image(
4963 ui,
4964 icons,
4965 "media.image.play",
4966 icon_image(BuiltInIcon::Play),
4967 widgets::ImageOptions::default()
4968 .with_layout(LayoutStyle::size(42.0, 42.0))
4969 .with_accessibility_label("Play icon"),
4970 );
4971 widgets::image(
4972 ui,
4973 icons,
4974 "media.image.warning",
4975 ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
4976 widgets::ImageOptions::default()
4977 .with_layout(LayoutStyle::size(42.0, 42.0))
4978 .with_accessibility_label("Warning icon"),
4979 );
4980 widgets::image(
4981 ui,
4982 icons,
4983 "media.image.info",
4984 ImageContent::new(BuiltInIcon::Info.key()).tinted(color(118, 183, 255)),
4985 widgets::ImageOptions::default()
4986 .with_layout(LayoutStyle::size(42.0, 42.0))
4987 .with_accessibility_label("Info icon"),
4988 );
4989 widgets::label(
4990 ui,
4991 body,
4992 "media.image.note",
4993 "Image widgets reference stable resource keys; the host resolves them to textures or vector assets.",
4994 text(12.0, color(166, 176, 190)),
4995 LayoutStyle::new().with_width_percent(1.0),
4996 );
4997}
4998
4999fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId) {
5000 let mut layout = LayoutStyle::column()
5001 .with_width_percent(1.0)
5002 .with_height(40.0)
5003 .with_flex_shrink(0.0);
5004 layout.as_taffy_style_mut().min_size.width = operad::length(0.0);
5005 layout.as_taffy_style_mut().min_size.height = operad::length(0.0);
5006 let body = widgets::scroll_area(ui, parent, "timeline", ScrollAxes::BOTH, layout);
5007 widgets::timeline_ruler(
5008 ui,
5009 body,
5010 "timeline.ruler",
5011 widgets::RulerSpec {
5012 range: widgets::TimelineRange::new(0.0, 12.0),
5013 width: 600.0,
5014 major_step: 2.0,
5015 minor_step: 0.5,
5016 label_every: 1,
5017 },
5018 widgets::TimelineRulerOptions::default(),
5019 );
5020}
5021
5022fn toast_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5023 let body = section(ui, parent, "toasts", "Toasts");
5024 let controls = row(ui, body, "toasts.controls", 10.0);
5025 button(
5026 ui,
5027 controls,
5028 "toasts.show",
5029 "Show toast",
5030 "toast.show",
5031 button_visual(48, 112, 184),
5032 );
5033 button(
5034 ui,
5035 controls,
5036 "toasts.hide",
5037 "Hide",
5038 "toast.hide",
5039 button_visual(58, 78, 96),
5040 );
5041 widgets::label(
5042 ui,
5043 body,
5044 "toasts.status",
5045 if state.toast_visible {
5046 "Toast overlay is visible."
5047 } else {
5048 "Toast overlay is hidden."
5049 },
5050 text(12.0, color(196, 210, 230)),
5051 LayoutStyle::new().with_width_percent(1.0),
5052 );
5053 widgets::label(
5054 ui,
5055 body,
5056 "toasts.action_status",
5057 format!("Action: {}", state.toast_action_status),
5058 text(12.0, color(154, 166, 184)),
5059 LayoutStyle::new().with_width_percent(1.0),
5060 );
5061}
5062
5063fn popup_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5064 let body = section(ui, parent, "popup_panel", "Popup panel");
5065 let controls = row(ui, body, "popup_panel.controls", 8.0);
5066 button(
5067 ui,
5068 controls,
5069 "popup_panel.toggle",
5070 if state.popup_open {
5071 "Close popup"
5072 } else {
5073 "Open popup"
5074 },
5075 "popup.toggle",
5076 button_visual(48, 112, 184),
5077 );
5078 if state.popup_open {
5079 let mut close =
5080 widgets::ButtonOptions::new(LayoutStyle::size(30.0, 30.0)).with_action("popup.close");
5081 close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
5082 close.hovered_visual = Some(button_visual(54, 70, 92));
5083 close.text_style = text(13.0, color(220, 228, 238));
5084 widgets::button(ui, controls, "popup_panel.inline_close", "x", close);
5085 }
5086 widgets::label(
5087 ui,
5088 body,
5089 "popup_panel.status",
5090 if state.popup_open {
5091 "Popup overlay is open."
5092 } else {
5093 "Popup overlay is closed."
5094 },
5095 text(12.0, color(196, 210, 230)),
5096 LayoutStyle::new().with_width_percent(1.0),
5097 );
5098 if state.popup_open {
5099 let panel = widgets::popup_panel(
5100 ui,
5101 parent,
5102 "popup_panel.inline_preview",
5103 UiRect::new(0.0, 20.0, 160.0, 104.0),
5104 widgets::PopupOptions {
5105 z_index: 4,
5106 accessibility: Some(
5107 AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
5108 ),
5109 ..Default::default()
5110 },
5111 );
5112 let content = ui.add_child(
5113 panel,
5114 UiNode::container(
5115 "popup_panel.inline_preview.body",
5116 LayoutStyle::column()
5117 .with_width_percent(1.0)
5118 .with_height_percent(1.0)
5119 .with_padding(10.0)
5120 .with_gap(8.0),
5121 ),
5122 );
5123 let header = row(ui, content, "popup_panel.inline_preview.header", 8.0);
5124 widgets::label(
5125 ui,
5126 header,
5127 "popup_panel.inline_preview.title",
5128 "Popup panel",
5129 text(12.0, color(226, 234, 246)),
5130 LayoutStyle::new().with_width_percent(1.0),
5131 );
5132 let mut close =
5133 widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0)).with_action("popup.close");
5134 close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
5135 close.hovered_visual = Some(button_visual(54, 70, 92));
5136 close.text_style = text(12.0, color(220, 228, 238));
5137 widgets::button(ui, header, "popup_panel.inline_preview.close", "x", close);
5138 widgets::label(
5139 ui,
5140 content,
5141 "popup_panel.inline_preview.text",
5142 "Overlay content",
5143 text(11.0, color(196, 210, 230)),
5144 LayoutStyle::new().with_width_percent(1.0),
5145 );
5146 widgets::spacer(
5147 ui,
5148 body,
5149 "popup_panel.inline_preview.space",
5150 LayoutStyle::new()
5151 .with_width_percent(1.0)
5152 .with_height(112.0)
5153 .with_flex_shrink(0.0),
5154 );
5155 }
5156}
5157
5158fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5159 let body = section(ui, parent, "styling", "Styling");
5160 let grid = ui.add_child(
5161 body,
5162 UiNode::container(
5163 "styling.grid",
5164 LayoutStyle::row()
5165 .with_width_percent(1.0)
5166 .with_height_percent(1.0)
5167 .gap(16.0),
5168 ),
5169 );
5170 let controls = ui.add_child(
5171 grid,
5172 UiNode::container(
5173 "styling.controls",
5174 LayoutStyle::column()
5175 .with_width(330.0)
5176 .with_height_percent(1.0)
5177 .with_flex_shrink(0.0)
5178 .gap(6.0),
5179 ),
5180 );
5181 style_checkbox(
5182 ui,
5183 controls,
5184 "styling.inner_same",
5185 "Inner margin same",
5186 state.styling.inner_same,
5187 );
5188 style_slider(
5189 ui,
5190 controls,
5191 "styling.inner",
5192 "Inner left",
5193 state.styling.inner_margin,
5194 0.0..32.0,
5195 );
5196 if !state.styling.inner_same {
5197 style_slider(
5198 ui,
5199 controls,
5200 "styling.inner_right",
5201 "Inner right",
5202 state.styling.inner_right,
5203 0.0..32.0,
5204 );
5205 style_slider(
5206 ui,
5207 controls,
5208 "styling.inner_top",
5209 "Inner top",
5210 state.styling.inner_top,
5211 0.0..32.0,
5212 );
5213 style_slider(
5214 ui,
5215 controls,
5216 "styling.inner_bottom",
5217 "Inner bottom",
5218 state.styling.inner_bottom,
5219 0.0..32.0,
5220 );
5221 }
5222 style_checkbox(
5223 ui,
5224 controls,
5225 "styling.outer_same",
5226 "Outer margin same",
5227 state.styling.outer_same,
5228 );
5229 style_slider(
5230 ui,
5231 controls,
5232 "styling.outer",
5233 "Outer left",
5234 state.styling.outer_margin,
5235 0.0..40.0,
5236 );
5237 if !state.styling.outer_same {
5238 style_slider(
5239 ui,
5240 controls,
5241 "styling.outer_right",
5242 "Outer right",
5243 state.styling.outer_right,
5244 0.0..40.0,
5245 );
5246 style_slider(
5247 ui,
5248 controls,
5249 "styling.outer_top",
5250 "Outer top",
5251 state.styling.outer_top,
5252 0.0..40.0,
5253 );
5254 style_slider(
5255 ui,
5256 controls,
5257 "styling.outer_bottom",
5258 "Outer bottom",
5259 state.styling.outer_bottom,
5260 0.0..40.0,
5261 );
5262 }
5263 style_checkbox(
5264 ui,
5265 controls,
5266 "styling.radius_same",
5267 "Corner radius same",
5268 state.styling.radius_same,
5269 );
5270 style_slider(
5271 ui,
5272 controls,
5273 "styling.radius",
5274 "Radius NW",
5275 state.styling.corner_radius,
5276 0.0..28.0,
5277 );
5278 if !state.styling.radius_same {
5279 style_slider(
5280 ui,
5281 controls,
5282 "styling.radius_ne",
5283 "Radius NE",
5284 state.styling.corner_ne,
5285 0.0..28.0,
5286 );
5287 style_slider(
5288 ui,
5289 controls,
5290 "styling.radius_sw",
5291 "Radius SW",
5292 state.styling.corner_sw,
5293 0.0..28.0,
5294 );
5295 style_slider(
5296 ui,
5297 controls,
5298 "styling.radius_se",
5299 "Radius SE",
5300 state.styling.corner_se,
5301 0.0..28.0,
5302 );
5303 }
5304 style_slider(
5305 ui,
5306 controls,
5307 "styling.shadow_x",
5308 "Shadow x",
5309 state.styling.shadow_x,
5310 -24.0..24.0,
5311 );
5312 style_slider(
5313 ui,
5314 controls,
5315 "styling.shadow_y",
5316 "Shadow y",
5317 state.styling.shadow_y,
5318 -24.0..24.0,
5319 );
5320 style_slider(
5321 ui,
5322 controls,
5323 "styling.shadow",
5324 "Shadow blur",
5325 state.styling.shadow_blur,
5326 0.0..32.0,
5327 );
5328 style_slider(
5329 ui,
5330 controls,
5331 "styling.shadow_spread",
5332 "Shadow spread",
5333 state.styling.shadow_spread,
5334 0.0..16.0,
5335 );
5336 style_slider(
5337 ui,
5338 controls,
5339 "styling.shadow_alpha",
5340 "Shadow color",
5341 state.styling.shadow_alpha,
5342 0.0..220.0,
5343 );
5344 style_slider(
5345 ui,
5346 controls,
5347 "styling.fill",
5348 "Fill color",
5349 state.styling.fill_tint,
5350 0.0..1.0,
5351 );
5352 style_slider(
5353 ui,
5354 controls,
5355 "styling.stroke_color",
5356 "Stroke color",
5357 state.styling.stroke_tint,
5358 0.0..1.0,
5359 );
5360 style_slider(
5361 ui,
5362 controls,
5363 "styling.stroke",
5364 "Stroke",
5365 state.styling.stroke_width,
5366 0.0..4.0,
5367 );
5368
5369 let preview = ui.add_child(
5370 grid,
5371 UiNode::container(
5372 "styling.preview",
5373 LayoutStyle::column()
5374 .with_width_percent(1.0)
5375 .with_height_percent(1.0)
5376 .padding(8.0),
5377 )
5378 .with_visual(UiVisual::panel(
5379 color(17, 20, 25),
5380 Some(StrokeStyle::new(color(56, 66, 82), 1.0)),
5381 4.0,
5382 )),
5383 );
5384 style_preview(ui, preview, state.styling);
5385}
5386
5387fn style_slider(
5388 ui: &mut UiDocument,
5389 parent: UiNodeId,
5390 name: &'static str,
5391 label: &'static str,
5392 value: f32,
5393 range: std::ops::Range<f32>,
5394) {
5395 let row = row(ui, parent, format!("{name}.row"), 8.0);
5396 widgets::label(
5397 ui,
5398 row,
5399 format!("{name}.label"),
5400 label,
5401 text(12.0, color(166, 176, 190)),
5402 LayoutStyle::new().with_width(118.0),
5403 );
5404 widgets::label(
5405 ui,
5406 row,
5407 format!("{name}.value"),
5408 if range.end <= 1.0 {
5409 format!("{value:.2}")
5410 } else {
5411 format!("{value:.0}")
5412 },
5413 text(12.0, color(226, 232, 242)),
5414 LayoutStyle::new().with_width(42.0),
5415 );
5416 let mut options = widgets::SliderOptions::default()
5417 .with_layout(
5418 LayoutStyle::new()
5419 .with_width(112.0)
5420 .with_height(20.0)
5421 .with_flex_shrink(0.0),
5422 )
5423 .with_value_edit_action(name);
5424 options.fill_color = color(120, 170, 230);
5425 widgets::slider(
5426 ui,
5427 row,
5428 format!("{name}.slider"),
5429 ((value - range.start) / (range.end - range.start).max(f32::EPSILON)).clamp(0.0, 1.0),
5430 0.0..1.0,
5431 options,
5432 );
5433}
5434
5435fn style_checkbox(
5436 ui: &mut UiDocument,
5437 parent: UiNodeId,
5438 name: &'static str,
5439 label: &'static str,
5440 checked: bool,
5441) {
5442 let mut options = widgets::CheckboxOptions::default().with_action(name);
5443 options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(22.0);
5444 options.text_style = text(12.0, color(220, 228, 238));
5445 widgets::checkbox(ui, parent, name, label, checked, options);
5446}
5447
5448fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
5449 let outer = styling.outer_edges();
5450 let inner = styling.inner_edges();
5451 let frame = UiRect::new(
5452 22.0 + outer[0],
5453 28.0 + outer[2],
5454 108.0 + inner[0] + inner[1],
5455 40.0 + inner[2] + inner[3],
5456 );
5457 let text_rect = UiRect::new(
5458 frame.x + inner[0],
5459 frame.y + inner[2],
5460 (frame.width - inner[0] - inner[1]).max(1.0),
5461 (frame.height - inner[2] - inner[3]).max(1.0),
5462 );
5463 ui.add_child(
5464 parent,
5465 UiNode::scene(
5466 "styling.preview.scene",
5467 vec![
5468 ScenePrimitive::Rect(
5469 PaintRect::solid(frame, styling.fill_color())
5470 .stroke(AlignedStroke::inside(StrokeStyle::new(
5471 styling.stroke_color(),
5472 styling.stroke_width,
5473 )))
5474 .corner_radii(styling.radii())
5475 .effect(PaintEffect::shadow(
5476 styling.shadow_color(),
5477 UiPoint::new(styling.shadow_x, styling.shadow_y),
5478 styling.shadow_blur,
5479 styling.shadow_spread,
5480 )),
5481 ),
5482 ScenePrimitive::Text(
5483 PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
5484 .horizontal_align(TextHorizontalAlign::Center)
5485 .vertical_align(TextVerticalAlign::Center)
5486 .multiline(false),
5487 ),
5488 ],
5489 LayoutStyle::new()
5490 .with_width_percent(1.0)
5491 .with_height(180.0)
5492 .with_flex_shrink(0.0),
5493 ),
5494 );
5495}
5496
5497fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
5498 let mut options = widgets::SliderOptions::default().with_layout(
5499 LayoutStyle::new()
5500 .with_width(width)
5501 .with_height(24.0)
5502 .with_flex_shrink(0.0),
5503 );
5504 options.fill_color = if state.slider_trailing_color {
5505 state.slider_trailing_picker.value
5506 } else {
5507 color(42, 49, 58)
5508 };
5509 options.thumb_shape = match state.slider_thumb_shape {
5510 SliderThumbChoice::Circle => widgets::SliderThumbShape::Circle,
5511 SliderThumbChoice::Square => widgets::SliderThumbShape::Square,
5512 SliderThumbChoice::Rectangle => widgets::SliderThumbShape::Rectangle,
5513 };
5514 options
5515}
5516
5517fn slider_number_input(
5518 ui: &mut UiDocument,
5519 parent: UiNodeId,
5520 name: &'static str,
5521 input: &TextInputState,
5522 focused: FocusedTextInput,
5523 state: &ShowcaseState,
5524 width: f32,
5525) {
5526 let mut options = TextInputOptions::default();
5527 options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
5528 options.text_style = text(12.0, color(230, 236, 246));
5529 options.placeholder_style = text(12.0, color(144, 156, 174));
5530 options.edit_action = Some(format!("{name}.edit").into());
5531 options.focused = state.focused_text == Some(focused);
5532 options.caret_visible = caret_visible(state.caret_phase);
5533 widgets::text_input(ui, parent, name, input, options);
5534}
5535
5536fn form_text_field(ui: &mut UiDocument, parent: UiNodeId, name: &'static str, value: &'static str) {
5537 let mut options = TextInputOptions::default();
5538 options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
5539 options.text_style = text(12.0, color(230, 236, 246));
5540 options.read_only = true;
5541 widgets::text_input(ui, parent, name, &TextInputState::new(value), options);
5542}
5543
5544fn slider_checkbox(
5545 ui: &mut UiDocument,
5546 parent: UiNodeId,
5547 name: &'static str,
5548 label: &'static str,
5549 checked: bool,
5550) {
5551 slider_checkbox_with_layout(
5552 ui,
5553 parent,
5554 name,
5555 label,
5556 checked,
5557 LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
5558 );
5559}
5560
5561fn slider_checkbox_with_layout(
5562 ui: &mut UiDocument,
5563 parent: UiNodeId,
5564 name: &'static str,
5565 label: &'static str,
5566 checked: bool,
5567 layout: LayoutStyle,
5568) {
5569 let mut options = widgets::CheckboxOptions::default().with_action(name);
5570 options.layout = layout;
5571 options.text_style = text(12.0, color(220, 228, 238));
5572 widgets::checkbox(ui, parent, name, label, checked, options);
5573}
5574
5575fn choice_button(
5576 ui: &mut UiDocument,
5577 parent: UiNodeId,
5578 name: &'static str,
5579 label: &'static str,
5580 selected: bool,
5581) {
5582 let mut options =
5583 widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
5584 .with_action(name);
5585 options.visual = if selected {
5586 button_visual(48, 112, 184)
5587 } else {
5588 button_visual(38, 46, 58)
5589 };
5590 options.hovered_visual = Some(button_visual(65, 86, 106));
5591 options.pressed_visual = Some(button_visual(34, 54, 84));
5592 options.text_style = text(12.0, color(238, 244, 252));
5593 widgets::button(ui, parent, name, label, options);
5594}
5595
5596fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
5597 ui.add_child(
5598 parent,
5599 UiNode::container(
5600 name,
5601 LayoutStyle::new()
5602 .with_width_percent(1.0)
5603 .with_height(1.0)
5604 .with_flex_shrink(0.0),
5605 )
5606 .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
5607 );
5608}
5609
5610fn canvas(ui: &mut UiDocument, parent: UiNodeId) {
5611 let body = section(ui, parent, "canvas", "Canvas");
5612 let mut options = widgets::CanvasOptions::default()
5613 .with_accessibility_label("Shader canvas")
5614 .with_action("canvas.rotate")
5615 .with_aspect_ratio(16.0 / 9.0);
5616 options.layout = LayoutStyle::new()
5617 .with_width_percent(1.0)
5618 .with_height_percent(1.0)
5619 .with_flex_grow(1.0)
5620 .with_flex_shrink(1.0);
5621 options.visual = UiVisual::panel(
5622 color(18, 22, 28),
5623 Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
5624 4.0,
5625 );
5626 widgets::canvas(
5627 ui,
5628 body,
5629 "canvas.shader",
5630 CanvasContent::new("canvas.shader").gpu_context(),
5631 options,
5632 );
5633}
5634
5635fn render_showcase_canvas(
5636 state: &mut ShowcaseState,
5637 context: NativeWgpuCanvasRenderContext<'_>,
5638) -> Result<CanvasRenderOutput, RenderError> {
5639 let size = context.surface_size();
5640 if state.cube.needs_render(size) {
5641 render_showcase_canvas_surface(state.cube, &context.surface)?;
5642 state.cube.mark_rendered(size);
5643 }
5644 Ok(CanvasRenderOutput::new())
5645}
5646
5647fn render_showcase_canvas_surface(
5648 cube: CanvasCubeState,
5649 surface: &WgpuCanvasContext<'_>,
5650) -> Result<(), RenderError> {
5651 let uniforms = canvas_cube_uniform_bytes(cube);
5652 surface.render_pass(
5653 WgpuCanvasRenderPass::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
5654 .label(Some("showcase.canvas"))
5655 .uniform_bytes(&uniforms[..])
5656 .clear_color(Some(color(18, 22, 28))),
5657 )
5658}
5659
5660fn canvas_cube_uniform_bytes(cube: CanvasCubeState) -> [u8; 16] {
5661 let mut bytes = [0_u8; 16];
5662 bytes[0..4].copy_from_slice(&cube.yaw.to_ne_bytes());
5663 bytes[4..8].copy_from_slice(&cube.pitch.to_ne_bytes());
5664 bytes
5665}
5666
5667fn section(
5668 ui: &mut UiDocument,
5669 parent: UiNodeId,
5670 name: impl Into<String>,
5671 _title: impl Into<String>,
5672) -> UiNodeId {
5673 let name = name.into();
5674 let mut layout = LayoutStyle::column()
5675 .with_width_percent(1.0)
5676 .with_height_percent(1.0)
5677 .with_flex_grow(1.0)
5678 .gap(10.0);
5679 layout.as_taffy_style_mut().min_size.width = operad::length(0.0);
5680 layout.as_taffy_style_mut().min_size.height = operad::length(0.0);
5681 widgets::scroll_area(
5682 ui,
5683 parent,
5684 format!("{name}.section_scroll"),
5685 ScrollAxes::VERTICAL,
5686 layout,
5687 )
5688}
5689
5690fn row(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>, gap: f32) -> UiNodeId {
5691 ui.add_child(
5692 parent,
5693 UiNode::container(name, LayoutStyle::row().with_width_percent(1.0).gap(gap)),
5694 )
5695}
5696
5697fn wrapping_row(
5698 ui: &mut UiDocument,
5699 parent: UiNodeId,
5700 name: impl Into<String>,
5701 gap: f32,
5702) -> UiNodeId {
5703 let mut layout = LayoutStyle::row().with_width_percent(1.0).gap(gap);
5704 layout.as_taffy_style_mut().flex_wrap = LayoutFlexWrap::Wrap.to_taffy();
5705 ui.add_child(parent, UiNode::container(name, layout))
5706}
5707
5708fn egui_panel_contents(
5709 ui: &mut UiDocument,
5710 parent: UiNodeId,
5711 name: &'static str,
5712 title: &'static str,
5713 offset_y: f32,
5714) {
5715 let header = ui.add_child(
5716 parent,
5717 UiNode::container(
5718 format!("{name}.egui_header"),
5719 LayoutStyle::row()
5720 .with_width_percent(1.0)
5721 .with_height(28.0)
5722 .with_padding(6.0)
5723 .with_flex_shrink(0.0),
5724 )
5725 .with_visual(UiVisual::panel(
5726 color(21, 26, 34),
5727 Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
5728 0.0,
5729 )),
5730 );
5731 widgets::label(
5732 ui,
5733 header,
5734 format!("{name}.egui_title"),
5735 title,
5736 text(12.0, color(226, 234, 246)),
5737 LayoutStyle::new().with_width_percent(1.0),
5738 );
5739 let scroll = widgets::scroll_area(
5740 ui,
5741 parent,
5742 format!("{name}.scroll_area"),
5743 ScrollAxes::VERTICAL,
5744 LayoutStyle::column()
5745 .with_width_percent(1.0)
5746 .with_height(0.0)
5747 .with_flex_grow(1.0)
5748 .with_padding(8.0)
5749 .with_gap(6.0),
5750 );
5751 ui.node_mut(scroll).action = Some(format!("{name}.scroll").into());
5752 if let Some(scroll_state) = ui.node_mut(scroll).scroll.as_mut() {
5753 scroll_state.offset.y = offset_y;
5754 }
5755 for (index, line) in lorem_lines().iter().take(8).enumerate() {
5756 widgets::label(
5757 ui,
5758 scroll,
5759 format!("{name}.egui_line.{index}"),
5760 *line,
5761 TextStyle {
5762 wrap: TextWrap::None,
5763 ..text(11.0, color(190, 202, 218))
5764 },
5765 LayoutStyle::new()
5766 .with_width_percent(1.0)
5767 .with_height(22.0)
5768 .with_flex_shrink(0.0),
5769 );
5770 }
5771}
5772
5773fn button(
5774 ui: &mut UiDocument,
5775 parent: UiNodeId,
5776 name: impl Into<String>,
5777 label: impl Into<String>,
5778 action: impl Into<String>,
5779 visual: UiVisual,
5780) -> UiNodeId {
5781 let mut options = widgets::ButtonOptions::new(LayoutStyle::new().with_height(32.0))
5782 .with_action(action.into());
5783 options.visual = visual;
5784 options.hovered_visual = Some(adjusted_button_visual(visual, 58));
5785 options.pressed_visual = Some(adjusted_button_visual(visual, -62));
5786 options.pressed_hovered_visual = Some(adjusted_button_visual(visual, 8));
5787 options.text_style = text(13.0, color(246, 249, 252));
5788 widgets::button(ui, parent, name, label, options)
5789}
5790
5791fn button_visual(r: u8, g: u8, b: u8) -> UiVisual {
5792 UiVisual::panel(
5793 color(r, g, b),
5794 Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
5795 4.0,
5796 )
5797}
5798
5799fn color_square_button_options(action: &'static str) -> widgets::ColorButtonOptions {
5800 widgets::ColorButtonOptions::default()
5801 .with_layout(LayoutStyle::size(30.0, 30.0).with_flex_shrink(0.0))
5802 .with_swatch_size(UiSize::new(30.0, 30.0))
5803 .with_action(action)
5804 .show_label(false)
5805}
5806
5807fn color_value_button_options(action: &'static str, width: f32) -> widgets::ColorButtonOptions {
5808 widgets::ColorButtonOptions::default()
5809 .with_layout(
5810 LayoutStyle::new()
5811 .with_width(width)
5812 .with_height(30.0)
5813 .with_flex_shrink(0.0),
5814 )
5815 .with_action(action)
5816}
5817
5818fn icon_image(icon: BuiltInIcon) -> ImageContent {
5819 ImageContent::new(icon.key()).tinted(color(220, 228, 238))
5820}
5821
5822fn adjusted_button_visual(visual: UiVisual, delta: i16) -> UiVisual {
5823 UiVisual::panel(
5824 adjust_color(visual.fill, delta),
5825 visual.stroke.map(|stroke| StrokeStyle {
5826 color: adjust_color(stroke.color, delta / 2),
5827 width: stroke.width,
5828 }),
5829 visual.corner_radius,
5830 )
5831}
5832
5833fn adjust_color(color: ColorRgba, delta: i16) -> ColorRgba {
5834 let channel = |value: u8| -> u8 { (i16::from(value) + delta).clamp(0, u8::MAX as i16) as u8 };
5835 ColorRgba::new(
5836 channel(color.r),
5837 channel(color.g),
5838 channel(color.b),
5839 color.a,
5840 )
5841}
5842
5843fn select_options() -> Vec<widgets::SelectOption> {
5844 vec![
5845 widgets::SelectOption::new("compact", "Compact"),
5846 widgets::SelectOption::new("comfortable", "Comfortable"),
5847 widgets::SelectOption::new("spacious", "Spacious"),
5848 widgets::SelectOption::new("disabled", "Disabled").disabled(),
5849 ]
5850}
5851
5852fn label_locale_options() -> Vec<widgets::SelectOption> {
5853 vec![
5854 widgets::SelectOption::new("en-US", "English"),
5855 widgets::SelectOption::new("es-MX", "Español"),
5856 widgets::SelectOption::new("fr-FR", "Français"),
5857 widgets::SelectOption::new("de-DE", "Deutsch"),
5858 widgets::SelectOption::new("it-IT", "Italiano"),
5859 widgets::SelectOption::new("pt-BR", "Português"),
5860 widgets::SelectOption::new("nl-NL", "Nederlands"),
5861 ]
5862}
5863
5864fn localized_label(locale_id: &str) -> &'static str {
5865 match locale_id {
5866 "en-US" => "Interface language: English",
5867 "fr-FR" => "Langue de l'interface : français",
5868 "de-DE" => "Sprache der Oberfläche: Deutsch",
5869 "it-IT" => "Lingua dell'interfaccia: italiano",
5870 "pt-BR" => "Idioma da interface: português",
5871 "nl-NL" => "Interfacetaal: Nederlands",
5872 _ => "Idioma de interfaz: español de México",
5873 }
5874}
5875
5876fn lorem_lines() -> [&'static str; 8] {
5877 [
5878 "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
5879 "Integer vitae arcu at neque feugiat posuere.",
5880 "Suspendisse potenti. Praesent eget sem non mauris luctus.",
5881 "Curabitur blandit, justo non gravida tristique, mi nunc.",
5882 "Donec at nibh vel sapien facilisis feugiat.",
5883 "Aliquam erat volutpat. Nam porttitor sem at ligula.",
5884 "Vivamus dictum eros vitae tortor aliquet, in tempor urna.",
5885 "Sed finibus velit non lectus efficitur, sed tempor orci.",
5886 ]
5887}
5888
5889fn menu_bar_menus(autosave: bool, grid: bool) -> Vec<widgets::MenuBarMenu> {
5890 vec![
5891 widgets::MenuBarMenu::new("file", "File", menu_items(autosave)),
5892 widgets::MenuBarMenu::new(
5893 "edit",
5894 "Edit",
5895 vec![
5896 widgets::MenuItem::command("undo", "Undo").shortcut("Ctrl+Z"),
5897 widgets::MenuItem::command("redo", "Redo").shortcut("Ctrl+Shift+Z"),
5898 ],
5899 ),
5900 widgets::MenuBarMenu::new(
5901 "view",
5902 "View",
5903 vec![widgets::MenuItem::check("grid", "Grid", grid)],
5904 ),
5905 ]
5906}
5907
5908fn menu_items(autosave: bool) -> Vec<widgets::MenuItem> {
5909 vec![
5910 widgets::MenuItem::command("new", "New").shortcut("Ctrl+N"),
5911 widgets::MenuItem::command("open", "Open").shortcut("Ctrl+O"),
5912 widgets::MenuItem::separator(),
5913 widgets::MenuItem::check("autosave", "Autosave", autosave),
5914 widgets::MenuItem::submenu(
5915 "recent",
5916 "Recent",
5917 vec![
5918 widgets::MenuItem::command("recent.one", "demo.rs"),
5919 widgets::MenuItem::command("recent.two", "notes.md"),
5920 ],
5921 ),
5922 widgets::MenuItem::command("delete", "Delete").destructive(),
5923 widgets::MenuItem::command("disabled", "Disabled").disabled(),
5924 ]
5925}
5926
5927fn menu_item_top_offset(items: &[widgets::MenuItem], index: usize) -> f32 {
5928 items
5929 .iter()
5930 .take(index)
5931 .map(|item| menu_item_height(Some(item)))
5932 .sum()
5933}
5934
5935fn menu_item_height(item: Option<&widgets::MenuItem>) -> f32 {
5936 if item.is_some_and(widgets::MenuItem::is_separator) {
5937 8.0
5938 } else {
5939 28.0
5940 }
5941}
5942
5943fn command_palette_items() -> Vec<widgets::CommandPaletteItem> {
5944 vec![
5945 widgets::CommandPaletteItem::new("open", "Open")
5946 .subtitle("Open a document")
5947 .shortcut("Ctrl+O")
5948 .keyword("file"),
5949 widgets::CommandPaletteItem::new("save", "Save")
5950 .subtitle("Write current changes")
5951 .shortcut("Ctrl+S"),
5952 widgets::CommandPaletteItem::new("format", "Format document")
5953 .subtitle("Apply source formatting")
5954 .keyword("code"),
5955 widgets::CommandPaletteItem::new("rename", "Rename symbol")
5956 .subtitle("Change every reference")
5957 .shortcut("F2"),
5958 widgets::CommandPaletteItem::new("toggle_sidebar", "Toggle sidebar")
5959 .subtitle("Show or hide the widget panel")
5960 .shortcut("Ctrl+B"),
5961 widgets::CommandPaletteItem::new("run", "Run current example")
5962 .subtitle("Launch showcase")
5963 .shortcut("Ctrl+R"),
5964 widgets::CommandPaletteItem::new("focus_canvas", "Focus canvas")
5965 .subtitle("Move interaction to the canvas window"),
5966 widgets::CommandPaletteItem::new("reset_layout", "Reset window layout")
5967 .subtitle("Restore the default showcase positions"),
5968 widgets::CommandPaletteItem::new("disabled", "Disabled command").disabled(),
5969 ]
5970}
5971
5972fn table_columns() -> Vec<widgets::TableColumn> {
5973 vec![
5974 widgets::TableColumn {
5975 id: "name".to_string(),
5976 label: "Name".to_string(),
5977 width: 160.0,
5978 },
5979 widgets::TableColumn {
5980 id: "status".to_string(),
5981 label: "Status".to_string(),
5982 width: 140.0,
5983 },
5984 widgets::TableColumn {
5985 id: "value".to_string(),
5986 label: "Value".to_string(),
5987 width: 100.0,
5988 },
5989 ]
5990}
5991
5992fn tree_items() -> Vec<widgets::TreeItem> {
5993 vec![
5994 widgets::TreeItem::new("root", "Project").with_children(vec![
5995 widgets::TreeItem::new("src", "src").with_children(vec![
5996 widgets::TreeItem::new("lib", "lib.rs"),
5997 widgets::TreeItem::new("widgets", "widgets.rs"),
5998 ]),
5999 widgets::TreeItem::new("assets", "assets").with_children(vec![
6000 widgets::TreeItem::new("shader", "shader.wgsl"),
6001 widgets::TreeItem::new("logo", "logo.png"),
6002 ]),
6003 widgets::TreeItem::new("target", "target").disabled(),
6004 ]),
6005 ]
6006}
6007
6008fn parse_calendar_date(value: &str) -> Option<CalendarDate> {
6009 let mut parts = value.split('-');
6010 let year = parts.next()?.parse().ok()?;
6011 let month = parts.next()?.parse().ok()?;
6012 let day = parts.next()?.parse().ok()?;
6013 CalendarDate::new(year, month, day)
6014}
6015
6016fn parse_table_cell(value: &str) -> Option<widgets::DataTableCellIndex> {
6017 let mut parts = value.split('.');
6018 let row = parts.next()?.parse().ok()?;
6019 let column = parts.next()?.parse().ok()?;
6020 if parts.next().is_some() {
6021 return None;
6022 }
6023 Some(widgets::DataTableCellIndex::new(row, column))
6024}
6025
6026fn unit(value: f32) -> f32 {
6027 value.clamp(0.0, 1.0)
6028}
6029
6030fn smooth_loop(phase: f32, offset: f32) -> f32 {
6031 0.5 - ((phase + offset).cos() * 0.5)
6032}
6033
6034fn create_system_clipboard() -> Option<arboard::Clipboard> {
6035 arboard::Clipboard::new().ok()
6036}
6037
6038fn profile_form_state() -> FormState {
6039 let mut form = FormState::new("profile")
6040 .with_field("name", "Operad")
6041 .with_field("email", "invalid@example")
6042 .with_field("role", "Designer");
6043 form.update_field("email", "invalid@example").unwrap();
6044 let request = form.begin_form_validation();
6045 let _ = form.apply_form_validation(
6046 FormValidationResult::new(request.generation)
6047 .with_field_messages(
6048 "email",
6049 vec![ValidationMessage::error("Use a complete email address")],
6050 )
6051 .with_form_message(ValidationMessage::warning("Unsaved profile changes")),
6052 );
6053 form
6054}
6055
6056fn scaled_slider(rect: UiRect, point: UiPoint, min: f32, max: f32) -> f32 {
6057 min + unit(widgets::slider_value_from_control_point(
6058 rect,
6059 point,
6060 0.0..1.0,
6061 )) * (max - min)
6062}
6063
6064fn scroll_state(offset_y: f32, viewport_height: f32, content_height: f32) -> operad::ScrollState {
6065 operad::ScrollState {
6066 axes: ScrollAxes::VERTICAL,
6067 offset: UiPoint::new(0.0, offset_y),
6068 viewport_size: UiSize::new(8.0, viewport_height),
6069 content_size: UiSize::new(8.0, content_height),
6070 }
6071}
6072
6073fn controls_list_viewport_height(viewport_height: f32) -> f32 {
6074 (viewport_height - 110.0).max(120.0)
6075}
6076
6077fn controls_scroll_state(offset_y: f32, viewport_height: f32) -> operad::ScrollState {
6078 let mut scroll = scroll_state(offset_y, viewport_height, controls_list_content_height());
6079 scroll.offset = scroll.clamp_offset(scroll.offset);
6080 scroll
6081}
6082
6083fn controls_list_content_height() -> f32 {
6084 SHOWCASE_WIDGET_WINDOW_IDS.len() as f32 * CONTROLS_WIDGET_ROW_HEIGHT
6085 + (SHOWCASE_WIDGET_WINDOW_IDS.len().saturating_sub(1)) as f32 * CONTROLS_WIDGET_ROW_GAP
6086}
6087
6088fn caret_visible(phase: f32) -> bool {
6089 phase.sin() >= 0.0
6090}
6091
6092fn open_url(url: &str) {
6093 #[cfg(target_os = "linux")]
6094 let _ = std::process::Command::new("xdg-open").arg(url).spawn();
6095 #[cfg(target_os = "macos")]
6096 let _ = std::process::Command::new("open").arg(url).spawn();
6097 #[cfg(target_os = "windows")]
6098 let _ = std::process::Command::new("cmd")
6099 .args(["/C", "start", "", url])
6100 .spawn();
6101 #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
6102 let _ = url;
6103}
6104
6105fn text(size: f32, color: ColorRgba) -> TextStyle {
6106 TextStyle {
6107 font_size: size,
6108 line_height: size + 5.0,
6109 color,
6110 ..Default::default()
6111 }
6112}
6113
6114fn color(r: u8, g: u8, b: u8) -> ColorRgba {
6115 ColorRgba::new(r, g, b, 255)
6116}
6117
6118#[cfg(test)]
6119mod tests {
6120 use super::*;
6121 use operad::{ApproxTextMeasurer, AuditWarning};
6122
6123 fn state_with_window(id: &str) -> ShowcaseState {
6124 let mut state = ShowcaseState::default();
6125 state.windows.clear_all();
6126 *state.windows.slot_mut(id).expect("known showcase window") = true;
6127 if id == "popup_panel" {
6128 state.popup_open = true;
6129 }
6130 if id == "overlays" {
6131 state.overlay_popup_open = true;
6132 state.overlay_modal_open = true;
6133 }
6134 state
6135 }
6136
6137 fn severe_layout_warning(warning: &AuditWarning) -> bool {
6138 matches!(
6139 warning,
6140 AuditWarning::NonFiniteRect { .. }
6141 | AuditWarning::EmptyInteractiveClip { .. }
6142 | AuditWarning::TextClipped { .. }
6143 | AuditWarning::NodeOutsideRoot { .. }
6144 | AuditWarning::PaintItemEmptyClip { .. }
6145 )
6146 }
6147
6148 #[test]
6149 fn showcase_windows_avoid_hard_clipping_at_common_viewport_sizes() {
6150 let viewports = [
6151 UiSize::new(900.0, 760.0),
6152 UiSize::new(720.0, 560.0),
6153 UiSize::new(1180.0, 820.0),
6154 ];
6155
6156 for viewport in viewports {
6157 for id in SHOWCASE_WIDGET_WINDOW_IDS {
6158 let state = state_with_window(id);
6159 let mut document = state.view(viewport);
6160 document
6161 .compute_layout(viewport, &mut ApproxTextMeasurer)
6162 .expect("showcase layout");
6163 let warnings = document
6164 .audit_layout()
6165 .into_iter()
6166 .filter(severe_layout_warning)
6167 .collect::<Vec<_>>();
6168 assert!(
6169 warnings.is_empty(),
6170 "window {id:?} at {viewport:?} emitted severe layout warnings: {warnings:#?}"
6171 );
6172 }
6173 }
6174 }
6175
6176 #[test]
6177 fn showcase_windows_survive_small_user_resizes() {
6178 let viewport = UiSize::new(900.0, 760.0);
6179 for id in SHOWCASE_WIDGET_WINDOW_IDS {
6180 let mut state = state_with_window(id);
6181 state
6182 .desktop
6183 .sizes
6184 .insert(id.to_string(), UiSize::new(220.0, 140.0));
6185 state.desktop.user_sized.insert(id.to_string());
6186 if id == "selection" {
6187 state.combo_open = true;
6188 state.dropdown.open = true;
6189 }
6190 if id == "slider" {
6191 state.slider_trailing_picker_open = true;
6192 }
6193 if id == "menus" {
6194 state.menu_button.open(&menu_items(state.menu_autosave));
6195 state
6196 .context_menu
6197 .open_with_items(UiPoint::new(160.0, 160.0), &menu_items(state.menu_autosave));
6198 }
6199 if id == "overlays" {
6200 state.overlay_popup_open = true;
6201 state.overlay_modal_open = true;
6202 }
6203 if id == "popup_panel" {
6204 state.popup_open = true;
6205 }
6206
6207 let mut document = state.view(viewport);
6208 document
6209 .compute_layout(viewport, &mut ApproxTextMeasurer)
6210 .expect("showcase layout");
6211 let warnings = document
6212 .audit_layout()
6213 .into_iter()
6214 .filter(severe_layout_warning)
6215 .collect::<Vec<_>>();
6216 assert!(
6217 warnings.is_empty(),
6218 "resized window {id:?} emitted severe layout warnings: {warnings:#?}"
6219 );
6220 }
6221 }
6222
6223 #[test]
6224 fn showcase_canvas_aspect_fits_when_window_is_short() {
6225 let mut state = state_with_window("canvas");
6226 state
6227 .desktop
6228 .sizes
6229 .insert("canvas".to_string(), UiSize::new(420.0, 160.0));
6230 state.desktop.user_sized.insert("canvas".to_string());
6231
6232 let viewport = UiSize::new(900.0, 760.0);
6233 let mut document = state.view(viewport);
6234 document
6235 .compute_layout(viewport, &mut ApproxTextMeasurer)
6236 .expect("showcase layout");
6237
6238 let canvas = document
6239 .nodes()
6240 .iter()
6241 .find(|node| node.name == "canvas.shader")
6242 .expect("canvas shader node");
6243 let rect = canvas.layout.rect;
6244 assert!(rect.width > 0.0 && rect.height > 0.0, "{rect:?}");
6245 assert!(
6246 (rect.width / rect.height - 16.0 / 9.0).abs() < 0.01,
6247 "{rect:?}"
6248 );
6249 assert!(rect.width < 300.0, "{rect:?}");
6250 }
6251
6252 #[test]
6253 fn showcase_slider_primary_track_width_is_stable_when_window_resizes() {
6254 let mut widths = Vec::new();
6255 for window_width in [430.0, 340.0] {
6256 let mut state = state_with_window("slider");
6257 state
6258 .desktop
6259 .sizes
6260 .insert("slider".to_string(), UiSize::new(window_width, 360.0));
6261 state.desktop.user_sized.insert("slider".to_string());
6262
6263 let viewport = UiSize::new(900.0, 760.0);
6264 let mut document = state.view(viewport);
6265 document
6266 .compute_layout(viewport, &mut ApproxTextMeasurer)
6267 .expect("showcase layout");
6268 let slider = document
6269 .nodes()
6270 .iter()
6271 .find(|node| node.name == "slider.value")
6272 .expect("primary slider node");
6273 widths.push(slider.layout.rect.width);
6274 }
6275
6276 assert!((widths[0] - 180.0).abs() < 0.01, "{widths:?}");
6277 assert!((widths[1] - 180.0).abs() < 0.01, "{widths:?}");
6278 }
6279
6280 #[test]
6281 fn showcase_slider_color_button_opens_inline_picker() {
6282 let mut state = state_with_window("slider");
6283 state.update(WidgetAction::activate(
6284 UiNodeId(0),
6285 "slider.trailing_color_button",
6286 ));
6287 assert!(state.slider_trailing_picker_open);
6288
6289 let viewport = UiSize::new(900.0, 760.0);
6290 let mut document = state.view(viewport);
6291 document
6292 .compute_layout(viewport, &mut ApproxTextMeasurer)
6293 .expect("showcase layout");
6294 assert!(document
6295 .nodes()
6296 .iter()
6297 .any(|node| node.name == "slider.trailing_picker"));
6298 assert!(!document
6299 .nodes()
6300 .iter()
6301 .any(|node| node.name == "slider.trailing_color_button.label"));
6302 }
6303
6304 #[test]
6305 fn showcase_progress_phase_does_not_wrap_on_tick() {
6306 let mut state = ShowcaseState::default();
6307 state.progress_phase = std::f32::consts::TAU - 0.001;
6308 state.update(WidgetAction::activate(UiNodeId(0), "runtime.tick"));
6309 assert!(state.progress_phase > std::f32::consts::TAU);
6310 }
6311}