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 ®istry,
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}