Skip to main content

UiSize

Struct UiSize 

Source
pub struct UiSize {
    pub width: f32,
    pub height: f32,
}

Fields§

§width: f32§height: f32

Implementations§

Source§

impl UiSize

Source

pub const ZERO: Self

Source

pub const fn new(width: f32, height: f32) -> Self

Examples found in repository?
examples/three_consumer_probe.rs (line 16)
7fn main() {
8    let probes = [
9        ("game_hud", build_game_hud()),
10        ("fabricad_panel", build_fabricad_panel()),
11        ("orbifold_editor", build_orbifold_editor()),
12    ];
13
14    for (name, mut document) in probes {
15        document
16            .compute_layout(UiSize::new(800.0, 600.0), &mut ApproxTextMeasurer)
17            .expect("probe layout should compute");
18        assert!(
19            document.audit_layout().is_empty(),
20            "{name} should have no basic layout audit warnings"
21        );
22        assert!(
23            !document.paint_list().is_empty(),
24            "{name} should produce renderer-neutral paint"
25        );
26        println!("{name}: {} paint items", document.paint_list().items.len());
27    }
28}
More examples
Hide additional examples
examples/native_wgpu_host.rs (line 36)
30fn main() -> Result<(), Box<dyn Error>> {
31    #[cfg(all(feature = "wgpu", feature = "native-window"))]
32    if std::env::var_os("OPERAD_RUN_WGPU_EXAMPLE_WINDOW").is_some() {
33        return run_windowed_wgpu_example();
34    }
35
36    let viewport = UiSize::new(640.0, 360.0);
37    let frame = sample_frame(viewport, RenderTarget::offscreen(PixelSize::new(640, 360)))?;
38
39    println!(
40        "native_wgpu_host: {} paint items, {} accessibility nodes",
41        frame.render_request.paint.items.len(),
42        frame.accessibility_tree.nodes.len()
43    );
44
45    #[cfg(feature = "wgpu")]
46    if std::env::var_os("OPERAD_RUN_WGPU_EXAMPLE").is_some() {
47        let mut renderer = WgpuRenderer::new();
48        let output = renderer.render_frame(frame.render_request, &EmptyResourceResolver)?;
49        println!(
50            "native_wgpu_host: rendered {} items into {:?}",
51            output.painted_items, output.target
52        );
53    }
54
55    Ok(())
56}
57
58fn sample_frame(
59    viewport: UiSize,
60    target: RenderTarget,
61) -> Result<HostDocumentFrameOutput, Box<dyn Error>> {
62    let mut document = build_document();
63    let mut measurer = ApproxTextMeasurer;
64    let host_output = HostFrameOutput::new(HostInteractionState::default());
65    Ok(process_document_frame(
66        &mut document,
67        &mut measurer,
68        HostDocumentFrameRequest::new(viewport, target, host_output),
69    )?)
70}
71
72#[cfg(all(feature = "wgpu", feature = "native-window"))]
73fn run_windowed_wgpu_example() -> Result<(), Box<dyn Error>> {
74    let event_loop = EventLoop::new()?;
75    let mut app = NativeWindowApp::new(window_frame_limit()?);
76    event_loop.run_app(&mut app)?;
77    if let Some(error) = app.error {
78        Err(error.into())
79    } else {
80        if app.presented_frames > 0 {
81            println!(
82                "native_wgpu_host: native surface presented {} frame(s), p95 render {:?}",
83                app.presented_frames,
84                percentile_duration(&app.render_samples, 95.0).unwrap_or_default()
85            );
86        }
87        Ok(())
88    }
89}
90
91#[cfg(all(feature = "wgpu", feature = "native-window"))]
92struct NativeWindowApp {
93    window: Option<Arc<Window>>,
94    window_id: Option<WindowId>,
95    renderer: Option<WgpuSurfaceRenderer<'static>>,
96    #[cfg(feature = "accesskit-winit")]
97    accesskit: Option<AccessKitWinitAdapter>,
98    error: Option<String>,
99    frame_limit: Option<usize>,
100    presented_frames: usize,
101    render_samples: Vec<Duration>,
102}
103
104#[cfg(all(feature = "wgpu", feature = "native-window"))]
105impl NativeWindowApp {
106    fn new(frame_limit: Option<usize>) -> Self {
107        Self {
108            window: None,
109            window_id: None,
110            renderer: None,
111            #[cfg(feature = "accesskit-winit")]
112            accesskit: None,
113            error: None,
114            frame_limit,
115            presented_frames: 0,
116            render_samples: Vec::new(),
117        }
118    }
119
120    fn init_window(&mut self, event_loop: &ActiveEventLoop) -> Result<(), Box<dyn Error>> {
121        let window = Arc::new(
122            event_loop.create_window(
123                Window::default_attributes()
124                    .with_title("Operad native WGPU host")
125                    .with_inner_size(PhysicalSize::new(640, 360))
126                    .with_visible(false),
127            )?,
128        );
129        let size = nonzero_window_size(window.inner_size());
130
131        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle());
132        let surface = instance.create_surface(window.clone())?;
133        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
134            compatible_surface: Some(&surface),
135            power_preference: wgpu::PowerPreference::default(),
136            force_fallback_adapter: false,
137        }))?;
138        let (device, queue) =
139            pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
140                label: Some("native-wgpu-host-device"),
141                required_features: wgpu::Features::empty(),
142                required_limits: wgpu::Limits::default(),
143                experimental_features: wgpu::ExperimentalFeatures::disabled(),
144                memory_hints: wgpu::MemoryHints::Performance,
145                trace: wgpu::Trace::Off,
146            }))?;
147        let surface_config = surface
148            .get_default_config(&adapter, size.width, size.height)
149            .ok_or("adapter does not support the native window surface")?;
150
151        self.window_id = Some(window.id());
152        self.renderer = Some(WgpuSurfaceRenderer::new(
153            surface,
154            device,
155            queue,
156            surface_config,
157        )?);
158        #[cfg(feature = "accesskit-winit")]
159        {
160            let viewport = UiSize::new(size.width as f32, size.height as f32);
161            let frame = sample_frame(viewport, RenderTarget::window("native-wgpu-host", viewport))?;
162            self.accesskit = Some(AccessKitWinitAdapter::new(
163                event_loop,
164                &window,
165                frame.accessibility_tree,
166                None,
167                AccessKitTreeOptions::default(),
168            ));
169        }
170        window.set_visible(true);
171        self.window = Some(window);
172        Ok(())
173    }
174
175    fn render(&mut self) -> Result<bool, Box<dyn Error>> {
176        let Some(window) = self.window.as_ref() else {
177            return Ok(false);
178        };
179        let Some(renderer) = self.renderer.as_mut() else {
180            return Ok(false);
181        };
182
183        let size = window.inner_size();
184        if size.width == 0 || size.height == 0 {
185            return Ok(false);
186        }
187        let viewport = UiSize::new(size.width as f32, size.height as f32);
188        let frame = sample_frame(viewport, RenderTarget::window("native-wgpu-host", viewport))?;
189        #[cfg(feature = "accesskit-winit")]
190        if let Some(accesskit) = self.accesskit.as_mut() {
191            accesskit.publish_tree(frame.accessibility_tree.clone(), None);
192        }
193        let output = renderer.render_frame(frame.render_request, &EmptyResourceResolver)?;
194        if output.snapshot.is_some() {
195            return Err("native surface smoke must present without snapshot readback".into());
196        }
197        if let Some(duration) = output.timings.duration("render") {
198            self.render_samples.push(duration);
199        }
200        self.presented_frames += 1;
201        Ok(self
202            .frame_limit
203            .is_some_and(|frame_limit| self.presented_frames >= frame_limit))
204    }
examples/showcase.rs (line 506)
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}

Trait Implementations§

Source§

impl Clone for UiSize

Source§

fn clone(&self) -> UiSize

Returns a duplicate of the value. Read more
1.0.0 (const: unstable) · Source§

fn clone_from(&mut self, source: &Self)

Performs copy-assignment from source. Read more
Source§

impl Debug for UiSize

Source§

fn fmt(&self, f: &mut Formatter<'_>) -> Result

Formats the value using the given formatter. Read more
Source§

impl PartialEq for UiSize

Source§

fn eq(&self, other: &UiSize) -> bool

Tests for self and other values to be equal, and is used by ==.
1.0.0 (const: unstable) · Source§

fn ne(&self, other: &Rhs) -> bool

Tests for !=. The default implementation is almost always sufficient, and should not be overridden without very good reason.
Source§

impl Copy for UiSize

Source§

impl StructuralPartialEq for UiSize

Auto Trait Implementations§

Blanket Implementations§

Source§

impl<T> Any for T
where T: 'static + ?Sized,

Source§

fn type_id(&self) -> TypeId

Gets the TypeId of self. Read more
Source§

impl<T> Borrow<T> for T
where T: ?Sized,

Source§

fn borrow(&self) -> &T

Immutably borrows from an owned value. Read more
Source§

impl<T> BorrowMut<T> for T
where T: ?Sized,

Source§

fn borrow_mut(&mut self) -> &mut T

Mutably borrows from an owned value. Read more
Source§

impl<T> CloneToUninit for T
where T: Clone,

Source§

unsafe fn clone_to_uninit(&self, dest: *mut u8)

🔬This is a nightly-only experimental API. (clone_to_uninit)
Performs copy-assignment from self to dest. Read more
Source§

impl<T> Downcast<T> for T

Source§

fn downcast(&self) -> &T

Source§

impl<T> Downcast for T
where T: Any,

Source§

fn into_any(self: Box<T>) -> Box<dyn Any>

Convert Box<dyn Trait> (where Trait: Downcast) to Box<dyn Any>. Box<dyn Any> can then be further downcast into Box<ConcreteType> where ConcreteType implements Trait.
Source§

fn into_any_rc(self: Rc<T>) -> Rc<dyn Any>

Convert Rc<Trait> (where Trait: Downcast) to Rc<Any>. Rc<Any> can then be further downcast into Rc<ConcreteType> where ConcreteType implements Trait.
Source§

fn as_any(&self) -> &(dyn Any + 'static)

Convert &Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &Any’s vtable from &Trait’s.
Source§

fn as_any_mut(&mut self) -> &mut (dyn Any + 'static)

Convert &mut Trait (where Trait: Downcast) to &Any. This is needed since Rust cannot generate &mut Any’s vtable from &mut Trait’s.
Source§

impl<T> DowncastSync for T
where T: Any + Send + Sync,

Source§

fn into_any_arc(self: Arc<T>) -> Arc<dyn Any + Sync + Send>

Convert Arc<Trait> (where Trait: Downcast) to Arc<Any>. Arc<Any> can then be further downcast into Arc<ConcreteType> where ConcreteType implements Trait.
Source§

impl<T> From<T> for T

Source§

fn from(t: T) -> T

Returns the argument unchanged.

Source§

impl<T> Instrument for T

Source§

fn instrument(self, span: Span) -> Instrumented<Self>

Instruments this type with the provided Span, returning an Instrumented wrapper. Read more
Source§

fn in_current_span(self) -> Instrumented<Self>

Instruments this type with the current Span, returning an Instrumented wrapper. Read more
Source§

impl<T, U> Into<U> for T
where U: From<T>,

Source§

fn into(self) -> U

Calls U::from(self).

That is, this conversion is whatever the implementation of From<T> for U chooses to do.

Source§

impl<T> ToOwned for T
where T: Clone,

Source§

type Owned = T

The resulting type after obtaining ownership.
Source§

fn to_owned(&self) -> T

Creates owned data from borrowed data, usually by cloning. Read more
Source§

fn clone_into(&self, target: &mut T)

Uses borrowed data to replace owned data, usually by cloning. Read more
Source§

impl<T, U> TryFrom<U> for T
where U: Into<T>,

Source§

type Error = Infallible

The type returned in the event of a conversion error.
Source§

fn try_from(value: U) -> Result<T, <T as TryFrom<U>>::Error>

Performs the conversion.
Source§

impl<T, U> TryInto<U> for T
where U: TryFrom<T>,

Source§

type Error = <U as TryFrom<T>>::Error

The type returned in the event of a conversion error.
Source§

fn try_into(self) -> Result<U, <U as TryFrom<T>>::Error>

Performs the conversion.
Source§

impl<T> Upcast<T> for T

Source§

fn upcast(&self) -> Option<&T>

Source§

impl<T> WithSubscriber for T

Source§

fn with_subscriber<S>(self, subscriber: S) -> WithDispatch<Self>
where S: Into<Dispatch>,

Attaches the provided Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

fn with_current_subscriber(self) -> WithDispatch<Self>

Attaches the current default Subscriber to this type, returning a WithDispatch wrapper. Read more
Source§

impl<T> WasmNotSend for T
where T: Send,

Source§

impl<T> WasmNotSendSync for T

Source§

impl<T> WasmNotSync for T
where T: Sync,