Skip to main content

showcase/
showcase.rs

1use web_time::{Duration, Instant};
2
3use operad::debug::{DebugInspectorSnapshot, DebugThemeSnapshot};
4use operad::forms::FormValidationResult;
5#[cfg(all(not(target_arch = "wasm32"), feature = "native-window"))]
6use operad::native::{
7    NativeWgpuCanvasRenderRegistry, NativeWindowHooks, NativeWindowOptions, NativeWindowResult,
8};
9use operad::platform::{
10    ClipboardResponse, DragBytes, DragOperation, DragPayload, PlatformResponse,
11    PlatformServiceResponse, UiLayer,
12};
13use operad::runtime::PlatformServiceClient;
14use operad::tooltips::{ShortcutFormatter, TooltipContent, TooltipPlacement};
15use operad::widgets::ext::{self as ext_widgets, CalendarDate};
16use operad::widgets::{scroll_area as scroll_area_widgets, scrollbar as scrollbar_widgets};
17use operad::widgets::{TextInputOptions, TextInputState};
18#[cfg(feature = "text-cosmic")]
19use operad::CosmicTextMeasurer;
20use operad::{
21    root_style, widgets, AccessibilityMeta, AccessibilityRole, AlignedStroke, AnimatedValues,
22    AnimationBlendBinding, AnimationCondition, AnimationMachine, AnimationState,
23    AnimationTransition, ApproxTextMeasurer, BuiltInIcon, CanvasContent, CanvasRenderProgram,
24    ClipBehavior, ColorRgba, CommandId, CommandMeta, CommandRegistry, CommandScope, CornerRadii,
25    DragDropSurfaceKind, DropPayloadFilter, DynamicLabelMeta, EditPhase, FocusRestoreTarget,
26    FontFamily, FontWeight, FormState, ImageContent, InputBehavior, Layout, LayoutDimension,
27    LayoutFlexWrap, LayoutGap, LayoutGridTrack, LayoutSize, LayoutStyle, LocaleId,
28    LocalizationPolicy, PaintEffect, PaintRect, PaintText, ScenePrimitive, ScrollAxes,
29    ShaderEffect, Shortcut, StrokeStyle, TextHorizontalAlign, TextStyle, TextVerticalAlign,
30    TextWrap, Theme, UiDocument, UiNode, UiNodeId, UiNodeStyle, UiPoint, UiPortalTarget, UiRect,
31    UiSize, UiVisual, ValidationMessage, WidgetAction, WidgetActionBinding, WidgetActionKind,
32    WidgetDrag, WidgetDragPhase, WidgetTextEdit, ANIMATION_INPUT_POINTER_NORM_X,
33};
34const RIGHT_PANEL_WIDTH: f32 = 300.0;
35const SHOWCASE_WINDOW_Z_BASE: i16 = 64;
36const SHOWCASE_WINDOW_Z_STRIDE: i16 = 32;
37const SHOWCASE_WINDOW_Z_MAX: i16 = 960;
38const SHOWCASE_TICK_RATE_HZ: f32 = 120.0;
39const SHOWCASE_FPS_SAMPLE_INTERVAL: Duration = Duration::from_millis(500);
40const SHOWCASE_ORGANIZE_BUTTON_RESERVED_HEIGHT: f32 = 44.0;
41const SHOWCASE_ORGANIZE_MEASURE_HEIGHT: f32 = 64_000.0;
42const SHOWCASE_PROGRESS_RADIANS_PER_SECOND: f32 = 1.08;
43const TEXT_CARET_BLINK_HZ: f32 = 1.1;
44const CONTROLS_WIDGET_ROW_HEIGHT: f32 = 28.0;
45const CONTROLS_WIDGET_ROW_GAP: f32 = 1.0;
46const SHOWCASE_DOCUMENT_NODE_CAPACITY: usize = 2_048;
47const ANIMATION_INPUT_OPEN: &str = "open";
48const ANIMATION_INPUT_PROGRESS: &str = "progress";
49const ANIMATION_INPUT_SCRUB: &str = "scrub";
50const ANIMATION_STAGE_MIN_WIDTH: f32 = 360.0;
51const ANIMATION_STAGE_HEIGHT: f32 = 170.0;
52const ANIMATION_ORB_SIZE: f32 = 96.0;
53const ANIMATION_SHAPE_SIZE: f32 = 96.0;
54const ANIMATION_PANEL_INSET_X: f32 = 24.0;
55const ANIMATION_PANEL_Y: f32 = 62.0;
56const ANIMATION_PANEL_WIDTH: f32 = 136.0;
57const ANIMATION_PANEL_HEIGHT: f32 = 46.0;
58
59const SHOWCASE_WIDGET_WINDOW_IDS: [&str; 30] = [
60    "labels",
61    "buttons",
62    "checkbox",
63    "toggles",
64    "slider",
65    "numeric",
66    "text_input",
67    "selection",
68    "menus",
69    "command_palette",
70    "date_picker",
71    "color_picker",
72    "color_buttons",
73    "progress",
74    "animation",
75    "lists_tables",
76    "property_inspector",
77    "diagnostics",
78    "trees",
79    "layout_widgets",
80    "containers",
81    "forms",
82    "overlays",
83    "drag_drop",
84    "media",
85    "timeline",
86    "toasts",
87    "popup_panel",
88    "canvas",
89    "styling",
90];
91
92#[cfg(all(not(target_arch = "wasm32"), feature = "native-window"))]
93fn main() -> NativeWindowResult {
94    let canvas_renderers = NativeWgpuCanvasRenderRegistry::new();
95    let hooks = NativeWindowHooks::new()
96        .with_before_render(|state: &mut ShowcaseState, metrics| {
97            state.last_desktop_size = desktop_size_for_viewport(metrics.viewport);
98            state.record_frame();
99        })
100        .with_platform_service_requests(|state: &mut ShowcaseState, _metrics| {
101            state.platform.drain_requests()
102        })
103        .with_platform_responses(|state: &mut ShowcaseState, responses| {
104            state.apply_platform_responses(responses);
105        });
106    operad::native::run_app_with_canvas_renderers_and_hooks(
107        NativeWindowOptions::new("showcase")
108            .with_size(900.0, 760.0)
109            .with_min_size(720.0, 560.0)
110            .with_tick_action("runtime.tick")
111            .with_tick_rate_hz(SHOWCASE_TICK_RATE_HZ),
112        ShowcaseState::default(),
113        ShowcaseState::update,
114        ShowcaseState::view,
115        canvas_renderers,
116        hooks,
117    )
118}
119
120#[cfg(target_arch = "wasm32")]
121pub async fn run_web() -> Result<(), wasm_bindgen::JsValue> {
122    let hooks = operad::web::WebRuntimeHooks::new()
123        .with_before_render(|state: &mut ShowcaseState, metrics| {
124            state.last_desktop_size = desktop_size_for_viewport(metrics.viewport);
125            state.record_frame();
126        })
127        .with_platform_service_requests(|state: &mut ShowcaseState, _metrics| {
128            state.platform.drain_requests()
129        })
130        .with_platform_responses(|state: &mut ShowcaseState, responses| {
131            state.apply_platform_responses(responses);
132        });
133
134    operad::web::run_app_with_hooks(
135        operad::web::WebRuntimeOptions::new("Operad showcase")
136            .with_canvas_id("operad-showcase-canvas")
137            .with_status_id("operad-showcase-status")
138            .with_target_name("showcase")
139            .with_tick_action("runtime.tick")
140            .with_tick_rate_hz(SHOWCASE_TICK_RATE_HZ),
141        ShowcaseState::default(),
142        ShowcaseState::update,
143        ShowcaseState::view,
144        hooks,
145    )
146    .await
147}
148
149struct ShowcaseState {
150    checked: bool,
151    slider: f32,
152    slider_left: f32,
153    slider_right: f32,
154    slider_value_text: TextInputState,
155    slider_left_text: TextInputState,
156    slider_right_text: TextInputState,
157    slider_step_value: f32,
158    slider_step_text: TextInputState,
159    slider_trailing_color: bool,
160    slider_trailing_picker: ext_widgets::ColorPickerState,
161    slider_trailing_picker_open: bool,
162    slider_thumb_shape: SliderThumbChoice,
163    slider_use_steps: bool,
164    slider_logarithmic: bool,
165    slider_clamping: widgets::SliderClamping,
166    slider_smart_aim: bool,
167    label_locale: ext_widgets::SelectMenuState,
168    label_link_visited: bool,
169    label_hyperlink_visited: bool,
170    label_link_status: &'static str,
171    color: ext_widgets::ColorPickerState,
172    date: ext_widgets::DatePickerModel,
173    radio_choice: &'static str,
174    switch_enabled: bool,
175    mixed_switch: ext_widgets::ToggleValue,
176    theme_preference: widgets::ThemePreference,
177    numeric_value: f32,
178    numeric_angle: f32,
179    numeric_tau: f32,
180    combo_open: bool,
181    combo_label: String,
182    dropdown: ext_widgets::SelectMenuState,
183    select_menu: ext_widgets::SelectMenuState,
184    text: TextInputState,
185    selectable_text: TextInputState,
186    singleline_text: TextInputState,
187    multiline_text: TextInputState,
188    text_area_text: TextInputState,
189    code_editor_text: TextInputState,
190    search_text: TextInputState,
191    password_text: TextInputState,
192    focused_text: Option<FocusedTextInput>,
193    platform: PlatformServiceClient,
194    clipboard_text: String,
195    pending_clipboard_paste: Option<FocusedTextInput>,
196    last_button: &'static str,
197    toggle_button: bool,
198    table_selection: ext_widgets::DataTableSelection,
199    tree: ext_widgets::TreeViewState,
200    outliner: ext_widgets::TreeViewState,
201    tree_virtual_scroll: f32,
202    toast_visible: bool,
203    toast_action_status: &'static str,
204    popup_open: bool,
205    progress_phase: f32,
206    animation_scrub: f32,
207    animation_open: bool,
208    animation_timed_expanded: bool,
209    animation_scrub_expanded: bool,
210    animation_state_expanded: bool,
211    animation_interaction_expanded: bool,
212    caret_phase: f32,
213    command_palette: ext_widgets::CommandPaletteState,
214    command_history: ext_widgets::CommandPaletteHistory,
215    last_command: String,
216    list_scroll: f32,
217    virtual_scroll: f32,
218    table_scroll: f32,
219    virtual_table_scroll: f32,
220    virtual_table_descending: bool,
221    virtual_table_ready_only: bool,
222    virtual_table_value_width: f32,
223    virtual_table_resize: Option<(f32, f32)>,
224    layout_preview_scroll: f32,
225    layout_left_scroll: f32,
226    layout_right_scroll: f32,
227    layout_inspector_scroll: f32,
228    layout_document_scroll: f32,
229    layout_assets_scroll: f32,
230    scrollbars: scrollbar_widgets::ScrollbarControllerState,
231    layout_tab: usize,
232    styling: StylingState,
233    styling_stroke_picker: ext_widgets::ColorPickerState,
234    styling_stroke_picker_open: bool,
235    styling_fill_picker: ext_widgets::ColorPickerState,
236    styling_fill_picker_open: bool,
237    styling_shadow_picker: ext_widgets::ColorPickerState,
238    styling_shadow_picker_open: bool,
239    cube: CanvasCubeState,
240    menu_bar: ext_widgets::MenuBarState,
241    menu_button: ext_widgets::MenuButtonState,
242    image_text_menu_button: ext_widgets::MenuButtonState,
243    image_menu_button: ext_widgets::MenuButtonState,
244    context_menu: ext_widgets::ContextMenuState,
245    menu_autosave: bool,
246    menu_grid: bool,
247    form: FormState,
248    form_name_text: TextInputState,
249    form_email_text: TextInputState,
250    form_role_text: TextInputState,
251    form_newsletter: bool,
252    form_status: String,
253    overlay_expanded: bool,
254    overlay_popup_open: bool,
255    overlay_modal_open: bool,
256    color_button_status: &'static str,
257    drag_drop_status: &'static str,
258    layout_split: ext_widgets::SplitPaneState,
259    layout_dock: ext_widgets::DockWorkspaceState,
260    diagnostics_animation_paused: bool,
261    diagnostics_animation_scrub: f32,
262    diagnostics_animation_active: bool,
263    diagnostics_animation_hover: f32,
264    diagnostics_animation_pulse_count: u32,
265    diagnostics_snapshot: DebugInspectorSnapshot,
266    containers_scroll: operad::ScrollState,
267    controls_scroll: operad::ScrollState,
268    color_copied_hex: Option<String>,
269    fps_last_sample: Instant,
270    fps_frames: u32,
271    fps: f32,
272    last_desktop_size: UiSize,
273    windows: ShowcaseWindows,
274    desktop: ext_widgets::FloatingDesktopState,
275}
276
277#[derive(Debug, Clone)]
278struct ShowcaseWindowMeasurement {
279    id: String,
280    size: UiSize,
281    min_size: UiSize,
282    collapsed_size: UiSize,
283}
284
285#[derive(Clone, Copy)]
286struct StylingState {
287    inner_same: bool,
288    inner_margin: f32,
289    inner_right: f32,
290    inner_top: f32,
291    inner_bottom: f32,
292    outer_same: bool,
293    outer_margin: f32,
294    outer_right: f32,
295    outer_top: f32,
296    outer_bottom: f32,
297    radius_same: bool,
298    corner_radius: f32,
299    corner_ne: f32,
300    corner_sw: f32,
301    corner_se: f32,
302    shadow_x: f32,
303    shadow_y: f32,
304    shadow_blur: f32,
305    shadow_spread: f32,
306    shadow: ColorRgba,
307    stroke_width: f32,
308    stroke: ColorRgba,
309    fill: ColorRgba,
310}
311
312impl Default for StylingState {
313    fn default() -> Self {
314        Self {
315            inner_same: true,
316            inner_margin: 12.0,
317            inner_right: 12.0,
318            inner_top: 12.0,
319            inner_bottom: 12.0,
320            outer_same: true,
321            outer_margin: 24.0,
322            outer_right: 24.0,
323            outer_top: 24.0,
324            outer_bottom: 24.0,
325            radius_same: true,
326            corner_radius: 12.0,
327            corner_ne: 12.0,
328            corner_sw: 12.0,
329            corner_se: 12.0,
330            shadow_x: 8.0,
331            shadow_y: 12.0,
332            shadow_blur: 16.0,
333            shadow_spread: 0.0,
334            shadow: ColorRgba::new(0, 0, 0, 140),
335            stroke_width: 1.0,
336            stroke: ColorRgba::new(198, 198, 205, 255),
337            fill: ColorRgba::new(100, 55, 205, 255),
338        }
339    }
340}
341
342impl StylingState {
343    fn inner_edges(self) -> [f32; 4] {
344        if self.inner_same {
345            [self.inner_margin; 4]
346        } else {
347            [
348                self.inner_margin,
349                self.inner_right,
350                self.inner_top,
351                self.inner_bottom,
352            ]
353        }
354    }
355
356    fn outer_edges(self) -> [f32; 4] {
357        if self.outer_same {
358            [self.outer_margin; 4]
359        } else {
360            [
361                self.outer_margin,
362                self.outer_right,
363                self.outer_top,
364                self.outer_bottom,
365            ]
366        }
367    }
368
369    fn radii(self) -> CornerRadii {
370        if self.radius_same {
371            CornerRadii::uniform(self.corner_radius)
372        } else {
373            CornerRadii::new(
374                self.corner_radius,
375                self.corner_ne,
376                self.corner_se,
377                self.corner_sw,
378            )
379        }
380    }
381
382    fn stroke_color(self) -> ColorRgba {
383        self.stroke
384    }
385
386    fn fill_color(self) -> ColorRgba {
387        self.fill
388    }
389
390    fn shadow_color(self) -> ColorRgba {
391        self.shadow
392    }
393}
394
395#[derive(Clone, Copy, Debug, PartialEq, Eq)]
396enum FocusedTextInput {
397    Editable,
398    Selectable,
399    Singleline,
400    Multiline,
401    TextArea,
402    CodeEditor,
403    Search,
404    Password,
405    FormName,
406    FormEmail,
407    FormRole,
408    SliderValue,
409    SliderRangeLeft,
410    SliderRangeRight,
411    SliderStep,
412}
413
414impl FocusedTextInput {
415    const fn is_read_only(self) -> bool {
416        matches!(self, Self::Selectable)
417    }
418
419    const fn is_multiline(self) -> bool {
420        matches!(self, Self::Multiline | Self::TextArea | Self::CodeEditor)
421    }
422}
423
424#[derive(Clone, Copy, Debug, PartialEq, Eq)]
425enum SliderThumbChoice {
426    Circle,
427    Square,
428    Rectangle,
429}
430
431#[derive(Clone, Copy)]
432struct CanvasCubeState {
433    yaw: f32,
434    pitch: f32,
435    drag_origin_yaw: f32,
436    drag_origin_pitch: f32,
437}
438
439impl Default for CanvasCubeState {
440    fn default() -> Self {
441        Self {
442            yaw: 0.82,
443            pitch: 0.52,
444            drag_origin_yaw: 0.82,
445            drag_origin_pitch: 0.52,
446        }
447    }
448}
449
450impl CanvasCubeState {
451    fn apply_drag(&mut self, drag: WidgetDrag) {
452        match drag.phase {
453            WidgetDragPhase::Begin => {
454                self.drag_origin_yaw = self.yaw;
455                self.drag_origin_pitch = self.pitch;
456                self.apply_drag_delta(drag.total_delta);
457            }
458            WidgetDragPhase::Update | WidgetDragPhase::Commit => {
459                self.apply_drag_delta(drag.total_delta);
460            }
461            WidgetDragPhase::Cancel => {
462                self.yaw = self.drag_origin_yaw;
463                self.pitch = self.drag_origin_pitch;
464            }
465        }
466    }
467
468    fn apply_drag_delta(&mut self, total_delta: UiPoint) {
469        self.yaw = self.drag_origin_yaw + total_delta.x * 0.012;
470        self.pitch = (self.drag_origin_pitch + total_delta.y * 0.012).clamp(-1.25, 1.25);
471    }
472}
473
474impl Default for ShowcaseState {
475    fn default() -> Self {
476        let text = TextInputState::new("Editable text");
477        let mut selectable_text = TextInputState::new("Selectable read-only text");
478        selectable_text.set_selection(0, "Selectable".len());
479        let form = profile_form_state();
480        let form_name_text = TextInputState::new(profile_form_value(&form, "name"));
481        let form_email_text = TextInputState::new(profile_form_value(&form, "email"));
482        let form_role_text = TextInputState::new(profile_form_value(&form, "role"));
483        let initial_select_options = select_options();
484        let windows = ShowcaseWindows::default();
485        let mut desktop = ext_widgets::FloatingDesktopState::with_visible_order(
486            SHOWCASE_WIDGET_WINDOW_IDS
487                .into_iter()
488                .filter(|id| windows.is_visible(id))
489                .map(str::to_string),
490            showcase_window_z_policy(),
491        );
492        for id in SHOWCASE_WIDGET_WINDOW_IDS
493            .into_iter()
494            .filter(|id| windows.is_visible(id))
495        {
496            desktop.ensure_window(id, window_defaults(id));
497        }
498
499        Self {
500            checked: true,
501            slider: 10.0,
502            slider_left: 1.0,
503            slider_right: 10000.0,
504            slider_value_text: TextInputState::new("10"),
505            slider_left_text: TextInputState::new("1"),
506            slider_right_text: TextInputState::new("10000"),
507            slider_step_value: 10.0,
508            slider_step_text: TextInputState::new("10"),
509            slider_trailing_color: true,
510            slider_trailing_picker: ext_widgets::ColorPickerState::new(color(120, 170, 230)),
511            slider_trailing_picker_open: false,
512            slider_thumb_shape: SliderThumbChoice::Circle,
513            slider_use_steps: false,
514            slider_logarithmic: true,
515            slider_clamping: widgets::SliderClamping::Always,
516            slider_smart_aim: true,
517            label_locale: ext_widgets::SelectMenuState::with_selected(1),
518            label_link_visited: false,
519            label_hyperlink_visited: false,
520            label_link_status: "No link action yet",
521            color: ext_widgets::ColorPickerState::new(color(118, 183, 255)),
522            date: ext_widgets::DatePickerModel::builder()
523                .selected(CalendarDate::new(2026, 5, 12))
524                .today(CalendarDate::new(2026, 5, 12))
525                .build(),
526            radio_choice: "compact",
527            switch_enabled: true,
528            mixed_switch: ext_widgets::ToggleValue::Mixed,
529            theme_preference: widgets::ThemePreference::Dark,
530            numeric_value: 42.0,
531            numeric_angle: 0.75,
532            numeric_tau: 0.75,
533            combo_open: false,
534            combo_label: "Compact".to_string(),
535            dropdown: ext_widgets::SelectMenuState::with_selected(1),
536            select_menu: ext_widgets::SelectMenuState::with_selected(0)
537                .with_open(&initial_select_options)
538                .with_active(&initial_select_options, 2),
539            text,
540            selectable_text,
541            singleline_text: TextInputState::new("Single line"),
542            multiline_text: TextInputState::new("First line\nSecond line").multiline(true),
543            text_area_text: TextInputState::new("Text area content").multiline(true),
544            code_editor_text: TextInputState::new("fn main() {\n    println!(\"showcase\");\n}")
545                .multiline(true),
546            search_text: TextInputState::new("widgets"),
547            password_text: TextInputState::new("correct horse"),
548            focused_text: None,
549            platform: PlatformServiceClient::new(),
550            clipboard_text: String::new(),
551            pending_clipboard_paste: None,
552            last_button: "None",
553            toggle_button: false,
554            table_selection: ext_widgets::DataTableSelection::single_row(2)
555                .with_active_cell(ext_widgets::DataTableCellIndex::new(2, 1)),
556            tree: ext_widgets::TreeViewState::expanded(["root"]),
557            outliner: ext_widgets::TreeViewState::expanded(["root", "assets"]),
558            tree_virtual_scroll: 96.0,
559            toast_visible: false,
560            toast_action_status: "No toast action",
561            popup_open: false,
562            progress_phase: 0.0,
563            animation_scrub: 0.0,
564            animation_open: false,
565            animation_timed_expanded: true,
566            animation_scrub_expanded: true,
567            animation_state_expanded: true,
568            animation_interaction_expanded: true,
569            caret_phase: 0.0,
570            command_palette: ext_widgets::CommandPaletteState::new()
571                .with_max_results(24)
572                .with_first_active_match(&command_palette_items()),
573            command_history: ext_widgets::CommandPaletteHistory::with_capacity(4),
574            last_command: "None".to_string(),
575            list_scroll: 0.0,
576            virtual_scroll: 0.0,
577            table_scroll: 0.0,
578            virtual_table_scroll: 0.0,
579            virtual_table_descending: false,
580            virtual_table_ready_only: false,
581            virtual_table_value_width: 70.0,
582            virtual_table_resize: None,
583            layout_preview_scroll: 0.0,
584            layout_left_scroll: 0.0,
585            layout_right_scroll: 0.0,
586            layout_inspector_scroll: 0.0,
587            layout_document_scroll: 0.0,
588            layout_assets_scroll: 0.0,
589            scrollbars: scrollbar_widgets::ScrollbarControllerState::new(),
590            layout_tab: 0,
591            styling: StylingState::default(),
592            styling_stroke_picker: ext_widgets::ColorPickerState::new(
593                StylingState::default().stroke_color(),
594            ),
595            styling_stroke_picker_open: false,
596            styling_fill_picker: ext_widgets::ColorPickerState::new(
597                StylingState::default().fill_color(),
598            ),
599            styling_fill_picker_open: false,
600            styling_shadow_picker: ext_widgets::ColorPickerState::new(
601                StylingState::default().shadow_color(),
602            ),
603            styling_shadow_picker_open: false,
604            cube: CanvasCubeState::default(),
605            menu_bar: ext_widgets::MenuBarState {
606                open_menu: Some(0),
607                active_item: Some(0),
608            },
609            menu_button: ext_widgets::MenuButtonState::new(),
610            image_text_menu_button: ext_widgets::MenuButtonState::new(),
611            image_menu_button: ext_widgets::MenuButtonState::new(),
612            context_menu: ext_widgets::ContextMenuState::closed(),
613            menu_autosave: true,
614            menu_grid: true,
615            form,
616            form_name_text,
617            form_email_text,
618            form_role_text,
619            form_newsletter: true,
620            form_status: "Unsaved profile changes".to_string(),
621            overlay_expanded: true,
622            overlay_popup_open: false,
623            overlay_modal_open: false,
624            color_button_status: "None",
625            drag_drop_status: "Idle",
626            layout_split: ext_widgets::SplitPaneState::new(0.44).with_min_sizes(80.0, 80.0),
627            layout_dock: ext_widgets::DockWorkspaceState::new(),
628            diagnostics_animation_paused: false,
629            diagnostics_animation_scrub: 0.35,
630            diagnostics_animation_active: true,
631            diagnostics_animation_hover: 0.35,
632            diagnostics_animation_pulse_count: 0,
633            diagnostics_snapshot: diagnostics_sample_snapshot_for(0.35, true),
634            containers_scroll: operad::ScrollState::new(ScrollAxes::BOTH)
635                .with_sizes(UiSize::new(260.0, 82.0), UiSize::new(440.0, 180.0))
636                .with_offset(UiPoint::new(24.0, 18.0)),
637            controls_scroll: operad::ScrollState::new(ScrollAxes::VERTICAL),
638            color_copied_hex: None,
639            fps_last_sample: Instant::now(),
640            fps_frames: 0,
641            fps: 0.0,
642            last_desktop_size: desktop_size_for_viewport(UiSize::new(900.0, 760.0)),
643            windows,
644            desktop,
645        }
646    }
647}
648
649struct ShowcaseWindows {
650    labels: bool,
651    buttons: bool,
652    checkbox: bool,
653    toggles: bool,
654    slider: bool,
655    numeric: bool,
656    text_input: bool,
657    selection: bool,
658    menus: bool,
659    command_palette: bool,
660    date_picker: bool,
661    color_picker: bool,
662    color_buttons: bool,
663    progress: bool,
664    animation: bool,
665    lists_tables: bool,
666    property_inspector: bool,
667    diagnostics: bool,
668    trees: bool,
669    layout_widgets: bool,
670    containers: bool,
671    forms: bool,
672    overlays: bool,
673    drag_drop: bool,
674    media: bool,
675    timeline: bool,
676    toasts: bool,
677    popup_panel: bool,
678    canvas: bool,
679    styling: bool,
680}
681
682impl Default for ShowcaseWindows {
683    fn default() -> Self {
684        Self {
685            labels: true,
686            buttons: true,
687            checkbox: false,
688            toggles: false,
689            slider: false,
690            numeric: false,
691            text_input: false,
692            selection: false,
693            menus: false,
694            command_palette: false,
695            date_picker: false,
696            color_picker: true,
697            color_buttons: false,
698            progress: false,
699            animation: false,
700            lists_tables: false,
701            property_inspector: false,
702            diagnostics: false,
703            trees: false,
704            layout_widgets: false,
705            containers: false,
706            forms: false,
707            overlays: false,
708            drag_drop: false,
709            media: false,
710            timeline: false,
711            toasts: false,
712            popup_panel: false,
713            canvas: true,
714            styling: false,
715        }
716    }
717}
718
719impl ShowcaseWindows {
720    fn is_visible(&self, id: &str) -> bool {
721        match id {
722            "labels" => self.labels,
723            "buttons" => self.buttons,
724            "checkbox" => self.checkbox,
725            "toggles" => self.toggles,
726            "slider" => self.slider,
727            "numeric" => self.numeric,
728            "text_input" => self.text_input,
729            "selection" => self.selection,
730            "menus" => self.menus,
731            "command_palette" => self.command_palette,
732            "date_picker" => self.date_picker,
733            "color_picker" => self.color_picker,
734            "color_buttons" => self.color_buttons,
735            "progress" => self.progress,
736            "animation" => self.animation,
737            "lists_tables" => self.lists_tables,
738            "property_inspector" => self.property_inspector,
739            "diagnostics" => self.diagnostics,
740            "trees" => self.trees,
741            "layout_widgets" => self.layout_widgets,
742            "containers" => self.containers,
743            "forms" => self.forms,
744            "overlays" => self.overlays,
745            "drag_drop" => self.drag_drop,
746            "media" => self.media,
747            "timeline" => self.timeline,
748            "toasts" => self.toasts,
749            "popup_panel" => self.popup_panel,
750            "canvas" => self.canvas,
751            "styling" => self.styling,
752            _ => false,
753        }
754    }
755
756    fn slot_mut(&mut self, id: &str) -> Option<&mut bool> {
757        match id {
758            "labels" => Some(&mut self.labels),
759            "buttons" => Some(&mut self.buttons),
760            "checkbox" => Some(&mut self.checkbox),
761            "toggles" => Some(&mut self.toggles),
762            "slider" => Some(&mut self.slider),
763            "numeric" => Some(&mut self.numeric),
764            "text_input" => Some(&mut self.text_input),
765            "selection" => Some(&mut self.selection),
766            "menus" => Some(&mut self.menus),
767            "command_palette" => Some(&mut self.command_palette),
768            "date_picker" => Some(&mut self.date_picker),
769            "color_picker" => Some(&mut self.color_picker),
770            "color_buttons" => Some(&mut self.color_buttons),
771            "progress" => Some(&mut self.progress),
772            "animation" => Some(&mut self.animation),
773            "lists_tables" => Some(&mut self.lists_tables),
774            "property_inspector" => Some(&mut self.property_inspector),
775            "diagnostics" => Some(&mut self.diagnostics),
776            "trees" => Some(&mut self.trees),
777            "layout_widgets" => Some(&mut self.layout_widgets),
778            "containers" => Some(&mut self.containers),
779            "forms" => Some(&mut self.forms),
780            "overlays" => Some(&mut self.overlays),
781            "drag_drop" => Some(&mut self.drag_drop),
782            "media" => Some(&mut self.media),
783            "timeline" => Some(&mut self.timeline),
784            "toasts" => Some(&mut self.toasts),
785            "popup_panel" => Some(&mut self.popup_panel),
786            "canvas" => Some(&mut self.canvas),
787            "styling" => Some(&mut self.styling),
788            _ => None,
789        }
790    }
791
792    fn toggle(&mut self, id: &str) -> Option<bool> {
793        if let Some(visible) = self.slot_mut(id) {
794            *visible = !*visible;
795            return Some(*visible);
796        }
797        None
798    }
799
800    fn close(&mut self, id: &str) {
801        if let Some(visible) = self.slot_mut(id) {
802            *visible = false;
803        }
804    }
805
806    fn clear_all(&mut self) {
807        for id in SHOWCASE_WIDGET_WINDOW_IDS {
808            if let Some(visible) = self.slot_mut(id) {
809                *visible = false;
810            }
811        }
812    }
813
814    fn open_all(&mut self) {
815        for id in SHOWCASE_WIDGET_WINDOW_IDS {
816            if let Some(visible) = self.slot_mut(id) {
817                *visible = true;
818            }
819        }
820    }
821}
822
823fn showcase_window_z_policy() -> ext_widgets::FloatingDesktopZPolicy {
824    ext_widgets::FloatingDesktopZPolicy::new(
825        SHOWCASE_WINDOW_Z_BASE,
826        SHOWCASE_WINDOW_Z_STRIDE,
827        SHOWCASE_WINDOW_Z_MAX,
828    )
829}
830
831fn window_defaults(id: &str) -> ext_widgets::FloatingWindowDefaults {
832    ext_widgets::FloatingWindowDefaults::new(
833        default_window_position(id),
834        default_window_size(id),
835        default_window_state_min_size(id),
836    )
837}
838
839fn desktop_size_for_viewport(viewport: UiSize) -> UiSize {
840    UiSize::new(
841        (viewport.width - RIGHT_PANEL_WIDTH).max(360.0),
842        viewport.height,
843    )
844}
845
846fn showcase_desktop_options(desktop_size: UiSize) -> ext_widgets::FloatingDesktopOptions {
847    let mut options = ext_widgets::FloatingDesktopOptions::new(desktop_size).with_layout(
848        LayoutStyle::new()
849            .with_width_percent(1.0)
850            .with_height_percent(1.0),
851    );
852    options = options.with_bounds_rect(UiRect::new(
853        0.0,
854        SHOWCASE_ORGANIZE_BUTTON_RESERVED_HEIGHT,
855        desktop_size.width,
856        (desktop_size.height - SHOWCASE_ORGANIZE_BUTTON_RESERVED_HEIGHT).max(0.0),
857    ));
858    options.base_z_index = SHOWCASE_WINDOW_Z_BASE;
859    options.window_z_stride = SHOWCASE_WINDOW_Z_STRIDE;
860    options.margin = 18.0;
861    options.gap = 14.0;
862    options
863}
864
865impl ShowcaseState {
866    fn record_frame(&mut self) {
867        self.fps_frames = self.fps_frames.saturating_add(1);
868        let now = Instant::now();
869        let elapsed = now
870            .checked_duration_since(self.fps_last_sample)
871            .unwrap_or(Duration::ZERO);
872        if elapsed < SHOWCASE_FPS_SAMPLE_INTERVAL {
873            return;
874        }
875        let seconds = elapsed.as_secs_f32().max(f32::EPSILON);
876        self.fps = self.fps_frames as f32 / seconds;
877        self.fps_frames = 0;
878        self.fps_last_sample = now;
879    }
880
881    fn organize_open_windows(&mut self) {
882        let desktop_size = self.last_desktop_size;
883        let options = showcase_desktop_options(desktop_size);
884        let arrange_rect = UiRect::new(
885            0.0,
886            SHOWCASE_ORGANIZE_BUTTON_RESERVED_HEIGHT,
887            desktop_size.width,
888            (desktop_size.height - SHOWCASE_ORGANIZE_BUTTON_RESERVED_HEIGHT).max(0.0),
889        );
890        let measured_sizes = self.measured_open_window_sizes(desktop_size);
891        let windows = SHOWCASE_WIDGET_WINDOW_IDS
892            .into_iter()
893            .filter(|id| self.windows.is_visible(id))
894            .map(|id| {
895                let mut defaults = window_defaults(id);
896                let mut collapsed_size =
897                    UiSize::new(defaults.min_size.width, options.title_bar_height);
898                if let Some(measurement) = measured_sizes
899                    .iter()
900                    .find(|measurement| measurement.id == id)
901                {
902                    defaults.size = UiSize::new(
903                        measurement.size.width.max(defaults.size.width),
904                        measurement.size.height.max(defaults.size.height),
905                    );
906                    defaults.min_size = UiSize::new(
907                        defaults.min_size.width.max(measurement.min_size.width),
908                        defaults.min_size.height.max(measurement.min_size.height),
909                    );
910                    collapsed_size = UiSize::new(
911                        collapsed_size.width.max(measurement.collapsed_size.width),
912                        collapsed_size.height.max(measurement.collapsed_size.height),
913                    );
914                }
915                ext_widgets::FloatingWindowOrganizeSpec::new(id, defaults)
916                    .with_collapsed_size(collapsed_size)
917            });
918        let _outcome = self
919            .desktop
920            .organize_window_specs_in_rect(windows, arrange_rect, &options);
921    }
922
923    fn measured_open_window_sizes(&self, desktop_size: UiSize) -> Vec<ShowcaseWindowMeasurement> {
924        let measure_height = desktop_size.height.max(SHOWCASE_ORGANIZE_MEASURE_HEIGHT);
925        let viewport = UiSize::new(desktop_size.width + RIGHT_PANEL_WIDTH, measure_height);
926        let mut document = self.view(viewport);
927        #[cfg(feature = "text-cosmic")]
928        let mut measurer = CosmicTextMeasurer::new();
929        #[cfg(not(feature = "text-cosmic"))]
930        let mut measurer = ApproxTextMeasurer;
931        if document.compute_layout(viewport, &mut measurer).is_err() {
932            return Vec::new();
933        }
934        let options = showcase_desktop_options(desktop_size);
935        SHOWCASE_WIDGET_WINDOW_IDS
936            .into_iter()
937            .filter(|id| self.windows.is_visible(id))
938            .filter_map(|id| {
939                let name = format!("showcase.windows.window.{id}");
940                let collapsed_size = showcase_collapsed_window_size(id, &options);
941                document
942                    .nodes()
943                    .iter()
944                    .find(|node| node.name() == name)
945                    .map(|node| {
946                        let min_size = node.style().layout_style().min_size();
947                        ShowcaseWindowMeasurement {
948                            id: id.to_string(),
949                            size: UiSize::new(node.layout().rect.width, node.layout().rect.height),
950                            min_size: UiSize::new(
951                                min_size
952                                    .and_then(|size| size.width.points_value())
953                                    .unwrap_or(node.layout().rect.width),
954                                min_size
955                                    .and_then(|size| size.height.points_value())
956                                    .unwrap_or(node.layout().rect.height),
957                            ),
958                            collapsed_size,
959                        }
960                    })
961            })
962            .collect()
963    }
964
965    fn update(&mut self, action: WidgetAction) {
966        let WidgetAction { binding, kind, .. } = action;
967        let WidgetActionBinding::Action(action_id) = binding else {
968            return;
969        };
970        let action_id = action_id.as_str();
971
972        let color_outcome = self.color.apply_action(
973            action_id,
974            kind.clone(),
975            ext_widgets::ColorPickerActionOptions::new("color").copy_hex("color.copy_hex"),
976        );
977        if color_outcome.update.is_some()
978            || color_outcome.effect.is_some()
979            || color_outcome.mode_changed
980        {
981            if let Some(ext_widgets::ColorPickerEffect::CopyHex(hex)) = color_outcome.effect {
982                self.copy_text_to_clipboard(&hex);
983                self.color_copied_hex = Some(hex);
984            }
985            return;
986        }
987        let color_buttons_outcome = self.color.apply_action(
988            action_id,
989            kind.clone(),
990            ext_widgets::ColorPickerActionOptions::new("color_buttons.hsva_2d"),
991        );
992        if color_buttons_outcome.update.is_some() || color_buttons_outcome.mode_changed {
993            self.color_button_status = "HSVA field";
994            return;
995        }
996        let slider_color_outcome = self.slider_trailing_picker.apply_action(
997            action_id,
998            kind.clone(),
999            ext_widgets::ColorPickerActionOptions::new("slider.trailing_picker"),
1000        );
1001        if slider_color_outcome.update.is_some() || slider_color_outcome.mode_changed {
1002            return;
1003        }
1004        let styling_stroke_outcome = self.styling_stroke_picker.apply_action(
1005            action_id,
1006            kind.clone(),
1007            ext_widgets::ColorPickerActionOptions::new("styling.stroke_picker"),
1008        );
1009        if styling_stroke_outcome.update.is_some() || styling_stroke_outcome.mode_changed {
1010            self.styling.stroke = self.styling_stroke_picker.value();
1011            return;
1012        }
1013        let styling_fill_outcome = self.styling_fill_picker.apply_action(
1014            action_id,
1015            kind.clone(),
1016            ext_widgets::ColorPickerActionOptions::new("styling.fill_picker"),
1017        );
1018        if styling_fill_outcome.update.is_some() || styling_fill_outcome.mode_changed {
1019            self.styling.fill = self.styling_fill_picker.value();
1020            return;
1021        }
1022        let styling_shadow_outcome = self.styling_shadow_picker.apply_action(
1023            action_id,
1024            kind.clone(),
1025            ext_widgets::ColorPickerActionOptions::new("styling.shadow_picker"),
1026        );
1027        if styling_shadow_outcome.update.is_some() || styling_shadow_outcome.mode_changed {
1028            self.styling.shadow = self.styling_shadow_picker.value();
1029            return;
1030        }
1031
1032        if action_id == "window.clear_all" {
1033            self.windows.clear_all();
1034            return;
1035        }
1036        if action_id == "window.add_all" {
1037            self.windows.open_all();
1038            for id in SHOWCASE_WIDGET_WINDOW_IDS {
1039                self.desktop.ensure_window(id, window_defaults(id));
1040                self.desktop.bring_to_front(id);
1041            }
1042            return;
1043        }
1044        if action_id == "window.organize_open" {
1045            self.organize_open_windows();
1046            return;
1047        }
1048        if let Some(id) = action_id.strip_prefix("window.toggle.") {
1049            if self.windows.toggle(id).unwrap_or(false) {
1050                self.desktop.ensure_window(id, window_defaults(id));
1051                self.desktop.bring_to_front(id);
1052            }
1053            return;
1054        }
1055        if let Some(id) = action_id.strip_prefix("window.close.") {
1056            self.windows.close(id);
1057            self.desktop.close(id);
1058            return;
1059        }
1060        if let Some(id) = action_id.strip_prefix("window.activate.") {
1061            self.desktop.bring_to_front(id);
1062            return;
1063        }
1064        if let Some(id) = action_id.strip_prefix("window.drag.") {
1065            if let WidgetActionKind::PointerEdit(edit) = kind {
1066                self.desktop
1067                    .apply_drag(id, edit, default_window_position(id));
1068            }
1069            return;
1070        }
1071        if let Some(id) = action_id.strip_prefix("window.resize.") {
1072            if let WidgetActionKind::PointerEdit(edit) = kind {
1073                self.desktop.apply_resize(id, edit, window_defaults(id));
1074            }
1075            return;
1076        }
1077        if let Some(id) = action_id.strip_prefix("window.collapse.") {
1078            self.desktop.toggle_collapsed(id);
1079            return;
1080        }
1081        if let Some(id) = window_for_action(action_id) {
1082            self.desktop.bring_to_front(id);
1083        }
1084        if action_id == "runtime.tick" {
1085            self.progress_phase += SHOWCASE_PROGRESS_RADIANS_PER_SECOND / SHOWCASE_TICK_RATE_HZ;
1086            self.caret_phase = (self.caret_phase
1087                + std::f32::consts::TAU * TEXT_CARET_BLINK_HZ / SHOWCASE_TICK_RATE_HZ)
1088                % std::f32::consts::TAU;
1089            return;
1090        }
1091        if action_id == "command_palette.search" {
1092            if let WidgetActionKind::TextEdit(edit) = kind {
1093                self.apply_command_palette_event(edit.event);
1094            }
1095            return;
1096        }
1097        if let Some(id) = action_id.strip_prefix("command_palette.item.") {
1098            self.select_command_palette_item(id);
1099            return;
1100        }
1101        if let Some(input) = focused_text_for_action(action_id) {
1102            if let WidgetActionKind::TextEdit(edit) = kind {
1103                self.apply_text_edit(input, edit);
1104            }
1105            return;
1106        }
1107
1108        match action_id {
1109            "labels.link" => {
1110                self.label_link_visited = true;
1111                self.label_link_status = "Internal link activated";
1112                return;
1113            }
1114            "labels.hyperlink" => {
1115                self.label_hyperlink_visited = true;
1116                self.label_link_status = "Opened docs.rs/operad";
1117                self.platform.open_url("https://docs.rs/operad");
1118                return;
1119            }
1120            "button.default" => self.last_button = "Default",
1121            "button.primary" => self.last_button = "Primary",
1122            "button.secondary" => self.last_button = "Secondary",
1123            "button.destructive" => self.last_button = "Destructive",
1124            "button.small" => self.last_button = "Small",
1125            "button.icon" => self.last_button = "Settings",
1126            "button.image" => self.last_button = "Folder",
1127            "button.reset" => {
1128                self.toggle_button = false;
1129                self.last_button = "Reset";
1130            }
1131            "button.toggle" => {
1132                self.toggle_button = !self.toggle_button;
1133                self.last_button = "Toggle";
1134            }
1135            "checkbox.enabled" => self.checked = !self.checked,
1136            "labels.locale.toggle" => {
1137                self.label_locale.toggle(&label_locale_options());
1138                return;
1139            }
1140            "toggles.switch" => self.switch_enabled = !self.switch_enabled,
1141            "toggles.mixed" => self.mixed_switch = self.mixed_switch.toggled(),
1142            "toggles.radio.compact" => self.radio_choice = "compact",
1143            "toggles.radio.comfortable" => self.radio_choice = "comfortable",
1144            "toggles.radio.spacious" => self.radio_choice = "spacious",
1145            "toggles.theme.system" => {
1146                self.theme_preference = widgets::ThemePreference::System;
1147                return;
1148            }
1149            "toggles.theme.light" => {
1150                self.theme_preference = widgets::ThemePreference::Light;
1151                return;
1152            }
1153            "toggles.theme.dark" => {
1154                self.theme_preference = widgets::ThemePreference::Dark;
1155                return;
1156            }
1157            "theme.preference.dark" => {
1158                self.theme_preference = if self.theme_preference.is_dark() {
1159                    widgets::ThemePreference::Light
1160                } else {
1161                    widgets::ThemePreference::Dark
1162                };
1163                return;
1164            }
1165            "combo.toggle" => self.combo_open = !self.combo_open,
1166            "selection.dropdown.toggle" => {
1167                self.dropdown.toggle(&select_options());
1168                return;
1169            }
1170            "menus.menu_button" => {
1171                let button_items = menu_items(self.menu_autosave);
1172                let outcome = self.menu_button.toggle(&button_items);
1173                if outcome.opened {
1174                    self.image_text_menu_button.close();
1175                    self.image_menu_button.close();
1176                    self.context_menu.close();
1177                }
1178                return;
1179            }
1180            "menus.image_text_menu_button" => {
1181                let button_items = menu_items(self.menu_autosave);
1182                let outcome = self.image_text_menu_button.toggle(&button_items);
1183                if outcome.opened {
1184                    self.menu_button.close();
1185                    self.image_menu_button.close();
1186                    self.context_menu.close();
1187                }
1188                return;
1189            }
1190            "menus.image_menu_button" => {
1191                let button_items = menu_items(self.menu_autosave);
1192                let outcome = self.image_menu_button.toggle(&button_items);
1193                if outcome.opened {
1194                    self.menu_button.close();
1195                    self.image_text_menu_button.close();
1196                    self.context_menu.close();
1197                }
1198                return;
1199            }
1200            "menus.context.open" => {
1201                self.context_menu
1202                    .open_with_items(UiPoint::new(0.0, 0.0), &menu_items(self.menu_autosave));
1203                self.menu_button.close();
1204                self.image_text_menu_button.close();
1205                self.image_menu_button.close();
1206                return;
1207            }
1208            "menus.context.close" => {
1209                self.context_menu.close();
1210                return;
1211            }
1212            "menus.bar.file" => {
1213                self.menu_bar
1214                    .open(&menu_bar_menus(self.menu_autosave, self.menu_grid), 0);
1215                return;
1216            }
1217            "menus.bar.edit" => {
1218                self.menu_bar
1219                    .open(&menu_bar_menus(self.menu_autosave, self.menu_grid), 1);
1220                return;
1221            }
1222            "menus.bar.view" => {
1223                self.menu_bar
1224                    .open(&menu_bar_menus(self.menu_autosave, self.menu_grid), 2);
1225                return;
1226            }
1227            "date.previous" => self.date.show_previous_month(),
1228            "date.next" => self.date.show_next_month(),
1229            "date.week.sunday" => {
1230                self.date.first_weekday = ext_widgets::Weekday::Sunday;
1231                return;
1232            }
1233            "date.week.monday" => {
1234                self.date.first_weekday = ext_widgets::Weekday::Monday;
1235                return;
1236            }
1237            "date.range.toggle" => {
1238                if self.date.min.is_some() || self.date.max.is_some() {
1239                    self.date.min = None;
1240                    self.date.max = None;
1241                } else {
1242                    self.date.min = CalendarDate::new(2026, 5, 4);
1243                    self.date.max = CalendarDate::new(2026, 5, 29);
1244                }
1245                return;
1246            }
1247            "toast.show" => {
1248                self.toast_visible = true;
1249                return;
1250            }
1251            "toast.hide" => {
1252                self.toast_visible = false;
1253                return;
1254            }
1255            id if id.starts_with("toast.dismiss.") => {
1256                self.toast_visible = false;
1257                return;
1258            }
1259            "toast.action.1.undo" => {
1260                self.toast_action_status = "Undo requested";
1261                return;
1262            }
1263            "popup.toggle" => {
1264                self.popup_open = !self.popup_open;
1265                return;
1266            }
1267            "popup.close" => {
1268                self.popup_open = false;
1269                return;
1270            }
1271            "layout.tab.preview" => {
1272                self.layout_tab = 0;
1273                return;
1274            }
1275            "layout.tab.settings" => {
1276                self.layout_tab = 1;
1277                return;
1278            }
1279            "forms.profile.submit" => {
1280                self.form.submit();
1281                self.form_status = "Submit requested".to_string();
1282                return;
1283            }
1284            "forms.profile.apply" => {
1285                self.form.apply();
1286                self.form_status = "Applied".to_string();
1287                return;
1288            }
1289            "forms.profile.cancel" => {
1290                self.form.cancel();
1291                self.sync_profile_form_text_fields();
1292                self.form_status = "Cancelled".to_string();
1293                return;
1294            }
1295            "forms.profile.reset" => {
1296                self.form = profile_form_state();
1297                self.form_newsletter = true;
1298                self.sync_profile_form_text_fields();
1299                self.form_status = "Reset".to_string();
1300                return;
1301            }
1302            "forms.profile.newsletter.toggle" => {
1303                self.form_newsletter = !self.form_newsletter;
1304                let _ = self.form.update_field(
1305                    "newsletter",
1306                    if self.form_newsletter {
1307                        "true"
1308                    } else {
1309                        "false"
1310                    },
1311                );
1312                self.validate_profile_form();
1313                self.form_status = "Editing profile".to_string();
1314                return;
1315            }
1316            "overlays.collapsing.toggle" => {
1317                self.overlay_expanded = !self.overlay_expanded;
1318                return;
1319            }
1320            "overlays.popup.toggle" => {
1321                self.overlay_popup_open = !self.overlay_popup_open;
1322                return;
1323            }
1324            "overlays.popup.close" => {
1325                self.overlay_popup_open = false;
1326                return;
1327            }
1328            "overlays.modal.open" => {
1329                self.overlay_modal_open = true;
1330                return;
1331            }
1332            "overlays.modal.close" => {
1333                self.overlay_modal_open = false;
1334                return;
1335            }
1336            "drag_drop.text_source" => {
1337                self.drag_drop_status = "Text drag started";
1338                return;
1339            }
1340            "drag_drop.file_source" => {
1341                self.drag_drop_status = "File drag started";
1342                return;
1343            }
1344            "drag_drop.bytes_source" => {
1345                self.drag_drop_status = "Image byte drag started";
1346                return;
1347            }
1348            "drag_drop.accept_text" => {
1349                self.drag_drop_status = "Text payload accepted";
1350                return;
1351            }
1352            "drag_drop.files_only" => {
1353                self.drag_drop_status = "File payload rejected";
1354                return;
1355            }
1356            "drag_drop.image_bytes" => {
1357                self.drag_drop_status = "Image bytes hovered";
1358                return;
1359            }
1360            "slider.trailing" => {
1361                self.slider_trailing_color = !self.slider_trailing_color;
1362                return;
1363            }
1364            "slider.trailing_color_button" => {
1365                self.slider_trailing_picker_open = !self.slider_trailing_picker_open;
1366                return;
1367            }
1368            "slider.thumb.circle" => {
1369                self.slider_thumb_shape = SliderThumbChoice::Circle;
1370                return;
1371            }
1372            "slider.thumb.square" => {
1373                self.slider_thumb_shape = SliderThumbChoice::Square;
1374                return;
1375            }
1376            "slider.thumb.rectangle" => {
1377                self.slider_thumb_shape = SliderThumbChoice::Rectangle;
1378                return;
1379            }
1380            "slider.steps" => {
1381                self.slider_use_steps = !self.slider_use_steps;
1382                if self.slider_use_steps {
1383                    self.set_slider_value(widgets::slider::round_slider_to_step(
1384                        self.slider,
1385                        self.slider_step(),
1386                    ));
1387                }
1388                return;
1389            }
1390            "slider.logarithmic" => {
1391                self.slider_logarithmic = !self.slider_logarithmic;
1392                return;
1393            }
1394            "slider.clamping.never" => {
1395                self.slider_clamping = widgets::SliderClamping::Never;
1396                return;
1397            }
1398            "slider.clamping.edits" => {
1399                self.slider_clamping = widgets::SliderClamping::Edits;
1400                return;
1401            }
1402            "slider.clamping.always" => {
1403                self.slider_clamping = widgets::SliderClamping::Always;
1404                self.clamp_slider_to_range();
1405                return;
1406            }
1407            "slider.smart_aim" => {
1408                self.slider_smart_aim = !self.slider_smart_aim;
1409                return;
1410            }
1411            "animation.open" => {
1412                self.animation_open = !self.animation_open;
1413                return;
1414            }
1415            "animation.timed.toggle" => {
1416                self.animation_timed_expanded = !self.animation_timed_expanded;
1417                return;
1418            }
1419            "animation.scrub.toggle" => {
1420                self.animation_scrub_expanded = !self.animation_scrub_expanded;
1421                return;
1422            }
1423            "animation.state.toggle" => {
1424                self.animation_state_expanded = !self.animation_state_expanded;
1425                return;
1426            }
1427            "animation.interaction.toggle" => {
1428                self.animation_interaction_expanded = !self.animation_interaction_expanded;
1429                return;
1430            }
1431            "animation.scrub" => {
1432                if let WidgetActionKind::PointerEdit(edit) = kind {
1433                    self.animation_scrub = scaled_slider(edit.target_rect, edit.position, 0.0, 1.0);
1434                }
1435                return;
1436            }
1437            "diagnostics.animation.controls.transport.pause_toggle" => {
1438                self.diagnostics_animation_paused = !self.diagnostics_animation_paused;
1439                return;
1440            }
1441            "diagnostics.animation.controls.transport.step" => {
1442                self.diagnostics_animation_paused = true;
1443                self.diagnostics_animation_scrub =
1444                    (self.diagnostics_animation_scrub + 1.0 / 12.0).min(1.0);
1445                return;
1446            }
1447            "diagnostics.animation.controls.transport.scrub" => {
1448                if let WidgetActionKind::PointerEdit(edit) = kind {
1449                    self.diagnostics_animation_scrub =
1450                        scaled_slider(edit.target_rect, edit.position, 0.0, 1.0);
1451                }
1452                return;
1453            }
1454            "diagnostics.animation.controls.input.active.toggle" => {
1455                self.diagnostics_animation_active = !self.diagnostics_animation_active;
1456                self.refresh_diagnostics_snapshot();
1457                return;
1458            }
1459            "diagnostics.animation.controls.input.hover.set" => {
1460                if let WidgetActionKind::PointerEdit(edit) = kind {
1461                    self.diagnostics_animation_hover =
1462                        scaled_slider(edit.target_rect, edit.position, 0.0, 1.0);
1463                    self.refresh_diagnostics_snapshot();
1464                }
1465                return;
1466            }
1467            "diagnostics.animation.controls.input.pulse.fire" => {
1468                self.diagnostics_animation_pulse_count =
1469                    self.diagnostics_animation_pulse_count.saturating_add(1);
1470                return;
1471            }
1472            "layout_widgets.float_inspector" => {
1473                let panel = ext_widgets::DockPanelDescriptor::new(
1474                    "inspector",
1475                    "Inspector",
1476                    ext_widgets::DockSide::Left,
1477                    120.0,
1478                );
1479                self.layout_dock
1480                    .float_panel(&panel, UiRect::new(20.0, 58.0, 236.0, 210.0));
1481                return;
1482            }
1483            "layout_widgets.dock_inspector" => {
1484                let panel = ext_widgets::DockPanelDescriptor::new(
1485                    "inspector",
1486                    "Inspector",
1487                    ext_widgets::DockSide::Left,
1488                    120.0,
1489                );
1490                self.layout_dock
1491                    .dock_panel(&panel, ext_widgets::DockSide::Left);
1492                return;
1493            }
1494            "layout_widgets.drawer.inspector" => {
1495                self.layout_dock.toggle_panel_hidden("inspector");
1496                return;
1497            }
1498            "layout_widgets.drawer.assets" => {
1499                self.layout_dock.toggle_panel_hidden("assets");
1500                return;
1501            }
1502            "layout_widgets.reorder.assets.before.inspector" => {
1503                let mut panels = base_layout_dock_panels();
1504                self.layout_dock.apply_order_to_panels(&mut panels);
1505                let payload = ext_widgets::dock_workspace::dock_panel_drag_payload("assets");
1506                self.layout_dock.apply_reorder_to_panels(
1507                    &mut panels,
1508                    &payload,
1509                    "inspector",
1510                    ext_widgets::DockPanelReorderPlacement::Before,
1511                );
1512                return;
1513            }
1514            "layout_widgets.reorder.assets.after.inspector" => {
1515                let mut panels = base_layout_dock_panels();
1516                self.layout_dock.apply_order_to_panels(&mut panels);
1517                let payload = ext_widgets::dock_workspace::dock_panel_drag_payload("assets");
1518                self.layout_dock.apply_reorder_to_panels(
1519                    &mut panels,
1520                    &payload,
1521                    "inspector",
1522                    ext_widgets::DockPanelReorderPlacement::After,
1523                );
1524                return;
1525            }
1526            "styling.stroke_color_button" => {
1527                self.styling_stroke_picker_open = !self.styling_stroke_picker_open;
1528                return;
1529            }
1530            "styling.fill_color_button" => {
1531                self.styling_fill_picker_open = !self.styling_fill_picker_open;
1532                return;
1533            }
1534            "styling.shadow_color_button" => {
1535                self.styling_shadow_picker_open = !self.styling_shadow_picker_open;
1536                return;
1537            }
1538            "styling.inner_same" => {
1539                self.styling.inner_same = !self.styling.inner_same;
1540                return;
1541            }
1542            "styling.outer_same" => {
1543                self.styling.outer_same = !self.styling.outer_same;
1544                return;
1545            }
1546            "styling.radius_same" => {
1547                self.styling.radius_same = !self.styling.radius_same;
1548                return;
1549            }
1550            _ => {}
1551        }
1552
1553        if action_id == "canvas.rotate" {
1554            if let WidgetActionKind::Drag(drag) = kind {
1555                self.cube.apply_drag(drag);
1556            }
1557            return;
1558        }
1559        if let WidgetActionKind::Scroll(scroll) = &kind {
1560            match action_id {
1561                "lists_tables.scroll_area.scroll" => self.list_scroll = scroll.offset().y,
1562                "lists_tables.virtual_list.scroll" => self.virtual_scroll = scroll.offset().y,
1563                "lists_tables.data_table.scroll" => self.table_scroll = scroll.offset().y,
1564                "lists_tables.virtualized_table.scroll" => {
1565                    self.virtual_table_scroll = scroll.offset().y
1566                }
1567                "layout.preview.scroll" => self.layout_preview_scroll = scroll.offset().y,
1568                "layout.left.scroll" => self.layout_left_scroll = scroll.offset().y,
1569                "layout.right.scroll" => self.layout_right_scroll = scroll.offset().y,
1570                "layout.inspector.scroll" => self.layout_inspector_scroll = scroll.offset().y,
1571                "layout.document.scroll" => self.layout_document_scroll = scroll.offset().y,
1572                "layout.assets.scroll" => self.layout_assets_scroll = scroll.offset().y,
1573                "trees.virtual.scroll" => self.tree_virtual_scroll = scroll.offset().y,
1574                "containers.scroll_area_with_bars.scroll" => {
1575                    self.containers_scroll.set_offset(scroll.offset());
1576                }
1577                "controls.widget_list.scroll" => {
1578                    self.controls_scroll = *scroll;
1579                    self.controls_scroll.set_offset(scroll.offset());
1580                }
1581                _ => {}
1582            }
1583            return;
1584        }
1585
1586        if let Some(date) = action_id
1587            .strip_prefix("date.day.")
1588            .and_then(parse_calendar_date)
1589        {
1590            self.date.select(date);
1591            return;
1592        }
1593
1594        if let Some(option_id) = action_id.strip_prefix("labels.locale.option.") {
1595            self.label_locale
1596                .select_id_and_close(&label_locale_options(), option_id);
1597            return;
1598        }
1599        if let Some(option_id) = action_id.strip_prefix("selection.dropdown.option.") {
1600            self.dropdown
1601                .select_id_and_close(&select_options(), option_id);
1602            return;
1603        }
1604        if let Some(option_id) = action_id.strip_prefix("selection.combo.option.") {
1605            if let Some(option) = select_options()
1606                .into_iter()
1607                .find(|option| option.id == option_id && option.enabled)
1608            {
1609                self.combo_label = option.label;
1610                self.combo_open = false;
1611            }
1612            return;
1613        }
1614        if let Some(option_id) = action_id.strip_prefix("selection.menu.option.") {
1615            self.select_menu.select_id(&select_options(), option_id);
1616            return;
1617        }
1618        if let Some(menu_id) = action_id.strip_prefix("menus.item.") {
1619            self.apply_menu_item(menu_id);
1620            return;
1621        }
1622        if let Some(menu_id) = action_id.strip_prefix("menus.context.") {
1623            self.apply_menu_item(menu_id);
1624            self.context_menu.close();
1625            return;
1626        }
1627        if let Some(kind) = action_id.strip_prefix("color_buttons.") {
1628            self.color_button_status = match kind {
1629                "compact" => "Compact",
1630                "swatch" => "Swatch",
1631                "rgb" => "RGB",
1632                "rgba" => "RGBA",
1633                "srgb" => "SRGB",
1634                "srgba" => "SRGBA",
1635                "hsva" => "HSVA",
1636                "oklch" => "OKLCH",
1637                "color32" => "Color32",
1638                "rgba_premultiplied" => "RGBA premultiplied",
1639                "rgba_unmultiplied" => "RGBA unmultiplied",
1640                "srgba_premultiplied" => "SRGBA premultiplied",
1641                "srgba_unmultiplied" => "SRGBA unmultiplied",
1642                _ => self.color_button_status,
1643            };
1644            return;
1645        }
1646        if let Some(row) = action_id
1647            .strip_prefix("lists_tables.data_table.row.")
1648            .and_then(|row| row.parse::<usize>().ok())
1649        {
1650            self.table_selection = ext_widgets::DataTableSelection::single_row(row)
1651                .with_active_cell(ext_widgets::DataTableCellIndex::new(row, 0));
1652            return;
1653        }
1654        if let Some(cell) = action_id
1655            .strip_prefix("lists_tables.data_table.cell.")
1656            .and_then(parse_table_cell)
1657        {
1658            self.table_selection =
1659                ext_widgets::DataTableSelection::single_row(cell.row).with_active_cell(cell);
1660            return;
1661        }
1662        match action_id {
1663            "lists_tables.virtualized_table.sort.name" => {
1664                self.virtual_table_descending = !self.virtual_table_descending;
1665                return;
1666            }
1667            "lists_tables.virtualized_table.filter.status" => {
1668                self.virtual_table_ready_only = !self.virtual_table_ready_only;
1669                self.virtual_table_scroll = 0.0;
1670                return;
1671            }
1672            "lists_tables.virtualized_table.resize.reset" => {
1673                self.virtual_table_value_width = 70.0;
1674                self.virtual_table_resize = None;
1675                return;
1676            }
1677            _ => {}
1678        }
1679        if let Some(row) = action_id
1680            .strip_prefix("lists_tables.virtualized_table.row.")
1681            .and_then(|row| row.parse::<usize>().ok())
1682        {
1683            self.table_selection = ext_widgets::DataTableSelection::single_row(row)
1684                .with_active_cell(ext_widgets::DataTableCellIndex::new(row, 0));
1685            return;
1686        }
1687        if let Some(cell) = action_id
1688            .strip_prefix("lists_tables.virtualized_table.cell.")
1689            .and_then(parse_table_cell)
1690        {
1691            self.table_selection =
1692                ext_widgets::DataTableSelection::single_row(cell.row).with_active_cell(cell);
1693            return;
1694        }
1695        if let Some(id) = action_id.strip_prefix("trees.tree.row.") {
1696            self.apply_tree_row(id, false);
1697            return;
1698        }
1699        if let Some(id) = action_id.strip_prefix("trees.outliner.row.") {
1700            self.apply_tree_row(id, true);
1701            return;
1702        }
1703
1704        let WidgetActionKind::PointerEdit(edit) = kind else {
1705            return;
1706        };
1707        match action_id {
1708            "numeric.drag_value" => {
1709                self.numeric_value = scaled_slider(edit.target_rect, edit.position, 0.0, 100.0);
1710            }
1711            "numeric.drag_angle" => {
1712                self.numeric_angle =
1713                    scaled_slider(edit.target_rect, edit.position, 0.0, 360.0).to_radians();
1714            }
1715            "numeric.drag_angle_tau" => {
1716                self.numeric_tau = scaled_slider(edit.target_rect, edit.position, 0.0, 1.0)
1717                    * std::f32::consts::TAU;
1718            }
1719            "layout_widgets.split_pane.handle" => {
1720                let total_extent = self
1721                    .desktop
1722                    .size("layout_widgets", default_window_size("layout_widgets"))
1723                    .width
1724                    - 48.0;
1725                let total_extent = total_extent.max(1.0);
1726                let handle_center = edit.target_rect.x + edit.target_rect.width * 0.5;
1727                self.layout_split
1728                    .resize_by(edit.position.x - handle_center, total_extent, 6.0);
1729            }
1730            "slider.value" => {
1731                self.set_slider_value(
1732                    self.slider_value_spec()
1733                        .value_from_control_point(edit.target_rect, edit.position),
1734                );
1735            }
1736            "slider.range_left" => {
1737                let value = widgets::slider::SliderValueSpec::new(0.0, self.slider_right.max(1.0))
1738                    .value_from_control_point(edit.target_rect, edit.position);
1739                self.set_slider_left(value.min(self.slider_right - 1.0));
1740            }
1741            "slider.range_right" => {
1742                let value = widgets::slider::SliderValueSpec::new(self.slider_left + 1.0, 10000.0)
1743                    .value_from_control_point(edit.target_rect, edit.position);
1744                self.set_slider_right(value.max(self.slider_left + 1.0));
1745            }
1746            "lists_tables.scroll_area.scrollbar" => {
1747                let scroll = scroll_state(self.list_scroll, 92.0, 6.0 * 26.0);
1748                self.list_scroll = self
1749                    .scrollbars
1750                    .apply_drag_for_target_rect(
1751                        "list",
1752                        scroll,
1753                        scrollbar_widgets::ScrollAxis::Vertical,
1754                        edit,
1755                    )
1756                    .y;
1757            }
1758            "lists_tables.virtual_list.scrollbar" => {
1759                let scroll = scroll_state(self.virtual_scroll, 112.0, 24.0 * 28.0);
1760                self.virtual_scroll = self
1761                    .scrollbars
1762                    .apply_drag_for_target_rect(
1763                        "virtual",
1764                        scroll,
1765                        scrollbar_widgets::ScrollAxis::Vertical,
1766                        edit,
1767                    )
1768                    .y;
1769            }
1770            "lists_tables.data_table.scrollbar" => {
1771                let scroll = scroll_state(self.table_scroll, 128.0, 16.0 * 28.0);
1772                self.table_scroll = self
1773                    .scrollbars
1774                    .apply_drag_for_target_rect(
1775                        "table",
1776                        scroll,
1777                        scrollbar_widgets::ScrollAxis::Vertical,
1778                        edit,
1779                    )
1780                    .y;
1781            }
1782            "lists_tables.virtualized_table.scrollbar" => {
1783                let row_count = virtual_table_visible_rows(self).len() as f32;
1784                let scroll = scroll_state(self.virtual_table_scroll, 128.0, row_count * 28.0);
1785                self.virtual_table_scroll = self
1786                    .scrollbars
1787                    .apply_drag_for_target_rect(
1788                        "virtual_table",
1789                        scroll,
1790                        scrollbar_widgets::ScrollAxis::Vertical,
1791                        edit,
1792                    )
1793                    .y;
1794            }
1795            "lists_tables.virtualized_table.resize.value" => match edit.phase.edit_phase() {
1796                EditPhase::Preview => {}
1797                EditPhase::BeginEdit => {
1798                    self.virtual_table_resize =
1799                        Some((self.virtual_table_value_width, edit.position.x));
1800                }
1801                EditPhase::UpdateEdit | EditPhase::CommitEdit => {
1802                    let (origin_width, origin_x) = self
1803                        .virtual_table_resize
1804                        .unwrap_or((self.virtual_table_value_width, edit.position.x));
1805                    self.virtual_table_value_width =
1806                        (origin_width + edit.position.x - origin_x).clamp(56.0, 180.0);
1807                    if edit.phase.edit_phase() == EditPhase::CommitEdit {
1808                        self.virtual_table_resize = None;
1809                    }
1810                }
1811                EditPhase::CancelEdit => {
1812                    if let Some((origin_width, _)) = self.virtual_table_resize.take() {
1813                        self.virtual_table_value_width = origin_width;
1814                    }
1815                }
1816            },
1817            "containers.scroll_area_with_bars.vertical-scrollbar" => {
1818                let offset = self.scrollbars.apply_drag_for_target_rect(
1819                    "containers.vertical",
1820                    self.containers_scroll,
1821                    scrollbar_widgets::ScrollAxis::Vertical,
1822                    edit,
1823                );
1824                self.containers_scroll.set_offset(offset);
1825            }
1826            "containers.scroll_area_with_bars.horizontal-scrollbar" => {
1827                let offset = self.scrollbars.apply_drag_for_target_rect(
1828                    "containers.horizontal",
1829                    self.containers_scroll,
1830                    scrollbar_widgets::ScrollAxis::Horizontal,
1831                    edit,
1832                );
1833                self.containers_scroll.set_offset(offset);
1834            }
1835            "controls.widget_list.scrollbar" => {
1836                let mut scroll =
1837                    controls_scroll_state_for_view(self.controls_scroll, edit.target_rect.height);
1838                let offset = self.scrollbars.apply_drag_for_target_rect(
1839                    "controls.widget_list",
1840                    scroll,
1841                    scrollbar_widgets::ScrollAxis::Vertical,
1842                    edit,
1843                );
1844                scroll.set_offset(offset);
1845                self.controls_scroll = scroll;
1846            }
1847            "styling.inner" => {
1848                self.styling.inner_margin =
1849                    scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1850                if self.styling.inner_same {
1851                    self.styling.inner_right = self.styling.inner_margin;
1852                    self.styling.inner_top = self.styling.inner_margin;
1853                    self.styling.inner_bottom = self.styling.inner_margin;
1854                }
1855            }
1856            "styling.inner_right" => {
1857                self.styling.inner_right =
1858                    scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1859            }
1860            "styling.inner_top" => {
1861                self.styling.inner_top = scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1862            }
1863            "styling.inner_bottom" => {
1864                self.styling.inner_bottom =
1865                    scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1866            }
1867            "styling.outer" => {
1868                self.styling.outer_margin =
1869                    scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
1870                if self.styling.outer_same {
1871                    self.styling.outer_right = self.styling.outer_margin;
1872                    self.styling.outer_top = self.styling.outer_margin;
1873                    self.styling.outer_bottom = self.styling.outer_margin;
1874                }
1875            }
1876            "styling.outer_right" => {
1877                self.styling.outer_right =
1878                    scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
1879            }
1880            "styling.outer_top" => {
1881                self.styling.outer_top = scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
1882            }
1883            "styling.outer_bottom" => {
1884                self.styling.outer_bottom =
1885                    scaled_slider(edit.target_rect, edit.position, 0.0, 40.0);
1886            }
1887            "styling.radius" => {
1888                self.styling.corner_radius =
1889                    scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
1890                if self.styling.radius_same {
1891                    self.styling.corner_ne = self.styling.corner_radius;
1892                    self.styling.corner_sw = self.styling.corner_radius;
1893                    self.styling.corner_se = self.styling.corner_radius;
1894                }
1895            }
1896            "styling.radius_ne" => {
1897                self.styling.corner_ne = scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
1898            }
1899            "styling.radius_sw" => {
1900                self.styling.corner_sw = scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
1901            }
1902            "styling.radius_se" => {
1903                self.styling.corner_se = scaled_slider(edit.target_rect, edit.position, 0.0, 28.0);
1904            }
1905            "styling.shadow_x" => {
1906                self.styling.shadow_x = scaled_slider(edit.target_rect, edit.position, -24.0, 24.0);
1907            }
1908            "styling.shadow_y" => {
1909                self.styling.shadow_y = scaled_slider(edit.target_rect, edit.position, -24.0, 24.0);
1910            }
1911            "styling.shadow" => {
1912                self.styling.shadow_blur =
1913                    scaled_slider(edit.target_rect, edit.position, 0.0, 32.0);
1914            }
1915            "styling.shadow_spread" => {
1916                self.styling.shadow_spread =
1917                    scaled_slider(edit.target_rect, edit.position, 0.0, 16.0);
1918            }
1919            "styling.stroke" => {
1920                self.styling.stroke_width =
1921                    scaled_slider(edit.target_rect, edit.position, 0.0, 4.0);
1922            }
1923            _ => {}
1924        }
1925    }
1926
1927    fn apply_command_palette_event(&mut self, event: operad::UiInputEvent) {
1928        let items = command_palette_items_with_history(&self.command_history);
1929        let outcome = self.command_palette.handle_event(&items, &event);
1930        if let Some(selection) = outcome.selected {
1931            self.select_command_palette_item(&selection.id);
1932        }
1933    }
1934
1935    fn select_command_palette_item(&mut self, id: &str) {
1936        if let Some(item) = command_palette_items_with_history(&self.command_history)
1937            .into_iter()
1938            .find(|item| item.id == id && item.enabled)
1939        {
1940            self.command_history.record(item.id.as_str());
1941            self.last_command = item.title;
1942            let items = command_palette_items_with_history(&self.command_history);
1943            self.command_palette.set_query("", &items);
1944        }
1945    }
1946
1947    fn text_edit_options(&self, input: FocusedTextInput) -> TextInputOptions {
1948        let mut options = TextInputOptions::default();
1949        options.focused = self.focused_text == Some(input);
1950        options.caret_visible = caret_visible(self.caret_phase);
1951        match input {
1952            FocusedTextInput::Editable => {
1953                options.layout = LayoutStyle::new().with_width(300.0).with_height(36.0);
1954                options.text_style = text(13.0, color(230, 236, 246));
1955                options.placeholder_style = text(13.0, color(144, 156, 174));
1956                options.placeholder = "Type here".to_string();
1957            }
1958            FocusedTextInput::Selectable => {
1959                options.layout = LayoutStyle::new().with_width(360.0).with_height(36.0);
1960                options.text_style = text(13.0, color(196, 210, 230));
1961                options.read_only = true;
1962                options.selectable = true;
1963            }
1964            FocusedTextInput::Singleline => {
1965                options.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
1966                options.text_style = text(13.0, color(230, 236, 246));
1967                options.placeholder = "Single line".to_string();
1968            }
1969            FocusedTextInput::Multiline => {
1970                options.layout = LayoutStyle::new().with_width(360.0).with_height(72.0);
1971                options.text_style = text(13.0, color(230, 236, 246));
1972            }
1973            FocusedTextInput::TextArea => {
1974                options.layout = LayoutStyle::new().with_width(360.0).with_height(66.0);
1975                options.text_style = text(13.0, color(230, 236, 246));
1976            }
1977            FocusedTextInput::CodeEditor => {
1978                options.layout = LayoutStyle::new().with_width(360.0).with_height(88.0);
1979                options.text_style = widgets::code_text_style();
1980            }
1981            FocusedTextInput::Search => {
1982                options.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
1983                options.text_style = text(13.0, color(230, 236, 246));
1984                options.placeholder = "Search".to_string();
1985            }
1986            FocusedTextInput::Password => {
1987                options.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
1988                options.text_style = text(13.0, color(230, 236, 246));
1989                options.placeholder = "Password".to_string();
1990            }
1991            FocusedTextInput::FormName
1992            | FocusedTextInput::FormEmail
1993            | FocusedTextInput::FormRole => {
1994                options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
1995                options.text_style = text(12.0, color(230, 236, 246));
1996                options.placeholder_style = text(12.0, color(144, 156, 174));
1997                options.placeholder = "Required".to_string();
1998            }
1999            FocusedTextInput::SliderValue | FocusedTextInput::SliderStep => {
2000                options.layout = LayoutStyle::new().with_width(86.0).with_height(28.0);
2001                options.text_style = text(12.0, color(230, 236, 246));
2002                options.placeholder_style = text(12.0, color(144, 156, 174));
2003            }
2004            FocusedTextInput::SliderRangeLeft | FocusedTextInput::SliderRangeRight => {
2005                options.layout = LayoutStyle::new().with_width(96.0).with_height(28.0);
2006                options.text_style = text(12.0, color(230, 236, 246));
2007                options.placeholder_style = text(12.0, color(144, 156, 174));
2008            }
2009        }
2010        options
2011    }
2012
2013    fn apply_text_edit(&mut self, input: FocusedTextInput, edit: WidgetTextEdit) {
2014        self.focused_text = Some(input);
2015        let options = self.text_edit_options(input);
2016        let outcome = self.text_state_mut(input).map(|state| {
2017            state.set_multiline(input.is_multiline());
2018            state.apply_widget_text_edit(&edit, &options)
2019        });
2020        if let Some(outcome) = outcome {
2021            self.apply_text_clipboard_outcome(input, outcome);
2022            self.sync_text_input_value(input);
2023        }
2024    }
2025
2026    fn apply_text_clipboard_outcome(
2027        &mut self,
2028        input: FocusedTextInput,
2029        outcome: widgets::text_input::TextInputOutcome,
2030    ) {
2031        match outcome.clipboard {
2032            Some(widgets::text_input::TextInputClipboardAction::Copy(text))
2033            | Some(widgets::text_input::TextInputClipboardAction::Cut(text)) => {
2034                self.copy_text_to_clipboard(&text);
2035            }
2036            Some(widgets::text_input::TextInputClipboardAction::Paste) => {
2037                self.pending_clipboard_paste = Some(input);
2038                self.platform.read_clipboard_text();
2039            }
2040            None => {}
2041        }
2042    }
2043
2044    fn text_state_mut(&mut self, input: FocusedTextInput) -> Option<&mut TextInputState> {
2045        match input {
2046            FocusedTextInput::Editable => Some(&mut self.text),
2047            FocusedTextInput::Selectable => Some(&mut self.selectable_text),
2048            FocusedTextInput::Singleline => Some(&mut self.singleline_text),
2049            FocusedTextInput::Multiline => Some(&mut self.multiline_text),
2050            FocusedTextInput::TextArea => Some(&mut self.text_area_text),
2051            FocusedTextInput::CodeEditor => Some(&mut self.code_editor_text),
2052            FocusedTextInput::Search => Some(&mut self.search_text),
2053            FocusedTextInput::Password => Some(&mut self.password_text),
2054            FocusedTextInput::FormName => Some(&mut self.form_name_text),
2055            FocusedTextInput::FormEmail => Some(&mut self.form_email_text),
2056            FocusedTextInput::FormRole => Some(&mut self.form_role_text),
2057            FocusedTextInput::SliderValue => Some(&mut self.slider_value_text),
2058            FocusedTextInput::SliderRangeLeft => Some(&mut self.slider_left_text),
2059            FocusedTextInput::SliderRangeRight => Some(&mut self.slider_right_text),
2060            FocusedTextInput::SliderStep => Some(&mut self.slider_step_text),
2061        }
2062    }
2063
2064    fn sync_text_input_value(&mut self, input: FocusedTextInput) {
2065        match input {
2066            FocusedTextInput::SliderValue => {
2067                if let Ok(value) = self.slider_value_text.text().parse::<f32>() {
2068                    self.apply_slider_value_from_text(value);
2069                }
2070            }
2071            FocusedTextInput::SliderRangeLeft => {
2072                if let Ok(value) = self.slider_left_text.text().parse::<f32>() {
2073                    self.apply_slider_left_from_text(value);
2074                }
2075            }
2076            FocusedTextInput::SliderRangeRight => {
2077                if let Ok(value) = self.slider_right_text.text().parse::<f32>() {
2078                    self.apply_slider_right_from_text(value);
2079                }
2080            }
2081            FocusedTextInput::SliderStep => {
2082                if let Ok(value) = self.slider_step_text.text().parse::<f32>() {
2083                    self.slider_step_value = value.abs().max(0.0001);
2084                    if self.slider_use_steps {
2085                        self.set_slider_value(widgets::slider::round_slider_to_step(
2086                            self.slider,
2087                            self.slider_step(),
2088                        ));
2089                    }
2090                }
2091            }
2092            FocusedTextInput::FormName => {
2093                self.update_profile_form_field("name", self.form_name_text.text().to_string());
2094            }
2095            FocusedTextInput::FormEmail => {
2096                self.update_profile_form_field("email", self.form_email_text.text().to_string());
2097            }
2098            FocusedTextInput::FormRole => {
2099                self.update_profile_form_field("role", self.form_role_text.text().to_string());
2100            }
2101            _ => {}
2102        }
2103    }
2104
2105    fn update_profile_form_field(&mut self, id: &'static str, value: String) {
2106        let _ = self.form.update_field(id, value);
2107        self.validate_profile_form();
2108        self.form_status = "Editing profile".to_string();
2109    }
2110
2111    fn sync_profile_form_text_fields(&mut self) {
2112        self.form_name_text = TextInputState::new(profile_form_value(&self.form, "name"));
2113        self.form_email_text = TextInputState::new(profile_form_value(&self.form, "email"));
2114        self.form_role_text = TextInputState::new(profile_form_value(&self.form, "role"));
2115    }
2116
2117    fn validate_profile_form(&mut self) {
2118        let request = self.form.begin_form_validation();
2119        let values = request.values.clone();
2120        let mut result = FormValidationResult::new(request.generation);
2121        let field_value = |id: &str| {
2122            values
2123                .iter()
2124                .find_map(|(field_id, value)| (field_id.as_str() == id).then_some(value.as_str()))
2125                .unwrap_or_default()
2126        };
2127        let name = field_value("name").trim();
2128        let email = field_value("email").trim();
2129        let role = field_value("role").trim();
2130
2131        if name.is_empty() {
2132            result = result
2133                .with_field_messages("name", vec![ValidationMessage::error("Name is required")]);
2134        }
2135        if !profile_email_valid(email) {
2136            result = result.with_field_messages(
2137                "email",
2138                vec![ValidationMessage::error("Use a complete email address")],
2139            );
2140        }
2141        if role.is_empty() {
2142            result = result.with_field_messages(
2143                "role",
2144                vec![ValidationMessage::warning("Role can be added later")],
2145            );
2146        }
2147        if self.form.dirty {
2148            result =
2149                result.with_form_message(ValidationMessage::warning("Unsaved profile changes"));
2150        }
2151        let _ = self.form.apply_form_validation(result);
2152    }
2153
2154    fn copy_text_to_clipboard(&mut self, text: &str) {
2155        self.clipboard_text = text.to_string();
2156        self.platform.write_clipboard_text(text);
2157    }
2158
2159    fn apply_platform_responses(&mut self, responses: &[PlatformServiceResponse]) {
2160        self.platform.record_responses(responses.iter().cloned());
2161        for response in responses {
2162            match &response.response {
2163                PlatformResponse::Clipboard(ClipboardResponse::Text(text)) => {
2164                    let pasted = text
2165                        .as_deref()
2166                        .filter(|text| !text.is_empty())
2167                        .unwrap_or(&self.clipboard_text)
2168                        .to_string();
2169                    self.apply_pending_clipboard_paste(&pasted);
2170                }
2171                PlatformResponse::Clipboard(ClipboardResponse::Unsupported)
2172                | PlatformResponse::Clipboard(ClipboardResponse::Error(_)) => {
2173                    let pasted = self.clipboard_text.clone();
2174                    self.apply_pending_clipboard_paste(&pasted);
2175                }
2176                _ => {}
2177            }
2178        }
2179    }
2180
2181    fn apply_pending_clipboard_paste(&mut self, pasted: &str) {
2182        let Some(input) = self.pending_clipboard_paste.take() else {
2183            return;
2184        };
2185        if input.is_read_only() {
2186            return;
2187        }
2188        if let Some(state) = self.text_state_mut(input) {
2189            state.paste_text(pasted);
2190        }
2191        self.sync_text_input_value(input);
2192    }
2193
2194    fn apply_menu_item(&mut self, id: &str) {
2195        let menus = menu_bar_menus(self.menu_autosave, self.menu_grid);
2196        self.menu_bar.set_active_item_by_id(&menus, id);
2197        if id == "autosave" {
2198            self.menu_autosave = !self.menu_autosave;
2199        } else if id == "grid" {
2200            self.menu_grid = !self.menu_grid;
2201        }
2202        self.menu_button.close();
2203        self.image_text_menu_button.close();
2204        self.image_menu_button.close();
2205    }
2206
2207    fn apply_tree_row(&mut self, id: &str, outliner: bool) {
2208        let roots = tree_items();
2209        let state = if outliner {
2210            &mut self.outliner
2211        } else {
2212            &mut self.tree
2213        };
2214        state.activate_visible_item_id(&roots, id);
2215    }
2216
2217    fn slider_value_spec(&self) -> widgets::slider::SliderValueSpec {
2218        let mut spec = widgets::slider::SliderValueSpec::new(self.slider_left, self.slider_right)
2219            .logarithmic(self.slider_logarithmic)
2220            .clamping(self.slider_clamping)
2221            .smart_aim(self.slider_smart_aim);
2222        if self.slider_use_steps {
2223            spec = spec.step(self.slider_step());
2224        }
2225        spec
2226    }
2227
2228    fn set_slider_value(&mut self, value: f32) {
2229        let value = self.slider_value_spec().adjust_value(value);
2230        self.slider = value;
2231        self.slider_value_text
2232            .set_text(widgets::slider::format_slider_value(value));
2233    }
2234
2235    fn apply_slider_value_from_text(&mut self, value: f32) {
2236        self.slider = if self.slider_clamping == widgets::SliderClamping::Always {
2237            self.slider_value_spec().clamp(value)
2238        } else {
2239            value
2240        };
2241    }
2242
2243    fn set_slider_left(&mut self, value: f32) {
2244        self.slider_left = value.min(self.slider_right - 1.0).max(0.0);
2245        self.slider_left_text
2246            .set_text(widgets::slider::format_slider_value(self.slider_left));
2247        if self.slider_clamping == widgets::SliderClamping::Always {
2248            self.clamp_slider_to_range();
2249        }
2250    }
2251
2252    fn apply_slider_left_from_text(&mut self, value: f32) {
2253        if value < self.slider_right {
2254            self.slider_left = value.max(0.0);
2255            if self.slider_clamping == widgets::SliderClamping::Always {
2256                self.slider = self.slider.clamp(self.slider_left, self.slider_right);
2257            }
2258        }
2259    }
2260
2261    fn set_slider_right(&mut self, value: f32) {
2262        self.slider_right = value.max(self.slider_left + 1.0).min(10000.0);
2263        self.slider_right_text
2264            .set_text(widgets::slider::format_slider_value(self.slider_right));
2265        if self.slider_clamping == widgets::SliderClamping::Always {
2266            self.clamp_slider_to_range();
2267        }
2268    }
2269
2270    fn apply_slider_right_from_text(&mut self, value: f32) {
2271        if value > self.slider_left {
2272            self.slider_right = value.min(10000.0);
2273            if self.slider_clamping == widgets::SliderClamping::Always {
2274                self.slider = self.slider.clamp(self.slider_left, self.slider_right);
2275            }
2276        }
2277    }
2278
2279    fn clamp_slider_to_range(&mut self) {
2280        self.set_slider_value(self.slider.clamp(self.slider_left, self.slider_right));
2281    }
2282
2283    fn slider_step(&self) -> f32 {
2284        self.slider_step_value.abs().max(0.0001)
2285    }
2286
2287    fn refresh_diagnostics_snapshot(&mut self) {
2288        self.diagnostics_snapshot = diagnostics_sample_snapshot(self);
2289    }
2290
2291    fn view(&self, viewport: UiSize) -> UiDocument {
2292        let mut ui = UiDocument::with_capacity(
2293            root_style(viewport.width, viewport.height),
2294            SHOWCASE_DOCUMENT_NODE_CAPACITY,
2295        );
2296        ui.node_mut(ui.root())
2297            .set_visual(UiVisual::panel(color(16, 20, 26), None, 0.0));
2298
2299        let root = ui.root();
2300        let shell = ui.add_child(
2301            root,
2302            UiNode::container(
2303                "showcase.shell",
2304                LayoutStyle::row().with_size(viewport.width, viewport.height),
2305            ),
2306        );
2307        let desktop_size = desktop_size_for_viewport(viewport);
2308        let desktop_width = desktop_size.width;
2309        let desktop = ui.add_child(
2310            shell,
2311            UiNode::container(
2312                "showcase.desktop",
2313                LayoutStyle::new()
2314                    .with_width(desktop_width)
2315                    .with_height(viewport.height)
2316                    .with_flex_shrink(1.0),
2317            )
2318            .with_visual(UiVisual::panel(color(15, 19, 25), None, 0.0)),
2319        );
2320        let controls = ui.add_child(
2321            shell,
2322            UiNode::container(
2323                "showcase.controls",
2324                LayoutStyle::column()
2325                    .with_width(RIGHT_PANEL_WIDTH)
2326                    .with_height(viewport.height)
2327                    .with_flex_shrink(0.0)
2328                    .padding(12.0)
2329                    .gap(4.0),
2330            )
2331            .with_visual(UiVisual::panel(
2332                color(21, 26, 33),
2333                Some(StrokeStyle::new(color(46, 56, 70), 1.0)),
2334                0.0,
2335            )),
2336        );
2337
2338        showcase_windows(&mut ui, desktop, self, desktop_size);
2339        organize_windows_button(&mut ui, desktop);
2340        fps_counter(&mut ui, desktop, self, viewport.height);
2341        control_panel(&mut ui, controls, self, viewport.height);
2342
2343        ui
2344    }
2345}
2346
2347fn organize_windows_button(ui: &mut UiDocument, desktop: UiNodeId) {
2348    let mut options =
2349        widgets::ButtonOptions::new(operad::layout::absolute(12.0, 12.0, 104.0, 28.0))
2350            .with_action("window.organize_open")
2351            .with_accessibility_label("Organize open windows");
2352    options.visual = UiVisual::panel(
2353        ColorRgba::new(20, 26, 34, 230),
2354        Some(StrokeStyle::new(color(76, 88, 106), 1.0)),
2355        4.0,
2356    );
2357    options.hovered_visual = Some(UiVisual::panel(
2358        color(45, 56, 70),
2359        Some(StrokeStyle::new(color(118, 144, 174), 1.0)),
2360        4.0,
2361    ));
2362    options.pressed_visual = Some(UiVisual::panel(
2363        color(18, 24, 32),
2364        Some(StrokeStyle::new(color(82, 104, 132), 1.0)),
2365        4.0,
2366    ));
2367    options.pressed_hovered_visual = Some(UiVisual::panel(
2368        color(36, 48, 62),
2369        Some(StrokeStyle::new(color(138, 170, 206), 1.0)),
2370        4.0,
2371    ));
2372    options.text_style = text(12.0, color(230, 236, 246));
2373    let button = widgets::button(
2374        ui,
2375        desktop,
2376        "showcase.organize_windows",
2377        "Organize",
2378        options,
2379    );
2380    ui.node_mut(button)
2381        .style_mut()
2382        .set_z_index(SHOWCASE_WINDOW_Z_MAX.saturating_add(20));
2383}
2384
2385fn fps_counter(
2386    ui: &mut UiDocument,
2387    desktop: UiNodeId,
2388    state: &ShowcaseState,
2389    viewport_height: f32,
2390) {
2391    let mut counter_style = UiNodeStyle::from(operad::layout::absolute(
2392        12.0,
2393        (viewport_height - 34.0).max(12.0),
2394        92.0,
2395        24.0,
2396    ));
2397    counter_style.set_z_index(SHOWCASE_WINDOW_Z_MAX.saturating_add(16));
2398    let counter = ui.add_child(
2399        desktop,
2400        UiNode::container("showcase.fps", counter_style)
2401            .with_visual(UiVisual::panel(
2402                ColorRgba::new(11, 15, 21, 210),
2403                Some(StrokeStyle::new(color(56, 68, 84), 1.0)),
2404                4.0,
2405            ))
2406            .with_accessibility(
2407                AccessibilityMeta::new(AccessibilityRole::Label).label("FPS counter"),
2408            ),
2409    );
2410    let fps = if state.fps > 0.0 {
2411        format!("{:.0} FPS", state.fps)
2412    } else {
2413        "-- FPS".to_string()
2414    };
2415    widgets::label(
2416        ui,
2417        counter,
2418        "showcase.fps.label",
2419        fps,
2420        text(11.0, color(198, 211, 230)),
2421        LayoutStyle::new()
2422            .with_width_percent(1.0)
2423            .with_height_percent(1.0)
2424            .padding(5.0),
2425    );
2426}
2427
2428fn showcase_windows(
2429    ui: &mut UiDocument,
2430    desktop: UiNodeId,
2431    state: &ShowcaseState,
2432    desktop_size: UiSize,
2433) {
2434    let windows = showcase_window_descriptors(state, desktop_size);
2435    let options = showcase_desktop_options(desktop_size);
2436    ext_widgets::floating_desktop(
2437        ui,
2438        desktop,
2439        "showcase.windows",
2440        &windows,
2441        options,
2442        |ui, window, descriptor| match descriptor.id.as_str() {
2443            "labels" => labels(ui, window, state),
2444            "buttons" => buttons(ui, window, state),
2445            "checkbox" => checkbox(ui, window, state),
2446            "toggles" => toggles(ui, window, state),
2447            "slider" => slider(ui, window, state),
2448            "numeric" => numeric_inputs(ui, window, state),
2449            "text_input" => text_input(ui, window, state),
2450            "selection" => selection_widgets(ui, window, state),
2451            "menus" => menu_widgets(ui, window, state),
2452            "command_palette" => command_palette(ui, window, state),
2453            "date_picker" => date_picker(ui, window, state),
2454            "color_picker" => color_picker(ui, window, state),
2455            "color_buttons" => color_buttons(ui, window, state),
2456            "progress" => progress_indicator(ui, window, state),
2457            "animation" => animation_widgets(ui, window, state),
2458            "lists_tables" => list_and_table_widgets(ui, window, state),
2459            "property_inspector" => property_inspector(ui, window, state),
2460            "diagnostics" => diagnostics_widgets(ui, window, state),
2461            "trees" => tree_widgets(ui, window, state),
2462            "layout_widgets" => tab_split_dock_widgets(ui, window, state),
2463            "containers" => container_widgets(ui, window, state),
2464            "forms" => form_widgets(ui, window, state),
2465            "overlays" => overlay_widgets(ui, window, state),
2466            "drag_drop" => drag_drop_widgets(ui, window, state),
2467            "media" => media_widgets(ui, window),
2468            "timeline" => timeline_ruler(ui, window),
2469            "toasts" => toast_controls(ui, window, state),
2470            "popup_panel" => popup_controls(ui, window, state),
2471            "canvas" => canvas(ui, window, state),
2472            "styling" => styling_widgets(ui, window, state),
2473            _ => {}
2474        },
2475    );
2476    showcase_overlays(ui, desktop, state, desktop_size);
2477}
2478
2479#[allow(clippy::field_reassign_with_default)]
2480fn showcase_overlays(
2481    ui: &mut UiDocument,
2482    desktop: UiNodeId,
2483    state: &ShowcaseState,
2484    desktop_size: UiSize,
2485) {
2486    if state.toast_visible {
2487        let overlay_width = 320.0;
2488        let mut overlay_style = UiNodeStyle::from(operad::layout::absolute(
2489            (desktop_size.width - overlay_width - 18.0).max(18.0),
2490            18.0,
2491            overlay_width,
2492            180.0,
2493        ));
2494        overlay_style.set_clip(ClipBehavior::None);
2495        overlay_style.set_z_index(6000);
2496        let overlay = ui.add_child(
2497            desktop,
2498            UiNode::container("showcase.toast_overlay", overlay_style),
2499        );
2500        let mut stack = ext_widgets::ToastStack::new(3);
2501        stack.push_toast(
2502            ext_widgets::Toast::new(
2503                ext_widgets::ToastId::new(1),
2504                ext_widgets::ToastSeverity::Success,
2505                "Saved",
2506                Some("All changes are written".to_string()),
2507                None,
2508            )
2509            .with_action(ext_widgets::ToastAction::new("undo", "Undo")),
2510        );
2511        stack.push(
2512            ext_widgets::ToastSeverity::Warning,
2513            "Autosave paused",
2514            Some("Changes are kept locally".to_string()),
2515            None,
2516        );
2517        let mut options = ext_widgets::ToastStackOptions::default();
2518        options.z_index = 6100;
2519        ext_widgets::toast_stack(ui, overlay, "showcase.toast_overlay.stack", &stack, options);
2520    }
2521
2522    if state.popup_open {
2523        let popup_width = 280.0;
2524        let popup_height = 110.0;
2525        let popup = ext_widgets::popup_panel(
2526            ui,
2527            desktop,
2528            "showcase.popup_overlay",
2529            UiRect::new(
2530                (desktop_size.width - popup_width - 36.0).max(18.0),
2531                220.0_f32.min((desktop_size.height - popup_height - 18.0).max(18.0)),
2532                popup_width,
2533                popup_height,
2534            ),
2535            ext_widgets::PopupOptions {
2536                z_index: 6100,
2537                accessibility: Some(
2538                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup panel"),
2539                ),
2540                ..Default::default()
2541            },
2542        );
2543        let body = ui.add_child(
2544            popup,
2545            UiNode::container(
2546                "showcase.popup_overlay.body",
2547                LayoutStyle::column()
2548                    .with_width_percent(1.0)
2549                    .with_height_percent(1.0)
2550                    .padding(12.0)
2551                    .gap(8.0),
2552            ),
2553        );
2554        let header = row(ui, body, "showcase.popup_overlay.header", 8.0);
2555        widgets::label(
2556            ui,
2557            header,
2558            "showcase.popup_overlay.title",
2559            "Popup panel",
2560            text(13.0, color(240, 244, 250)),
2561            LayoutStyle::new().with_width_percent(1.0),
2562        );
2563        let mut close =
2564            widgets::ButtonOptions::new(LayoutStyle::size(28.0, 24.0)).with_action("popup.close");
2565        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
2566        close.hovered_visual = Some(button_visual(54, 70, 92));
2567        close.text_style = text(13.0, color(220, 228, 238));
2568        widgets::button(ui, header, "showcase.popup_overlay.close", "x", close);
2569        widgets::label(
2570            ui,
2571            body,
2572            "showcase.popup_overlay.body_text",
2573            "This surface is rendered as an overlay.",
2574            text(12.0, color(196, 210, 230)),
2575            LayoutStyle::new().with_width_percent(1.0),
2576        );
2577    }
2578}
2579
2580fn showcase_window_descriptors(
2581    state: &ShowcaseState,
2582    desktop_size: UiSize,
2583) -> Vec<ext_widgets::FloatingWindowDescriptor> {
2584    let wide = (desktop_size.width - 36.0).clamp(320.0, 720.0);
2585    let medium = (desktop_size.width - 36.0).clamp(300.0, 604.0);
2586    let buttons_width = medium.min(620.0);
2587    let mut windows = Vec::new();
2588    push_window(
2589        &mut windows,
2590        state.windows.labels,
2591        "labels",
2592        "Labels",
2593        UiSize::new(380.0, 460.0),
2594    );
2595    push_window(
2596        &mut windows,
2597        state.windows.buttons,
2598        "buttons",
2599        "Buttons",
2600        UiSize::new(buttons_width, 220.0),
2601    );
2602    push_window(
2603        &mut windows,
2604        state.windows.checkbox,
2605        "checkbox",
2606        "Checkbox",
2607        UiSize::new(250.0, 72.0),
2608    );
2609    push_window(
2610        &mut windows,
2611        state.windows.toggles,
2612        "toggles",
2613        "Radio and toggles",
2614        UiSize::new(360.0, 320.0),
2615    );
2616    push_window(
2617        &mut windows,
2618        state.windows.slider,
2619        "slider",
2620        "Slider",
2621        UiSize::new(430.0, 560.0),
2622    );
2623    push_window(
2624        &mut windows,
2625        state.windows.numeric,
2626        "numeric",
2627        "Numeric input",
2628        UiSize::new(360.0, 180.0),
2629    );
2630    push_window(
2631        &mut windows,
2632        state.windows.text_input,
2633        "text_input",
2634        "Text input",
2635        UiSize::new(520.0, 560.0),
2636    );
2637    push_window(
2638        &mut windows,
2639        state.windows.selection,
2640        "selection",
2641        "Select controls",
2642        UiSize::new(360.0, 360.0),
2643    );
2644    push_window(
2645        &mut windows,
2646        state.windows.menus,
2647        "menus",
2648        "Menus",
2649        UiSize::new(wide, 520.0),
2650    );
2651    push_window(
2652        &mut windows,
2653        state.windows.command_palette,
2654        "command_palette",
2655        "Command palette",
2656        UiSize::new(520.0, 320.0),
2657    );
2658    push_window(
2659        &mut windows,
2660        state.windows.date_picker,
2661        "date_picker",
2662        "Date picker",
2663        UiSize::new(430.0, 390.0),
2664    );
2665    push_window(
2666        &mut windows,
2667        state.windows.color_picker,
2668        "color_picker",
2669        "Color picker",
2670        UiSize::new(340.0, 390.0),
2671    );
2672    push_window(
2673        &mut windows,
2674        state.windows.color_buttons,
2675        "color_buttons",
2676        "Color buttons",
2677        UiSize::new(430.0, 360.0),
2678    );
2679    push_window(
2680        &mut windows,
2681        state.windows.progress,
2682        "progress",
2683        "Progress indicator",
2684        UiSize::new(500.0, 168.0),
2685    );
2686    push_window(
2687        &mut windows,
2688        state.windows.animation,
2689        "animation",
2690        "Animation",
2691        UiSize::new(520.0, 430.0),
2692    );
2693    push_window(
2694        &mut windows,
2695        state.windows.lists_tables,
2696        "lists_tables",
2697        "Lists and tables",
2698        UiSize::new(wide, 620.0),
2699    );
2700    push_window(
2701        &mut windows,
2702        state.windows.property_inspector,
2703        "property_inspector",
2704        "Property inspector",
2705        UiSize::new(330.0, 250.0),
2706    );
2707    push_window(
2708        &mut windows,
2709        state.windows.diagnostics,
2710        "diagnostics",
2711        "Diagnostics",
2712        UiSize::new(640.0, 760.0),
2713    );
2714    push_window(
2715        &mut windows,
2716        state.windows.trees,
2717        "trees",
2718        "Trees",
2719        UiSize::new(430.0, 390.0),
2720    );
2721    push_window(
2722        &mut windows,
2723        state.windows.layout_widgets,
2724        "layout_widgets",
2725        "Layout widgets",
2726        UiSize::new(wide.min(560.0), 400.0),
2727    );
2728    push_window(
2729        &mut windows,
2730        state.windows.containers,
2731        "containers",
2732        "Containers",
2733        UiSize::new(560.0, 640.0),
2734    );
2735    push_window(
2736        &mut windows,
2737        state.windows.forms,
2738        "forms",
2739        "Forms",
2740        UiSize::new(520.0, 620.0),
2741    );
2742    push_window(
2743        &mut windows,
2744        state.windows.overlays,
2745        "overlays",
2746        "Overlays",
2747        UiSize::new(560.0, 560.0),
2748    );
2749    push_window(
2750        &mut windows,
2751        state.windows.drag_drop,
2752        "drag_drop",
2753        "Drag and drop",
2754        UiSize::new(500.0, 460.0),
2755    );
2756    push_window(
2757        &mut windows,
2758        state.windows.media,
2759        "media",
2760        "Media",
2761        UiSize::new(520.0, 430.0),
2762    );
2763    push_window(
2764        &mut windows,
2765        state.windows.timeline,
2766        "timeline",
2767        "Timeline",
2768        UiSize::new(600.0, 120.0),
2769    );
2770    push_window(
2771        &mut windows,
2772        state.windows.toasts,
2773        "toasts",
2774        "Toasts",
2775        UiSize::new(320.0, 270.0),
2776    );
2777    push_window(
2778        &mut windows,
2779        state.windows.popup_panel,
2780        "popup_panel",
2781        "Popup panel",
2782        UiSize::new(360.0, 200.0),
2783    );
2784    push_window(
2785        &mut windows,
2786        state.windows.canvas,
2787        "canvas",
2788        "Canvas",
2789        UiSize::new(560.0, 390.0),
2790    );
2791    push_window(
2792        &mut windows,
2793        state.windows.styling,
2794        "styling",
2795        "Styling",
2796        UiSize::new(540.0, 440.0),
2797    );
2798    for window in &mut windows {
2799        window.drag_action = Some(WidgetActionBinding::action(format!(
2800            "window.drag.{}",
2801            window.id
2802        )));
2803        window.collapse_action = Some(WidgetActionBinding::action(format!(
2804            "window.collapse.{}",
2805            window.id
2806        )));
2807        window.resize_action = Some(WidgetActionBinding::action(format!(
2808            "window.resize.{}",
2809            window.id
2810        )));
2811        state
2812            .desktop
2813            .apply_to_descriptor(window, window_defaults(window.id.as_str()));
2814    }
2815    windows
2816}
2817
2818fn push_window(
2819    windows: &mut Vec<ext_widgets::FloatingWindowDescriptor>,
2820    visible: bool,
2821    id: &'static str,
2822    title: &'static str,
2823    preferred_size: UiSize,
2824) {
2825    if visible {
2826        let mut window = ext_widgets::FloatingWindowDescriptor::new(id, title, preferred_size)
2827            .with_min_size(default_window_state_min_size(id))
2828            .with_auto_size_to_content(false)
2829            .with_activate_action(format!("window.activate.{id}"))
2830            .with_close_action(format!("window.close.{id}"));
2831        if id == "animation" {
2832            window = window.with_content_min_size(UiSize::new(
2833                ANIMATION_STAGE_MIN_WIDTH,
2834                ANIMATION_STAGE_HEIGHT * 4.0,
2835            ));
2836        } else if id == "layout_widgets" {
2837            window = window.with_content_min_size(UiSize::new(620.0, 360.0));
2838        }
2839        windows.push(window);
2840    }
2841}
2842
2843fn default_window_size(id: &str) -> UiSize {
2844    match id {
2845        "labels" => UiSize::new(380.0, 460.0),
2846        "buttons" => UiSize::new(604.0, 220.0),
2847        "checkbox" => UiSize::new(250.0, 72.0),
2848        "toggles" => UiSize::new(360.0, 380.0),
2849        "slider" => UiSize::new(430.0, 560.0),
2850        "numeric" => UiSize::new(430.0, 180.0),
2851        "text_input" => UiSize::new(520.0, 640.0),
2852        "selection" => UiSize::new(360.0, 360.0),
2853        "menus" => UiSize::new(640.0, 640.0),
2854        "command_palette" => UiSize::new(520.0, 320.0),
2855        "date_picker" => UiSize::new(284.0, 390.0),
2856        "color_picker" => UiSize::new(340.0, 390.0),
2857        "color_buttons" => UiSize::new(430.0, 360.0),
2858        "progress" => UiSize::new(500.0, 168.0),
2859        "animation" => UiSize::new(520.0, 430.0),
2860        "lists_tables" => UiSize::new(600.0, 700.0),
2861        "property_inspector" => UiSize::new(330.0, 250.0),
2862        "diagnostics" => UiSize::new(640.0, 760.0),
2863        "trees" => UiSize::new(430.0, 450.0),
2864        "layout_widgets" => UiSize::new(560.0, 400.0),
2865        "containers" => UiSize::new(560.0, 640.0),
2866        "forms" => UiSize::new(520.0, 620.0),
2867        "overlays" => UiSize::new(560.0, 560.0),
2868        "drag_drop" => UiSize::new(500.0, 460.0),
2869        "media" => UiSize::new(520.0, 430.0),
2870        "timeline" => UiSize::new(600.0, 120.0),
2871        "toasts" => UiSize::new(320.0, 270.0),
2872        "popup_panel" => UiSize::new(360.0, 200.0),
2873        "canvas" => UiSize::new(560.0, 390.0),
2874        "styling" => UiSize::new(640.0, 560.0),
2875        _ => UiSize::new(300.0, 180.0),
2876    }
2877}
2878
2879fn default_window_state_min_size(_id: &str) -> UiSize {
2880    UiSize::new(160.0, 96.0)
2881}
2882
2883fn showcase_window_title(id: &str) -> &'static str {
2884    match id {
2885        "labels" => "Labels",
2886        "buttons" => "Buttons",
2887        "checkbox" => "Checkbox",
2888        "toggles" => "Radio and toggles",
2889        "slider" => "Slider",
2890        "numeric" => "Numeric input",
2891        "text_input" => "Text input",
2892        "selection" => "Select controls",
2893        "menus" => "Menus",
2894        "command_palette" => "Command palette",
2895        "date_picker" => "Date picker",
2896        "color_picker" => "Color picker",
2897        "color_buttons" => "Color buttons",
2898        "progress" => "Progress indicator",
2899        "animation" => "Animation",
2900        "lists_tables" => "Lists and tables",
2901        "property_inspector" => "Property inspector",
2902        "diagnostics" => "Diagnostics",
2903        "trees" => "Trees",
2904        "layout_widgets" => "Layout widgets",
2905        "containers" => "Containers",
2906        "forms" => "Forms",
2907        "overlays" => "Overlays",
2908        "drag_drop" => "Drag and drop",
2909        "media" => "Media",
2910        "timeline" => "Timeline",
2911        "toasts" => "Toasts",
2912        "popup_panel" => "Popup panel",
2913        "canvas" => "Canvas",
2914        "styling" => "Styling",
2915        _ => "Window",
2916    }
2917}
2918
2919fn showcase_collapsed_window_size(
2920    id: &str,
2921    options: &ext_widgets::FloatingDesktopOptions,
2922) -> UiSize {
2923    let min_size = default_window_state_min_size(id);
2924    let padding = options.content_padding.max(0.0);
2925    let button = options.close_button_size.max(1.0);
2926    let control_width = (button + 8.0) * 2.0;
2927    let font_size = options.title_style.font_size.max(1.0);
2928    let title_width =
2929        (showcase_window_title(id).chars().count() as f32 * font_size * 0.55).max(font_size);
2930    UiSize::new(
2931        min_size
2932            .width
2933            .max(padding * 2.0 + control_width + title_width),
2934        options.title_bar_height.max(1.0),
2935    )
2936}
2937
2938fn default_window_position(id: &str) -> UiPoint {
2939    match id {
2940        "labels" => UiPoint::new(18.0, 18.0),
2941        "buttons" => UiPoint::new(420.0, 18.0),
2942        "checkbox" => UiPoint::new(360.0, 18.0),
2943        "toggles" => UiPoint::new(360.0, 110.0),
2944        "slider" => UiPoint::new(360.0, 110.0),
2945        "numeric" => UiPoint::new(360.0, 260.0),
2946        "text_input" => UiPoint::new(360.0, 18.0),
2947        "selection" => UiPoint::new(360.0, 404.0),
2948        "menus" => UiPoint::new(18.0, 18.0),
2949        "command_palette" => UiPoint::new(68.0, 88.0),
2950        "date_picker" => UiPoint::new(300.0, 170.0),
2951        "color_picker" => UiPoint::new(18.0, 560.0),
2952        "color_buttons" => UiPoint::new(380.0, 500.0),
2953        "progress" => UiPoint::new(72.0, 540.0),
2954        "animation" => UiPoint::new(180.0, 170.0),
2955        "lists_tables" => UiPoint::new(18.0, 90.0),
2956        "property_inspector" => UiPoint::new(300.0, 420.0),
2957        "diagnostics" => UiPoint::new(640.0, 70.0),
2958        "trees" => UiPoint::new(36.0, 220.0),
2959        "layout_widgets" => UiPoint::new(18.0, 18.0),
2960        "containers" => UiPoint::new(48.0, 120.0),
2961        "forms" => UiPoint::new(120.0, 160.0),
2962        "overlays" => UiPoint::new(80.0, 110.0),
2963        "drag_drop" => UiPoint::new(210.0, 250.0),
2964        "media" => UiPoint::new(120.0, 360.0),
2965        "timeline" => UiPoint::new(18.0, 620.0),
2966        "toasts" => UiPoint::new(320.0, 70.0),
2967        "popup_panel" => UiPoint::new(320.0, 370.0),
2968        "canvas" => UiPoint::new(280.0, 390.0),
2969        "styling" => UiPoint::new(86.0, 118.0),
2970        _ => UiPoint::new(18.0, 18.0),
2971    }
2972}
2973
2974fn window_for_action(action_id: &str) -> Option<&'static str> {
2975    match action_id {
2976        id if id.starts_with("labels.") => Some("labels"),
2977        id if id.starts_with("button.") => Some("buttons"),
2978        id if id.starts_with("checkbox.") => Some("checkbox"),
2979        id if id.starts_with("toggles.") => Some("toggles"),
2980        id if id.starts_with("theme.preference.") => Some("toggles"),
2981        id if id.starts_with("slider.") => Some("slider"),
2982        id if id.starts_with("numeric.") => Some("numeric"),
2983        id if id.starts_with("text.") => Some("text_input"),
2984        id if id.starts_with("combo.")
2985            || id.starts_with("selection.dropdown.")
2986            || id.starts_with("selection.menu.") =>
2987        {
2988            Some("selection")
2989        }
2990        id if id.starts_with("menus.") => Some("menus"),
2991        id if id.starts_with("command_palette.") => Some("command_palette"),
2992        id if id.starts_with("date.") => Some("date_picker"),
2993        id if id.starts_with("color.") => Some("color_picker"),
2994        id if id.starts_with("color_buttons.") => Some("color_buttons"),
2995        id if id.starts_with("progress.") => Some("progress"),
2996        id if id.starts_with("animation.") => Some("animation"),
2997        id if id.starts_with("lists_tables.") => Some("lists_tables"),
2998        id if id.starts_with("property_inspector.") => Some("property_inspector"),
2999        id if id.starts_with("diagnostics.") => Some("diagnostics"),
3000        id if id.starts_with("trees.") => Some("trees"),
3001        id if id.starts_with("layout.") || id.starts_with("layout_widgets.") => {
3002            Some("layout_widgets")
3003        }
3004        id if id.starts_with("containers.") => Some("containers"),
3005        id if id.starts_with("forms.") => Some("forms"),
3006        id if id.starts_with("overlays.") => Some("overlays"),
3007        id if id.starts_with("drag_drop.") => Some("drag_drop"),
3008        id if id.starts_with("media.") => Some("media"),
3009        id if id.starts_with("toast.") => Some("toasts"),
3010        id if id.starts_with("popup.") => Some("popup_panel"),
3011        id if id.starts_with("canvas.") => Some("canvas"),
3012        id if id.starts_with("styling.") => Some("styling"),
3013        _ => None,
3014    }
3015}
3016
3017fn focused_text_for_action(action_id: &str) -> Option<FocusedTextInput> {
3018    Some(match action_id {
3019        "text.input.edit" => FocusedTextInput::Editable,
3020        "text.selectable.edit" => FocusedTextInput::Selectable,
3021        "text.singleline.edit" => FocusedTextInput::Singleline,
3022        "text.multiline.edit" => FocusedTextInput::Multiline,
3023        "text.area.edit" => FocusedTextInput::TextArea,
3024        "text.code_editor.edit" => FocusedTextInput::CodeEditor,
3025        "text.search.edit" => FocusedTextInput::Search,
3026        "text.password.edit" => FocusedTextInput::Password,
3027        "forms.profile.name.input.edit" => FocusedTextInput::FormName,
3028        "forms.profile.email.input.edit" => FocusedTextInput::FormEmail,
3029        "forms.profile.role.input.edit" => FocusedTextInput::FormRole,
3030        "slider.value_text.edit" => FocusedTextInput::SliderValue,
3031        "slider.left_text.edit" => FocusedTextInput::SliderRangeLeft,
3032        "slider.right_text.edit" => FocusedTextInput::SliderRangeRight,
3033        "slider.step_text.edit" => FocusedTextInput::SliderStep,
3034        _ => return None,
3035    })
3036}
3037
3038fn control_panel(
3039    ui: &mut UiDocument,
3040    parent: UiNodeId,
3041    state: &ShowcaseState,
3042    viewport_height: f32,
3043) {
3044    widgets::label(
3045        ui,
3046        parent,
3047        "controls.title",
3048        "Widgets",
3049        text(16.0, color(244, 248, 252)),
3050        LayoutStyle::new().with_width_percent(1.0),
3051    );
3052    let list_viewport_height = controls_list_viewport_height(viewport_height);
3053    let controls_scroll =
3054        controls_scroll_state_for_view(state.controls_scroll, list_viewport_height);
3055    let list_nodes = scroll_area_widgets::scroll_container_shell(
3056        ui,
3057        parent,
3058        "controls.widget_list",
3059        controls_scroll,
3060        widgets::ScrollContainerOptions::default()
3061            .with_layout(
3062                LayoutStyle::column()
3063                    .with_width_percent(1.0)
3064                    .with_height(list_viewport_height)
3065                    .with_flex_grow(1.0)
3066                    .with_flex_shrink(1.0),
3067            )
3068            .with_viewport_layout(
3069                LayoutStyle::column()
3070                    .with_width(0.0)
3071                    .with_height_percent(1.0)
3072                    .with_flex_grow(1.0)
3073                    .with_flex_shrink(1.0)
3074                    .gap(CONTROLS_WIDGET_ROW_GAP),
3075            )
3076            .with_axes(ScrollAxes::VERTICAL)
3077            .with_scrollbar_thickness(8.0)
3078            .with_gap(2.0)
3079            .with_action_prefix("controls.widget_list")
3080            .with_vertical_scrollbar(
3081                scrollbar_widgets::ScrollbarOptions::default()
3082                    .with_action("controls.widget_list.scrollbar"),
3083            ),
3084    );
3085    let list = list_nodes.viewport;
3086
3087    window_toggle(ui, list, "labels", "Labels", state.windows.labels);
3088    window_toggle(ui, list, "buttons", "Buttons", state.windows.buttons);
3089    window_toggle(ui, list, "checkbox", "Checkbox", state.windows.checkbox);
3090    window_toggle(
3091        ui,
3092        list,
3093        "toggles",
3094        "Radio and toggles",
3095        state.windows.toggles,
3096    );
3097    window_toggle(ui, list, "slider", "Slider", state.windows.slider);
3098    window_toggle(ui, list, "numeric", "Numeric input", state.windows.numeric);
3099    window_toggle(
3100        ui,
3101        list,
3102        "text_input",
3103        "Text input",
3104        state.windows.text_input,
3105    );
3106    window_toggle(
3107        ui,
3108        list,
3109        "selection",
3110        "Select controls",
3111        state.windows.selection,
3112    );
3113    window_toggle(ui, list, "menus", "Menus", state.windows.menus);
3114    window_toggle(
3115        ui,
3116        list,
3117        "command_palette",
3118        "Command palette",
3119        state.windows.command_palette,
3120    );
3121    window_toggle(
3122        ui,
3123        list,
3124        "date_picker",
3125        "Date picker",
3126        state.windows.date_picker,
3127    );
3128    window_toggle(
3129        ui,
3130        list,
3131        "color_picker",
3132        "Color picker",
3133        state.windows.color_picker,
3134    );
3135    window_toggle(
3136        ui,
3137        list,
3138        "color_buttons",
3139        "Color buttons",
3140        state.windows.color_buttons,
3141    );
3142    window_toggle(
3143        ui,
3144        list,
3145        "progress",
3146        "Progress indicator",
3147        state.windows.progress,
3148    );
3149    window_toggle(ui, list, "animation", "Animation", state.windows.animation);
3150    window_toggle(
3151        ui,
3152        list,
3153        "lists_tables",
3154        "Lists and tables",
3155        state.windows.lists_tables,
3156    );
3157    window_toggle(
3158        ui,
3159        list,
3160        "property_inspector",
3161        "Property inspector",
3162        state.windows.property_inspector,
3163    );
3164    window_toggle(
3165        ui,
3166        list,
3167        "diagnostics",
3168        "Diagnostics",
3169        state.windows.diagnostics,
3170    );
3171    window_toggle(ui, list, "trees", "Trees", state.windows.trees);
3172    window_toggle(
3173        ui,
3174        list,
3175        "layout_widgets",
3176        "Layout widgets",
3177        state.windows.layout_widgets,
3178    );
3179    window_toggle(
3180        ui,
3181        list,
3182        "containers",
3183        "Containers",
3184        state.windows.containers,
3185    );
3186    window_toggle(ui, list, "forms", "Forms", state.windows.forms);
3187    window_toggle(ui, list, "overlays", "Overlays", state.windows.overlays);
3188    window_toggle(
3189        ui,
3190        list,
3191        "drag_drop",
3192        "Drag and drop",
3193        state.windows.drag_drop,
3194    );
3195    window_toggle(ui, list, "media", "Media", state.windows.media);
3196    window_toggle(ui, list, "timeline", "Timeline", state.windows.timeline);
3197    window_toggle(ui, list, "toasts", "Toasts", state.windows.toasts);
3198    window_toggle(
3199        ui,
3200        list,
3201        "popup_panel",
3202        "Popup panel",
3203        state.windows.popup_panel,
3204    );
3205    window_toggle(ui, list, "canvas", "Canvas", state.windows.canvas);
3206    window_toggle(ui, list, "styling", "Styling", state.windows.styling);
3207
3208    ui.add_child(
3209        parent,
3210        UiNode::container(
3211            "controls.clear_all.spacer",
3212            LayoutStyle::new()
3213                .with_width_percent(1.0)
3214                .with_height(1.0)
3215                .with_flex_grow(1.0)
3216                .with_flex_shrink(1.0),
3217        ),
3218    );
3219    let actions = ui.add_child(
3220        parent,
3221        UiNode::container(
3222            "controls.bulk_actions",
3223            LayoutStyle::row()
3224                .with_width_percent(1.0)
3225                .with_height(30.0)
3226                .with_flex_shrink(0.0)
3227                .gap(8.0),
3228        ),
3229    );
3230    control_action_button(
3231        ui,
3232        actions,
3233        "controls.add_all",
3234        "Add all",
3235        "window.add_all",
3236        "Add all widgets",
3237    );
3238    control_action_button(
3239        ui,
3240        actions,
3241        "controls.clear_all",
3242        "Clear all",
3243        "window.clear_all",
3244        "Clear all widgets",
3245    );
3246}
3247
3248fn control_action_button(
3249    ui: &mut UiDocument,
3250    parent: UiNodeId,
3251    name: &'static str,
3252    label: &'static str,
3253    action: &'static str,
3254    accessibility_label: &'static str,
3255) {
3256    let mut options = widgets::ButtonOptions::new(
3257        LayoutStyle::new()
3258            .with_width(0.0)
3259            .with_height_percent(1.0)
3260            .with_flex_grow(1.0)
3261            .with_flex_shrink(1.0),
3262    )
3263    .with_action(action);
3264    options.visual = UiVisual::panel(
3265        color(31, 38, 48),
3266        Some(StrokeStyle::new(color(76, 88, 106), 1.0)),
3267        4.0,
3268    );
3269    options.hovered_visual = Some(UiVisual::panel(
3270        color(45, 56, 70),
3271        Some(StrokeStyle::new(color(118, 144, 174), 1.0)),
3272        4.0,
3273    ));
3274    options.pressed_visual = Some(UiVisual::panel(
3275        color(20, 27, 36),
3276        Some(StrokeStyle::new(color(82, 104, 132), 1.0)),
3277        4.0,
3278    ));
3279    options.pressed_hovered_visual = Some(UiVisual::panel(
3280        color(36, 48, 62),
3281        Some(StrokeStyle::new(color(138, 170, 206), 1.0)),
3282        4.0,
3283    ));
3284    options.text_style = text(12.0, color(230, 236, 246));
3285    options.accessibility_label = Some(accessibility_label.to_string());
3286    widgets::button(ui, parent, name, label, options);
3287}
3288
3289fn window_toggle(
3290    ui: &mut UiDocument,
3291    parent: UiNodeId,
3292    id: &'static str,
3293    label: &'static str,
3294    checked: bool,
3295) {
3296    let mut options =
3297        widgets::CheckboxOptions::default().with_action(format!("window.toggle.{id}"));
3298    options.layout = LayoutStyle::new()
3299        .with_width_percent(1.0)
3300        .with_height(CONTROLS_WIDGET_ROW_HEIGHT);
3301    options.text_style = text(12.0, color(220, 228, 238));
3302    widgets::checkbox(
3303        ui,
3304        parent,
3305        format!("controls.{id}"),
3306        label,
3307        checked,
3308        options,
3309    );
3310}
3311
3312#[allow(clippy::field_reassign_with_default)]
3313fn labels(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3314    let body = section(ui, parent, "labels", "Labels");
3315    ui.set_node_style(
3316        body,
3317        LayoutStyle::column()
3318            .with_width_percent(1.0)
3319            .with_height_percent(1.0)
3320            .with_flex_grow(1.0)
3321            .gap(10.0),
3322    );
3323    widgets::label(
3324        ui,
3325        body,
3326        "labels.plain",
3327        "Plain label",
3328        text(13.0, color(226, 232, 242)),
3329        LayoutStyle::new().with_width_percent(1.0),
3330    );
3331    let locale_items = label_locale_options();
3332    let locale_id = state
3333        .label_locale
3334        .selected_id(&locale_items)
3335        .unwrap_or("es-MX");
3336    let localization =
3337        LocalizationPolicy::new(LocaleId::new(locale_id).unwrap_or_else(|_| LocaleId::default()));
3338    let locale_row = ui.add_child(
3339        body,
3340        UiNode::container(
3341            "labels.locale.row",
3342            LayoutStyle::row()
3343                .with_width_percent(1.0)
3344                .with_align_items(taffy::prelude::AlignItems::Center)
3345                .gap(10.0),
3346        ),
3347    );
3348    let locale_label_width = 270.0;
3349    let locale_dropdown_width = 148.0;
3350    let locale_gap = 10.0;
3351    widgets::localized_label(
3352        ui,
3353        locale_row,
3354        "labels.localized",
3355        DynamicLabelMeta::keyed("showcase.localized.greeting", localized_label(locale_id)),
3356        Some(&localization),
3357        text(13.0, color(170, 202, 255)),
3358        LayoutStyle::new().with_width(locale_label_width),
3359    );
3360    let mut locale_options = ext_widgets::DropdownSelectOptions::default();
3361    locale_options.trigger_layout = LayoutStyle::row()
3362        .with_width(locale_dropdown_width)
3363        .with_height(30.0)
3364        .with_align_items(taffy::prelude::AlignItems::Center)
3365        .with_justify_content(taffy::prelude::JustifyContent::Center)
3366        .padding(6.0);
3367    locale_options.text_style = text(13.0, color(226, 232, 242));
3368    locale_options.accessibility_label = Some("Locale".to_string());
3369    locale_options.menu =
3370        ext_widgets::SelectMenuOptions::default().with_action_prefix("labels.locale");
3371    locale_options.menu.width = locale_dropdown_width;
3372    locale_options.menu.row_height = 30.0;
3373    locale_options.menu.max_visible_rows = locale_items.len();
3374    locale_options.menu.text_style = text(13.0, color(226, 232, 242));
3375    locale_options.menu.portal = UiPortalTarget::Parent;
3376    let locale_nodes = ext_widgets::dropdown_select(
3377        ui,
3378        locale_row,
3379        "labels.locale",
3380        &locale_items,
3381        &state.label_locale,
3382        Some(ext_widgets::AnchoredPopup::new(
3383            UiRect::new(
3384                locale_label_width + locale_gap,
3385                0.0,
3386                locale_dropdown_width,
3387                30.0,
3388            ),
3389            UiRect::new(0.0, 0.0, 460.0, 260.0),
3390            ext_widgets::PopupPlacement::default().with_viewport_margin(0.0),
3391        )),
3392        locale_options,
3393    );
3394    ui.node_mut(locale_nodes.trigger)
3395        .set_action("labels.locale.toggle");
3396    widgets::label(
3397        ui,
3398        body,
3399        "labels.muted",
3400        "Muted helper label",
3401        text(12.0, color(154, 166, 184)),
3402        LayoutStyle::new().with_width_percent(1.0),
3403    );
3404
3405    let sizes = ui.add_child(
3406        body,
3407        UiNode::container(
3408            "labels.sizes",
3409            LayoutStyle::row()
3410                .with_width_percent(1.0)
3411                .with_align_items(taffy::prelude::AlignItems::FlexEnd)
3412                .gap(12.0),
3413        ),
3414    );
3415    widgets::label(
3416        ui,
3417        sizes,
3418        "labels.size.small",
3419        "12px",
3420        text(12.0, color(226, 232, 242)),
3421        LayoutStyle::new(),
3422    );
3423    widgets::label(
3424        ui,
3425        sizes,
3426        "labels.size.default",
3427        "13px",
3428        text(13.0, color(226, 232, 242)),
3429        LayoutStyle::new(),
3430    );
3431    widgets::label(
3432        ui,
3433        sizes,
3434        "labels.size.large",
3435        "18px",
3436        text(18.0, color(246, 249, 252)),
3437        LayoutStyle::new(),
3438    );
3439    widgets::label(
3440        ui,
3441        sizes,
3442        "labels.size.display",
3443        "24px",
3444        text(24.0, color(246, 249, 252)),
3445        LayoutStyle::new(),
3446    );
3447
3448    let style_row = row(ui, body, "labels.styles", 12.0);
3449    let mut bold = text(13.0, color(246, 249, 252));
3450    bold.weight = FontWeight::BOLD;
3451    widgets::label(
3452        ui,
3453        style_row,
3454        "labels.style.bold",
3455        "Bold",
3456        bold,
3457        LayoutStyle::new(),
3458    );
3459    widgets::label(
3460        ui,
3461        style_row,
3462        "labels.style.weak",
3463        "Muted",
3464        text(13.0, color(154, 166, 184)),
3465        LayoutStyle::new(),
3466    );
3467
3468    let font_row = row(ui, body, "labels.fonts", 12.0);
3469    let mut serif = text(13.0, color(226, 232, 242));
3470    serif.family = FontFamily::Serif;
3471    widgets::label(
3472        ui,
3473        font_row,
3474        "labels.font.serif",
3475        "Serif",
3476        serif,
3477        LayoutStyle::new(),
3478    );
3479    let mut mono = text(13.0, color(226, 232, 242));
3480    mono.family = FontFamily::Monospace;
3481    widgets::label(
3482        ui,
3483        font_row,
3484        "labels.font.mono",
3485        "Monospace",
3486        mono,
3487        LayoutStyle::new(),
3488    );
3489
3490    let code_panel = ui.add_child(
3491        body,
3492        UiNode::container(
3493            "labels.code.panel",
3494            LayoutStyle::new()
3495                .with_width_percent(1.0)
3496                .padding(8.0)
3497                .with_height(36.0),
3498        )
3499        .with_visual(UiVisual::panel(
3500            color(10, 14, 20),
3501            Some(StrokeStyle::new(color(47, 59, 74), 1.0)),
3502            4.0,
3503        )),
3504    );
3505    widgets::code_label(
3506        ui,
3507        code_panel,
3508        "labels.code",
3509        "let label = widgets::label(...);",
3510        LayoutStyle::new().with_width_percent(1.0),
3511    );
3512
3513    let colors = row(ui, body, "labels.colors", 14.0);
3514    widgets::colored_label(
3515        ui,
3516        colors,
3517        "labels.color.green",
3518        "Green",
3519        color(111, 203, 159),
3520        LayoutStyle::new(),
3521    );
3522    widgets::colored_label(
3523        ui,
3524        colors,
3525        "labels.color.yellow",
3526        "Yellow",
3527        color(232, 196, 101),
3528        LayoutStyle::new(),
3529    );
3530    widgets::colored_label(
3531        ui,
3532        colors,
3533        "labels.color.red",
3534        "Red",
3535        color(244, 118, 118),
3536        LayoutStyle::new(),
3537    );
3538
3539    let wrap_row = wrapping_row(ui, body, "labels.wrap.row", 10.0);
3540    let wrap_word = ui.add_child(
3541        wrap_row,
3542        UiNode::container(
3543            "labels.wrap.word.panel",
3544            LayoutStyle::column().with_width(172.0).padding(8.0),
3545        )
3546        .with_visual(UiVisual::panel(
3547            color(18, 23, 31),
3548            Some(StrokeStyle::new(color(47, 59, 74), 1.0)),
3549            4.0,
3550        )),
3551    );
3552    widgets::wrapped_label(
3553        ui,
3554        wrap_word,
3555        "labels.wrap.word",
3556        "Word wrapping keeps this sentence readable in a narrow box.",
3557        TextWrap::Word,
3558        LayoutStyle::new().with_width_percent(1.0),
3559    );
3560    let wrap_glyph = ui.add_child(
3561        wrap_row,
3562        UiNode::container(
3563            "labels.wrap.glyph.panel",
3564            LayoutStyle::column().with_width(172.0).padding(8.0),
3565        )
3566        .with_visual(UiVisual::panel(
3567            color(18, 23, 31),
3568            Some(StrokeStyle::new(color(47, 59, 74), 1.0)),
3569            4.0,
3570        )),
3571    );
3572    widgets::wrapped_label(
3573        ui,
3574        wrap_glyph,
3575        "labels.wrap.glyph",
3576        "LongIdentifierWithoutSpaces",
3577        TextWrap::Glyph,
3578        LayoutStyle::new().with_width_percent(1.0),
3579    );
3580
3581    let links = wrapping_row(ui, body, "labels.links", 12.0);
3582    widgets::link(
3583        ui,
3584        links,
3585        "labels.link",
3586        "Internal action",
3587        widgets::LinkOptions::default()
3588            .visited(state.label_link_visited)
3589            .with_action("labels.link"),
3590    );
3591    widgets::hyperlink(
3592        ui,
3593        links,
3594        "labels.hyperlink",
3595        "Open docs.rs",
3596        "https://docs.rs/operad",
3597        widgets::LinkOptions::default()
3598            .visited(state.label_hyperlink_visited)
3599            .with_action("labels.hyperlink"),
3600    );
3601    if state.label_link_status != "No link action yet" {
3602        widgets::label(
3603            ui,
3604            body,
3605            "labels.status",
3606            format!("Last action: {}", state.label_link_status),
3607            text(12.0, color(154, 166, 184)),
3608            LayoutStyle::new().with_width_percent(1.0),
3609        );
3610    }
3611}
3612
3613fn buttons(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3614    let body = section(ui, parent, "buttons", "Buttons");
3615    let primary_row = wrapping_row(ui, body, "buttons.row", 10.0);
3616    button(
3617        ui,
3618        primary_row,
3619        "button.default",
3620        "Default",
3621        "button.default",
3622        button_visual(38, 46, 58),
3623    );
3624    button(
3625        ui,
3626        primary_row,
3627        "button.primary",
3628        "Primary",
3629        "button.primary",
3630        button_visual(48, 112, 184),
3631    );
3632    button(
3633        ui,
3634        primary_row,
3635        "button.secondary",
3636        "Secondary",
3637        "button.secondary",
3638        button_visual(58, 78, 96),
3639    );
3640    button(
3641        ui,
3642        primary_row,
3643        "button.destructive",
3644        "Destructive",
3645        "button.destructive",
3646        button_visual(157, 65, 73),
3647    );
3648    let mut disabled = widgets::ButtonOptions::new(LayoutStyle::size(92.0, 32.0));
3649    disabled.enabled = false;
3650    disabled.visual = button_visual(40, 44, 52);
3651    disabled.text_style = text(13.0, color(138, 146, 158));
3652    widgets::button(ui, primary_row, "button.disabled", "Disabled", disabled);
3653    let second_row = wrapping_row(ui, body, "buttons.row.options", 10.0);
3654    button(
3655        ui,
3656        second_row,
3657        "button.momentary",
3658        "Press only",
3659        "button.default",
3660        button_visual(42, 50, 62),
3661    );
3662    let mut toggle =
3663        widgets::ButtonOptions::new(LayoutStyle::size(112.0, 32.0)).with_action("button.toggle");
3664    toggle.pressed = state.toggle_button;
3665    toggle.visual = button_visual(42, 50, 62);
3666    toggle.hovered_visual = Some(button_visual(62, 74, 92));
3667    toggle.pressed_visual = Some(button_visual(86, 64, 156));
3668    toggle.pressed_hovered_visual = Some(button_visual(126, 94, 218));
3669    toggle.text_style = text(13.0, color(246, 249, 252));
3670    widgets::button(
3671        ui,
3672        second_row,
3673        "button.toggle",
3674        if state.toggle_button {
3675            "Toggle on"
3676        } else {
3677            "Toggle off"
3678        },
3679        toggle,
3680    );
3681    let mut forced_pressed = widgets::ButtonOptions::new(LayoutStyle::size(112.0, 32.0));
3682    forced_pressed.pressed = true;
3683    forced_pressed.visual = button_visual(42, 50, 62);
3684    forced_pressed.hovered_visual = Some(button_visual(62, 74, 92));
3685    forced_pressed.pressed_visual = Some(button_visual(38, 82, 136));
3686    forced_pressed.pressed_hovered_visual = Some(button_visual(62, 126, 196));
3687    forced_pressed.text_style = text(13.0, color(246, 249, 252));
3688    widgets::button(
3689        ui,
3690        second_row,
3691        "button.state.pressed",
3692        "Pressed",
3693        forced_pressed,
3694    );
3695    let helper_row = wrapping_row(ui, body, "buttons.row.helpers", 10.0);
3696    widgets::small_button(
3697        ui,
3698        helper_row,
3699        "button.small",
3700        "Small",
3701        widgets::ButtonOptions::default().with_action("button.small"),
3702    );
3703    widgets::icon_button(
3704        ui,
3705        helper_row,
3706        "button.icon",
3707        icon_image(BuiltInIcon::Settings),
3708        "Settings",
3709        widgets::ButtonOptions::default().with_action("button.icon"),
3710    );
3711    widgets::image_button(
3712        ui,
3713        helper_row,
3714        "button.image",
3715        icon_image(BuiltInIcon::Folder),
3716        "Folder",
3717        widgets::ButtonOptions::default().with_action("button.image"),
3718    );
3719    widgets::reset_button(
3720        ui,
3721        helper_row,
3722        "button.reset",
3723        state.toggle_button,
3724        widgets::ButtonOptions::default().with_action("button.reset"),
3725    );
3726    widgets::toggle_button(
3727        ui,
3728        helper_row,
3729        "button.toggle_helper",
3730        "Toggle helper",
3731        state.toggle_button,
3732        widgets::ButtonOptions::default().with_action("button.toggle"),
3733    );
3734    widgets::label(
3735        ui,
3736        body,
3737        "buttons.last",
3738        format!("Last pressed: {}", state.last_button),
3739        text(12.0, color(154, 166, 184)),
3740        LayoutStyle::new().with_width_percent(1.0),
3741    );
3742}
3743
3744fn checkbox(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3745    let body = section(ui, parent, "checkbox", "Checkbox");
3746    let mut options = widgets::CheckboxOptions::default().with_action("checkbox.enabled");
3747    options.text_style = text(13.0, color(222, 228, 238));
3748    widgets::checkbox(
3749        ui,
3750        body,
3751        "checkbox.enabled",
3752        if state.checked { "Enabled" } else { "Disabled" },
3753        state.checked,
3754        options,
3755    );
3756}
3757
3758fn toggles(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3759    let body = section(ui, parent, "toggles", "Radio and toggles");
3760    let radio_options = [
3761        widgets::RadioOption::new("compact", "Compact").with_action("toggles.radio.compact"),
3762        widgets::RadioOption::new("comfortable", "Comfortable")
3763            .with_action("toggles.radio.comfortable"),
3764        widgets::RadioOption::new("spacious", "Spacious").with_action("toggles.radio.spacious"),
3765        widgets::RadioOption::new("disabled", "Disabled").enabled(false),
3766    ];
3767    widgets::radio_group(
3768        ui,
3769        body,
3770        "toggles.radio_group",
3771        &radio_options,
3772        Some(state.radio_choice),
3773        widgets::RadioGroupOptions::default(),
3774    );
3775    widgets::radio_button(
3776        ui,
3777        body,
3778        "toggles.radio_single",
3779        "Standalone radio button",
3780        true,
3781        widgets::RadioButtonOptions::default().with_action("toggles.radio.compact"),
3782    );
3783    widgets::toggle_switch(
3784        ui,
3785        body,
3786        "toggles.switch",
3787        if state.switch_enabled {
3788            "Switch on"
3789        } else {
3790            "Switch off"
3791        },
3792        ext_widgets::ToggleValue::from(state.switch_enabled),
3793        widgets::ToggleSwitchOptions::default().with_action("toggles.switch"),
3794    );
3795    widgets::toggle_switch(
3796        ui,
3797        body,
3798        "toggles.mixed",
3799        match state.mixed_switch {
3800            ext_widgets::ToggleValue::Mixed => "Mixed switch",
3801            ext_widgets::ToggleValue::On => "Mixed switch on",
3802            ext_widgets::ToggleValue::Off => "Mixed switch off",
3803        },
3804        state.mixed_switch,
3805        widgets::ToggleSwitchOptions::default().with_action("toggles.mixed"),
3806    );
3807    widgets::theme_preference_buttons(
3808        ui,
3809        body,
3810        "toggles.theme_buttons",
3811        state.theme_preference,
3812        widgets::ThemePreferenceButtonsOptions::default().with_action_prefix("toggles.theme"),
3813    );
3814    widgets::theme_preference_switch(
3815        ui,
3816        body,
3817        "toggles.theme_switch",
3818        state.theme_preference,
3819        widgets::ThemePreferenceSwitchOptions::default().with_action("theme.preference.dark"),
3820    );
3821}
3822
3823fn slider(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
3824    let body = section(ui, parent, "slider", "Slider");
3825    widgets::label(
3826        ui,
3827        body,
3828        "slider.note",
3829        "Click a slider value to edit it with the keyboard.",
3830        text(12.0, color(166, 176, 190)),
3831        LayoutStyle::new().with_width_percent(1.0),
3832    );
3833
3834    let value_row = row(ui, body, "slider.value.row", 10.0);
3835    let options = slider_options(state, 180.0).with_value_edit_action("slider.value");
3836    let slider_unit = state.slider_value_spec().normalize(state.slider);
3837    widgets::slider(
3838        ui,
3839        value_row,
3840        "slider.value",
3841        slider_unit,
3842        0.0..1.0,
3843        options.clone(),
3844    );
3845    slider_number_input(
3846        ui,
3847        value_row,
3848        "slider.value_text",
3849        &state.slider_value_text,
3850        FocusedTextInput::SliderValue,
3851        state,
3852        86.0,
3853    );
3854    widgets::label(
3855        ui,
3856        value_row,
3857        "slider.value.label",
3858        "f64 demo slider",
3859        text(12.0, color(186, 198, 216)),
3860        LayoutStyle::new().with_width_percent(1.0),
3861    );
3862
3863    widgets::label(
3864        ui,
3865        body,
3866        "slider.precision",
3867        format!(
3868            "Displayed value: {}    Full precision: {:.6}",
3869            widgets::slider::format_slider_value(state.slider),
3870            state.slider
3871        ),
3872        text(11.0, color(154, 166, 184)),
3873        LayoutStyle::new().with_width_percent(1.0),
3874    );
3875
3876    divider(ui, body, "slider.divider.range");
3877    widgets::label(
3878        ui,
3879        body,
3880        "slider.range.label",
3881        "Slider range",
3882        text(12.0, color(220, 228, 238)),
3883        LayoutStyle::new().with_width_percent(1.0),
3884    );
3885    let left_row = row(ui, body, "slider.range.left.row", 10.0);
3886    let left_options = widgets::SliderOptions::default()
3887        .with_layout(
3888            LayoutStyle::new()
3889                .with_width(180.0)
3890                .with_height(24.0)
3891                .with_flex_shrink(0.0),
3892        )
3893        .with_value_edit_action("slider.range_left");
3894    widgets::slider(
3895        ui,
3896        left_row,
3897        "slider.range_left",
3898        state.slider_left,
3899        0.0..state.slider_right.max(1.0),
3900        left_options,
3901    );
3902    slider_number_input(
3903        ui,
3904        left_row,
3905        "slider.left_text",
3906        &state.slider_left_text,
3907        FocusedTextInput::SliderRangeLeft,
3908        state,
3909        96.0,
3910    );
3911    widgets::label(
3912        ui,
3913        left_row,
3914        "slider.range.left.label",
3915        "left",
3916        text(12.0, color(186, 198, 216)),
3917        LayoutStyle::new().with_width(46.0),
3918    );
3919    let right_row = row(ui, body, "slider.range.right.row", 10.0);
3920    let right_options = widgets::SliderOptions::default()
3921        .with_layout(
3922            LayoutStyle::new()
3923                .with_width(180.0)
3924                .with_height(24.0)
3925                .with_flex_shrink(0.0),
3926        )
3927        .with_value_edit_action("slider.range_right");
3928    widgets::slider(
3929        ui,
3930        right_row,
3931        "slider.range_right",
3932        state.slider_right,
3933        (state.slider_left + 1.0)..10000.0,
3934        right_options,
3935    );
3936    slider_number_input(
3937        ui,
3938        right_row,
3939        "slider.right_text",
3940        &state.slider_right_text,
3941        FocusedTextInput::SliderRangeRight,
3942        state,
3943        96.0,
3944    );
3945    widgets::label(
3946        ui,
3947        right_row,
3948        "slider.range.right.label",
3949        "right",
3950        text(12.0, color(186, 198, 216)),
3951        LayoutStyle::new().with_width(46.0),
3952    );
3953
3954    divider(ui, body, "slider.divider.trailing");
3955    let trailing_row = row(ui, body, "slider.trailing.row", 8.0);
3956    slider_checkbox_with_layout(
3957        ui,
3958        trailing_row,
3959        "slider.trailing",
3960        "Trailing color",
3961        state.slider_trailing_color,
3962        LayoutStyle::new()
3963            .with_width(142.0)
3964            .with_height(30.0)
3965            .with_flex_shrink(0.0),
3966    );
3967    ext_widgets::color_edit_button(
3968        ui,
3969        trailing_row,
3970        "slider.trailing_color_button",
3971        state.slider_trailing_picker.value(),
3972        color_square_button_options("slider.trailing_color_button")
3973            .with_format(ext_widgets::ColorValueFormat::Rgb)
3974            .accessibility_label("Pick trailing slider color"),
3975    );
3976    if state.slider_trailing_picker_open {
3977        ext_widgets::color_picker(
3978            ui,
3979            body,
3980            "slider.trailing_picker",
3981            &state.slider_trailing_picker,
3982            ext_widgets::ColorPickerOptions::default()
3983                .with_label("Trailing slider color")
3984                .with_action_prefix("slider.trailing_picker"),
3985        );
3986    }
3987    let thumb_row = row(ui, body, "slider.thumb.row", 8.0);
3988    widgets::label(
3989        ui,
3990        thumb_row,
3991        "slider.thumb.label",
3992        "Thumb",
3993        text(12.0, color(166, 176, 190)),
3994        LayoutStyle::new().with_width(64.0),
3995    );
3996    choice_button(
3997        ui,
3998        thumb_row,
3999        "slider.thumb.circle",
4000        "Circle",
4001        state.slider_thumb_shape == SliderThumbChoice::Circle,
4002    );
4003    choice_button(
4004        ui,
4005        thumb_row,
4006        "slider.thumb.square",
4007        "Square",
4008        state.slider_thumb_shape == SliderThumbChoice::Square,
4009    );
4010    choice_button(
4011        ui,
4012        thumb_row,
4013        "slider.thumb.rectangle",
4014        "Rectangle",
4015        state.slider_thumb_shape == SliderThumbChoice::Rectangle,
4016    );
4017    slider_checkbox(
4018        ui,
4019        body,
4020        "slider.steps",
4021        "Use steps",
4022        state.slider_use_steps,
4023    );
4024    let step_row = row(ui, body, "slider.step.row", 10.0);
4025    widgets::label(
4026        ui,
4027        step_row,
4028        "slider.step.label",
4029        "Step value",
4030        text(12.0, color(166, 176, 190)),
4031        LayoutStyle::new().with_width(74.0),
4032    );
4033    slider_number_input(
4034        ui,
4035        step_row,
4036        "slider.step_text",
4037        &state.slider_step_text,
4038        FocusedTextInput::SliderStep,
4039        state,
4040        86.0,
4041    );
4042    slider_checkbox(
4043        ui,
4044        body,
4045        "slider.logarithmic",
4046        "Logarithmic",
4047        state.slider_logarithmic,
4048    );
4049    let clamp_row = row(ui, body, "slider.clamping.row", 8.0);
4050    widgets::label(
4051        ui,
4052        clamp_row,
4053        "slider.clamping.label",
4054        "Clamping",
4055        text(12.0, color(166, 176, 190)),
4056        LayoutStyle::new().with_width(74.0),
4057    );
4058    choice_button(
4059        ui,
4060        clamp_row,
4061        "slider.clamping.never",
4062        "Never",
4063        state.slider_clamping == widgets::SliderClamping::Never,
4064    );
4065    choice_button(
4066        ui,
4067        clamp_row,
4068        "slider.clamping.edits",
4069        "Edits",
4070        state.slider_clamping == widgets::SliderClamping::Edits,
4071    );
4072    choice_button(
4073        ui,
4074        clamp_row,
4075        "slider.clamping.always",
4076        "Always",
4077        state.slider_clamping == widgets::SliderClamping::Always,
4078    );
4079    slider_checkbox(
4080        ui,
4081        body,
4082        "slider.smart_aim",
4083        "Smart aim",
4084        state.slider_smart_aim,
4085    );
4086}
4087
4088fn numeric_inputs(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4089    let body = section(ui, parent, "numeric", "Numeric input");
4090    let row_one = row(ui, body, "numeric.row.values", 10.0);
4091    widgets::drag_value_input(
4092        ui,
4093        row_one,
4094        "numeric.drag_value",
4095        state.numeric_value as f64,
4096        widgets::DragValueOptions::default()
4097            .with_range(ext_widgets::NumericRange::new(0.0, 100.0))
4098            .with_precision(ext_widgets::NumericPrecision::decimals(1))
4099            .with_unit(ext_widgets::NumericUnitFormat::default().suffix(" px"))
4100            .with_action("numeric.drag_value"),
4101    );
4102    widgets::drag_angle(
4103        ui,
4104        row_one,
4105        "numeric.drag_angle",
4106        state.numeric_angle as f64,
4107        widgets::DragValueOptions::default().with_action("numeric.drag_angle"),
4108    );
4109    widgets::drag_angle_tau(
4110        ui,
4111        row_one,
4112        "numeric.drag_angle_tau",
4113        state.numeric_tau as f64,
4114        widgets::DragValueOptions::default().with_action("numeric.drag_angle_tau"),
4115    );
4116    widgets::label(
4117        ui,
4118        body,
4119        "numeric.note",
4120        "Drag values expose spinbutton semantics and unit-aware formatting.",
4121        text(12.0, color(166, 176, 190)),
4122        LayoutStyle::new().with_width_percent(1.0),
4123    );
4124}
4125
4126#[allow(clippy::field_reassign_with_default)]
4127fn selection_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4128    let body = section(ui, parent, "selection", "Select controls");
4129    let select_width = 180.0;
4130
4131    widgets::label(
4132        ui,
4133        body,
4134        "selection.combo.label",
4135        "Combo box",
4136        text(12.0, color(166, 176, 190)),
4137        LayoutStyle::new().with_width_percent(1.0),
4138    );
4139
4140    let mut options = widgets::ComboBoxOptions::default();
4141    options.accessibility_label = Some("Display density".to_string());
4142    options.text_style = text(13.0, color(230, 236, 246));
4143    options.layout = LayoutStyle::new()
4144        .with_width(select_width)
4145        .with_height(30.0);
4146    let combo_anchor = ui.add_child(
4147        body,
4148        UiNode::container(
4149            "selection.combo.anchor",
4150            LayoutStyle::new()
4151                .with_width(select_width)
4152                .with_height(30.0),
4153        ),
4154    );
4155    let combo = widgets::combo_box(
4156        ui,
4157        combo_anchor,
4158        "combo.toggle",
4159        state.combo_label.clone(),
4160        state.combo_open,
4161        options,
4162    );
4163    ui.node_mut(combo).set_action("combo.toggle");
4164    let select_options = select_options();
4165    if state.combo_open {
4166        let combo_state = select_options
4167            .iter()
4168            .position(|option| option.label == state.combo_label)
4169            .map(ext_widgets::SelectMenuState::with_selected)
4170            .unwrap_or_default()
4171            .with_open(&select_options);
4172        ext_widgets::select_menu_popup(
4173            ui,
4174            combo_anchor,
4175            "selection.combo_menu",
4176            ext_widgets::AnchoredPopup::new(
4177                UiRect::new(0.0, 0.0, select_width, 30.0),
4178                UiRect::new(0.0, 0.0, 320.0, 308.0),
4179                ext_widgets::PopupPlacement::default().with_viewport_margin(0.0),
4180            ),
4181            &select_options,
4182            &combo_state,
4183            select_menu_options(select_width).with_action_prefix("selection.combo"),
4184        );
4185    }
4186
4187    widgets::label(
4188        ui,
4189        body,
4190        "selection.menu.label",
4191        "Select menu",
4192        text(12.0, color(166, 176, 190)),
4193        LayoutStyle::new().with_width_percent(1.0),
4194    );
4195    ext_widgets::select_menu(
4196        ui,
4197        body,
4198        "selection.select_menu",
4199        &select_options,
4200        &state.select_menu,
4201        ext_widgets::SelectMenuOptions::default().with_action_prefix("selection.menu"),
4202    );
4203    widgets::label(
4204        ui,
4205        body,
4206        "selection.dropdown.label",
4207        "Dropdown select",
4208        text(12.0, color(166, 176, 190)),
4209        LayoutStyle::new().with_width_percent(1.0),
4210    );
4211    let mut dropdown_options = ext_widgets::DropdownSelectOptions::default();
4212    dropdown_options.menu =
4213        select_menu_options(select_width).with_action_prefix("selection.dropdown");
4214    let dropdown_anchor = ui.add_child(
4215        body,
4216        UiNode::container(
4217            "selection.dropdown.anchor",
4218            LayoutStyle::new()
4219                .with_width(select_width)
4220                .with_height(30.0),
4221        ),
4222    );
4223    let dropdown_nodes = ext_widgets::dropdown_select(
4224        ui,
4225        dropdown_anchor,
4226        "selection.dropdown",
4227        &select_options,
4228        &state.dropdown,
4229        Some(ext_widgets::AnchoredPopup::new(
4230            UiRect::new(0.0, 0.0, select_width, 30.0),
4231            UiRect::new(0.0, 0.0, 320.0, 308.0),
4232            ext_widgets::PopupPlacement::default().with_viewport_margin(0.0),
4233        )),
4234        dropdown_options,
4235    );
4236    ui.node_mut(dropdown_nodes.trigger)
4237        .set_action("selection.dropdown.toggle");
4238}
4239
4240#[allow(clippy::field_reassign_with_default)]
4241fn select_menu_options(width: f32) -> ext_widgets::SelectMenuOptions {
4242    let mut options = ext_widgets::SelectMenuOptions::default();
4243    options.width = width;
4244    options.portal = UiPortalTarget::Parent;
4245    options
4246}
4247
4248#[allow(clippy::field_reassign_with_default)]
4249fn text_input(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4250    let body = section(ui, parent, "text_input", "Text input");
4251    let mut options = TextInputOptions::default();
4252    options.placeholder = "Type here".to_string();
4253    options.layout = LayoutStyle::new().with_width(300.0).with_height(36.0);
4254    options.text_style = text(13.0, color(230, 236, 246));
4255    options.placeholder_style = text(13.0, color(144, 156, 174));
4256    options.edit_action = Some("text.input.edit".into());
4257    options.focused = state.focused_text == Some(FocusedTextInput::Editable);
4258    options.caret_visible = caret_visible(state.caret_phase);
4259    widgets::text_input(ui, body, "text.input", &state.text, options);
4260
4261    let mut selectable_options = TextInputOptions::default();
4262    selectable_options.layout = LayoutStyle::new().with_width(360.0).with_height(36.0);
4263    selectable_options.text_style = text(13.0, color(196, 210, 230));
4264    selectable_options.read_only = true;
4265    selectable_options.selectable = true;
4266    selectable_options.focused = state.focused_text == Some(FocusedTextInput::Selectable);
4267    selectable_options.edit_action = Some("text.selectable.edit".into());
4268    selectable_options.caret_visible = caret_visible(state.caret_phase);
4269    widgets::text_input(
4270        ui,
4271        body,
4272        "text.selectable",
4273        &state.selectable_text,
4274        selectable_options,
4275    );
4276
4277    let mut singleline = TextInputOptions::default();
4278    singleline.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
4279    singleline.text_style = text(13.0, color(230, 236, 246));
4280    singleline.placeholder = "Single line".to_string();
4281    singleline.edit_action = Some("text.singleline.edit".into());
4282    singleline.focused = state.focused_text == Some(FocusedTextInput::Singleline);
4283    singleline.caret_visible = caret_visible(state.caret_phase);
4284    widgets::singleline_text_input(
4285        ui,
4286        body,
4287        "text.singleline",
4288        &state.singleline_text,
4289        singleline,
4290    );
4291
4292    let mut multiline = TextInputOptions::default();
4293    multiline.layout = LayoutStyle::new().with_width(360.0).with_height(72.0);
4294    multiline.text_style = text(13.0, color(230, 236, 246));
4295    multiline.edit_action = Some("text.multiline.edit".into());
4296    multiline.focused = state.focused_text == Some(FocusedTextInput::Multiline);
4297    multiline.caret_visible = caret_visible(state.caret_phase);
4298    widgets::multiline_text_input(ui, body, "text.multiline", &state.multiline_text, multiline);
4299
4300    let mut area = TextInputOptions::default();
4301    area.layout = LayoutStyle::new().with_width(360.0).with_height(66.0);
4302    area.text_style = text(13.0, color(230, 236, 246));
4303    area.edit_action = Some("text.area.edit".into());
4304    area.focused = state.focused_text == Some(FocusedTextInput::TextArea);
4305    area.caret_visible = caret_visible(state.caret_phase);
4306    widgets::text_area(ui, body, "text.area", &state.text_area_text, area);
4307
4308    let mut code = TextInputOptions::default();
4309    code.layout = LayoutStyle::new().with_width(360.0).with_height(88.0);
4310    code.edit_action = Some("text.code_editor.edit".into());
4311    code.focused = state.focused_text == Some(FocusedTextInput::CodeEditor);
4312    code.caret_visible = caret_visible(state.caret_phase);
4313    widgets::code_editor(ui, body, "text.code_editor", &state.code_editor_text, code);
4314
4315    let mut search = TextInputOptions::default();
4316    search.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
4317    search.text_style = text(13.0, color(230, 236, 246));
4318    search.edit_action = Some("text.search.edit".into());
4319    search.focused = state.focused_text == Some(FocusedTextInput::Search);
4320    search.caret_visible = caret_visible(state.caret_phase);
4321    widgets::search_input(ui, body, "text.search", &state.search_text, search);
4322
4323    let mut password = TextInputOptions::default();
4324    password.layout = LayoutStyle::new().with_width(300.0).with_height(34.0);
4325    password.text_style = text(13.0, color(230, 236, 246));
4326    password.edit_action = Some("text.password.edit".into());
4327    password.focused = state.focused_text == Some(FocusedTextInput::Password);
4328    password.caret_visible = caret_visible(state.caret_phase);
4329    widgets::password_input(ui, body, "text.password", &state.password_text, password);
4330}
4331
4332fn date_picker(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4333    let body = section(ui, parent, "date", "Date picker");
4334    let controls = row(ui, body, "date.options", 8.0);
4335    choice_button(
4336        ui,
4337        controls,
4338        "date.week.sunday",
4339        "Sun first",
4340        state.date.first_weekday == ext_widgets::Weekday::Sunday,
4341    );
4342    choice_button(
4343        ui,
4344        controls,
4345        "date.week.monday",
4346        "Mon first",
4347        state.date.first_weekday == ext_widgets::Weekday::Monday,
4348    );
4349    let mut range_button =
4350        widgets::ButtonOptions::new(LayoutStyle::new().with_width(92.0).with_height(28.0))
4351            .with_action("date.range.toggle");
4352    range_button.visual = if state.date.min.is_some() || state.date.max.is_some() {
4353        button_visual(48, 112, 184)
4354    } else {
4355        button_visual(38, 46, 58)
4356    };
4357    range_button.hovered_visual = Some(button_visual(65, 86, 106));
4358    range_button.text_style = text(12.0, color(238, 244, 252));
4359    widgets::button(
4360        ui,
4361        controls,
4362        "date.range.toggle",
4363        "Limit range",
4364        range_button,
4365    );
4366    ext_widgets::date_picker(
4367        ui,
4368        body,
4369        "date.picker",
4370        &state.date,
4371        ext_widgets::DatePickerOptions::default().with_action_prefix("date"),
4372    );
4373    widgets::label(
4374        ui,
4375        body,
4376        "date.selected",
4377        format!(
4378            "Selected: {}",
4379            state
4380                .date
4381                .selected
4382                .map_or_else(|| "None".to_string(), CalendarDate::iso_string)
4383        ),
4384        text(11.0, color(154, 166, 184)),
4385        LayoutStyle::new().with_width_percent(1.0),
4386    );
4387}
4388
4389fn color_picker(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4390    let body = section(ui, parent, "color", "Color picker");
4391    ext_widgets::color_picker(
4392        ui,
4393        body,
4394        "color.picker",
4395        &state.color,
4396        ext_widgets::ColorPickerOptions::default()
4397            .with_action_prefix("color")
4398            .with_copy_hex_action("color.copy_hex")
4399            .with_copy_hex_label("Copy"),
4400    );
4401    if let Some(hex) = &state.color_copied_hex {
4402        widgets::label(
4403            ui,
4404            body,
4405            "color.copied",
4406            format!("Copied {hex}"),
4407            text(11.0, color(154, 166, 184)),
4408            LayoutStyle::new().with_width_percent(1.0),
4409        );
4410    }
4411}
4412
4413fn color_buttons(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4414    let body = section(ui, parent, "color_buttons", "Color buttons");
4415    let current_color = state.color.value();
4416
4417    widgets::label(
4418        ui,
4419        body,
4420        "color_buttons.edit_label",
4421        "Color edit button",
4422        text(12.0, color(166, 176, 190)),
4423        LayoutStyle::new().with_width_percent(1.0),
4424    );
4425    let edit_row = row(ui, body, "color_buttons.edit_row", 8.0);
4426    ext_widgets::color_edit_button(
4427        ui,
4428        edit_row,
4429        "color_buttons.compact",
4430        current_color,
4431        color_square_button_options("color_buttons.compact")
4432            .with_format(ext_widgets::ColorValueFormat::Rgb)
4433            .accessibility_label("Edit RGB color"),
4434    );
4435    widgets::label(
4436        ui,
4437        edit_row,
4438        "color_buttons.hex_value",
4439        ext_widgets::color_picker::format_hex_color(current_color, false),
4440        text(12.0, color(220, 228, 238)),
4441        LayoutStyle::new().with_width(92.0),
4442    );
4443
4444    widgets::label(
4445        ui,
4446        body,
4447        "color_buttons.format_label",
4448        "Value formats",
4449        text(12.0, color(166, 176, 190)),
4450        LayoutStyle::new().with_width_percent(1.0),
4451    );
4452    let rgb_row = row(ui, body, "color_buttons.rgb_row", 8.0);
4453    widgets::label(
4454        ui,
4455        rgb_row,
4456        "color_buttons.rgb_label",
4457        "RGB",
4458        text(12.0, color(186, 198, 216)),
4459        LayoutStyle::new().with_width(48.0),
4460    );
4461    ext_widgets::color_edit_button(
4462        ui,
4463        rgb_row,
4464        "color_buttons.rgb",
4465        current_color,
4466        color_value_button_options("color_buttons.rgb", 180.0)
4467            .with_format(ext_widgets::ColorValueFormat::Rgb),
4468    );
4469    let rgba_row = row(ui, body, "color_buttons.rgba_row", 8.0);
4470    widgets::label(
4471        ui,
4472        rgba_row,
4473        "color_buttons.rgba_label",
4474        "RGBA",
4475        text(12.0, color(186, 198, 216)),
4476        LayoutStyle::new().with_width(48.0),
4477    );
4478    ext_widgets::color_edit_button(
4479        ui,
4480        rgba_row,
4481        "color_buttons.rgba",
4482        current_color,
4483        color_value_button_options("color_buttons.rgba", 230.0)
4484            .with_format(ext_widgets::ColorValueFormat::Rgba),
4485    );
4486    let hsva_row = row(ui, body, "color_buttons.hsva_row", 8.0);
4487    widgets::label(
4488        ui,
4489        hsva_row,
4490        "color_buttons.hsva_label",
4491        "HSVA",
4492        text(12.0, color(186, 198, 216)),
4493        LayoutStyle::new().with_width(48.0),
4494    );
4495    ext_widgets::color_edit_button(
4496        ui,
4497        hsva_row,
4498        "color_buttons.hsva",
4499        current_color,
4500        color_value_button_options("color_buttons.hsva", 260.0)
4501            .with_format(ext_widgets::ColorValueFormat::Hsva),
4502    );
4503
4504    widgets::label(
4505        ui,
4506        body,
4507        "color_buttons.field_label",
4508        "2D color field",
4509        text(12.0, color(166, 176, 190)),
4510        LayoutStyle::new().with_width_percent(1.0),
4511    );
4512    ext_widgets::color_picker::color_picker_hsva_2d(
4513        ui,
4514        body,
4515        "color_buttons.hsva_2d",
4516        state.color.hsv(),
4517        ext_widgets::ColorHsva2dOptions::default()
4518            .with_layout(LayoutStyle::new().with_width(204.0).with_height(112.0))
4519            .with_action_prefix("color_buttons.hsva_2d"),
4520    );
4521    widgets::label(
4522        ui,
4523        body,
4524        "color_buttons.status",
4525        format!("Last activated: {}", state.color_button_status),
4526        text(11.0, color(154, 166, 184)),
4527        LayoutStyle::new().with_width_percent(1.0),
4528    );
4529}
4530
4531fn menu_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4532    let body = section(ui, parent, "menus", "Menus");
4533    let menus = menu_bar_menus(state.menu_autosave, state.menu_grid);
4534    let active_items = state
4535        .menu_bar
4536        .open_menu
4537        .and_then(|index| menus.get(index))
4538        .map(|menu| menu.items.clone())
4539        .unwrap_or_default();
4540    ext_widgets::menu_bar(
4541        ui,
4542        body,
4543        "menus.menu_bar",
4544        &menus,
4545        &state.menu_bar,
4546        None,
4547        ext_widgets::MenuBarOptions::default().with_action_prefix("menus.bar"),
4548    );
4549
4550    if !active_items.is_empty() {
4551        ext_widgets::menu_list(
4552            ui,
4553            body,
4554            "menus.menu_list",
4555            &active_items,
4556            state.menu_bar.active_item,
4557            ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
4558        );
4559        if let Some(active_item) = state.menu_bar.active_item {
4560            if let Some(children) = active_items
4561                .get(active_item)
4562                .and_then(|item| item.children())
4563            {
4564                ext_widgets::menu_list_popup(
4565                    ui,
4566                    body,
4567                    "menus.submenu",
4568                    ext_widgets::AnchoredPopup::new(
4569                        UiRect::new(
4570                            0.0,
4571                            40.0 + menu_item_top_offset(&active_items, active_item),
4572                            240.0,
4573                            menu_item_height(active_items.get(active_item)),
4574                        ),
4575                        UiRect::new(0.0, 0.0, 680.0, 468.0),
4576                        ext_widgets::PopupPlacement::new(
4577                            ext_widgets::PopupSide::Right,
4578                            ext_widgets::PopupAlign::Start,
4579                        )
4580                        .with_offset(4.0),
4581                    ),
4582                    children,
4583                    Some(0),
4584                    ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
4585                );
4586            }
4587        }
4588    }
4589    divider(ui, body, "menus.divider.buttons");
4590    let button_row = row(ui, body, "menus.buttons", 8.0);
4591    let button_items = menu_items(state.menu_autosave);
4592    ext_widgets::menu_button(
4593        ui,
4594        button_row,
4595        "menus.menu_button",
4596        "Menu button",
4597        &button_items,
4598        &state.menu_button,
4599        None,
4600        ext_widgets::MenuButtonOptions::default().with_action("menus.menu_button"),
4601    );
4602    ext_widgets::image_text_menu_button(
4603        ui,
4604        button_row,
4605        "menus.image_text_menu_button",
4606        "Image text",
4607        icon_image(BuiltInIcon::Folder),
4608        &button_items,
4609        &state.image_text_menu_button,
4610        None,
4611        ext_widgets::MenuButtonOptions::default().with_action("menus.image_text_menu_button"),
4612    );
4613    ext_widgets::image_menu_button(
4614        ui,
4615        button_row,
4616        "menus.image_menu_button",
4617        icon_image(BuiltInIcon::Settings),
4618        &button_items,
4619        &state.image_menu_button,
4620        None,
4621        ext_widgets::MenuButtonOptions::default().with_action("menus.image_menu_button"),
4622    );
4623    if state.menu_button.open || state.image_text_menu_button.open || state.image_menu_button.open {
4624        let active = state
4625            .menu_button
4626            .navigation
4627            .active_path
4628            .first()
4629            .copied()
4630            .or_else(|| {
4631                state
4632                    .image_text_menu_button
4633                    .navigation
4634                    .active_path
4635                    .first()
4636                    .copied()
4637            })
4638            .or_else(|| {
4639                state
4640                    .image_menu_button
4641                    .navigation
4642                    .active_path
4643                    .first()
4644                    .copied()
4645            });
4646        ext_widgets::menu_list(
4647            ui,
4648            body,
4649            "menus.button_menu",
4650            &button_items,
4651            active,
4652            ext_widgets::MenuListOptions::default().with_action_prefix("menus.item"),
4653        );
4654    }
4655
4656    let context_row = row(ui, body, "menus.context.controls", 8.0);
4657    button(
4658        ui,
4659        context_row,
4660        "menus.context.open",
4661        "Open context",
4662        "menus.context.open",
4663        button_visual(48, 112, 184),
4664    );
4665    button(
4666        ui,
4667        context_row,
4668        "menus.context.close",
4669        "Close",
4670        "menus.context.close",
4671        button_visual(58, 78, 96),
4672    );
4673    let mut context_options =
4674        ext_widgets::MenuListOptions::default().with_action_prefix("menus.context");
4675    context_options.width = 180.0;
4676    context_options.max_visible_rows = 4;
4677    let _ = ext_widgets::context_menu(
4678        ui,
4679        parent,
4680        "menus.context_menu",
4681        &button_items,
4682        &state.context_menu,
4683        UiRect::new(0.0, 0.0, 180.0, 120.0),
4684        ext_widgets::PopupPlacement::default(),
4685        context_options,
4686    );
4687}
4688
4689fn command_palette(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4690    let body = section(ui, parent, "command_palette", "Command palette");
4691    let items = command_palette_items_with_history(&state.command_history);
4692    let mut options =
4693        ext_widgets::CommandPaletteOptions::default().with_action_prefix("command_palette");
4694    options.width = 480.0;
4695    options.row_height = 44.0;
4696    options.max_visible_rows = 5;
4697    options.text_style = text(13.0, color(238, 244, 252));
4698    options.muted_text_style = text(11.0, color(166, 178, 196));
4699    ext_widgets::command_palette(
4700        ui,
4701        body,
4702        "command_palette.panel",
4703        &items,
4704        &state.command_palette,
4705        None,
4706        options,
4707    );
4708    widgets::label(
4709        ui,
4710        body,
4711        "command_palette.last",
4712        format!("Last command: {}", state.last_command),
4713        text(12.0, color(154, 166, 184)),
4714        LayoutStyle::new().with_width_percent(1.0),
4715    );
4716}
4717
4718#[allow(clippy::field_reassign_with_default)]
4719fn progress_indicator(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4720    let body = section(ui, parent, "progress", "Progress indicator");
4721    let animated = smooth_loop(state.progress_phase * 0.85, 0.0) * 100.0;
4722    let mut progress = ext_widgets::ProgressIndicatorOptions::default();
4723    progress.layout = LayoutStyle::new().with_width_percent(1.0).with_height(10.0);
4724    progress.accessibility_label = Some("Progress".to_string());
4725    ext_widgets::progress_indicator(
4726        ui,
4727        body,
4728        "progress.primary",
4729        ext_widgets::ProgressIndicatorValue::percent(animated),
4730        progress,
4731    );
4732    let compact_value = smooth_loop(state.progress_phase * 1.15, 0.7) * 100.0;
4733    let mut compact = ext_widgets::ProgressIndicatorOptions::default();
4734    compact.layout = LayoutStyle::new().with_width_percent(1.0).with_height(6.0);
4735    compact.fill_visual = UiVisual::panel(color(111, 203, 159), None, 3.0);
4736    ext_widgets::progress_indicator(
4737        ui,
4738        body,
4739        "progress.compact",
4740        ext_widgets::ProgressIndicatorValue::percent(compact_value),
4741        compact,
4742    );
4743    let warning_value = smooth_loop(state.progress_phase * 0.65, 1.4) * 100.0;
4744    let mut warning = ext_widgets::ProgressIndicatorOptions::default();
4745    warning.layout = LayoutStyle::new().with_width_percent(1.0).with_height(14.0);
4746    warning.fill_visual = UiVisual::panel(color(232, 186, 88), None, 4.0);
4747    ext_widgets::progress_indicator(
4748        ui,
4749        body,
4750        "progress.warning",
4751        ext_widgets::ProgressIndicatorValue::percent(warning_value),
4752        warning,
4753    );
4754    let spinner_row = row(ui, body, "progress.spinner.row", 8.0);
4755    widgets::spinner(
4756        ui,
4757        spinner_row,
4758        "progress.spinner",
4759        widgets::SpinnerOptions::default()
4760            .with_phase(state.progress_phase)
4761            .with_accessibility_label("Loading spinner"),
4762    );
4763    widgets::label(
4764        ui,
4765        spinner_row,
4766        "progress.spinner.label",
4767        "Spinner",
4768        text(12.0, color(196, 210, 230)),
4769        LayoutStyle::new().with_width_percent(1.0),
4770    );
4771}
4772
4773fn animation_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
4774    let body = section(ui, parent, "animation", "Animation");
4775
4776    if let Some(section) = animation_section(
4777        ui,
4778        body,
4779        "animation.timed",
4780        "Timed playback",
4781        state.animation_timed_expanded,
4782    ) {
4783        let live_stage = animation_stage(ui, section, "animation.live.stage");
4784        let live_amount = smooth_loop(state.progress_phase * 1.65, 0.0);
4785        let live_values = animation_blend_machine(
4786            ANIMATION_INPUT_PROGRESS,
4787            live_amount,
4788            UiPoint::new(220.0, 0.0),
4789            0.88,
4790            1.10,
4791            1.0,
4792        )
4793        .with_bool_input("looping", true)
4794        .values();
4795        ui.add_child(
4796            live_stage,
4797            UiNode::scene(
4798                "animation.live.orb",
4799                animation_orb_primitives(
4800                    color(108, 180, 255),
4801                    ANIMATION_ORB_SIZE * live_values.scale,
4802                    UiPoint::new(
4803                        28.0 + live_values.translate.x,
4804                        37.0 + live_values.translate.y,
4805                    ),
4806                ),
4807                animation_scene_layout(),
4808            )
4809            .with_accessibility(
4810                AccessibilityMeta::new(AccessibilityRole::Image).label("Looping orb"),
4811            ),
4812        );
4813    }
4814
4815    if let Some(section) = animation_section(
4816        ui,
4817        body,
4818        "animation.scrub",
4819        "Scrubbed input",
4820        state.animation_scrub_expanded,
4821    ) {
4822        let scrub_row = row(ui, section, "animation.scrub.row", 10.0);
4823        widgets::slider(
4824            ui,
4825            scrub_row,
4826            "animation.scrub",
4827            state.animation_scrub,
4828            0.0..1.0,
4829            widgets::SliderOptions::default()
4830                .with_layout(
4831                    LayoutStyle::new()
4832                        .with_width(200.0)
4833                        .with_height(28.0)
4834                        .with_flex_shrink(0.0),
4835                )
4836                .with_value_edit_action("animation.scrub"),
4837        );
4838        widgets::label(
4839            ui,
4840            scrub_row,
4841            "animation.scrub.value",
4842            format!("{:.0}%", state.animation_scrub * 100.0),
4843            text(12.0, color(186, 198, 216)),
4844            LayoutStyle::new().with_width_percent(1.0),
4845        );
4846        let scrub_stage = animation_stage(ui, section, "animation.scrub.stage");
4847        let scrub_values = animation_blend_machine(
4848            ANIMATION_INPUT_SCRUB,
4849            state.animation_scrub,
4850            UiPoint::new(220.0, 0.0),
4851            0.82,
4852            1.14,
4853            1.0,
4854        )
4855        .values();
4856        ui.add_child(
4857            scrub_stage,
4858            UiNode::scene(
4859                "animation.scrub.shape",
4860                animation_morph_shape_primitives(
4861                    color(111, 203, 159),
4862                    ANIMATION_SHAPE_SIZE * scrub_values.scale,
4863                    UiPoint::new(
4864                        28.0 + scrub_values.translate.x,
4865                        37.0 + scrub_values.translate.y,
4866                    ),
4867                    scrub_values.morph,
4868                ),
4869                animation_scene_layout(),
4870            )
4871            .with_accessibility(
4872                AccessibilityMeta::new(AccessibilityRole::Image).label("Scrubbed morphing shape"),
4873            ),
4874        );
4875    }
4876
4877    if let Some(section) = animation_section(
4878        ui,
4879        body,
4880        "animation.state",
4881        "Boolean input transition",
4882        state.animation_state_expanded,
4883    ) {
4884        let state_row = row(ui, section, "animation.state.row", 10.0);
4885        let mut open = widgets::ButtonOptions::new(
4886            LayoutStyle::new()
4887                .with_width(92.0)
4888                .with_height(30.0)
4889                .with_flex_shrink(0.0),
4890        )
4891        .with_action("animation.open");
4892        open.visual = if state.animation_open {
4893            button_visual(48, 112, 184)
4894        } else {
4895            button_visual(38, 46, 58)
4896        };
4897        open.hovered_visual = Some(button_visual(65, 86, 106));
4898        open.pressed_visual = Some(button_visual(34, 54, 84));
4899        open.text_style = text(12.0, color(238, 244, 252));
4900        widgets::button(
4901            ui,
4902            state_row,
4903            "animation.open",
4904            if state.animation_open {
4905                "Close"
4906            } else {
4907                "Open"
4908            },
4909            open,
4910        );
4911        let open_stage = animation_stage(ui, section, "animation.state.stage");
4912        let panel_offset = if state.animation_open {
4913            UiPoint::new(
4914                ANIMATION_STAGE_MIN_WIDTH - ANIMATION_PANEL_WIDTH - ANIMATION_PANEL_INSET_X,
4915                ANIMATION_PANEL_Y,
4916            )
4917        } else {
4918            UiPoint::new(ANIMATION_PANEL_INSET_X, ANIMATION_PANEL_Y)
4919        };
4920        ui.add_child(
4921            open_stage,
4922            UiNode::scene(
4923                "animation.state.panel",
4924                animation_panel_primitives(panel_offset),
4925                animation_scene_layout(),
4926            )
4927            .with_animation(animation_open_machine(state.animation_open))
4928            .with_accessibility(
4929                AccessibilityMeta::new(AccessibilityRole::Image).label("Open state panel"),
4930            ),
4931        );
4932    }
4933
4934    if let Some(section) = animation_section(
4935        ui,
4936        body,
4937        "animation.interaction",
4938        "Interaction inputs",
4939        state.animation_interaction_expanded,
4940    ) {
4941        let interaction_stage = animation_stage(ui, section, "animation.interaction.stage");
4942        ui.add_child(
4943            interaction_stage,
4944            UiNode::scene(
4945                "animation.interaction.target",
4946                animation_interaction_primitives(
4947                    color(176, 126, 230),
4948                    ANIMATION_ORB_SIZE,
4949                    UiPoint::new(40.0, 37.0),
4950                ),
4951                animation_scene_layout(),
4952            )
4953            .with_input(InputBehavior::BUTTON)
4954            .with_animation(animation_interaction_machine())
4955            .with_accessibility(
4956                AccessibilityMeta::new(AccessibilityRole::Button)
4957                    .label("Interaction animation target")
4958                    .focusable(),
4959            ),
4960        );
4961    }
4962}
4963
4964fn animation_section(
4965    ui: &mut UiDocument,
4966    parent: UiNodeId,
4967    name: &'static str,
4968    title: &'static str,
4969    expanded: bool,
4970) -> Option<UiNodeId> {
4971    let mut options = widgets::CollapsingHeaderOptions::default()
4972        .expanded(expanded)
4973        .with_toggle_action(format!("{name}.toggle"));
4974    options.text_style = text(12.0, color(220, 228, 238));
4975    options.indicator_text_style = text(12.0, color(186, 198, 216));
4976    options.header_visual = UiVisual::panel(
4977        color(21, 26, 33),
4978        Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
4979        4.0,
4980    );
4981    options.hovered_visual = UiVisual::panel(color(38, 48, 61), None, 4.0);
4982    options.pressed_visual = UiVisual::panel(color(27, 36, 48), None, 4.0);
4983    options.body_layout = LayoutStyle::column()
4984        .with_width_percent(1.0)
4985        .with_padding(0.0)
4986        .with_gap(10.0);
4987    widgets::collapsing_header(ui, parent, name, title, options).body
4988}
4989
4990fn animation_stage(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
4991    let layout = LayoutStyle::row()
4992        .with_width_percent(1.0)
4993        .with_height(ANIMATION_STAGE_HEIGHT)
4994        .with_align_items(taffy::prelude::AlignItems::Center)
4995        .with_flex_shrink(0.0);
4996    let layout = operad::layout::with_min_size(
4997        layout,
4998        operad::length(ANIMATION_STAGE_MIN_WIDTH),
4999        operad::length(ANIMATION_STAGE_HEIGHT),
5000    );
5001    ui.add_child(
5002        parent,
5003        UiNode::container(name, layout).with_visual(UiVisual::panel(
5004            color(16, 21, 28),
5005            Some(StrokeStyle::new(color(48, 58, 72), 1.0)),
5006            6.0,
5007        )),
5008    )
5009}
5010
5011fn animation_scene_layout() -> LayoutStyle {
5012    let layout = LayoutStyle::new()
5013        .with_width_percent(1.0)
5014        .with_height_percent(1.0)
5015        .with_flex_grow(1.0)
5016        .with_flex_shrink(1.0);
5017    operad::layout::with_min_size(layout, operad::length(0.0), operad::length(0.0))
5018}
5019
5020fn animation_blend_machine(
5021    input: &'static str,
5022    value: f32,
5023    translate: UiPoint,
5024    start_scale: f32,
5025    end_scale: f32,
5026    end_opacity: f32,
5027) -> AnimationMachine {
5028    let start_values = AnimatedValues::new(0.45, UiPoint::new(0.0, 0.0), start_scale);
5029    let end_values = AnimatedValues::new(end_opacity, translate, end_scale).with_morph(1.0);
5030    AnimationMachine::new(
5031        vec![
5032            AnimationState::new("start", start_values),
5033            AnimationState::new("end", end_values),
5034        ],
5035        Vec::new(),
5036        "start",
5037    )
5038    .unwrap_or_else(|_| AnimationMachine::single_state("start", start_values))
5039    .with_number_input(input, value)
5040    .with_blend_binding(AnimationBlendBinding::new(input, "start", "end"))
5041}
5042
5043fn animation_open_machine(open: bool) -> AnimationMachine {
5044    let closed_values = AnimatedValues::new(0.35, UiPoint::new(0.0, 0.0), 1.0);
5045    let open_values = AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0);
5046    let fallback_values = if open { open_values } else { closed_values };
5047    AnimationMachine::new(
5048        vec![
5049            AnimationState::new("closed", closed_values),
5050            AnimationState::new("open", open_values),
5051        ],
5052        vec![
5053            AnimationTransition::when(
5054                "closed",
5055                "open",
5056                AnimationCondition::bool(ANIMATION_INPUT_OPEN, true),
5057                0.18,
5058            ),
5059            AnimationTransition::when(
5060                "open",
5061                "closed",
5062                AnimationCondition::bool(ANIMATION_INPUT_OPEN, false),
5063                0.14,
5064            ),
5065        ],
5066        "closed",
5067    )
5068    .unwrap_or_else(|_| AnimationMachine::single_state("closed", fallback_values))
5069    .with_bool_input(ANIMATION_INPUT_OPEN, open)
5070}
5071
5072fn animation_interaction_machine() -> AnimationMachine {
5073    let rest_values = AnimatedValues::new(0.72, UiPoint::new(0.0, 0.0), 1.0);
5074    let right_values = AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0).with_morph(1.0);
5075    AnimationMachine::new(
5076        vec![
5077            AnimationState::new("rest", rest_values),
5078            AnimationState::new("right", right_values),
5079        ],
5080        Vec::new(),
5081        "rest",
5082    )
5083    .unwrap_or_else(|_| AnimationMachine::single_state("rest", rest_values))
5084    .with_number_input(ANIMATION_INPUT_POINTER_NORM_X, 0.0)
5085    .with_blend_binding(AnimationBlendBinding::new(
5086        ANIMATION_INPUT_POINTER_NORM_X,
5087        "rest",
5088        "right",
5089    ))
5090}
5091
5092fn animation_interaction_primitives(
5093    fill: ColorRgba,
5094    size: f32,
5095    offset: UiPoint,
5096) -> Vec<ScenePrimitive> {
5097    vec![
5098        ScenePrimitive::MorphPolygon {
5099            from_points: animation_square_points(size, offset),
5100            to_points: animation_pentagon_points(size, offset),
5101            amount: 0.0,
5102            fill,
5103            stroke: Some(StrokeStyle::new(color(236, 244, 255), 1.0)),
5104        },
5105        ScenePrimitive::Circle {
5106            center: UiPoint::new(offset.x + size * 0.34, offset.y + size * 0.30),
5107            radius: size * 0.10,
5108            fill: color(244, 248, 255),
5109            stroke: None,
5110        },
5111    ]
5112}
5113
5114fn animation_orb_primitives(fill: ColorRgba, size: f32, offset: UiPoint) -> Vec<ScenePrimitive> {
5115    let center = size * 0.5;
5116    let radius = size * 0.44;
5117    vec![
5118        ScenePrimitive::Circle {
5119            center: UiPoint::new(offset.x + center, offset.y + center),
5120            radius,
5121            fill,
5122            stroke: Some(StrokeStyle::new(color(236, 244, 255), 1.0)),
5123        },
5124        ScenePrimitive::Circle {
5125            center: UiPoint::new(offset.x + size * 0.34, offset.y + size * 0.30),
5126            radius: size * 0.12,
5127            fill: color(244, 248, 255),
5128            stroke: None,
5129        },
5130    ]
5131}
5132
5133fn animation_morph_shape_primitives(
5134    fill: ColorRgba,
5135    size: f32,
5136    offset: UiPoint,
5137    amount: f32,
5138) -> Vec<ScenePrimitive> {
5139    vec![ScenePrimitive::MorphPolygon {
5140        from_points: animation_square_points(size, offset),
5141        to_points: animation_pentagon_points(size, offset),
5142        amount,
5143        fill,
5144        stroke: Some(StrokeStyle::new(color(226, 246, 236), 1.0)),
5145    }]
5146}
5147
5148fn animation_square_points(size: f32, offset: UiPoint) -> Vec<UiPoint> {
5149    let inset = size * 0.08;
5150    let left = offset.x + inset;
5151    let top = offset.y + inset;
5152    let right = offset.x + size - inset;
5153    let bottom = offset.y + size - inset;
5154    let center_x = offset.x + size * 0.5;
5155    vec![
5156        UiPoint::new(center_x, top),
5157        UiPoint::new(right, top),
5158        UiPoint::new(right, bottom),
5159        UiPoint::new(left, bottom),
5160        UiPoint::new(left, top),
5161    ]
5162}
5163
5164fn animation_pentagon_points(size: f32, offset: UiPoint) -> Vec<UiPoint> {
5165    let center = size * 0.5;
5166    let radius = size * 0.46;
5167    (0..5)
5168        .map(|index| {
5169            let angle = -std::f32::consts::FRAC_PI_2 + index as f32 * std::f32::consts::TAU / 5.0;
5170            UiPoint::new(
5171                offset.x + center + angle.cos() * radius,
5172                offset.y + center + angle.sin() * radius,
5173            )
5174        })
5175        .collect()
5176}
5177
5178fn animation_panel_primitives(offset: UiPoint) -> Vec<ScenePrimitive> {
5179    vec![ScenePrimitive::Rect(
5180        PaintRect::solid(
5181            UiRect::new(
5182                offset.x,
5183                offset.y,
5184                ANIMATION_PANEL_WIDTH,
5185                ANIMATION_PANEL_HEIGHT,
5186            ),
5187            color(232, 186, 88),
5188        )
5189        .stroke(AlignedStroke::inside(StrokeStyle::new(
5190            color(255, 226, 154),
5191            1.0,
5192        )))
5193        .corner_radii(CornerRadii::uniform(6.0)),
5194    )]
5195}
5196
5197fn list_and_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5198    let body = section(ui, parent, "lists_tables", "Lists and tables");
5199
5200    let scroll_shell = row(ui, body, "lists_tables.scroll_area.shell", 8.0);
5201    let nested_scroll = widgets::scroll_area(
5202        ui,
5203        scroll_shell,
5204        "lists_tables.scroll_area",
5205        ScrollAxes::VERTICAL,
5206        LayoutStyle::column()
5207            .with_width(0.0)
5208            .with_flex_grow(1.0)
5209            .with_height(92.0),
5210    );
5211    ui.node_mut(nested_scroll)
5212        .set_action("lists_tables.scroll_area.scroll");
5213    if let Some(scroll) = ui.node_mut(nested_scroll).scroll_mut() {
5214        scroll.set_offset(UiPoint::new(0.0, state.list_scroll));
5215    }
5216    for index in 0..6 {
5217        widgets::label(
5218            ui,
5219            nested_scroll,
5220            format!("lists_tables.scroll_area.row.{index}"),
5221            format!("Scroll row {}", index + 1),
5222            text(12.0, color(200, 212, 228)),
5223            LayoutStyle::new()
5224                .with_width_percent(1.0)
5225                .with_height(26.0)
5226                .with_flex_shrink(0.0),
5227        );
5228    }
5229    scrollbar_widgets::scrollbar(
5230        ui,
5231        scroll_shell,
5232        "lists_tables.scroll_area.scrollbar",
5233        scroll_state(state.list_scroll, 92.0, 6.0 * 26.0),
5234        scrollbar_widgets::ScrollAxis::Vertical,
5235        scrollbar_widgets::ScrollbarOptions::default()
5236            .with_layout(LayoutStyle::size(8.0, 92.0))
5237            .with_track_size(UiSize::new(8.0, 92.0))
5238            .with_action("lists_tables.scroll_area.scrollbar"),
5239    );
5240
5241    widgets::table_header(ui, body, "lists_tables.table_header", &table_columns());
5242
5243    let virtual_shell = row(ui, body, "lists_tables.virtual_list.shell", 8.0);
5244    let virtual_list = widgets::virtual_list(
5245        ui,
5246        virtual_shell,
5247        "lists_tables.virtual_list",
5248        widgets::VirtualListSpec {
5249            row_count: 24,
5250            row_height: 28.0,
5251            viewport_height: 112.0,
5252            scroll_offset: state.virtual_scroll,
5253            overscan: 1,
5254        },
5255        |ui, row_parent, row| {
5256            widgets::label(
5257                ui,
5258                row_parent,
5259                format!("lists_tables.virtual_list.row.{row}"),
5260                format!("Virtual row {}", row + 1),
5261                text(12.0, color(214, 224, 238)),
5262                LayoutStyle::new()
5263                    .with_width_percent(1.0)
5264                    .with_height(28.0)
5265                    .with_flex_shrink(0.0),
5266            );
5267        },
5268    );
5269    ui.node_mut(virtual_list)
5270        .set_action("lists_tables.virtual_list.scroll");
5271    scrollbar_widgets::scrollbar(
5272        ui,
5273        virtual_shell,
5274        "lists_tables.virtual_list.scrollbar",
5275        scroll_state(state.virtual_scroll, 112.0, 24.0 * 28.0),
5276        scrollbar_widgets::ScrollAxis::Vertical,
5277        scrollbar_widgets::ScrollbarOptions::default()
5278            .with_layout(LayoutStyle::size(8.0, 112.0))
5279            .with_track_size(UiSize::new(8.0, 112.0))
5280            .with_action("lists_tables.virtual_list.scrollbar"),
5281    );
5282
5283    let table_shell = row(ui, body, "lists_tables.data_table.shell", 8.0);
5284    let table_scroll = widgets::scroll_area(
5285        ui,
5286        table_shell,
5287        "lists_tables.data_table",
5288        ScrollAxes::VERTICAL,
5289        LayoutStyle::column()
5290            .with_width(0.0)
5291            .with_flex_grow(1.0)
5292            .with_height(128.0),
5293    );
5294    ui.node_mut(table_scroll)
5295        .set_action("lists_tables.data_table.scroll");
5296    if let Some(scroll) = ui.node_mut(table_scroll).scroll_mut() {
5297        scroll.set_offset(UiPoint::new(0.0, state.table_scroll));
5298    }
5299    for row_index in 0..16 {
5300        data_table_row(ui, table_scroll, row_index, state);
5301    }
5302    scrollbar_widgets::scrollbar(
5303        ui,
5304        table_shell,
5305        "lists_tables.data_table.scrollbar",
5306        scroll_state(state.table_scroll, 128.0, 16.0 * 28.0),
5307        scrollbar_widgets::ScrollAxis::Vertical,
5308        scrollbar_widgets::ScrollbarOptions::default()
5309            .with_layout(LayoutStyle::size(8.0, 128.0))
5310            .with_track_size(UiSize::new(8.0, 128.0))
5311            .with_action("lists_tables.data_table.scrollbar"),
5312    );
5313
5314    let virtual_controls = wrapping_row(ui, body, "lists_tables.virtualized_table.controls", 8.0);
5315    button(
5316        ui,
5317        virtual_controls,
5318        "lists_tables.virtualized_table.sort.name",
5319        if state.virtual_table_descending {
5320            "Name desc"
5321        } else {
5322            "Name asc"
5323        },
5324        "lists_tables.virtualized_table.sort.name",
5325        button_visual(38, 52, 70),
5326    );
5327    button(
5328        ui,
5329        virtual_controls,
5330        "lists_tables.virtualized_table.filter.status",
5331        if state.virtual_table_ready_only {
5332            "Ready only"
5333        } else {
5334            "All status"
5335        },
5336        "lists_tables.virtualized_table.filter.status",
5337        button_visual(38, 52, 70),
5338    );
5339    button(
5340        ui,
5341        virtual_controls,
5342        "lists_tables.virtualized_table.resize.reset",
5343        "Reset width",
5344        "lists_tables.virtualized_table.resize.reset",
5345        button_visual(38, 52, 70),
5346    );
5347
5348    let columns = virtual_table_columns(state);
5349    let visible_rows = virtual_table_visible_rows(state);
5350    let mut table_options = ext_widgets::DataTableOptions::default()
5351        .with_row_action_prefix("lists_tables.virtualized_table")
5352        .with_cell_action_prefix("lists_tables.virtualized_table")
5353        .with_scroll_action("lists_tables.virtualized_table.scroll");
5354    table_options.layout = LayoutStyle::column()
5355        .with_width(0.0)
5356        .with_flex_grow(1.0)
5357        .with_flex_shrink(1.0);
5358    table_options.selection = state.table_selection.clone();
5359    let virtual_shell = row(ui, body, "lists_tables.virtualized_table.shell", 8.0);
5360    ext_widgets::virtualized_data_table(
5361        ui,
5362        virtual_shell,
5363        "lists_tables.virtualized_table",
5364        &columns,
5365        ext_widgets::VirtualDataTableSpec {
5366            row_count: visible_rows.len(),
5367            row_height: 28.0,
5368            viewport_width: 420.0,
5369            viewport_height: 128.0,
5370            scroll_offset: UiPoint::new(0.0, state.virtual_table_scroll),
5371            overscan_rows: 1,
5372        },
5373        table_options,
5374        |ui, cell_parent, cell| {
5375            let source_row = visible_rows.get(cell.row).copied().unwrap_or(cell.row);
5376            let value = virtual_table_cell_value(source_row, cell.column);
5377            widgets::label(
5378                ui,
5379                cell_parent,
5380                format!(
5381                    "lists_tables.virtualized_table.cell.{}.{}.label",
5382                    cell.row, cell.column
5383                ),
5384                value,
5385                text(12.0, color(220, 228, 238)),
5386                LayoutStyle::new().with_width_percent(1.0),
5387            );
5388        },
5389    );
5390    scrollbar_widgets::scrollbar(
5391        ui,
5392        virtual_shell,
5393        "lists_tables.virtualized_table.scrollbar",
5394        scroll_state(
5395            state.virtual_table_scroll,
5396            128.0,
5397            visible_rows.len() as f32 * 28.0,
5398        ),
5399        scrollbar_widgets::ScrollAxis::Vertical,
5400        scrollbar_widgets::ScrollbarOptions::default()
5401            .with_layout(LayoutStyle::size(8.0, 158.0))
5402            .with_track_size(UiSize::new(8.0, 158.0))
5403            .with_action("lists_tables.virtualized_table.scrollbar"),
5404    );
5405}
5406
5407fn data_table_row(ui: &mut UiDocument, parent: UiNodeId, row_index: usize, state: &ShowcaseState) {
5408    let selected = state.table_selection.contains_row(row_index);
5409    let row = ui.add_child(
5410        parent,
5411        UiNode::container(
5412            format!("lists_tables.data_table.row.{row_index}"),
5413            LayoutStyle::row()
5414                .with_width_percent(1.0)
5415                .with_height(28.0)
5416                .with_flex_shrink(0.0),
5417        )
5418        .with_input(operad::InputBehavior::BUTTON)
5419        .with_action(format!("lists_tables.data_table.row.{row_index}"))
5420        .with_visual(if selected {
5421            UiVisual::panel(color(45, 73, 109), None, 0.0)
5422        } else {
5423            UiVisual::TRANSPARENT
5424        }),
5425    );
5426    let values = [
5427        format!("Item {}", row_index + 1),
5428        if row_index % 2 == 0 {
5429            "Ready".to_string()
5430        } else {
5431            "Pending".to_string()
5432        },
5433        format!("{}%", 40 + row_index * 3),
5434    ];
5435    let widths = [0.42, 0.33, 0.25];
5436    for (column, value) in values.into_iter().enumerate() {
5437        let cell = ui.add_child(
5438            row,
5439            UiNode::container(
5440                format!("lists_tables.data_table.cell.{row_index}.{column}"),
5441                LayoutStyle::new()
5442                    .with_width_percent(widths[column])
5443                    .with_height_percent(1.0)
5444                    .padding(6.0),
5445            )
5446            .with_input(operad::InputBehavior::BUTTON)
5447            .with_action(format!("lists_tables.data_table.cell.{row_index}.{column}")),
5448        );
5449        widgets::label(
5450            ui,
5451            cell,
5452            format!("lists_tables.data_table.cell.{row_index}.{column}.label"),
5453            value,
5454            text(12.0, color(222, 230, 240)),
5455            LayoutStyle::new().with_width_percent(1.0),
5456        );
5457    }
5458}
5459
5460#[allow(clippy::field_reassign_with_default)]
5461fn property_inspector(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5462    let body = section(ui, parent, "property_inspector", "Property inspector");
5463    widgets::label(
5464        ui,
5465        body,
5466        "property_inspector.target",
5467        "Inspecting: Styling preview",
5468        text(12.0, color(196, 210, 230)),
5469        LayoutStyle::new().with_width_percent(1.0),
5470    );
5471    let mut options = ext_widgets::PropertyInspectorOptions::default();
5472    options.selected_index = Some(0);
5473    options.label_width = 120.0;
5474    options.row_height = 30.0;
5475    ext_widgets::property_inspector_grid(
5476        ui,
5477        body,
5478        "property_inspector.grid",
5479        &[
5480            ext_widgets::PropertyGridRow::new("target", "Widget", "Button preview").read_only(),
5481            ext_widgets::PropertyGridRow::new(
5482                "inner",
5483                "Inner margin",
5484                format!("{:.0}px", state.styling.inner_margin),
5485            )
5486            .with_kind(ext_widgets::PropertyValueKind::Number),
5487            ext_widgets::PropertyGridRow::new(
5488                "outer",
5489                "Outer margin",
5490                format!("{:.0}px", state.styling.outer_margin),
5491            )
5492            .with_kind(ext_widgets::PropertyValueKind::Number),
5493            ext_widgets::PropertyGridRow::new(
5494                "radius",
5495                "Corner radius",
5496                format!("{:.0}px", state.styling.corner_radius),
5497            )
5498            .with_kind(ext_widgets::PropertyValueKind::Number),
5499            ext_widgets::PropertyGridRow::new(
5500                "stroke",
5501                "Stroke",
5502                format!("{:.1}px", state.styling.stroke_width),
5503            )
5504            .with_kind(ext_widgets::PropertyValueKind::Number)
5505            .changed(),
5506            ext_widgets::PropertyGridRow::new("state", "Source", "Styling widget").read_only(),
5507        ],
5508        options,
5509    );
5510}
5511
5512fn diagnostics_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5513    let body = section(ui, parent, "diagnostics", "Diagnostics");
5514
5515    widgets::label(
5516        ui,
5517        body,
5518        "diagnostics.layout.title",
5519        "Layout and animation inspector",
5520        text(14.0, color(222, 230, 240)),
5521        LayoutStyle::new().with_width_percent(1.0),
5522    );
5523    let debug_snapshot = &state.diagnostics_snapshot;
5524    ext_widgets::debug_inspector_panel(
5525        ui,
5526        body,
5527        "diagnostics.inspector",
5528        debug_snapshot,
5529        ext_widgets::DebugInspectorPanelOptions {
5530            selected_node: Some("diagnostics.sample.preview".to_owned()),
5531            label_width: 104.0,
5532            max_layout_rows: 5,
5533            max_animation_rows: 1,
5534            show_animation: false,
5535            ..Default::default()
5536        },
5537    );
5538    ext_widgets::animation_state_graph_panel(
5539        ui,
5540        body,
5541        "diagnostics.animation.graph",
5542        debug_snapshot.animation("diagnostics.sample.preview"),
5543        ext_widgets::AnimationStateGraphPanelOptions {
5544            state_width: 72.0,
5545            state_height: 28.0,
5546            edge_row_height: 22.0,
5547            max_edges: 2,
5548            action_prefix: Some("diagnostics.animation.graph".to_owned()),
5549            ..Default::default()
5550        },
5551    );
5552    ext_widgets::animation_inspector_controls_panel(
5553        ui,
5554        body,
5555        "diagnostics.animation.controls",
5556        debug_snapshot.animation("diagnostics.sample.preview"),
5557        ext_widgets::AnimationInspectorControlsOptions {
5558            max_inputs: 3,
5559            paused: state.diagnostics_animation_paused,
5560            scrub_progress: Some(state.diagnostics_animation_scrub),
5561            action_prefix: Some("diagnostics.animation.controls".to_owned()),
5562            ..Default::default()
5563        },
5564    );
5565    widgets::label(
5566        ui,
5567        body,
5568        "diagnostics.animation.controls.status",
5569        format!(
5570            "scrub {:.0}%  hover {:.0}%  pulses {}",
5571            state.diagnostics_animation_scrub * 100.0,
5572            state.diagnostics_animation_hover * 100.0,
5573            state.diagnostics_animation_pulse_count
5574        ),
5575        text(12.0, color(166, 180, 198)),
5576        LayoutStyle::new().with_width_percent(1.0),
5577    );
5578
5579    widgets::label(
5580        ui,
5581        body,
5582        "diagnostics.a11y.title",
5583        "Accessibility overlay",
5584        text(14.0, color(222, 230, 240)),
5585        LayoutStyle::new().with_width_percent(1.0),
5586    );
5587    let mut overlay_preview_style = UiNodeStyle::from(
5588        LayoutStyle::new()
5589            .with_width(320.0)
5590            .with_height(140.0)
5591            .with_flex_shrink(0.0),
5592    );
5593    overlay_preview_style.set_clip(ClipBehavior::Clip);
5594    let overlay_preview = ui.add_child(
5595        body,
5596        UiNode::container("diagnostics.a11y.preview", overlay_preview_style).with_visual(
5597            UiVisual::panel(
5598                color(12, 17, 24),
5599                Some(StrokeStyle::new(color(47, 62, 82), 1.0)),
5600                4.0,
5601            ),
5602        ),
5603    );
5604    let mut overlay_options = ext_widgets::AccessibilityDebugOverlayOptions {
5605        action_prefix: Some("diagnostics.a11y.visual".to_owned()),
5606        ..Default::default()
5607    };
5608    overlay_options.show_labels = false;
5609    ext_widgets::accessibility_debug_overlay(
5610        ui,
5611        overlay_preview,
5612        "diagnostics.a11y.visual",
5613        &debug_snapshot,
5614        overlay_options,
5615    );
5616    ext_widgets::accessibility_overlay_panel(
5617        ui,
5618        body,
5619        "diagnostics.a11y",
5620        &debug_snapshot,
5621        ext_widgets::AccessibilityOverlayPanelOptions {
5622            label_width: 118.0,
5623            max_rows: 1,
5624            action_prefix: Some("diagnostics.a11y".to_owned()),
5625            ..Default::default()
5626        },
5627    );
5628
5629    let diagnostic_columns = ui.add_child(
5630        body,
5631        UiNode::container(
5632            "diagnostics.columns",
5633            LayoutStyle::column()
5634                .with_width_percent(1.0)
5635                .with_flex_shrink(0.0)
5636                .gap(10.0),
5637        ),
5638    );
5639    let command_column = ui.add_child(
5640        diagnostic_columns,
5641        UiNode::container(
5642            "diagnostics.commands.column",
5643            LayoutStyle::column()
5644                .with_width_percent(1.0)
5645                .with_flex_shrink(0.0)
5646                .gap(8.0),
5647        ),
5648    );
5649    let theme_column = ui.add_child(
5650        diagnostic_columns,
5651        UiNode::container(
5652            "diagnostics.theme.column",
5653            LayoutStyle::column()
5654                .with_width_percent(1.0)
5655                .with_flex_shrink(0.0)
5656                .gap(8.0),
5657        ),
5658    );
5659
5660    widgets::label(
5661        ui,
5662        command_column,
5663        "diagnostics.commands.title",
5664        "Command registry",
5665        text(14.0, color(222, 230, 240)),
5666        LayoutStyle::new().with_width_percent(1.0),
5667    );
5668    let registry = diagnostics_command_registry();
5669    ext_widgets::command_diagnostics_panel(
5670        ui,
5671        command_column,
5672        "diagnostics.commands",
5673        &registry,
5674        &[CommandScope::Global, CommandScope::Panel],
5675        &ShortcutFormatter::default(),
5676        ext_widgets::CommandDiagnosticsPanelOptions {
5677            label_width: 92.0,
5678            max_command_rows: 3,
5679            max_conflict_rows: 1,
5680            action_prefix: Some("diagnostics.commands".to_owned()),
5681            ..Default::default()
5682        },
5683    );
5684
5685    widgets::label(
5686        ui,
5687        theme_column,
5688        "diagnostics.theme.title",
5689        "Theme editor",
5690        text(14.0, color(222, 230, 240)),
5691        LayoutStyle::new().with_width_percent(1.0),
5692    );
5693    let theme_snapshot = DebugThemeSnapshot::from_theme(&Theme::dark());
5694    ext_widgets::theme_editor_panel(
5695        ui,
5696        theme_column,
5697        "diagnostics.theme",
5698        &theme_snapshot,
5699        ext_widgets::ThemeEditorPanelOptions {
5700            label_width: 92.0,
5701            max_token_rows: 1,
5702            max_component_rows: 1,
5703            action_prefix: Some("diagnostics.theme".to_owned()),
5704            ..Default::default()
5705        },
5706    );
5707}
5708
5709fn diagnostics_sample_snapshot(state: &ShowcaseState) -> DebugInspectorSnapshot {
5710    diagnostics_sample_snapshot_for(
5711        state.diagnostics_animation_hover,
5712        state.diagnostics_animation_active,
5713    )
5714}
5715
5716fn diagnostics_sample_snapshot_for(hover: f32, active: bool) -> DebugInspectorSnapshot {
5717    let mut sample = UiDocument::new(root_style(320.0, 180.0));
5718    let card = sample.add_child(
5719        sample.root(),
5720        UiNode::container(
5721            "diagnostics.sample.card",
5722            LayoutStyle::column()
5723                .with_width_percent(1.0)
5724                .with_height(120.0)
5725                .padding(12.0)
5726                .gap(8.0),
5727        )
5728        .with_visual(UiVisual::panel(
5729            color(16, 22, 30),
5730            Some(StrokeStyle::new(color(62, 77, 98), 1.0)),
5731            6.0,
5732        ))
5733        .with_accessibility(
5734            AccessibilityMeta::new(AccessibilityRole::Group).label("Diagnostics sample"),
5735        ),
5736    );
5737    sample.add_child(
5738        card,
5739        UiNode::container(
5740            "diagnostics.sample.preview",
5741            LayoutStyle::new().with_width(160.0).with_height(38.0),
5742        )
5743        .with_input(InputBehavior::BUTTON)
5744        .with_visual(UiVisual::panel(
5745            color(52, 112, 180),
5746            Some(StrokeStyle::new(color(116, 183, 255), 1.0)),
5747            5.0,
5748        ))
5749        .with_accessibility(
5750            AccessibilityMeta::new(AccessibilityRole::Button)
5751                .label("Preview action")
5752                .focusable(),
5753        )
5754        .with_animation(
5755            AnimationMachine::new(
5756                vec![
5757                    AnimationState::new(
5758                        "idle",
5759                        AnimatedValues::new(1.0, UiPoint::new(0.0, 0.0), 1.0),
5760                    ),
5761                    AnimationState::new(
5762                        "hot",
5763                        AnimatedValues::new(0.92, UiPoint::new(18.0, 0.0), 1.08),
5764                    ),
5765                ],
5766                vec![AnimationTransition::when(
5767                    "idle",
5768                    "hot",
5769                    AnimationCondition::bool("active", true),
5770                    0.18,
5771                )],
5772                "idle",
5773            )
5774            .expect("sample animation")
5775            .with_number_input("hover", hover)
5776            .with_blend_binding(AnimationBlendBinding::new("hover", "idle", "hot"))
5777            .with_bool_input("active", active)
5778            .with_trigger_input("pulse"),
5779        ),
5780    );
5781    widgets::label(
5782        &mut sample,
5783        card,
5784        "diagnostics.sample.label",
5785        "Sample node",
5786        text(12.0, color(198, 210, 226)),
5787        LayoutStyle::new().with_width_percent(1.0),
5788    );
5789    sample
5790        .compute_layout(UiSize::new(320.0, 180.0), &mut ApproxTextMeasurer)
5791        .expect("sample layout");
5792    DebugInspectorSnapshot::from_document(&sample, &mut ApproxTextMeasurer)
5793}
5794
5795fn diagnostics_command_registry() -> CommandRegistry {
5796    let mut registry = CommandRegistry::new();
5797    registry
5798        .register(
5799            CommandMeta::new("diagnostics.palette", "Open command palette")
5800                .description("Show command search")
5801                .category("Debug"),
5802        )
5803        .expect("command");
5804    registry
5805        .register(
5806            CommandMeta::new("diagnostics.inspect", "Inspect selected node")
5807                .description("Focus the layout inspector")
5808                .category("Debug"),
5809        )
5810        .expect("command");
5811    registry
5812        .register(
5813            CommandMeta::new("diagnostics.record", "Start interaction recording")
5814                .description("Capture replay steps")
5815                .category("Testing"),
5816        )
5817        .expect("command");
5818    registry
5819        .register(CommandMeta::new(
5820            "diagnostics.export_theme",
5821            "Export theme patch",
5822        ))
5823        .expect("command");
5824    registry
5825        .bind_shortcut(
5826            CommandScope::Global,
5827            Shortcut::ctrl('k'),
5828            "diagnostics.palette",
5829        )
5830        .expect("shortcut");
5831    registry
5832        .bind_shortcut(
5833            CommandScope::Panel,
5834            Shortcut::ctrl('i'),
5835            "diagnostics.inspect",
5836        )
5837        .expect("shortcut");
5838    registry
5839        .bind_shortcut(
5840            CommandScope::Panel,
5841            Shortcut::ctrl('r'),
5842            "diagnostics.record",
5843        )
5844        .expect("shortcut");
5845    registry
5846        .disable("diagnostics.export_theme", "No changes to export")
5847        .expect("disable");
5848    registry
5849}
5850
5851fn tree_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5852    let body = section(ui, parent, "trees", "Tree view");
5853    ext_widgets::tree_view(
5854        ui,
5855        body,
5856        "trees.tree_view",
5857        &tree_items(),
5858        &state.tree,
5859        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.tree"),
5860    );
5861    ext_widgets::outliner(
5862        ui,
5863        body,
5864        "trees.outliner",
5865        &tree_items(),
5866        &state.outliner,
5867        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.outliner"),
5868    );
5869    let virtual_state = ext_widgets::TreeViewState::expanded(["root"]);
5870    let virtual_nodes = ext_widgets::virtualized_tree_view(
5871        ui,
5872        body,
5873        "trees.virtual",
5874        &virtual_tree_items(),
5875        &virtual_state,
5876        ext_widgets::VirtualTreeViewSpec::new(24.0, 112.0)
5877            .scroll_offset(state.tree_virtual_scroll)
5878            .overscan_rows(1),
5879        ext_widgets::TreeViewOptions::default().with_row_action_prefix("trees.virtual"),
5880    );
5881    ui.node_mut(virtual_nodes.body)
5882        .set_action("trees.virtual.scroll");
5883    tree_table_widgets(ui, body, state);
5884}
5885
5886fn tree_table_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5887    let tree_state = ext_widgets::TreeViewState::expanded(["root", "branch-a"]);
5888    let rows = tree_state.visible_items(&tree_table_items());
5889    let columns = [
5890        ext_widgets::DataTableColumn::new("name", "Name", 220.0),
5891        ext_widgets::DataTableColumn::new("kind", "Kind", 84.0),
5892        ext_widgets::DataTableColumn::new("status", "Status", 92.0),
5893    ];
5894    let mut options = ext_widgets::DataTableOptions::default()
5895        .with_row_action_prefix("trees.table")
5896        .with_cell_action_prefix("trees.table");
5897    options.layout = LayoutStyle::column()
5898        .with_width_percent(1.0)
5899        .with_height(132.0)
5900        .with_flex_shrink(0.0);
5901    ext_widgets::virtualized_data_table(
5902        ui,
5903        parent,
5904        "trees.table",
5905        &columns,
5906        ext_widgets::VirtualDataTableSpec {
5907            row_count: rows.len(),
5908            row_height: 24.0,
5909            viewport_width: 396.0,
5910            viewport_height: 96.0,
5911            scroll_offset: UiPoint::new(0.0, state.tree_virtual_scroll),
5912            overscan_rows: 1,
5913        },
5914        options,
5915        |ui, cell_parent, cell| {
5916            let value = rows
5917                .get(cell.row)
5918                .map(|item| tree_table_cell_value(item, cell.column))
5919                .unwrap_or_default();
5920            widgets::label(
5921                ui,
5922                cell_parent,
5923                format!("trees.table.cell.{}.{}.label", cell.row, cell.column),
5924                value,
5925                text(12.0, color(220, 228, 238)),
5926                LayoutStyle::new().with_width_percent(1.0),
5927            );
5928        },
5929    );
5930}
5931
5932fn tree_table_cell_value(item: &ext_widgets::TreeVisibleItem, column: usize) -> String {
5933    match column {
5934        0 => format!("{}{}", "  ".repeat(item.depth), item.label),
5935        1 => {
5936            if item.has_children() {
5937                "Folder".to_owned()
5938            } else {
5939                "File".to_owned()
5940            }
5941        }
5942        _ => {
5943            if item.disabled {
5944                "Locked".to_owned()
5945            } else if item.expanded {
5946                "Expanded".to_owned()
5947            } else {
5948                "Ready".to_owned()
5949            }
5950        }
5951    }
5952}
5953
5954fn tab_split_dock_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
5955    let body = section_with_min_viewport(
5956        ui,
5957        parent,
5958        "layout_widgets",
5959        "Dock workspace",
5960        UiSize::new(546.0, 360.0),
5961    );
5962    let shell = ui.add_child(
5963        body,
5964        UiNode::container(
5965            "layout_widgets.dock_shell",
5966            LayoutStyle::column()
5967                .with_width_percent(1.0)
5968                .with_height(360.0)
5969                .with_flex_shrink(0.0),
5970        )
5971        .with_visual(UiVisual::panel(
5972            color(13, 17, 23),
5973            Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
5974            0.0,
5975        )),
5976    );
5977
5978    let mut panels = base_layout_dock_panels();
5979    state.layout_dock.apply_order_to_panels(&mut panels);
5980    state.layout_dock.apply_visibility_to_panels(&mut panels);
5981
5982    let mut drawer_options = ext_widgets::DockDrawerRailOptions::default();
5983    drawer_options.layout = LayoutStyle::row()
5984        .with_width_percent(1.0)
5985        .with_height(34.0)
5986        .with_padding(4.0)
5987        .with_gap(4.0);
5988    ext_widgets::dock_drawer_rail(
5989        ui,
5990        shell,
5991        "layout_widgets.dock.drawers",
5992        &[
5993            ext_widgets::DockDrawerDescriptor::new(
5994                "inspector",
5995                "Inspector",
5996                "inspector",
5997                ext_widgets::DockSide::Left,
5998            )
5999            .open(!state.layout_dock.is_hidden("inspector"))
6000            .with_action("layout_widgets.drawer.inspector"),
6001            ext_widgets::DockDrawerDescriptor::new(
6002                "assets",
6003                "Assets",
6004                "assets",
6005                ext_widgets::DockSide::Right,
6006            )
6007            .open(!state.layout_dock.is_hidden("assets"))
6008            .with_action("layout_widgets.drawer.assets"),
6009        ],
6010        drawer_options,
6011    );
6012
6013    let mut options = ext_widgets::DockWorkspaceOptions::default();
6014    options.layout = LayoutStyle::column()
6015        .with_width_percent(1.0)
6016        .with_height(0.0)
6017        .with_flex_grow(1.0);
6018    options.show_titles = false;
6019    options.panel_visual = UiVisual::panel(
6020        color(18, 22, 29),
6021        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
6022        0.0,
6023    );
6024    options.center_visual = UiVisual::panel(
6025        color(15, 19, 25),
6026        Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
6027        0.0,
6028    );
6029
6030    ext_widgets::dock_workspace(
6031        ui,
6032        shell,
6033        "layout_widgets.dock",
6034        &panels,
6035        options,
6036        |ui, parent, panel| match panel.id.as_str() {
6037            "inspector" => egui_panel_contents(
6038                ui,
6039                parent,
6040                "layout.inspector",
6041                "Inspector",
6042                state.layout_inspector_scroll,
6043            ),
6044            "assets" => egui_panel_contents(
6045                ui,
6046                parent,
6047                "layout.assets",
6048                "Assets",
6049                state.layout_assets_scroll,
6050            ),
6051            _ => dock_document_panel(ui, parent, state),
6052        },
6053    );
6054
6055    if let Some(floating) = state.layout_dock.floating_panel("inspector") {
6056        let floating_panel = ui.add_child(
6057            shell,
6058            UiNode::container(
6059                "layout_widgets.floating.inspector",
6060                operad::layout::absolute(
6061                    floating.rect.x,
6062                    floating.rect.y,
6063                    floating.rect.width,
6064                    floating.rect.height,
6065                ),
6066            )
6067            .with_visual(UiVisual::panel(
6068                color(18, 22, 29),
6069                Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
6070                4.0,
6071            )),
6072        );
6073        egui_panel_contents(
6074            ui,
6075            floating_panel,
6076            "layout.inspector_floating",
6077            "Inspector",
6078            state.layout_inspector_scroll,
6079        );
6080    }
6081}
6082
6083fn base_layout_dock_panels() -> Vec<ext_widgets::DockPanelDescriptor> {
6084    vec![
6085        ext_widgets::DockPanelDescriptor::new(
6086            "inspector",
6087            "Inspector",
6088            ext_widgets::DockSide::Left,
6089            120.0,
6090        )
6091        .with_min_size(104.0)
6092        .resizable(true),
6093        ext_widgets::DockPanelDescriptor::center("document", "Document"),
6094        ext_widgets::DockPanelDescriptor::new(
6095            "assets",
6096            "Assets",
6097            ext_widgets::DockSide::Right,
6098            104.0,
6099        )
6100        .with_min_size(94.0)
6101        .resizable(true),
6102    ]
6103}
6104
6105fn dock_document_panel(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6106    let content = ui.add_child(
6107        parent,
6108        UiNode::container(
6109            "layout_widgets.document.content",
6110            LayoutStyle::column()
6111                .with_width_percent(1.0)
6112                .with_height_percent(1.0)
6113                .with_padding(8.0)
6114                .with_gap(8.0),
6115        ),
6116    );
6117
6118    let controls = wrapping_row(ui, content, "layout_widgets.dock.controls", 8.0);
6119    let (action, label) = if state.layout_dock.is_floating("inspector") {
6120        ("layout_widgets.dock_inspector", "Dock inspector")
6121    } else {
6122        ("layout_widgets.float_inspector", "Float inspector")
6123    };
6124    let mut float_button = widgets::ButtonOptions::new(
6125        LayoutStyle::new()
6126            .with_width(132.0)
6127            .with_height(28.0)
6128            .with_flex_shrink(0.0),
6129    )
6130    .with_action(action);
6131    float_button.visual = button_visual(40, 52, 68);
6132    float_button.hovered_visual = Some(button_visual(54, 70, 92));
6133    float_button.text_style = text(12.0, color(232, 238, 248));
6134    widgets::button(
6135        ui,
6136        controls,
6137        "layout_widgets.dock.float_inspector",
6138        label,
6139        float_button,
6140    );
6141
6142    let mut before_button = widgets::ButtonOptions::new(
6143        LayoutStyle::new()
6144            .with_width(136.0)
6145            .with_height(28.0)
6146            .with_flex_shrink(0.0),
6147    )
6148    .with_action("layout_widgets.reorder.assets.before.inspector");
6149    before_button.visual = button_visual(34, 44, 58);
6150    before_button.hovered_visual = Some(button_visual(48, 64, 84));
6151    before_button.text_style = text(12.0, color(232, 238, 248));
6152    widgets::button(
6153        ui,
6154        controls,
6155        "layout_widgets.dock.assets_before_inspector",
6156        "Assets before",
6157        before_button,
6158    );
6159
6160    let mut after_button = widgets::ButtonOptions::new(
6161        LayoutStyle::new()
6162            .with_width(126.0)
6163            .with_height(28.0)
6164            .with_flex_shrink(0.0),
6165    )
6166    .with_action("layout_widgets.reorder.assets.after.inspector");
6167    after_button.visual = button_visual(34, 44, 58);
6168    after_button.hovered_visual = Some(button_visual(48, 64, 84));
6169    after_button.text_style = text(12.0, color(232, 238, 248));
6170    widgets::button(
6171        ui,
6172        controls,
6173        "layout_widgets.dock.assets_after_inspector",
6174        "Assets after",
6175        after_button,
6176    );
6177
6178    let zones = ext_widgets::dock_workspace::dock_workspace_drop_zones(
6179        "layout_widgets.dock",
6180        UiRect::new(0.0, 0.0, 520.0, 340.0),
6181        ext_widgets::DockWorkspaceDragOptions::default()
6182            .allowed_sides([
6183                ext_widgets::DockSide::Left,
6184                ext_widgets::DockSide::Right,
6185                ext_widgets::DockSide::Center,
6186            ])
6187            .edge_thickness(44.0),
6188    );
6189    let targets = wrapping_row(ui, content, "layout_widgets.dock.targets", 6.0);
6190    for zone in zones {
6191        dock_drop_target_chip(ui, targets, &zone);
6192    }
6193
6194    let mut panels = base_layout_dock_panels();
6195    state.layout_dock.apply_order_to_panels(&mut panels);
6196    let reorder_targets: Vec<_> = [
6197        ext_widgets::DockSide::Left,
6198        ext_widgets::DockSide::Right,
6199        ext_widgets::DockSide::Center,
6200    ]
6201    .into_iter()
6202    .flat_map(|side| {
6203        ext_widgets::dock_workspace::dock_panel_reorder_drop_targets(
6204            "layout_widgets.dock",
6205            &panels,
6206            side,
6207            UiRect::new(0.0, 0.0, 180.0, 120.0),
6208            ext_widgets::DockWorkspaceReorderOptions::default().target_thickness(20.0),
6209        )
6210    })
6211    .collect();
6212    let reorder_row = wrapping_row(ui, content, "layout_widgets.dock.reorder_targets", 6.0);
6213    for target in reorder_targets {
6214        dock_reorder_target_chip(ui, reorder_row, &target);
6215    }
6216
6217    let tabs = [
6218        ext_widgets::TabItem::new("preview", "Preview"),
6219        ext_widgets::TabItem::new("log", "Output").dirty(),
6220        ext_widgets::TabItem::new("settings", "Settings").closable(),
6221    ];
6222    let mut tab_options = ext_widgets::TabGroupOptions::default();
6223    tab_options.layout = LayoutStyle::column()
6224        .with_width_percent(1.0)
6225        .with_height(0.0)
6226        .with_flex_grow(1.0);
6227    tab_options.tab_strip_height = 30.0;
6228    tab_options.min_tab_width = 92.0;
6229    tab_options.text_style = text(12.0, color(226, 234, 246));
6230    tab_options.muted_text_style = text(12.0, color(150, 162, 178));
6231    ext_widgets::tab_group(
6232        ui,
6233        content,
6234        "layout_widgets.document.tabs",
6235        &tabs,
6236        ext_widgets::TabGroupState::selected(0),
6237        tab_options,
6238        |ui, panel, _index| {
6239            widgets::label(
6240                ui,
6241                panel,
6242                "layout_widgets.document.tabs.preview.body",
6243                "Workspace preview",
6244                text(12.0, color(190, 202, 218)),
6245                LayoutStyle::new().with_width_percent(1.0).with_height(26.0),
6246            );
6247        },
6248    );
6249}
6250
6251fn dock_drop_target_chip(
6252    ui: &mut UiDocument,
6253    parent: UiNodeId,
6254    zone: &ext_widgets::DockWorkspaceDropZone,
6255) -> UiNodeId {
6256    let chip = ui.add_child(
6257        parent,
6258        UiNode::container(
6259            format!("{}.chip", zone.target.id.as_str()),
6260            LayoutStyle::row()
6261                .with_width(78.0)
6262                .with_height(26.0)
6263                .with_padding(6.0)
6264                .with_flex_shrink(0.0),
6265        )
6266        .with_input(InputBehavior::BUTTON)
6267        .with_visual(UiVisual::panel(
6268            color(24, 32, 42),
6269            Some(StrokeStyle::new(color(78, 94, 116), 1.0)),
6270            4.0,
6271        ))
6272        .with_accessibility(zone.target.accessibility_meta()),
6273    );
6274    widgets::label(
6275        ui,
6276        chip,
6277        format!("{}.label", zone.target.id.as_str()),
6278        dock_drop_target_short_label(zone.placement),
6279        text(11.0, color(206, 216, 230)),
6280        LayoutStyle::new().with_width_percent(1.0),
6281    );
6282    chip
6283}
6284
6285fn dock_reorder_target_chip(
6286    ui: &mut UiDocument,
6287    parent: UiNodeId,
6288    target: &ext_widgets::DockPanelReorderTarget,
6289) -> UiNodeId {
6290    let chip = ui.add_child(
6291        parent,
6292        UiNode::container(
6293            format!("{}.chip", target.target.id.as_str()),
6294            LayoutStyle::row()
6295                .with_width(104.0)
6296                .with_height(26.0)
6297                .with_padding(6.0)
6298                .with_flex_shrink(0.0),
6299        )
6300        .with_input(InputBehavior::BUTTON)
6301        .with_visual(UiVisual::panel(
6302            color(22, 34, 42),
6303            Some(StrokeStyle::new(color(80, 112, 128), 1.0)),
6304            4.0,
6305        ))
6306        .with_accessibility(target.target.accessibility_meta()),
6307    );
6308    widgets::label(
6309        ui,
6310        chip,
6311        format!("{}.label", target.target.id.as_str()),
6312        dock_reorder_target_short_label(target),
6313        text(11.0, color(206, 216, 230)),
6314        LayoutStyle::new().with_width_percent(1.0),
6315    );
6316    chip
6317}
6318
6319fn dock_drop_target_short_label(placement: ext_widgets::DockDropPlacement) -> &'static str {
6320    match placement {
6321        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Left) => "Left",
6322        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Right) => "Right",
6323        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Center) => "Center",
6324        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Top) => "Top",
6325        ext_widgets::DockDropPlacement::Dock(ext_widgets::DockSide::Bottom) => "Bottom",
6326        ext_widgets::DockDropPlacement::Floating => "Float",
6327    }
6328}
6329
6330fn dock_reorder_target_short_label(target: &ext_widgets::DockPanelReorderTarget) -> String {
6331    let placement = match target.placement {
6332        ext_widgets::DockPanelReorderPlacement::Before => "Before",
6333        ext_widgets::DockPanelReorderPlacement::After => "After",
6334    };
6335    format!("{placement} {}", target.panel_id)
6336}
6337
6338fn container_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6339    let body = section(ui, parent, "containers", "Containers");
6340
6341    let frame = widgets::frame(
6342        ui,
6343        body,
6344        "containers.frame",
6345        widgets::FrameOptions::default().with_layout(
6346            LayoutStyle::column()
6347                .with_width_percent(1.0)
6348                .with_height(64.0)
6349                .with_padding(8.0)
6350                .with_gap(6.0),
6351        ),
6352    );
6353    widgets::strong_label(
6354        ui,
6355        frame,
6356        "containers.frame.title",
6357        "Frame",
6358        LayoutStyle::new().with_width_percent(1.0),
6359    );
6360    widgets::weak_label(
6361        ui,
6362        frame,
6363        "containers.frame.body",
6364        "Default framed surface with padding, stroke, and clipping.",
6365        LayoutStyle::new().with_width_percent(1.0),
6366    );
6367
6368    let group = widgets::group(ui, body, "containers.group");
6369    widgets::label(
6370        ui,
6371        group,
6372        "containers.group.label",
6373        "Group helper",
6374        text(12.0, color(220, 228, 238)),
6375        LayoutStyle::new().with_width_percent(1.0),
6376    );
6377    let generic_panel = widgets::panel(
6378        ui,
6379        body,
6380        "containers.panel",
6381        widgets::PanelOptions::group().with_layout(
6382            LayoutStyle::column()
6383                .with_width_percent(1.0)
6384                .with_height(44.0)
6385                .with_padding(8.0),
6386        ),
6387    );
6388    widgets::label(
6389        ui,
6390        generic_panel,
6391        "containers.panel.label",
6392        "Generic panel",
6393        text(12.0, color(220, 228, 238)),
6394        LayoutStyle::new().with_width_percent(1.0),
6395    );
6396    let group_panel = widgets::group_panel(ui, body, "containers.group_panel");
6397    widgets::label(
6398        ui,
6399        group_panel,
6400        "containers.group_panel.label",
6401        "Group panel",
6402        text(12.0, color(220, 228, 238)),
6403        LayoutStyle::new().with_width_percent(1.0),
6404    );
6405
6406    widgets::separator(
6407        ui,
6408        body,
6409        "containers.separator",
6410        widgets::SeparatorOptions::default(),
6411    );
6412    widgets::spacer(
6413        ui,
6414        body,
6415        "containers.spacer",
6416        LayoutStyle::new()
6417            .with_width_percent(1.0)
6418            .with_height(8.0)
6419            .with_flex_shrink(0.0),
6420    );
6421
6422    let grid = widgets::grid::grid(
6423        ui,
6424        body,
6425        "containers.grid",
6426        widgets::grid::GridOptions::default().with_layout(
6427            LayoutStyle::column()
6428                .with_width_percent(1.0)
6429                .with_height(78.0)
6430                .with_gap(4.0),
6431        ),
6432    );
6433    for row_index in 0..2 {
6434        let row = widgets::grid::grid_row(
6435            ui,
6436            grid,
6437            format!("containers.grid.row.{row_index}"),
6438            widgets::grid::GridRowOptions::default(),
6439        );
6440        for column_index in 0..3 {
6441            widgets::grid::grid_text_cell(
6442                ui,
6443                row,
6444                format!("containers.grid.row.{row_index}.cell.{column_index}"),
6445                format!("R{} C{}", row_index + 1, column_index + 1),
6446                widgets::grid::GridCellOptions {
6447                    text_style: text(12.0, color(214, 224, 238)),
6448                    ..Default::default()
6449                },
6450            );
6451        }
6452    }
6453
6454    widgets::sides(
6455        ui,
6456        body,
6457        "containers.sides",
6458        widgets::SidesOptions::default()
6459            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
6460            .with_gap(8.0)
6461            .with_visual(UiVisual::panel(
6462                color(20, 25, 32),
6463                Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
6464                4.0,
6465            )),
6466        |ui, left| {
6467            widgets::label(
6468                ui,
6469                left,
6470                "containers.sides.left.label",
6471                "Left side",
6472                text(12.0, color(220, 228, 238)),
6473                LayoutStyle::new().with_width_percent(1.0),
6474            );
6475        },
6476        |ui, right| {
6477            widgets::label(
6478                ui,
6479                right,
6480                "containers.sides.right.label",
6481                "Right side",
6482                text(12.0, color(220, 228, 238)),
6483                LayoutStyle::new().with_width_percent(1.0),
6484            );
6485        },
6486    );
6487
6488    widgets::columns(
6489        ui,
6490        body,
6491        "containers.columns",
6492        3,
6493        widgets::ColumnsOptions::default()
6494            .with_layout(LayoutStyle::row().with_width_percent(1.0).with_height(48.0))
6495            .with_gap(8.0),
6496        |ui, column, index| {
6497            widgets::label(
6498                ui,
6499                column,
6500                format!("containers.columns.{index}.label"),
6501                format!("Column {}", index + 1),
6502                text(12.0, color(220, 228, 238)),
6503                LayoutStyle::new().with_width_percent(1.0),
6504            );
6505        },
6506    );
6507
6508    let indented = widgets::indented_section(
6509        ui,
6510        body,
6511        "containers.indented",
6512        widgets::IndentOptions::default().with_amount(24.0),
6513    );
6514    widgets::label(
6515        ui,
6516        indented,
6517        "containers.indented.label",
6518        "Indented section",
6519        text(12.0, color(196, 210, 230)),
6520        LayoutStyle::new().with_width_percent(1.0),
6521    );
6522
6523    widgets::resize_container(
6524        ui,
6525        body,
6526        "containers.resize_container",
6527        widgets::ResizeContainerOptions::default().with_layout(
6528            LayoutStyle::column()
6529                .with_width_percent(1.0)
6530                .with_height(92.0)
6531                .with_flex_shrink(0.0),
6532        ),
6533        |ui, content| {
6534            widgets::label(
6535                ui,
6536                content,
6537                "containers.resize_container.label",
6538                "Resize container",
6539                text(12.0, color(220, 228, 238)),
6540                LayoutStyle::new().with_width_percent(1.0),
6541            );
6542        },
6543    );
6544    widgets::container::resize_handle(
6545        ui,
6546        body,
6547        "containers.resize_handle",
6548        widgets::container::ResizeHandleOptions::default()
6549            .with_layout(LayoutStyle::size(20.0, 20.0))
6550            .accessibility_label("Inline resize handle"),
6551    );
6552
6553    widgets::scene(
6554        ui,
6555        body,
6556        "containers.scene",
6557        vec![
6558            ScenePrimitive::Rect(
6559                PaintRect::solid(UiRect::new(8.0, 12.0, 108.0, 46.0), color(48, 112, 184))
6560                    .stroke(AlignedStroke::inside(StrokeStyle::new(
6561                        color(132, 174, 222),
6562                        1.0,
6563                    )))
6564                    .corner_radii(CornerRadii::uniform(6.0)),
6565            ),
6566            ScenePrimitive::Circle {
6567                center: UiPoint::new(150.0, 35.0),
6568                radius: 22.0,
6569                fill: color(111, 203, 159),
6570                stroke: Some(StrokeStyle::new(color(176, 236, 206), 1.0)),
6571            },
6572            ScenePrimitive::Line {
6573                from: UiPoint::new(188.0, 18.0),
6574                to: UiPoint::new(238.0, 52.0),
6575                stroke: StrokeStyle::new(color(232, 186, 88), 3.0),
6576            },
6577        ],
6578        widgets::SceneOptions::default()
6579            .with_layout(LayoutStyle::new().with_width(260.0).with_height(70.0))
6580            .accessibility_label("Scene primitives"),
6581    );
6582
6583    let panel_shell = widgets::frame(
6584        ui,
6585        body,
6586        "containers.panels",
6587        widgets::FrameOptions::default().with_layout(
6588            LayoutStyle::column()
6589                .with_width_percent(1.0)
6590                .with_height(160.0)
6591                .with_padding(0.0)
6592                .with_gap(0.0),
6593        ),
6594    );
6595    let top = widgets::top_panel(ui, panel_shell, "containers.panels.top", 28.0);
6596    widgets::label(
6597        ui,
6598        top,
6599        "containers.panels.top.label",
6600        "Top panel",
6601        text(12.0, color(220, 228, 238)),
6602        LayoutStyle::new().with_width_percent(1.0),
6603    );
6604    let middle = row(ui, panel_shell, "containers.panels.middle", 0.0);
6605    let left = widgets::side_panel(
6606        ui,
6607        middle,
6608        "containers.panels.side",
6609        widgets::SidePanelSide::Left,
6610        90.0,
6611    );
6612    widgets::label(
6613        ui,
6614        left,
6615        "containers.panels.side.label",
6616        "Side",
6617        text(12.0, color(220, 228, 238)),
6618        LayoutStyle::new().with_width_percent(1.0),
6619    );
6620    let left = widgets::left_panel(ui, middle, "containers.panels.left", 90.0);
6621    widgets::label(
6622        ui,
6623        left,
6624        "containers.panels.left.label",
6625        "Left",
6626        text(12.0, color(220, 228, 238)),
6627        LayoutStyle::new().with_width_percent(1.0),
6628    );
6629    let center = widgets::central_panel(ui, middle, "containers.panels.center");
6630    widgets::label(
6631        ui,
6632        center,
6633        "containers.panels.center.label",
6634        "Central panel",
6635        text(12.0, color(220, 228, 238)),
6636        LayoutStyle::new().with_width_percent(1.0),
6637    );
6638    let right = widgets::right_panel(ui, middle, "containers.panels.right", 110.0);
6639    widgets::label(
6640        ui,
6641        right,
6642        "containers.panels.right.label",
6643        "Right",
6644        text(12.0, color(220, 228, 238)),
6645        LayoutStyle::new().with_width_percent(1.0),
6646    );
6647    let bottom = widgets::bottom_panel(ui, panel_shell, "containers.panels.bottom", 28.0);
6648    widgets::label(
6649        ui,
6650        bottom,
6651        "containers.panels.bottom.label",
6652        "Bottom panel",
6653        text(12.0, color(220, 228, 238)),
6654        LayoutStyle::new().with_width_percent(1.0),
6655    );
6656
6657    widgets::scroll_container(
6658        ui,
6659        body,
6660        "containers.scroll_area_with_bars",
6661        state.containers_scroll,
6662        widgets::ScrollContainerOptions::default()
6663            .with_axes(ScrollAxes::BOTH)
6664            .with_layout(LayoutStyle::column().with_width(300.0).with_height(116.0)),
6665        |ui, viewport| {
6666            for index in 0..5 {
6667                widgets::label(
6668                    ui,
6669                    viewport,
6670                    format!("containers.scroll_area_with_bars.row.{index}"),
6671                    format!("Scrollable row {}", index + 1),
6672                    text(12.0, color(200, 212, 228)),
6673                    LayoutStyle::new()
6674                        .with_width(420.0)
6675                        .with_height(28.0)
6676                        .with_flex_shrink(0.0),
6677                );
6678            }
6679        },
6680    );
6681
6682    let area_host = ui.add_child(
6683        body,
6684        UiNode::container(
6685            "containers.area.host",
6686            LayoutStyle::new()
6687                .with_width_percent(1.0)
6688                .with_height(82.0)
6689                .with_flex_shrink(0.0),
6690        )
6691        .with_visual(UiVisual::panel(
6692            color(17, 20, 25),
6693            Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
6694            4.0,
6695        )),
6696    );
6697    widgets::container::area(
6698        ui,
6699        area_host,
6700        "containers.area",
6701        widgets::container::AreaOptions::new(UiRect::new(14.0, 14.0, 180.0, 44.0))
6702            .with_visual(UiVisual::panel(color(39, 72, 109), None, 4.0))
6703            .accessibility_label("Absolute positioned area"),
6704        |ui, area| {
6705            widgets::label(
6706                ui,
6707                area,
6708                "containers.area.label",
6709                "Area",
6710                text(12.0, color(238, 244, 252)),
6711                LayoutStyle::new().with_width_percent(1.0),
6712            );
6713        },
6714    );
6715}
6716
6717fn form_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6718    let body = section_with_min_viewport(ui, parent, "forms", "Forms", UiSize::new(390.0, 0.0));
6719    let section = widgets::form_section(
6720        ui,
6721        body,
6722        "forms.profile",
6723        Some("Profile".to_string()),
6724        widgets::FormSectionOptions::default().with_layout(
6725            LayoutStyle::column()
6726                .with_width_percent(1.0)
6727                .with_padding(12.0)
6728                .with_gap(10.0),
6729        ),
6730    );
6731    let status_row = wrapping_row(ui, section.root, "forms.profile.status_flags", 6.0);
6732    form_status_chip(
6733        ui,
6734        status_row,
6735        "forms.profile.status.dirty",
6736        "dirty",
6737        state.form.dirty,
6738    );
6739    form_status_chip(
6740        ui,
6741        status_row,
6742        "forms.profile.status.pending",
6743        "pending",
6744        state.form.pending,
6745    );
6746    form_status_chip(
6747        ui,
6748        status_row,
6749        "forms.profile.status.submitted",
6750        "submitted",
6751        state.form.submitted,
6752    );
6753
6754    let mut name_options = widgets::FormRowOptions::default().required();
6755    if state.form_name_text.text().trim().is_empty() {
6756        name_options = name_options.invalid("Name is required");
6757    }
6758    let name = widgets::form_row(ui, section.root, "forms.profile.name", name_options);
6759    widgets::field_label(
6760        ui,
6761        name,
6762        "forms.profile.name.label",
6763        "Name",
6764        widgets::FieldLabelOptions::default().required(),
6765    );
6766    form_text_field(
6767        ui,
6768        name,
6769        "forms.profile.name.input",
6770        &state.form_name_text,
6771        FocusedTextInput::FormName,
6772        state,
6773    );
6774    if state.form_name_text.text().trim().is_empty() {
6775        widgets::field_validation_message(
6776            ui,
6777            name,
6778            "forms.profile.name.validation",
6779            ValidationMessage::error("Name is required"),
6780            widgets::ValidationMessageOptions::default(),
6781        );
6782    } else {
6783        widgets::field_help_text(
6784            ui,
6785            name,
6786            "forms.profile.name.help",
6787            "Shown in window titles and project lists.",
6788            widgets::FieldHelpOptions::default(),
6789        );
6790    }
6791
6792    let mut email_options = widgets::FormRowOptions::default().required();
6793    if !profile_email_valid(state.form_email_text.text()) {
6794        email_options = email_options.invalid("Use a complete email address");
6795    }
6796    let email = widgets::form_row(ui, section.root, "forms.profile.email", email_options);
6797    widgets::field_label(
6798        ui,
6799        email,
6800        "forms.profile.email.label",
6801        "Email",
6802        widgets::FieldLabelOptions::default().required(),
6803    );
6804    form_text_field(
6805        ui,
6806        email,
6807        "forms.profile.email.input",
6808        &state.form_email_text,
6809        FocusedTextInput::FormEmail,
6810        state,
6811    );
6812    if profile_email_valid(state.form_email_text.text()) {
6813        widgets::field_help_text(
6814            ui,
6815            email,
6816            "forms.profile.email.help",
6817            "Used for workspace invites and notifications.",
6818            widgets::FieldHelpOptions::default(),
6819        );
6820    } else {
6821        widgets::field_validation_message(
6822            ui,
6823            email,
6824            "forms.profile.email.validation",
6825            ValidationMessage::error("Use a complete email address"),
6826            widgets::ValidationMessageOptions::default(),
6827        );
6828    }
6829
6830    let role = widgets::form_row(
6831        ui,
6832        section.root,
6833        "forms.profile.role",
6834        widgets::FormRowOptions::default(),
6835    );
6836    widgets::field_label(
6837        ui,
6838        role,
6839        "forms.profile.role.label",
6840        "Role",
6841        widgets::FieldLabelOptions::default(),
6842    );
6843    form_text_field(
6844        ui,
6845        role,
6846        "forms.profile.role.input",
6847        &state.form_role_text,
6848        FocusedTextInput::FormRole,
6849        state,
6850    );
6851    widgets::field_validation_message(
6852        ui,
6853        role,
6854        "forms.profile.role.help",
6855        if state.form_role_text.text().trim().is_empty() {
6856            ValidationMessage::warning("Role can be added later")
6857        } else {
6858            ValidationMessage::info(
6859                "Form rows compose labels, controls, help, and validation text.",
6860            )
6861        },
6862        widgets::ValidationMessageOptions::default(),
6863    );
6864
6865    let newsletter = widgets::form_row(
6866        ui,
6867        section.root,
6868        "forms.profile.newsletter",
6869        widgets::FormRowOptions::default().with_accessibility_label("Newsletter preference"),
6870    );
6871    let mut newsletter_options =
6872        widgets::CheckboxOptions::default().with_action("forms.profile.newsletter.toggle");
6873    newsletter_options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
6874    newsletter_options.text_style = text(12.0, color(220, 228, 238));
6875    widgets::checkbox(
6876        ui,
6877        newsletter,
6878        "forms.profile.newsletter.input",
6879        "Send release notes",
6880        state.form_newsletter,
6881        newsletter_options,
6882    );
6883    widgets::field_help_text(
6884        ui,
6885        newsletter,
6886        "forms.profile.newsletter.help",
6887        "Checkboxes participate in the same form state as text fields.",
6888        widgets::FieldHelpOptions::default(),
6889    );
6890
6891    widgets::form_error_summary(
6892        ui,
6893        section.root,
6894        "forms.profile.errors",
6895        &state.form,
6896        widgets::FormErrorSummaryOptions::default(),
6897    );
6898    let action_layout = Layout::row()
6899        .size(LayoutSize::new(
6900            LayoutDimension::percent(1.0),
6901            LayoutDimension::Auto,
6902        ))
6903        .gap(LayoutGap::points(8.0, 8.0))
6904        .flex_wrap(LayoutFlexWrap::Wrap)
6905        .to_layout_style();
6906    widgets::form_action_buttons(
6907        ui,
6908        section.root,
6909        "forms.profile.actions",
6910        &state.form,
6911        widgets::FormActionButtonsOptions::default()
6912            .with_layout(action_layout)
6913            .include_reset(true)
6914            .with_action_prefix("forms.profile"),
6915    );
6916    widgets::label(
6917        ui,
6918        section.root,
6919        "forms.profile.status",
6920        format!("Status: {}", state.form_status),
6921        text(11.0, color(154, 166, 184)),
6922        LayoutStyle::new().with_width_percent(1.0),
6923    );
6924}
6925
6926fn overlay_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
6927    let body =
6928        section_with_min_viewport(ui, parent, "overlays", "Overlays", UiSize::new(420.0, 0.0));
6929    let header = widgets::collapsing_header(
6930        ui,
6931        body,
6932        "overlays.collapsing",
6933        "Collapsing header",
6934        widgets::CollapsingHeaderOptions::default()
6935            .expanded(state.overlay_expanded)
6936            .with_toggle_action("overlays.collapsing.toggle"),
6937    );
6938    if let Some(panel) = header.body {
6939        widgets::label(
6940            ui,
6941            panel,
6942            "overlays.collapsing.body",
6943            "Expanded content lives under the header and remains part of normal layout.",
6944            text(12.0, color(196, 210, 230)),
6945            LayoutStyle::new().with_width_percent(1.0),
6946        );
6947    }
6948
6949    let controls = wrapping_row(ui, body, "overlays.controls", 8.0);
6950    button(
6951        ui,
6952        controls,
6953        "overlays.popup.toggle",
6954        if state.overlay_popup_open {
6955            "Close popup"
6956        } else {
6957            "Open popup"
6958        },
6959        "overlays.popup.toggle",
6960        button_visual(48, 112, 184),
6961    );
6962    button(
6963        ui,
6964        controls,
6965        "overlays.modal.open",
6966        "Open modal",
6967        "overlays.modal.open",
6968        button_visual(58, 78, 96),
6969    );
6970
6971    let tooltip = TooltipContent::new("Tooltip")
6972        .body("Tooltip boxes are overlay surfaces with title, body, and shortcut text.")
6973        .shortcut_label("Ctrl+K")
6974        .disabled_reason("Disabled reasons can be announced without changing the trigger.");
6975    let mut tooltip_options = widgets::TooltipBoxOptions::default()
6976        .with_layout(
6977            LayoutStyle::column()
6978                .with_width(280.0)
6979                .with_padding(8.0)
6980                .with_gap(4.0),
6981        )
6982        .with_animation(None);
6983    tooltip_options.layer = UiLayer::AppContent;
6984    tooltip_options.z_index = 0;
6985    widgets::tooltip_box(ui, body, "overlays.tooltip", tooltip, tooltip_options);
6986
6987    let tooltip_anchor = row(ui, body, "overlays.tooltip_anchor", 8.0);
6988    widgets::label(
6989        ui,
6990        tooltip_anchor,
6991        "overlays.tooltip_anchor.label",
6992        "Tooltip placement clamps to its viewport.",
6993        text(12.0, color(166, 176, 190)),
6994        LayoutStyle::new().with_width_percent(1.0),
6995    );
6996    let clamped_rect = widgets::tooltip::tooltip_rect(
6997        UiRect::new(328.0, 12.0, 54.0, 24.0),
6998        UiSize::new(176.0, 58.0),
6999        UiRect::new(0.0, 0.0, 420.0, 190.0),
7000        TooltipPlacement::Right,
7001        8.0,
7002        None,
7003    );
7004    let clamped_preview = ui.add_child(
7005        body,
7006        UiNode::container(
7007            "overlays.tooltip_rect.preview",
7008            LayoutStyle::new()
7009                .with_width_percent(1.0)
7010                .with_height(78.0)
7011                .with_flex_shrink(0.0),
7012        )
7013        .with_visual(UiVisual::panel(
7014            color(12, 16, 22),
7015            Some(StrokeStyle::new(color(52, 64, 80), 1.0)),
7016            4.0,
7017        )),
7018    );
7019    ui.add_child(
7020        clamped_preview,
7021        UiNode::scene(
7022            "overlays.tooltip_rect.scene",
7023            vec![
7024                ScenePrimitive::Rect(
7025                    PaintRect::solid(UiRect::new(328.0, 12.0, 54.0, 24.0), color(48, 112, 184))
7026                        .corner_radii(CornerRadii::uniform(3.0)),
7027                ),
7028                ScenePrimitive::Rect(
7029                    PaintRect::solid(clamped_rect, color(24, 29, 38))
7030                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7031                            color(92, 106, 128),
7032                            1.0,
7033                        )))
7034                        .corner_radii(CornerRadii::uniform(4.0)),
7035                ),
7036            ],
7037            LayoutStyle::new()
7038                .with_width_percent(1.0)
7039                .with_height_percent(1.0),
7040        ),
7041    );
7042
7043    if state.overlay_popup_open {
7044        let popup = ext_widgets::popup_panel(
7045            ui,
7046            parent,
7047            "overlays.popup_panel",
7048            UiRect::new(18.0, 150.0, 220.0, 112.0),
7049            ext_widgets::PopupOptions {
7050                z_index: 20,
7051                portal: UiPortalTarget::Parent,
7052                accessibility: Some(
7053                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup"),
7054                ),
7055                ..Default::default()
7056            },
7057        );
7058        let popup_body = ui.add_child(
7059            popup,
7060            UiNode::container(
7061                "overlays.popup_panel.body",
7062                LayoutStyle::column()
7063                    .with_width_percent(1.0)
7064                    .with_height_percent(1.0)
7065                    .with_padding(10.0)
7066                    .with_gap(6.0),
7067            ),
7068        );
7069        let popup_header = row(ui, popup_body, "overlays.popup_panel.header", 8.0);
7070        widgets::label(
7071            ui,
7072            popup_header,
7073            "overlays.popup_panel.label",
7074            "Popup panel",
7075            text(12.0, color(220, 228, 238)),
7076            LayoutStyle::new().with_width_percent(1.0),
7077        );
7078        let mut close = widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0))
7079            .with_action("overlays.popup.close");
7080        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7081        close.hovered_visual = Some(button_visual(54, 70, 92));
7082        close.text_style = text(12.0, color(220, 228, 238));
7083        widgets::button(ui, popup_header, "overlays.popup_panel.close", "x", close);
7084        widgets::label(
7085            ui,
7086            popup_body,
7087            "overlays.popup_panel.body_text",
7088            "Popup content is conditionally rendered.",
7089            text(11.0, color(196, 210, 230)),
7090            LayoutStyle::new().with_width_percent(1.0),
7091        );
7092    }
7093
7094    if state.overlay_modal_open {
7095        let modal = widgets::modal_dialog(
7096            ui,
7097            parent,
7098            "overlays.modal",
7099            "Modal dialog",
7100            widgets::ModalDialogOptions::default()
7101                .with_size(320.0, 180.0)
7102                .with_close_action("overlays.modal.close")
7103                .with_dismissal(ext_widgets::DialogDismissal::MODAL)
7104                .with_focus_restore(FocusRestoreTarget::Previous),
7105        );
7106        widgets::label(
7107            ui,
7108            modal.body,
7109            "overlays.modal.body.text",
7110            "Modal dialogs are portaled to the application overlay, include a scrim, and trap focus.",
7111            text(12.0, color(220, 228, 238)),
7112            LayoutStyle::new().with_width_percent(1.0),
7113        );
7114        button(
7115            ui,
7116            modal.body,
7117            "overlays.modal.body.close",
7118            "Close modal",
7119            "overlays.modal.close",
7120            button_visual(48, 112, 184),
7121        );
7122    }
7123}
7124
7125fn drag_drop_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7126    let body = section_with_min_viewport(
7127        ui,
7128        parent,
7129        "drag_drop",
7130        "Drag and drop",
7131        UiSize::new(420.0, 0.0),
7132    );
7133    widgets::label(
7134        ui,
7135        body,
7136        "drag_drop.sources.label",
7137        "Drag sources",
7138        text(12.0, color(166, 176, 190)),
7139        LayoutStyle::new().with_width_percent(1.0),
7140    );
7141    let sources = wrapping_row(ui, body, "drag_drop.sources", 8.0);
7142    widgets::dnd_drag_source(
7143        ui,
7144        sources,
7145        "drag_drop.text_source",
7146        "Text payload",
7147        DragPayload::text("Operad payload"),
7148        widgets::DragSourceOptions::default()
7149            .with_layout(drag_source_layout())
7150            .with_kind(DragDropSurfaceKind::ListRow)
7151            .with_allowed_operations([DragOperation::Copy, DragOperation::Move])
7152            .with_action("drag_drop.text_source")
7153            .with_accessibility_hint("Start a text drag operation"),
7154    );
7155    widgets::dnd_drag_source(
7156        ui,
7157        sources,
7158        "drag_drop.file_source",
7159        "File payload",
7160        DragPayload::files(["/tmp/showcase.scene"]),
7161        widgets::DragSourceOptions::default()
7162            .with_layout(drag_source_layout())
7163            .with_kind(DragDropSurfaceKind::Asset)
7164            .with_drag_image_policy(widgets::DragImagePolicy::image_key(
7165                BuiltInIcon::Folder.key(),
7166                UiSize::new(120.0, 36.0),
7167                UiPoint::new(10.0, 10.0),
7168            ))
7169            .with_allowed_operations([DragOperation::Copy])
7170            .with_action("drag_drop.file_source"),
7171    );
7172    widgets::dnd_drag_source(
7173        ui,
7174        sources,
7175        "drag_drop.bytes_source",
7176        "Image bytes",
7177        DragPayload::bytes(DragBytes::new("image/png", vec![137, 80, 78, 71]).name("sprite.png")),
7178        widgets::DragSourceOptions::default()
7179            .with_layout(drag_source_layout())
7180            .with_kind(DragDropSurfaceKind::Asset)
7181            .with_action("drag_drop.bytes_source")
7182            .without_drag_image(),
7183    );
7184
7185    widgets::label(
7186        ui,
7187        body,
7188        "drag_drop.zones.label",
7189        "Drop zones",
7190        text(12.0, color(166, 176, 190)),
7191        LayoutStyle::new().with_width_percent(1.0),
7192    );
7193    let zones = wrapping_row(ui, body, "drag_drop.zones", 8.0);
7194    let accepted_options = widgets::DropZoneOptions::default()
7195        .with_layout(drop_zone_layout())
7196        .with_kind(DragDropSurfaceKind::EditorSurface)
7197        .with_accepted_payload(DropPayloadFilter::empty().text())
7198        .with_accepted_operations([DragOperation::Copy, DragOperation::Move])
7199        .with_action("drag_drop.accept_text")
7200        .with_accessibility_hint("Accepts text payloads");
7201    let accepted = widgets::dnd_drop_zone(
7202        ui,
7203        zones,
7204        "drag_drop.accept_text",
7205        "Text accepted",
7206        accepted_options.clone(),
7207    );
7208    widgets::drag_drop::dnd_apply_drop_zone_preview(
7209        ui,
7210        accepted.root,
7211        &accepted_options,
7212        widgets::drag_drop::DropZonePreviewState::Accepted,
7213    );
7214
7215    let rejected_options = widgets::DropZoneOptions::default()
7216        .with_layout(drop_zone_layout())
7217        .with_kind(DragDropSurfaceKind::Asset)
7218        .with_accepted_payload(DropPayloadFilter::empty().files())
7219        .with_action("drag_drop.files_only");
7220    let rejected = widgets::dnd_drop_zone(
7221        ui,
7222        zones,
7223        "drag_drop.files_only",
7224        "Files only",
7225        rejected_options.clone(),
7226    );
7227    widgets::drag_drop::dnd_apply_drop_zone_preview(
7228        ui,
7229        rejected.root,
7230        &rejected_options,
7231        widgets::drag_drop::DropZonePreviewState::Rejected,
7232    );
7233    let image_options = widgets::DropZoneOptions::default()
7234        .with_layout(drop_zone_layout())
7235        .with_kind(DragDropSurfaceKind::Asset)
7236        .with_accepted_payload(DropPayloadFilter::empty().mime_type("image/*"))
7237        .with_accepted_operations([DragOperation::Copy])
7238        .with_action("drag_drop.image_bytes");
7239    let image_zone = widgets::dnd_drop_zone(
7240        ui,
7241        zones,
7242        "drag_drop.image_bytes",
7243        "Image bytes",
7244        image_options.clone(),
7245    );
7246    widgets::drag_drop::dnd_apply_drop_zone_preview(
7247        ui,
7248        image_zone.root,
7249        &image_options,
7250        widgets::drag_drop::DropZonePreviewState::Hovered,
7251    );
7252
7253    let disabled_options = widgets::DropZoneOptions::default()
7254        .with_layout(drop_zone_layout())
7255        .with_kind(DragDropSurfaceKind::EditorSurface)
7256        .with_accepted_payload(DropPayloadFilter::any())
7257        .with_action("drag_drop.disabled")
7258        .disabled();
7259    let disabled_zone = widgets::dnd_drop_zone(
7260        ui,
7261        zones,
7262        "drag_drop.disabled",
7263        "Disabled",
7264        disabled_options.clone(),
7265    );
7266    widgets::drag_drop::dnd_apply_drop_zone_preview(
7267        ui,
7268        disabled_zone.root,
7269        &disabled_options,
7270        widgets::drag_drop::DropZonePreviewState::Disabled,
7271    );
7272
7273    let operation_row = wrapping_row(ui, body, "drag_drop.operations", 6.0);
7274    dnd_operation_chip(ui, operation_row, "drag_drop.operation.copy", "copy");
7275    dnd_operation_chip(ui, operation_row, "drag_drop.operation.move", "move");
7276    dnd_operation_chip(ui, operation_row, "drag_drop.operation.link", "link");
7277    widgets::label(
7278        ui,
7279        body,
7280        "drag_drop.status",
7281        format!("Status: {}", state.drag_drop_status),
7282        text(11.0, color(154, 166, 184)),
7283        LayoutStyle::new().with_width_percent(1.0),
7284    );
7285}
7286
7287fn media_widgets(ui: &mut UiDocument, parent: UiNodeId) {
7288    let body = section_with_min_viewport(ui, parent, "media", "Media", UiSize::new(430.0, 0.0));
7289    widgets::label(
7290        ui,
7291        body,
7292        "media.icons.label",
7293        "Built-in icons",
7294        text(12.0, color(166, 176, 190)),
7295        LayoutStyle::new().with_width_percent(1.0),
7296    );
7297    let icons = wrapping_row(ui, body, "media.icons", 8.0);
7298    for icon in BuiltInIcon::COMMON {
7299        media_icon_tile(ui, icons, icon);
7300    }
7301
7302    widgets::label(
7303        ui,
7304        body,
7305        "media.variants.label",
7306        "Image variants",
7307        text(12.0, color(166, 176, 190)),
7308        LayoutStyle::new().with_width_percent(1.0),
7309    );
7310    let variants = wrapping_row(ui, body, "media.variants", 10.0);
7311    widgets::image(
7312        ui,
7313        variants,
7314        "media.image.untinted",
7315        icon_image(BuiltInIcon::Play),
7316        widgets::ImageOptions::default()
7317            .with_layout(media_preview_image_layout())
7318            .with_accessibility_label("Untinted play icon"),
7319    );
7320    widgets::image(
7321        ui,
7322        variants,
7323        "media.image.warning",
7324        ImageContent::new(BuiltInIcon::Warning.key()).tinted(color(232, 186, 88)),
7325        widgets::ImageOptions::default()
7326            .with_layout(media_preview_image_layout())
7327            .with_accessibility_label("Tinted warning icon"),
7328    );
7329    widgets::image(
7330        ui,
7331        variants,
7332        "media.image.shader",
7333        ImageContent::new(BuiltInIcon::Grid.key()).tinted(color(118, 183, 255)),
7334        widgets::ImageOptions::default()
7335            .with_layout(media_preview_image_layout())
7336            .with_shader(ShaderEffect::new("media.preview.tint").uniform("amount", 0.5))
7337            .with_accessibility_label("Shader-decorated grid icon"),
7338    );
7339    widgets::label(
7340        ui,
7341        body,
7342        "media.image.note",
7343        "Image widgets reference stable resource keys; the host resolves them to textures, vector assets, tinting, or shader-backed resources.",
7344        text(12.0, color(166, 176, 190)),
7345        LayoutStyle::new().with_width_percent(1.0),
7346    );
7347}
7348
7349fn timeline_ruler(ui: &mut UiDocument, parent: UiNodeId) {
7350    let layout = LayoutStyle::column()
7351        .with_width_percent(1.0)
7352        .with_height(40.0)
7353        .with_flex_shrink(0.0);
7354    let layout = operad::layout::with_min_size(layout, operad::length(0.0), operad::length(0.0));
7355    let body = widgets::scroll_area(ui, parent, "timeline", ScrollAxes::BOTH, layout);
7356    ext_widgets::timeline_ruler(
7357        ui,
7358        body,
7359        "timeline.ruler",
7360        ext_widgets::RulerSpec {
7361            range: ext_widgets::TimelineRange::new(0.0, 12.0),
7362            width: 600.0,
7363            major_step: 2.0,
7364            minor_step: 0.5,
7365            label_every: 1,
7366        },
7367        ext_widgets::TimelineRulerOptions::default(),
7368    );
7369}
7370
7371fn toast_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7372    let body = section(ui, parent, "toasts", "Toasts");
7373    let controls = row(ui, body, "toasts.controls", 10.0);
7374    button(
7375        ui,
7376        controls,
7377        "toasts.show",
7378        "Show toast",
7379        "toast.show",
7380        button_visual(48, 112, 184),
7381    );
7382    button(
7383        ui,
7384        controls,
7385        "toasts.hide",
7386        "Hide",
7387        "toast.hide",
7388        button_visual(58, 78, 96),
7389    );
7390    widgets::label(
7391        ui,
7392        body,
7393        "toasts.status",
7394        if state.toast_visible {
7395            "Toast overlay is visible."
7396        } else {
7397            "Toast overlay is hidden."
7398        },
7399        text(12.0, color(196, 210, 230)),
7400        LayoutStyle::new().with_width_percent(1.0),
7401    );
7402    widgets::label(
7403        ui,
7404        body,
7405        "toasts.action_status",
7406        format!("Action: {}", state.toast_action_status),
7407        text(12.0, color(154, 166, 184)),
7408        LayoutStyle::new().with_width_percent(1.0),
7409    );
7410}
7411
7412fn popup_controls(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7413    let body = section(ui, parent, "popup_panel", "Popup panel");
7414    let controls = row(ui, body, "popup_panel.controls", 8.0);
7415    button(
7416        ui,
7417        controls,
7418        "popup_panel.toggle",
7419        if state.popup_open {
7420            "Close popup"
7421        } else {
7422            "Open popup"
7423        },
7424        "popup.toggle",
7425        button_visual(48, 112, 184),
7426    );
7427    if state.popup_open {
7428        let mut close =
7429            widgets::ButtonOptions::new(LayoutStyle::size(30.0, 30.0)).with_action("popup.close");
7430        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7431        close.hovered_visual = Some(button_visual(54, 70, 92));
7432        close.text_style = text(13.0, color(220, 228, 238));
7433        widgets::button(ui, controls, "popup_panel.inline_close", "x", close);
7434    }
7435    widgets::label(
7436        ui,
7437        body,
7438        "popup_panel.status",
7439        if state.popup_open {
7440            "Popup overlay is open."
7441        } else {
7442            "Popup overlay is closed."
7443        },
7444        text(12.0, color(196, 210, 230)),
7445        LayoutStyle::new().with_width_percent(1.0),
7446    );
7447    if state.popup_open {
7448        let panel = ext_widgets::popup_panel(
7449            ui,
7450            parent,
7451            "popup_panel.inline_preview",
7452            UiRect::new(0.0, 20.0, 160.0, 104.0),
7453            ext_widgets::PopupOptions {
7454                z_index: 4,
7455                portal: UiPortalTarget::Parent,
7456                accessibility: Some(
7457                    AccessibilityMeta::new(AccessibilityRole::Dialog).label("Popup preview"),
7458                ),
7459                ..Default::default()
7460            },
7461        );
7462        let content = ui.add_child(
7463            panel,
7464            UiNode::container(
7465                "popup_panel.inline_preview.body",
7466                LayoutStyle::column()
7467                    .with_width_percent(1.0)
7468                    .with_height_percent(1.0)
7469                    .with_padding(10.0)
7470                    .with_gap(8.0),
7471            ),
7472        );
7473        let header = row(ui, content, "popup_panel.inline_preview.header", 8.0);
7474        widgets::label(
7475            ui,
7476            header,
7477            "popup_panel.inline_preview.title",
7478            "Popup panel",
7479            text(12.0, color(226, 234, 246)),
7480            LayoutStyle::new().with_width_percent(1.0),
7481        );
7482        let mut close =
7483            widgets::ButtonOptions::new(LayoutStyle::size(26.0, 22.0)).with_action("popup.close");
7484        close.visual = UiVisual::panel(color(28, 34, 43), None, 3.0);
7485        close.hovered_visual = Some(button_visual(54, 70, 92));
7486        close.text_style = text(12.0, color(220, 228, 238));
7487        widgets::button(ui, header, "popup_panel.inline_preview.close", "x", close);
7488        widgets::label(
7489            ui,
7490            content,
7491            "popup_panel.inline_preview.text",
7492            "Overlay content",
7493            text(11.0, color(196, 210, 230)),
7494            LayoutStyle::new().with_width_percent(1.0),
7495        );
7496        widgets::spacer(
7497            ui,
7498            body,
7499            "popup_panel.inline_preview.space",
7500            LayoutStyle::new()
7501                .with_width_percent(1.0)
7502                .with_height(112.0)
7503                .with_flex_shrink(0.0),
7504        );
7505    }
7506}
7507
7508fn styling_widgets(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7509    let body = section(ui, parent, "styling", "Styling");
7510    let grid_layout = operad::layout::with_grid_template_columns(
7511        Layout::grid()
7512            .size(LayoutSize::percent(1.0, 1.0))
7513            .gap(LayoutGap::points(10.0, 10.0))
7514            .to_layout_style(),
7515        [
7516            LayoutGridTrack::points(300.0),
7517            LayoutGridTrack::points(1.0),
7518            LayoutGridTrack::points(210.0),
7519        ],
7520    );
7521    let grid = ui.add_child(body, UiNode::container("styling.grid", grid_layout));
7522    let controls = ui.add_child(
7523        grid,
7524        UiNode::container(
7525            "styling.controls",
7526            LayoutStyle::column()
7527                .with_width(300.0)
7528                .with_height_percent(1.0)
7529                .with_flex_shrink(0.0)
7530                .gap(6.0),
7531        ),
7532    );
7533    style_edge_group(
7534        ui,
7535        controls,
7536        "styling.inner",
7537        "Inner margin",
7538        "styling.inner_same",
7539        state.styling.inner_same,
7540        [
7541            ("Left", "styling.inner", state.styling.inner_margin),
7542            ("Right", "styling.inner_right", state.styling.inner_right),
7543            ("Top", "styling.inner_top", state.styling.inner_top),
7544            ("Bottom", "styling.inner_bottom", state.styling.inner_bottom),
7545        ],
7546        0.0..32.0,
7547    );
7548    style_edge_group(
7549        ui,
7550        controls,
7551        "styling.outer",
7552        "Outer margin",
7553        "styling.outer_same",
7554        state.styling.outer_same,
7555        [
7556            ("Left", "styling.outer", state.styling.outer_margin),
7557            ("Right", "styling.outer_right", state.styling.outer_right),
7558            ("Top", "styling.outer_top", state.styling.outer_top),
7559            ("Bottom", "styling.outer_bottom", state.styling.outer_bottom),
7560        ],
7561        0.0..40.0,
7562    );
7563    style_edge_group(
7564        ui,
7565        controls,
7566        "styling.radius",
7567        "Corner radius",
7568        "styling.radius_same",
7569        state.styling.radius_same,
7570        [
7571            ("NW", "styling.radius", state.styling.corner_radius),
7572            ("NE", "styling.radius_ne", state.styling.corner_ne),
7573            ("SW", "styling.radius_sw", state.styling.corner_sw),
7574            ("SE", "styling.radius_se", state.styling.corner_se),
7575        ],
7576        0.0..28.0,
7577    );
7578    style_shadow_group(ui, controls, state);
7579    style_color_button_row(
7580        ui,
7581        controls,
7582        "styling.fill_color_button",
7583        "Fill",
7584        state.styling.fill_color(),
7585        "Pick fill color",
7586    );
7587    if state.styling_fill_picker_open {
7588        ext_widgets::color_picker(
7589            ui,
7590            controls,
7591            "styling.fill_picker",
7592            &state.styling_fill_picker,
7593            ext_widgets::ColorPickerOptions::default()
7594                .with_label("Fill")
7595                .with_action_prefix("styling.fill_picker"),
7596        );
7597    }
7598    style_stroke_row(ui, controls, state);
7599    if state.styling_stroke_picker_open {
7600        ext_widgets::color_picker(
7601            ui,
7602            controls,
7603            "styling.stroke_picker",
7604            &state.styling_stroke_picker,
7605            ext_widgets::ColorPickerOptions::default()
7606                .with_label("Stroke color")
7607                .with_action_prefix("styling.stroke_picker"),
7608        );
7609    }
7610    widgets::separator(
7611        ui,
7612        grid,
7613        "styling.preview.separator",
7614        widgets::SeparatorOptions::vertical().with_layout(
7615            LayoutStyle::new()
7616                .with_width(1.0)
7617                .with_height_percent(1.0)
7618                .with_flex_shrink(0.0),
7619        ),
7620    );
7621
7622    let preview = ui.add_child(
7623        grid,
7624        UiNode::container(
7625            "styling.preview",
7626            LayoutStyle::column()
7627                .with_width(210.0)
7628                .with_height_percent(1.0)
7629                .with_flex_shrink(0.0)
7630                .padding(8.0),
7631        )
7632        .with_visual(UiVisual::panel(color(17, 20, 25), None, 0.0)),
7633    );
7634    style_preview(ui, preview, state.styling);
7635}
7636
7637#[allow(clippy::too_many_arguments)]
7638fn style_edge_group(
7639    ui: &mut UiDocument,
7640    parent: UiNodeId,
7641    name: &'static str,
7642    title: &'static str,
7643    same_action: &'static str,
7644    same: bool,
7645    values: [(&'static str, &'static str, f32); 4],
7646    range: std::ops::Range<f32>,
7647) {
7648    let group = style_control_group(ui, parent, format!("{name}.group"));
7649    style_group_title(ui, group, format!("{name}.title"), title);
7650    let fields = ui.add_child(
7651        group,
7652        UiNode::container(
7653            format!("{name}.fields"),
7654            LayoutStyle::column()
7655                .with_width(138.0)
7656                .with_flex_shrink(0.0)
7657                .gap(3.0),
7658        ),
7659    );
7660    style_compact_checkbox(ui, fields, same_action, "same", same);
7661    if same {
7662        style_number_row(ui, fields, values[0].1, "All", values[0].2, range, 0);
7663    } else {
7664        for (label, action, value) in values {
7665            style_number_row(ui, fields, action, label, value, range.clone(), 0);
7666        }
7667    }
7668}
7669
7670fn style_shadow_group(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7671    let group = style_control_group(ui, parent, "styling.shadow.group");
7672    style_group_title(ui, group, "styling.shadow.title", "Shadow");
7673    let fields = ui.add_child(
7674        group,
7675        UiNode::container(
7676            "styling.shadow.fields",
7677            LayoutStyle::column()
7678                .with_width(174.0)
7679                .with_flex_shrink(0.0)
7680                .gap(4.0),
7681        ),
7682    );
7683    let offsets = row(ui, fields, "styling.shadow.offsets", 6.0);
7684    style_inline_number(
7685        ui,
7686        offsets,
7687        "styling.shadow_x",
7688        "x",
7689        state.styling.shadow_x,
7690        -24.0..24.0,
7691        0,
7692    );
7693    style_inline_number(
7694        ui,
7695        offsets,
7696        "styling.shadow_y",
7697        "y",
7698        state.styling.shadow_y,
7699        -24.0..24.0,
7700        0,
7701    );
7702    let spread = row(ui, fields, "styling.shadow.blur_spread", 6.0);
7703    style_inline_number(
7704        ui,
7705        spread,
7706        "styling.shadow",
7707        "blur",
7708        state.styling.shadow_blur,
7709        0.0..32.0,
7710        0,
7711    );
7712    style_inline_number(
7713        ui,
7714        spread,
7715        "styling.shadow_spread",
7716        "spread",
7717        state.styling.shadow_spread,
7718        0.0..16.0,
7719        0,
7720    );
7721    style_color_button_row(
7722        ui,
7723        fields,
7724        "styling.shadow_color_button",
7725        "",
7726        state.styling.shadow_color(),
7727        "Pick shadow color",
7728    );
7729    if state.styling_shadow_picker_open {
7730        ext_widgets::color_picker(
7731            ui,
7732            fields,
7733            "styling.shadow_picker",
7734            &state.styling_shadow_picker,
7735            ext_widgets::ColorPickerOptions::default()
7736                .with_label("Shadow color")
7737                .with_action_prefix("styling.shadow_picker"),
7738        );
7739    }
7740}
7741
7742fn style_stroke_row(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
7743    let row = row(ui, parent, "styling.stroke.row", 8.0);
7744    widgets::label(
7745        ui,
7746        row,
7747        "styling.stroke.label",
7748        "Stroke",
7749        text(12.0, color(166, 176, 190)),
7750        LayoutStyle::new().with_width(86.0).with_flex_shrink(0.0),
7751    );
7752    style_value_input(
7753        ui,
7754        row,
7755        "styling.stroke",
7756        state.styling.stroke_width,
7757        0.0..4.0,
7758        1,
7759    );
7760    ext_widgets::color_edit_button(
7761        ui,
7762        row,
7763        "styling.stroke_color_button",
7764        state.styling.stroke_color(),
7765        color_mini_button_options("styling.stroke_color_button")
7766            .with_format(ext_widgets::ColorValueFormat::Rgba)
7767            .accessibility_label("Pick stroke color"),
7768    );
7769    let mut options = widgets::SliderOptions::default()
7770        .with_layout(
7771            LayoutStyle::new()
7772                .with_width(60.0)
7773                .with_height(20.0)
7774                .with_flex_shrink(0.0),
7775        )
7776        .with_value_edit_action("styling.stroke");
7777    options.fill_color = color(120, 170, 230);
7778    widgets::slider(
7779        ui,
7780        row,
7781        "styling.stroke.slider",
7782        (state.styling.stroke_width / 4.0).clamp(0.0, 1.0),
7783        0.0..1.0,
7784        options,
7785    );
7786}
7787
7788fn style_control_group(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>) -> UiNodeId {
7789    ui.add_child(
7790        parent,
7791        UiNode::container(
7792            name,
7793            LayoutStyle::row()
7794                .with_width_percent(1.0)
7795                .with_flex_shrink(0.0)
7796                .padding(4.0)
7797                .gap(8.0),
7798        )
7799        .with_visual(UiVisual::panel(color(23, 27, 33), None, 2.0)),
7800    )
7801}
7802
7803fn style_group_title(
7804    ui: &mut UiDocument,
7805    parent: UiNodeId,
7806    name: impl Into<String>,
7807    label: &'static str,
7808) {
7809    widgets::label(
7810        ui,
7811        parent,
7812        name,
7813        label,
7814        text(12.0, color(166, 176, 190)),
7815        LayoutStyle::new()
7816            .with_width(88.0)
7817            .with_flex_shrink(0.0)
7818            .with_height(22.0),
7819    );
7820}
7821
7822fn style_color_button_row(
7823    ui: &mut UiDocument,
7824    parent: UiNodeId,
7825    action: &'static str,
7826    label: &'static str,
7827    value: ColorRgba,
7828    accessibility_label: &'static str,
7829) {
7830    let row = row(ui, parent, format!("{action}.row"), 8.0);
7831    if !label.is_empty() {
7832        widgets::label(
7833            ui,
7834            row,
7835            format!("{action}.label"),
7836            label,
7837            text(12.0, color(166, 176, 190)),
7838            LayoutStyle::new()
7839                .with_width(86.0)
7840                .with_flex_shrink(0.0)
7841                .with_height(24.0),
7842        );
7843    }
7844    ext_widgets::color_edit_button(
7845        ui,
7846        row,
7847        action,
7848        value,
7849        color_mini_button_options(action)
7850            .with_format(ext_widgets::ColorValueFormat::Rgba)
7851            .accessibility_label(accessibility_label),
7852    );
7853    widgets::label(
7854        ui,
7855        row,
7856        format!("{action}.value"),
7857        ext_widgets::color_picker::format_hex_color(value, value.a < 255),
7858        text(12.0, color(226, 232, 242)),
7859        LayoutStyle::new().with_width(96.0).with_height(24.0),
7860    );
7861}
7862
7863fn style_number_row(
7864    ui: &mut UiDocument,
7865    parent: UiNodeId,
7866    name: &'static str,
7867    label: &'static str,
7868    value: f32,
7869    range: std::ops::Range<f32>,
7870    decimals: u8,
7871) {
7872    let row = row(ui, parent, format!("{name}.row"), 6.0);
7873    widgets::label(
7874        ui,
7875        row,
7876        format!("{name}.label"),
7877        label,
7878        text(12.0, color(166, 176, 190)),
7879        LayoutStyle::new().with_width(48.0).with_height(22.0),
7880    );
7881    style_value_input(ui, row, name, value, range, decimals);
7882}
7883
7884fn style_inline_number(
7885    ui: &mut UiDocument,
7886    parent: UiNodeId,
7887    name: &'static str,
7888    label: &'static str,
7889    value: f32,
7890    range: std::ops::Range<f32>,
7891    decimals: u8,
7892) {
7893    let row = row(ui, parent, format!("{name}.inline"), 3.0);
7894    widgets::label(
7895        ui,
7896        row,
7897        format!("{name}.inline_label"),
7898        format!("{label}:"),
7899        text(12.0, color(166, 176, 190)),
7900        LayoutStyle::new()
7901            .with_width(if label.len() > 1 { 42.0 } else { 16.0 })
7902            .with_height(22.0),
7903    );
7904    style_value_input(ui, row, name, value, range, decimals);
7905}
7906
7907fn style_value_input(
7908    ui: &mut UiDocument,
7909    parent: UiNodeId,
7910    name: &'static str,
7911    value: f32,
7912    range: std::ops::Range<f32>,
7913    decimals: u8,
7914) {
7915    let mut options = widgets::DragValueOptions::default()
7916        .with_layout(
7917            LayoutStyle::new()
7918                .with_width(42.0)
7919                .with_height(22.0)
7920                .with_flex_shrink(0.0),
7921        )
7922        .with_range(ext_widgets::NumericRange::new(
7923            f64::from(range.start),
7924            f64::from(range.end),
7925        ))
7926        .with_precision(ext_widgets::NumericPrecision::decimals(decimals))
7927        .with_action(name);
7928    options.text_style = text(12.0, color(226, 232, 242));
7929    widgets::drag_value_input(ui, parent, name, f64::from(value), options);
7930}
7931
7932fn style_compact_checkbox(
7933    ui: &mut UiDocument,
7934    parent: UiNodeId,
7935    name: &'static str,
7936    label: &'static str,
7937    checked: bool,
7938) {
7939    let mut options = widgets::CheckboxOptions::default().with_action(name);
7940    options.layout = LayoutStyle::new().with_width(92.0).with_height(22.0);
7941    options.text_style = text(12.0, color(220, 228, 238));
7942    widgets::checkbox(ui, parent, name, label, checked, options);
7943}
7944
7945fn color_mini_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
7946    ext_widgets::ColorButtonOptions::default()
7947        .with_layout(LayoutStyle::size(28.0, 24.0).with_flex_shrink(0.0))
7948        .with_swatch_size(UiSize::new(22.0, 18.0))
7949        .with_action(action)
7950        .show_label(false)
7951}
7952
7953fn style_preview(ui: &mut UiDocument, parent: UiNodeId, styling: StylingState) {
7954    let outer = styling.outer_edges();
7955    let inner = styling.inner_edges();
7956    let frame = UiRect::new(
7957        22.0 + outer[0],
7958        28.0 + outer[2],
7959        108.0 + inner[0] + inner[1],
7960        40.0 + inner[2] + inner[3],
7961    );
7962    let text_rect = UiRect::new(
7963        frame.x + inner[0],
7964        frame.y + inner[2],
7965        (frame.width - inner[0] - inner[1]).max(1.0),
7966        (frame.height - inner[2] - inner[3]).max(1.0),
7967    );
7968    ui.add_child(
7969        parent,
7970        UiNode::scene(
7971            "styling.preview.scene",
7972            vec![
7973                ScenePrimitive::Rect(
7974                    PaintRect::solid(frame, styling.fill_color())
7975                        .stroke(AlignedStroke::inside(StrokeStyle::new(
7976                            styling.stroke_color(),
7977                            styling.stroke_width,
7978                        )))
7979                        .corner_radii(styling.radii())
7980                        .effect(PaintEffect::shadow(
7981                            styling.shadow_color(),
7982                            UiPoint::new(styling.shadow_x, styling.shadow_y),
7983                            styling.shadow_blur,
7984                            styling.shadow_spread,
7985                        )),
7986                ),
7987                ScenePrimitive::Text(
7988                    PaintText::new("Content", text_rect, text(13.0, color(255, 255, 255)))
7989                        .horizontal_align(TextHorizontalAlign::Center)
7990                        .vertical_align(TextVerticalAlign::Center)
7991                        .multiline(false),
7992                ),
7993            ],
7994            LayoutStyle::new()
7995                .with_width_percent(1.0)
7996                .with_height(180.0)
7997                .with_flex_shrink(0.0),
7998        ),
7999    );
8000}
8001
8002fn slider_options(state: &ShowcaseState, width: f32) -> widgets::SliderOptions {
8003    let mut options = widgets::SliderOptions::default().with_layout(
8004        LayoutStyle::new()
8005            .with_width(width)
8006            .with_height(24.0)
8007            .with_flex_shrink(0.0),
8008    );
8009    options.fill_color = if state.slider_trailing_color {
8010        state.slider_trailing_picker.value()
8011    } else {
8012        color(42, 49, 58)
8013    };
8014    options.thumb_shape = match state.slider_thumb_shape {
8015        SliderThumbChoice::Circle => widgets::slider::SliderThumbShape::Circle,
8016        SliderThumbChoice::Square => widgets::slider::SliderThumbShape::Square,
8017        SliderThumbChoice::Rectangle => widgets::slider::SliderThumbShape::Rectangle,
8018    };
8019    options
8020}
8021
8022#[allow(clippy::field_reassign_with_default)]
8023fn slider_number_input(
8024    ui: &mut UiDocument,
8025    parent: UiNodeId,
8026    name: &'static str,
8027    input: &TextInputState,
8028    focused: FocusedTextInput,
8029    state: &ShowcaseState,
8030    width: f32,
8031) {
8032    let mut options = TextInputOptions::default();
8033    options.layout = LayoutStyle::new().with_width(width).with_height(28.0);
8034    options.text_style = text(12.0, color(230, 236, 246));
8035    options.placeholder_style = text(12.0, color(144, 156, 174));
8036    options.edit_action = Some(format!("{name}.edit").into());
8037    options.focused = state.focused_text == Some(focused);
8038    options.caret_visible = caret_visible(state.caret_phase);
8039    widgets::text_input(ui, parent, name, input, options);
8040}
8041
8042fn form_status_chip(
8043    ui: &mut UiDocument,
8044    parent: UiNodeId,
8045    name: &'static str,
8046    label: &'static str,
8047    active: bool,
8048) {
8049    let chip = ui.add_child(
8050        parent,
8051        UiNode::container(
8052            name,
8053            LayoutStyle::new()
8054                .with_width(82.0)
8055                .with_height(24.0)
8056                .with_padding(4.0)
8057                .with_flex_shrink(0.0),
8058        )
8059        .with_visual(UiVisual::panel(
8060            if active {
8061                color(35, 74, 54)
8062            } else {
8063                color(28, 34, 43)
8064            },
8065            Some(StrokeStyle::new(
8066                if active {
8067                    color(90, 160, 112)
8068                } else {
8069                    color(60, 72, 88)
8070                },
8071                1.0,
8072            )),
8073            4.0,
8074        )),
8075    );
8076    widgets::label(
8077        ui,
8078        chip,
8079        format!("{name}.label"),
8080        label,
8081        text(11.0, color(218, 228, 240)),
8082        LayoutStyle::new()
8083            .with_width_percent(1.0)
8084            .with_height_percent(1.0),
8085    );
8086}
8087
8088#[allow(clippy::field_reassign_with_default)]
8089fn form_text_field(
8090    ui: &mut UiDocument,
8091    parent: UiNodeId,
8092    name: &'static str,
8093    input: &TextInputState,
8094    focused: FocusedTextInput,
8095    state: &ShowcaseState,
8096) {
8097    let mut options = TextInputOptions::default();
8098    options.layout = LayoutStyle::new().with_width_percent(1.0).with_height(30.0);
8099    options.text_style = text(12.0, color(230, 236, 246));
8100    options.placeholder_style = text(12.0, color(144, 156, 174));
8101    options.placeholder = "Required".to_string();
8102    options.edit_action = Some(format!("{name}.edit").into());
8103    options.focused = state.focused_text == Some(focused);
8104    options.caret_visible = caret_visible(state.caret_phase);
8105    widgets::text_input(ui, parent, name, input, options);
8106}
8107
8108fn profile_email_valid(email: &str) -> bool {
8109    let email = email.trim();
8110    let Some((local, domain)) = email.split_once('@') else {
8111        return false;
8112    };
8113    !local.is_empty() && domain.contains('.') && !domain.ends_with('.')
8114}
8115
8116fn drag_source_layout() -> LayoutStyle {
8117    LayoutStyle::row()
8118        .with_width(128.0)
8119        .with_height(40.0)
8120        .with_padding(8.0)
8121        .with_gap(6.0)
8122        .with_flex_shrink(0.0)
8123}
8124
8125fn drop_zone_layout() -> LayoutStyle {
8126    LayoutStyle::column()
8127        .with_width(128.0)
8128        .with_height(78.0)
8129        .with_padding(10.0)
8130        .with_gap(6.0)
8131        .with_flex_shrink(0.0)
8132}
8133
8134fn dnd_operation_chip(
8135    ui: &mut UiDocument,
8136    parent: UiNodeId,
8137    name: &'static str,
8138    label: &'static str,
8139) {
8140    let chip = ui.add_child(
8141        parent,
8142        UiNode::container(
8143            name,
8144            LayoutStyle::new()
8145                .with_width(58.0)
8146                .with_height(22.0)
8147                .with_padding(3.0)
8148                .with_flex_shrink(0.0),
8149        )
8150        .with_visual(UiVisual::panel(
8151            color(26, 32, 42),
8152            Some(StrokeStyle::new(color(62, 76, 94), 1.0)),
8153            3.0,
8154        )),
8155    );
8156    widgets::label(
8157        ui,
8158        chip,
8159        format!("{name}.label"),
8160        label,
8161        text(11.0, color(190, 204, 222)),
8162        LayoutStyle::new()
8163            .with_width_percent(1.0)
8164            .with_height_percent(1.0),
8165    );
8166}
8167
8168fn media_preview_image_layout() -> LayoutStyle {
8169    LayoutStyle::size(46.0, 46.0).with_flex_shrink(0.0)
8170}
8171
8172fn media_icon_tile(ui: &mut UiDocument, parent: UiNodeId, icon: BuiltInIcon) {
8173    let name = icon.key().replace('.', "_").replace('-', "_");
8174    let tile = ui.add_child(
8175        parent,
8176        UiNode::container(
8177            format!("media.icon_tile.{name}"),
8178            LayoutStyle::column()
8179                .with_width(70.0)
8180                .with_height(78.0)
8181                .with_padding(6.0)
8182                .with_gap(4.0)
8183                .with_flex_shrink(0.0),
8184        )
8185        .with_visual(UiVisual::panel(
8186            color(17, 22, 30),
8187            Some(StrokeStyle::new(color(50, 62, 78), 1.0)),
8188            4.0,
8189        )),
8190    );
8191    widgets::image(
8192        ui,
8193        tile,
8194        format!("media.icon.{name}"),
8195        icon_image(icon),
8196        widgets::ImageOptions::default()
8197            .with_layout(LayoutStyle::size(28.0, 28.0))
8198            .with_accessibility_label(icon.label()),
8199    );
8200    widgets::label(
8201        ui,
8202        tile,
8203        format!("media.icon_label.{name}"),
8204        icon.label(),
8205        text(9.0, color(180, 194, 214)),
8206        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
8207    );
8208}
8209
8210fn slider_checkbox(
8211    ui: &mut UiDocument,
8212    parent: UiNodeId,
8213    name: &'static str,
8214    label: &'static str,
8215    checked: bool,
8216) {
8217    slider_checkbox_with_layout(
8218        ui,
8219        parent,
8220        name,
8221        label,
8222        checked,
8223        LayoutStyle::new().with_width_percent(1.0).with_height(30.0),
8224    );
8225}
8226
8227fn slider_checkbox_with_layout(
8228    ui: &mut UiDocument,
8229    parent: UiNodeId,
8230    name: &'static str,
8231    label: &'static str,
8232    checked: bool,
8233    layout: LayoutStyle,
8234) {
8235    let mut options = widgets::CheckboxOptions::default().with_action(name);
8236    options.layout = layout;
8237    options.text_style = text(12.0, color(220, 228, 238));
8238    widgets::checkbox(ui, parent, name, label, checked, options);
8239}
8240
8241fn choice_button(
8242    ui: &mut UiDocument,
8243    parent: UiNodeId,
8244    name: &'static str,
8245    label: &'static str,
8246    selected: bool,
8247) {
8248    let mut options =
8249        widgets::ButtonOptions::new(LayoutStyle::new().with_width(78.0).with_height(28.0))
8250            .with_action(name);
8251    options.visual = if selected {
8252        button_visual(48, 112, 184)
8253    } else {
8254        button_visual(38, 46, 58)
8255    };
8256    options.hovered_visual = Some(button_visual(65, 86, 106));
8257    options.pressed_visual = Some(button_visual(34, 54, 84));
8258    options.text_style = text(12.0, color(238, 244, 252));
8259    widgets::button(ui, parent, name, label, options);
8260}
8261
8262fn divider(ui: &mut UiDocument, parent: UiNodeId, name: &'static str) {
8263    ui.add_child(
8264        parent,
8265        UiNode::container(
8266            name,
8267            LayoutStyle::new()
8268                .with_width_percent(1.0)
8269                .with_height(1.0)
8270                .with_flex_shrink(0.0),
8271        )
8272        .with_visual(UiVisual::panel(color(48, 58, 72), None, 0.0)),
8273    );
8274}
8275
8276fn canvas(ui: &mut UiDocument, parent: UiNodeId, state: &ShowcaseState) {
8277    let body = section(ui, parent, "canvas", "Canvas");
8278    let mut options = widgets::CanvasOptions::default()
8279        .with_accessibility_label("Shader canvas")
8280        .with_action("canvas.rotate")
8281        .with_aspect_ratio(16.0 / 9.0);
8282    options.layout = LayoutStyle::new()
8283        .with_width_percent(1.0)
8284        .with_height_percent(1.0)
8285        .with_flex_grow(1.0)
8286        .with_flex_shrink(1.0);
8287    options.visual = UiVisual::panel(
8288        color(18, 22, 28),
8289        Some(StrokeStyle::new(color(58, 68, 84), 1.0)),
8290        4.0,
8291    );
8292    widgets::canvas(
8293        ui,
8294        body,
8295        "canvas.shader",
8296        CanvasContent::new("canvas.shader").program(showcase_canvas_program(state.cube)),
8297        options,
8298    );
8299}
8300
8301fn showcase_canvas_program(cube: CanvasCubeState) -> CanvasRenderProgram {
8302    CanvasRenderProgram::wgsl(include_str!("shaders/showcase_canvas.wgsl"))
8303        .label("showcase.canvas")
8304        .constant("CUBE_YAW", cube.yaw as f64)
8305        .constant("CUBE_PITCH", cube.pitch as f64)
8306        .clear_color(Some(color(18, 22, 28)))
8307}
8308
8309fn section(
8310    ui: &mut UiDocument,
8311    parent: UiNodeId,
8312    name: impl Into<String>,
8313    _title: impl Into<String>,
8314) -> UiNodeId {
8315    section_with_min_viewport(ui, parent, name, _title, UiSize::ZERO)
8316}
8317
8318fn section_with_min_viewport(
8319    ui: &mut UiDocument,
8320    parent: UiNodeId,
8321    name: impl Into<String>,
8322    _title: impl Into<String>,
8323    min_viewport_size: UiSize,
8324) -> UiNodeId {
8325    let name = name.into();
8326    let layout = Layout::column()
8327        .size(LayoutSize::percent(1.0, 1.0))
8328        .min_size(LayoutSize::points(
8329            min_viewport_size.width.max(0.0),
8330            min_viewport_size.height.max(0.0),
8331        ))
8332        .gap(LayoutGap::points(10.0, 10.0))
8333        .flex(1.0, 1.0, LayoutDimension::Auto)
8334        .to_layout_style();
8335    widgets::scroll_area(
8336        ui,
8337        parent,
8338        format!("{name}.section_scroll"),
8339        ScrollAxes::BOTH,
8340        layout,
8341    )
8342}
8343
8344fn row(ui: &mut UiDocument, parent: UiNodeId, name: impl Into<String>, gap: f32) -> UiNodeId {
8345    ui.add_child(
8346        parent,
8347        UiNode::container(
8348            name,
8349            Layout::row()
8350                .size(LayoutSize::new(
8351                    LayoutDimension::percent(1.0),
8352                    LayoutDimension::Auto,
8353                ))
8354                .gap(LayoutGap::points(gap, gap))
8355                .to_layout_style(),
8356        ),
8357    )
8358}
8359
8360fn wrapping_row(
8361    ui: &mut UiDocument,
8362    parent: UiNodeId,
8363    name: impl Into<String>,
8364    gap: f32,
8365) -> UiNodeId {
8366    ui.add_child(
8367        parent,
8368        UiNode::container(
8369            name,
8370            Layout::row()
8371                .size(LayoutSize::new(
8372                    LayoutDimension::percent(1.0),
8373                    LayoutDimension::Auto,
8374                ))
8375                .gap(LayoutGap::points(gap, gap))
8376                .flex_wrap(LayoutFlexWrap::Wrap)
8377                .to_layout_style(),
8378        ),
8379    )
8380}
8381
8382fn egui_panel_contents(
8383    ui: &mut UiDocument,
8384    parent: UiNodeId,
8385    name: &'static str,
8386    title: &'static str,
8387    offset_y: f32,
8388) {
8389    let header = ui.add_child(
8390        parent,
8391        UiNode::container(
8392            format!("{name}.egui_header"),
8393            LayoutStyle::row()
8394                .with_width_percent(1.0)
8395                .with_height(28.0)
8396                .with_padding(6.0)
8397                .with_flex_shrink(0.0),
8398        )
8399        .with_visual(UiVisual::panel(
8400            color(21, 26, 34),
8401            Some(StrokeStyle::new(color(54, 65, 80), 1.0)),
8402            0.0,
8403        )),
8404    );
8405    widgets::label(
8406        ui,
8407        header,
8408        format!("{name}.egui_title"),
8409        title,
8410        text(12.0, color(226, 234, 246)),
8411        LayoutStyle::new().with_width_percent(1.0),
8412    );
8413    let scroll = widgets::scroll_area(
8414        ui,
8415        parent,
8416        format!("{name}.scroll_area"),
8417        ScrollAxes::VERTICAL,
8418        LayoutStyle::column()
8419            .with_width_percent(1.0)
8420            .with_height(0.0)
8421            .with_flex_grow(1.0)
8422            .with_padding(8.0)
8423            .with_gap(6.0),
8424    );
8425    ui.node_mut(scroll).set_action(format!("{name}.scroll"));
8426    if let Some(scroll_state) = ui.node_mut(scroll).scroll_mut() {
8427        scroll_state.set_offset(UiPoint::new(0.0, offset_y));
8428    }
8429    for (index, line) in lorem_lines().iter().take(8).enumerate() {
8430        widgets::label(
8431            ui,
8432            scroll,
8433            format!("{name}.egui_line.{index}"),
8434            *line,
8435            TextStyle {
8436                wrap: TextWrap::None,
8437                ..text(11.0, color(190, 202, 218))
8438            },
8439            LayoutStyle::new()
8440                .with_width_percent(1.0)
8441                .with_height(22.0)
8442                .with_flex_shrink(0.0),
8443        );
8444    }
8445}
8446
8447fn button(
8448    ui: &mut UiDocument,
8449    parent: UiNodeId,
8450    name: impl Into<String>,
8451    label: impl Into<String>,
8452    action: impl Into<String>,
8453    visual: UiVisual,
8454) -> UiNodeId {
8455    let mut options = widgets::ButtonOptions::new(LayoutStyle::new().with_height(32.0))
8456        .with_action(action.into());
8457    options.visual = visual;
8458    options.hovered_visual = Some(adjusted_button_visual(visual, 58));
8459    options.pressed_visual = Some(adjusted_button_visual(visual, -62));
8460    options.pressed_hovered_visual = Some(adjusted_button_visual(visual, 8));
8461    options.text_style = text(13.0, color(246, 249, 252));
8462    widgets::button(ui, parent, name, label, options)
8463}
8464
8465fn button_visual(r: u8, g: u8, b: u8) -> UiVisual {
8466    UiVisual::panel(
8467        color(r, g, b),
8468        Some(StrokeStyle::new(color(86, 102, 124), 1.0)),
8469        4.0,
8470    )
8471}
8472
8473fn color_square_button_options(action: &'static str) -> ext_widgets::ColorButtonOptions {
8474    ext_widgets::ColorButtonOptions::default()
8475        .with_layout(LayoutStyle::size(30.0, 30.0).with_flex_shrink(0.0))
8476        .with_swatch_size(UiSize::new(30.0, 30.0))
8477        .with_action(action)
8478        .show_label(false)
8479}
8480
8481fn color_value_button_options(action: &'static str, width: f32) -> ext_widgets::ColorButtonOptions {
8482    ext_widgets::ColorButtonOptions::default()
8483        .with_layout(
8484            LayoutStyle::new()
8485                .with_width(width)
8486                .with_height(30.0)
8487                .with_flex_shrink(0.0),
8488        )
8489        .with_action(action)
8490}
8491
8492fn icon_image(icon: BuiltInIcon) -> ImageContent {
8493    ImageContent::new(icon.key()).tinted(color(220, 228, 238))
8494}
8495
8496fn adjusted_button_visual(visual: UiVisual, delta: i16) -> UiVisual {
8497    UiVisual::panel(
8498        adjust_color(visual.fill, delta),
8499        visual.stroke.map(|stroke| StrokeStyle {
8500            color: adjust_color(stroke.color, delta / 2),
8501            width: stroke.width,
8502        }),
8503        visual.corner_radius,
8504    )
8505}
8506
8507fn adjust_color(color: ColorRgba, delta: i16) -> ColorRgba {
8508    let channel = |value: u8| -> u8 { (i16::from(value) + delta).clamp(0, u8::MAX as i16) as u8 };
8509    ColorRgba::new(
8510        channel(color.r),
8511        channel(color.g),
8512        channel(color.b),
8513        color.a,
8514    )
8515}
8516
8517fn select_options() -> Vec<ext_widgets::SelectOption> {
8518    vec![
8519        ext_widgets::SelectOption::new("compact", "Compact"),
8520        ext_widgets::SelectOption::new("comfortable", "Comfortable"),
8521        ext_widgets::SelectOption::new("spacious", "Spacious"),
8522        ext_widgets::SelectOption::new("disabled", "Disabled").disabled(),
8523    ]
8524}
8525
8526fn label_locale_options() -> Vec<ext_widgets::SelectOption> {
8527    vec![
8528        ext_widgets::SelectOption::new("en-US", "English"),
8529        ext_widgets::SelectOption::new("es-MX", "Español"),
8530        ext_widgets::SelectOption::new("fr-FR", "Français"),
8531        ext_widgets::SelectOption::new("de-DE", "Deutsch"),
8532        ext_widgets::SelectOption::new("it-IT", "Italiano"),
8533        ext_widgets::SelectOption::new("pt-BR", "Português"),
8534        ext_widgets::SelectOption::new("nl-NL", "Nederlands"),
8535    ]
8536}
8537
8538fn localized_label(locale_id: &str) -> &'static str {
8539    match locale_id {
8540        "en-US" => "Interface language: English",
8541        "fr-FR" => "Langue de l'interface : français",
8542        "de-DE" => "Sprache der Oberfläche: Deutsch",
8543        "it-IT" => "Lingua dell'interfaccia: italiano",
8544        "pt-BR" => "Idioma da interface: português",
8545        "nl-NL" => "Interfacetaal: Nederlands",
8546        _ => "Idioma de interfaz: español de México",
8547    }
8548}
8549
8550fn lorem_lines() -> [&'static str; 8] {
8551    [
8552        "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
8553        "Integer vitae arcu at neque feugiat posuere.",
8554        "Suspendisse potenti. Praesent eget sem non mauris luctus.",
8555        "Curabitur blandit, justo non gravida tristique, mi nunc.",
8556        "Donec at nibh vel sapien facilisis feugiat.",
8557        "Aliquam erat volutpat. Nam porttitor sem at ligula.",
8558        "Vivamus dictum eros vitae tortor aliquet, in tempor urna.",
8559        "Sed finibus velit non lectus efficitur, sed tempor orci.",
8560    ]
8561}
8562
8563fn menu_bar_menus(autosave: bool, grid: bool) -> Vec<ext_widgets::MenuBarMenu> {
8564    vec![
8565        ext_widgets::MenuBarMenu::new("file", "File", menu_items(autosave)),
8566        ext_widgets::MenuBarMenu::new(
8567            "edit",
8568            "Edit",
8569            vec![
8570                ext_widgets::MenuItem::command("undo", "Undo").shortcut("Ctrl+Z"),
8571                ext_widgets::MenuItem::command("redo", "Redo").shortcut("Ctrl+Shift+Z"),
8572            ],
8573        ),
8574        ext_widgets::MenuBarMenu::new(
8575            "view",
8576            "View",
8577            vec![ext_widgets::MenuItem::check("grid", "Grid", grid)],
8578        ),
8579    ]
8580}
8581
8582fn menu_items(autosave: bool) -> Vec<ext_widgets::MenuItem> {
8583    vec![
8584        ext_widgets::MenuItem::command("new", "New").shortcut("Ctrl+N"),
8585        ext_widgets::MenuItem::command("open", "Open").shortcut("Ctrl+O"),
8586        ext_widgets::MenuItem::separator(),
8587        ext_widgets::MenuItem::check("autosave", "Autosave", autosave),
8588        ext_widgets::MenuItem::submenu(
8589            "recent",
8590            "Recent",
8591            vec![
8592                ext_widgets::MenuItem::command("recent.one", "demo.rs"),
8593                ext_widgets::MenuItem::command("recent.two", "notes.md"),
8594            ],
8595        ),
8596        ext_widgets::MenuItem::command("delete", "Delete").destructive(),
8597        ext_widgets::MenuItem::command("disabled", "Disabled").disabled(),
8598    ]
8599}
8600
8601fn menu_item_top_offset(items: &[ext_widgets::MenuItem], index: usize) -> f32 {
8602    items
8603        .iter()
8604        .take(index)
8605        .map(|item| menu_item_height(Some(item)))
8606        .sum()
8607}
8608
8609fn menu_item_height(item: Option<&ext_widgets::MenuItem>) -> f32 {
8610    if item.is_some_and(ext_widgets::MenuItem::is_separator) {
8611        8.0
8612    } else {
8613        28.0
8614    }
8615}
8616
8617fn command_palette_items() -> Vec<ext_widgets::CommandPaletteItem> {
8618    vec![
8619        ext_widgets::CommandPaletteItem::new("open", "Open")
8620            .subtitle("Open a document")
8621            .shortcut("Ctrl+O")
8622            .keyword("file"),
8623        ext_widgets::CommandPaletteItem::new("save", "Save")
8624            .subtitle("Write current changes")
8625            .shortcut("Ctrl+S"),
8626        ext_widgets::CommandPaletteItem::new("format", "Format document")
8627            .subtitle("Apply source formatting")
8628            .keyword("code"),
8629        ext_widgets::CommandPaletteItem::new("rename", "Rename symbol")
8630            .subtitle("Change every reference")
8631            .shortcut("F2"),
8632        ext_widgets::CommandPaletteItem::new("toggle_sidebar", "Toggle sidebar")
8633            .subtitle("Show or hide the widget panel")
8634            .shortcut("Ctrl+B"),
8635        ext_widgets::CommandPaletteItem::new("run", "Run current example")
8636            .subtitle("Launch showcase")
8637            .shortcut("Ctrl+R"),
8638        ext_widgets::CommandPaletteItem::new("focus_canvas", "Focus canvas")
8639            .subtitle("Move interaction to the canvas window"),
8640        ext_widgets::CommandPaletteItem::new("reset_layout", "Reset window layout")
8641            .subtitle("Restore the default showcase positions"),
8642        ext_widgets::CommandPaletteItem::new("disabled", "Disabled command").disabled(),
8643    ]
8644}
8645
8646fn command_palette_items_with_history(
8647    history: &ext_widgets::CommandPaletteHistory,
8648) -> Vec<ext_widgets::CommandPaletteItem> {
8649    let mut items = command_palette_items()
8650        .into_iter()
8651        .map(|item| {
8652            let command = CommandId::from(item.id.as_str());
8653            if history.is_recent(&command) {
8654                item.keyword("recent")
8655            } else {
8656                item
8657            }
8658        })
8659        .collect::<Vec<_>>();
8660    items.sort_by(|left, right| {
8661        let left_id = CommandId::from(left.id.as_str());
8662        let right_id = CommandId::from(right.id.as_str());
8663        match (
8664            history.recency_rank(&left_id),
8665            history.recency_rank(&right_id),
8666        ) {
8667            (Some(left_rank), Some(right_rank)) => left_rank.cmp(&right_rank),
8668            (Some(_), None) => std::cmp::Ordering::Less,
8669            (None, Some(_)) => std::cmp::Ordering::Greater,
8670            (None, None) => left.title.cmp(&right.title),
8671        }
8672    });
8673    items
8674}
8675
8676fn table_columns() -> Vec<widgets::TableColumn> {
8677    vec![
8678        widgets::TableColumn {
8679            id: "name".to_string(),
8680            label: "Name".to_string(),
8681            width: 160.0,
8682        },
8683        widgets::TableColumn {
8684            id: "status".to_string(),
8685            label: "Status".to_string(),
8686            width: 140.0,
8687        },
8688        widgets::TableColumn {
8689            id: "value".to_string(),
8690            label: "Value".to_string(),
8691            width: 100.0,
8692        },
8693    ]
8694}
8695
8696fn virtual_table_columns(state: &ShowcaseState) -> Vec<ext_widgets::DataTableColumn> {
8697    let sort = if state.virtual_table_descending {
8698        ext_widgets::DataTableSortState::descending()
8699    } else {
8700        ext_widgets::DataTableSortState::ascending()
8701    };
8702    let filter = if state.virtual_table_ready_only {
8703        ext_widgets::DataTableFilterState::active("status").with_value("Ready")
8704    } else {
8705        ext_widgets::DataTableFilterState::inactive()
8706    };
8707    vec![
8708        ext_widgets::DataTableColumn::new("name", "Virtualized", 160.0)
8709            .with_sort(sort)
8710            .sortable("lists_tables.virtualized_table.sort.name"),
8711        ext_widgets::DataTableColumn::new("status", "Status", 110.0)
8712            .with_filter(filter)
8713            .filterable("lists_tables.virtualized_table.filter.status"),
8714        ext_widgets::DataTableColumn::new("value", "Value", state.virtual_table_value_width)
8715            .with_min_width(56.0)
8716            .with_alignment(ext_widgets::DataCellAlignment::End)
8717            .resize_command("lists_tables.virtualized_table.resize.value"),
8718    ]
8719}
8720
8721fn virtual_table_visible_rows(state: &ShowcaseState) -> Vec<usize> {
8722    let mut rows = (0..32)
8723        .filter(|row| !state.virtual_table_ready_only || row % 2 == 0)
8724        .collect::<Vec<_>>();
8725    if state.virtual_table_descending {
8726        rows.reverse();
8727    }
8728    rows
8729}
8730
8731fn virtual_table_cell_value(source_row: usize, column: usize) -> String {
8732    match column {
8733        0 => format!("Virtual row {}", source_row + 1),
8734        1 if source_row % 2 == 0 => "Ready".to_string(),
8735        1 => "Pending".to_string(),
8736        _ => format!("{}%", 30 + source_row * 2),
8737    }
8738}
8739
8740fn tree_items() -> Vec<ext_widgets::TreeItem> {
8741    vec![
8742        ext_widgets::TreeItem::new("root", "Project").with_children(vec![
8743            ext_widgets::TreeItem::new("src", "src").with_children(vec![
8744                ext_widgets::TreeItem::new("lib", "lib.rs"),
8745                ext_widgets::TreeItem::new("widgets", "widgets.rs"),
8746            ]),
8747            ext_widgets::TreeItem::new("assets", "assets").with_children(vec![
8748                ext_widgets::TreeItem::new("shader", "shader.wgsl"),
8749                ext_widgets::TreeItem::new("logo", "logo.png"),
8750            ]),
8751            ext_widgets::TreeItem::new("target", "target").disabled(),
8752        ]),
8753    ]
8754}
8755
8756fn virtual_tree_items() -> Vec<ext_widgets::TreeItem> {
8757    vec![
8758        ext_widgets::TreeItem::new("root", "Large project").with_children(
8759            (0..48)
8760                .map(|index| {
8761                    ext_widgets::TreeItem::new(
8762                        format!("file-{index:02}"),
8763                        format!("File {index:02}.rs"),
8764                    )
8765                })
8766                .collect(),
8767        ),
8768    ]
8769}
8770
8771fn tree_table_items() -> Vec<ext_widgets::TreeItem> {
8772    vec![
8773        ext_widgets::TreeItem::new("root", "Workspace").with_children(vec![
8774            ext_widgets::TreeItem::new("branch-a", "Interface").with_children(vec![
8775                ext_widgets::TreeItem::new("widgets", "widgets.rs"),
8776                ext_widgets::TreeItem::new("layout", "layout.rs"),
8777            ]),
8778            ext_widgets::TreeItem::new("branch-b", "Renderer").with_children(vec![
8779                ext_widgets::TreeItem::new("wgpu", "wgpu.rs"),
8780                ext_widgets::TreeItem::new("paint", "paint.rs").disabled(),
8781            ]),
8782            ext_widgets::TreeItem::new("docs", "docs"),
8783        ]),
8784    ]
8785}
8786
8787fn parse_calendar_date(value: &str) -> Option<CalendarDate> {
8788    let mut parts = value.split('-');
8789    let year = parts.next()?.parse().ok()?;
8790    let month = parts.next()?.parse().ok()?;
8791    let day = parts.next()?.parse().ok()?;
8792    CalendarDate::new(year, month, day)
8793}
8794
8795fn parse_table_cell(value: &str) -> Option<ext_widgets::DataTableCellIndex> {
8796    let mut parts = value.split('.');
8797    let row = parts.next()?.parse().ok()?;
8798    let column = parts.next()?.parse().ok()?;
8799    if parts.next().is_some() {
8800        return None;
8801    }
8802    Some(ext_widgets::DataTableCellIndex::new(row, column))
8803}
8804
8805fn unit(value: f32) -> f32 {
8806    value.clamp(0.0, 1.0)
8807}
8808
8809fn smooth_loop(phase: f32, offset: f32) -> f32 {
8810    0.5 - ((phase + offset).cos() * 0.5)
8811}
8812
8813fn profile_form_state() -> FormState {
8814    let mut form = FormState::new("profile")
8815        .with_field("name", "Operad")
8816        .with_field("email", "ada@example.com")
8817        .with_field("role", "Designer")
8818        .with_field("newsletter", "true");
8819    let _ = form.update_field("email", "invalid@example");
8820    let request = form.begin_form_validation();
8821    let _ = form.apply_form_validation(
8822        FormValidationResult::new(request.generation)
8823            .with_field_messages(
8824                "email",
8825                vec![ValidationMessage::error("Use a complete email address")],
8826            )
8827            .with_form_message(ValidationMessage::warning("Unsaved profile changes")),
8828    );
8829    form
8830}
8831
8832fn profile_form_value(form: &FormState, id: &str) -> String {
8833    form.fields
8834        .iter()
8835        .find_map(|(field_id, field)| (field_id.as_str() == id).then(|| field.value.clone()))
8836        .unwrap_or_default()
8837}
8838
8839fn scaled_slider(rect: UiRect, point: UiPoint, min: f32, max: f32) -> f32 {
8840    min + unit(widgets::slider::slider_value_from_control_point(
8841        rect,
8842        point,
8843        0.0..1.0,
8844    )) * (max - min)
8845}
8846
8847fn scroll_state(offset_y: f32, viewport_height: f32, content_height: f32) -> operad::ScrollState {
8848    operad::ScrollState::new(ScrollAxes::VERTICAL)
8849        .with_sizes(
8850            UiSize::new(8.0, viewport_height),
8851            UiSize::new(8.0, content_height),
8852        )
8853        .with_offset(UiPoint::new(0.0, offset_y))
8854}
8855
8856fn controls_list_viewport_height(viewport_height: f32) -> f32 {
8857    (viewport_height - 110.0).max(120.0)
8858}
8859
8860fn controls_scroll_state_for_view(
8861    saved: operad::ScrollState,
8862    viewport_height: f32,
8863) -> operad::ScrollState {
8864    let viewport_height = if saved.viewport_size().height > f32::EPSILON {
8865        saved.viewport_size().height
8866    } else {
8867        viewport_height
8868    };
8869    let content_height = if saved.content_size().height > f32::EPSILON {
8870        saved.content_size().height
8871    } else {
8872        controls_list_content_height()
8873    };
8874    scroll_state(saved.offset().y, viewport_height, content_height)
8875}
8876
8877fn controls_list_content_height() -> f32 {
8878    SHOWCASE_WIDGET_WINDOW_IDS.len() as f32 * CONTROLS_WIDGET_ROW_HEIGHT
8879        + (SHOWCASE_WIDGET_WINDOW_IDS.len().saturating_sub(1)) as f32 * CONTROLS_WIDGET_ROW_GAP
8880}
8881
8882fn caret_visible(phase: f32) -> bool {
8883    phase.sin() >= 0.0
8884}
8885
8886fn text(size: f32, color: ColorRgba) -> TextStyle {
8887    TextStyle {
8888        font_size: size,
8889        line_height: size + 5.0,
8890        color,
8891        ..Default::default()
8892    }
8893}
8894
8895fn color(r: u8, g: u8, b: u8) -> ColorRgba {
8896    ColorRgba::new(r, g, b, 255)
8897}