@effindomv2/fui-rs 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/COMMERCIAL.md +7 -0
  2. package/Cargo.toml +34 -0
  3. package/LICENSE.md +9 -0
  4. package/README.md +88 -0
  5. package/package.json +54 -0
  6. package/scripts/build.sh +227 -0
  7. package/scripts/check-runtime-dependency.sh +41 -0
  8. package/scripts/framework-host-services.ts +40 -0
  9. package/scripts/generate-host-events.ts +17 -0
  10. package/scripts/generate-host-services.ts +28 -0
  11. package/scripts/hostgen/common.ts +73 -0
  12. package/scripts/hostgen/registry.ts +159 -0
  13. package/scripts/hostgen/rust-host-events.ts +171 -0
  14. package/scripts/hostgen/rust-host-services.ts +257 -0
  15. package/src/animation.rs +625 -0
  16. package/src/app.rs +324 -0
  17. package/src/assets.rs +572 -0
  18. package/src/bindings/mod.rs +1 -0
  19. package/src/bindings/ui.rs +705 -0
  20. package/src/bitmap.rs +317 -0
  21. package/src/bridge_callbacks.rs +242 -0
  22. package/src/context_menu_manager.rs +332 -0
  23. package/src/controls/anti_selection_area.rs +54 -0
  24. package/src/controls/button.rs +542 -0
  25. package/src/controls/checkbox.rs +372 -0
  26. package/src/controls/combobox.rs +1574 -0
  27. package/src/controls/context_menu.rs +1367 -0
  28. package/src/controls/control_template_set.rs +46 -0
  29. package/src/controls/control_tokens.rs +596 -0
  30. package/src/controls/dialog.rs +590 -0
  31. package/src/controls/dropdown.rs +1010 -0
  32. package/src/controls/form.rs +229 -0
  33. package/src/controls/internal/button_presenter.rs +142 -0
  34. package/src/controls/internal/checkbox_indicator_presenter.rs +210 -0
  35. package/src/controls/internal/dropdown_chevron_presenter.rs +134 -0
  36. package/src/controls/internal/dropdown_field_presenter.rs +269 -0
  37. package/src/controls/internal/dropdown_option_row_presenter.rs +184 -0
  38. package/src/controls/internal/mod.rs +13 -0
  39. package/src/controls/internal/pressable_indicator_presenter.rs +26 -0
  40. package/src/controls/internal/pressable_labeled_control.rs +492 -0
  41. package/src/controls/internal/radio_indicator_presenter.rs +208 -0
  42. package/src/controls/internal/selectable_popup_list.rs +597 -0
  43. package/src/controls/internal/slider_presenter.rs +282 -0
  44. package/src/controls/internal/switch_indicator_presenter.rs +235 -0
  45. package/src/controls/internal/text_input_core.rs +1074 -0
  46. package/src/controls/internal/text_input_presenter.rs +108 -0
  47. package/src/controls/mod.rs +235 -0
  48. package/src/controls/nav_link.rs +395 -0
  49. package/src/controls/popup.rs +191 -0
  50. package/src/controls/progress_bar.rs +287 -0
  51. package/src/controls/radio_button.rs +348 -0
  52. package/src/controls/radio_group.rs +283 -0
  53. package/src/controls/selection_area.rs +82 -0
  54. package/src/controls/shared.rs +68 -0
  55. package/src/controls/slider.rs +701 -0
  56. package/src/controls/switch.rs +293 -0
  57. package/src/controls/templating.rs +49 -0
  58. package/src/controls/tests.rs +3905 -0
  59. package/src/controls/text_area.rs +207 -0
  60. package/src/controls/text_input.rs +192 -0
  61. package/src/debug.rs +146 -0
  62. package/src/drag_drop.rs +424 -0
  63. package/src/drag_gesture.rs +258 -0
  64. package/src/drawing.rs +385 -0
  65. package/src/event.rs +1603 -0
  66. package/src/external_drop.rs +467 -0
  67. package/src/fetch.rs +500 -0
  68. package/src/ffi.rs +3542 -0
  69. package/src/file.rs +1677 -0
  70. package/src/focus_adorner.rs +307 -0
  71. package/src/focus_visibility.rs +121 -0
  72. package/src/frame_scheduler.rs +159 -0
  73. package/src/frame_signal.rs +64 -0
  74. package/src/generated/ffi.rs +798 -0
  75. package/src/generated/framework_host_services.rs +74 -0
  76. package/src/generated/mod.rs +2 -0
  77. package/src/host_services.rs +162 -0
  78. package/src/image_sampling.rs +78 -0
  79. package/src/keyboard_scroll.rs +137 -0
  80. package/src/keyboard_scroll_tracker.rs +385 -0
  81. package/src/lib.rs +547 -0
  82. package/src/logger.rs +56 -0
  83. package/src/mobile_text_selection_toolbar.rs +1034 -0
  84. package/src/navigation.rs +48 -0
  85. package/src/node/core.rs +2210 -0
  86. package/src/node/custom_drawable.rs +149 -0
  87. package/src/node/flex_box.rs +874 -0
  88. package/src/node/grid.rs +304 -0
  89. package/src/node/helpers.rs +442 -0
  90. package/src/node/image.rs +506 -0
  91. package/src/node/mod.rs +315 -0
  92. package/src/node/scroll_bar.rs +845 -0
  93. package/src/node/scroll_box.rs +514 -0
  94. package/src/node/scroll_state.rs +121 -0
  95. package/src/node/scroll_view.rs +557 -0
  96. package/src/node/svg_node.rs +474 -0
  97. package/src/node/text_node.rs +633 -0
  98. package/src/node/virtual_list.rs +678 -0
  99. package/src/panic_hook.rs +36 -0
  100. package/src/persisted.rs +274 -0
  101. package/src/platform.rs +556 -0
  102. package/src/popup_presenter.rs +344 -0
  103. package/src/selection_handle_adorner.rs +838 -0
  104. package/src/signal.rs +134 -0
  105. package/src/text.rs +1065 -0
  106. package/src/theme.rs +571 -0
  107. package/src/timers.rs +106 -0
  108. package/src/tool_tip.rs +167 -0
  109. package/src/tool_tip_manager.rs +691 -0
  110. package/src/transitions.rs +41 -0
  111. package/src/typography.rs +520 -0
  112. package/src/viewport.rs +108 -0
  113. package/src/worker.rs +308 -0
  114. package/src/worker_job.rs +82 -0
  115. package/src/worker_runtime.rs +172 -0
@@ -0,0 +1,1010 @@
1
+ use super::control_template_set::get_control_templates;
2
+ use super::internal::dropdown_chevron_presenter::{
3
+ create_default_dropdown_chevron_presenter, DropdownChevronPresenter, DropdownChevronTemplate,
4
+ DropdownChevronVisualState,
5
+ };
6
+ use super::internal::dropdown_field_presenter::{
7
+ create_default_dropdown_field_presenter, DropdownFieldPresenter, DropdownFieldTemplate,
8
+ DropdownFieldVisualState,
9
+ };
10
+ use super::internal::dropdown_option_row_presenter::DropdownOptionRowTemplate;
11
+ use super::internal::selectable_popup_list::{
12
+ SelectablePopupList, SelectablePopupListOwner, SELECTABLE_POPUP_LIST_PANEL_PADDING,
13
+ };
14
+ use super::{DropdownChangedEventArgs, DropdownColors, DropdownSizing};
15
+ use crate::bindings::ui;
16
+ use crate::event::{self, KeyEventArgs};
17
+ use crate::ffi::{AlignItems, CursorStyle, FlexDirection, KeyEventType, NodeType, SemanticRole};
18
+ use crate::focus_adorner;
19
+ use crate::focus_visibility;
20
+ use crate::logger;
21
+ use crate::node::{row, FlexBox, FlexBoxSurface, HasFlexBoxRoot, Node, NodeRef, WeakFlexBox};
22
+ use crate::persisted::{persisted_value_adapter, PersistedInt32Codec};
23
+ use crate::signal::SubscriptionGuard;
24
+ use crate::theme::{current_theme, subscribe};
25
+ use std::cell::{Cell, RefCell};
26
+ use std::rc::{Rc, Weak};
27
+
28
+ const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
29
+
30
+ fn is_activation_key(event: &KeyEventArgs) -> bool {
31
+ event.key == "Enter" || event.key == " " || event.key == "ArrowDown"
32
+ }
33
+
34
+ fn create_field_presenter(
35
+ template: Option<Rc<dyn DropdownFieldTemplate>>,
36
+ sizing: Option<DropdownSizing>,
37
+ ) -> Rc<dyn DropdownFieldPresenter> {
38
+ if let Some(template) = template {
39
+ return template.create(sizing);
40
+ }
41
+ if let Some(template) = get_control_templates().and_then(|set| set.dropdown_field) {
42
+ return template.create(sizing);
43
+ }
44
+ create_default_dropdown_field_presenter(sizing)
45
+ }
46
+
47
+ fn create_chevron_presenter(
48
+ template: Option<Rc<dyn DropdownChevronTemplate>>,
49
+ sizing: Option<DropdownSizing>,
50
+ ) -> Rc<dyn DropdownChevronPresenter> {
51
+ if let Some(template) = template {
52
+ return template.create(sizing);
53
+ }
54
+ if let Some(template) = get_control_templates().and_then(|set| set.dropdown_chevron) {
55
+ return template.create(sizing);
56
+ }
57
+ create_default_dropdown_chevron_presenter(sizing)
58
+ }
59
+
60
+ #[derive(Clone, Debug, PartialEq, Eq)]
61
+ pub struct DropdownItem {
62
+ pub value: String,
63
+ pub label: String,
64
+ }
65
+
66
+ impl DropdownItem {
67
+ pub fn new(value: impl Into<String>, label: impl Into<String>) -> Self {
68
+ Self {
69
+ value: value.into(),
70
+ label: label.into(),
71
+ }
72
+ }
73
+
74
+ pub fn from_value(value: impl Into<String>) -> Self {
75
+ let value = value.into();
76
+ Self {
77
+ label: value.clone(),
78
+ value,
79
+ }
80
+ }
81
+ }
82
+
83
+ #[derive(Clone)]
84
+ pub struct Dropdown {
85
+ root: FlexBox,
86
+ shared: Rc<DropdownShared>,
87
+ }
88
+
89
+ struct DropdownShared {
90
+ self_weak: RefCell<Weak<DropdownShared>>,
91
+ root: WeakFlexBox,
92
+ field_template_value: RefCell<Option<Rc<dyn DropdownFieldTemplate>>>,
93
+ chevron_template_value: RefCell<Option<Rc<dyn DropdownChevronTemplate>>>,
94
+ option_row_template_value: RefCell<Option<Rc<dyn DropdownOptionRowTemplate>>>,
95
+ sizing_value: Cell<Option<DropdownSizing>>,
96
+ colors_value: Cell<Option<DropdownColors>>,
97
+ field_presenter: RefCell<Rc<dyn DropdownFieldPresenter>>,
98
+ chevron_presenter: RefCell<Rc<dyn DropdownChevronPresenter>>,
99
+ popup_list: SelectablePopupList,
100
+ items_value: RefCell<Vec<DropdownItem>>,
101
+ open_state: Cell<bool>,
102
+ pointer_pressed_state: Cell<bool>,
103
+ hovered_state: Cell<bool>,
104
+ focused_state: Cell<bool>,
105
+ key_filter_token: Cell<u32>,
106
+ selected_index_value: Cell<i32>,
107
+ highlighted_index_value: Cell<i32>,
108
+ popup_panel_color_value: Cell<u32>,
109
+ popup_panel_background_blur_sigma_value: Cell<f32>,
110
+ popup_panel_color_overridden: Cell<bool>,
111
+ popup_panel_background_blur_overridden: Cell<bool>,
112
+ changed_callback: RefCell<Option<Rc<dyn Fn(DropdownChangedEventArgs<DropdownItem>)>>>,
113
+ theme_guard: RefCell<Option<SubscriptionGuard>>,
114
+ focus_visibility_guard: RefCell<Option<SubscriptionGuard>>,
115
+ }
116
+
117
+ thread_local! {
118
+ static ACTIVE_DROPDOWN: RefCell<Option<Weak<DropdownShared>>> = const { RefCell::new(None) };
119
+ static DROPDOWN_SCROLL_HOOK_REGISTERED: Cell<bool> = const { Cell::new(false) };
120
+ }
121
+
122
+ impl Default for Dropdown {
123
+ fn default() -> Self {
124
+ Self::new()
125
+ }
126
+ }
127
+
128
+ impl Dropdown {
129
+ pub fn new() -> Self {
130
+ Self::ensure_scroll_hook();
131
+ let root = row();
132
+ root.semantic_role(SemanticRole::ComboBox)
133
+ .focusable(true, 0)
134
+ .interactive(true)
135
+ .cursor(CursorStyle::Pointer)
136
+ .flex_direction(FlexDirection::Row)
137
+ .align_items(AlignItems::Center)
138
+ .reflect_semantic_disabled_from_enabled()
139
+ .default_semantic_label("Dropdown");
140
+
141
+ let weak_root = root.downgrade();
142
+ let field_presenter = create_field_presenter(None, None);
143
+ field_presenter.root().fill_width();
144
+ let chevron_presenter = create_chevron_presenter(None, None);
145
+ field_presenter
146
+ .chevron_host()
147
+ .child(&chevron_presenter.root());
148
+
149
+ let shared_slot: Rc<RefCell<Option<Weak<DropdownShared>>>> = Rc::new(RefCell::new(None));
150
+ let owner = SelectablePopupListOwner {
151
+ item_count: {
152
+ let shared_slot = shared_slot.clone();
153
+ Rc::new(move || {
154
+ shared_slot
155
+ .borrow()
156
+ .as_ref()
157
+ .and_then(Weak::upgrade)
158
+ .map(|shared| shared.items_value.borrow().len() as i32)
159
+ .unwrap_or(0)
160
+ })
161
+ },
162
+ item_label: {
163
+ let shared_slot = shared_slot.clone();
164
+ Rc::new(move |index| {
165
+ shared_slot
166
+ .borrow()
167
+ .as_ref()
168
+ .and_then(Weak::upgrade)
169
+ .and_then(|shared| shared.items_value.borrow().get(index as usize).cloned())
170
+ .map(|item| item.label)
171
+ .unwrap_or_default()
172
+ })
173
+ },
174
+ item_selected: {
175
+ let shared_slot = shared_slot.clone();
176
+ Rc::new(move |index| {
177
+ shared_slot
178
+ .borrow()
179
+ .as_ref()
180
+ .and_then(Weak::upgrade)
181
+ .is_some_and(|shared| shared.selected_index_value.get() == index)
182
+ })
183
+ },
184
+ enabled: {
185
+ let weak_root = weak_root.clone();
186
+ Rc::new(move || {
187
+ weak_root
188
+ .upgrade()
189
+ .is_some_and(|root| root.retained_node_ref().is_enabled_for_routing())
190
+ })
191
+ },
192
+ highlight_index: {
193
+ let shared_slot = shared_slot.clone();
194
+ Rc::new(move |index| {
195
+ if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
196
+ shared.highlight_index(index);
197
+ }
198
+ })
199
+ },
200
+ activate_index: {
201
+ let shared_slot = shared_slot.clone();
202
+ Rc::new(move |index| {
203
+ if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
204
+ shared.popup_list_activate_index(index);
205
+ }
206
+ })
207
+ },
208
+ pointer_down: Rc::new(move |_index| {}),
209
+ pointer_up: Rc::new(move |_index| {}),
210
+ };
211
+ let popup_list = SelectablePopupList::new(owner);
212
+ let shared = Rc::new(DropdownShared {
213
+ self_weak: RefCell::new(Weak::new()),
214
+ root: weak_root,
215
+ field_template_value: RefCell::new(None),
216
+ chevron_template_value: RefCell::new(None),
217
+ option_row_template_value: RefCell::new(None),
218
+ sizing_value: Cell::new(None),
219
+ colors_value: Cell::new(None),
220
+ field_presenter: RefCell::new(field_presenter.clone()),
221
+ chevron_presenter: RefCell::new(chevron_presenter.clone()),
222
+ popup_list: popup_list.clone(),
223
+ items_value: RefCell::new(Vec::new()),
224
+ open_state: Cell::new(false),
225
+ pointer_pressed_state: Cell::new(false),
226
+ hovered_state: Cell::new(false),
227
+ focused_state: Cell::new(false),
228
+ key_filter_token: Cell::new(0),
229
+ selected_index_value: Cell::new(-1),
230
+ highlighted_index_value: Cell::new(-1),
231
+ popup_panel_color_value: Cell::new(0x00000000),
232
+ popup_panel_background_blur_sigma_value: Cell::new(DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA),
233
+ popup_panel_color_overridden: Cell::new(false),
234
+ popup_panel_background_blur_overridden: Cell::new(false),
235
+ changed_callback: RefCell::new(None),
236
+ theme_guard: RefCell::new(None),
237
+ focus_visibility_guard: RefCell::new(None),
238
+ });
239
+ *shared.self_weak.borrow_mut() = Rc::downgrade(&shared);
240
+ *shared_slot.borrow_mut() = Some(Rc::downgrade(&shared));
241
+
242
+ popup_list.popup_presenter.overlay_node().on_click({
243
+ let weak_shared = Rc::downgrade(&shared);
244
+ move |_event| {
245
+ if let Some(shared) = weak_shared.upgrade() {
246
+ shared.close();
247
+ }
248
+ }
249
+ });
250
+
251
+ root.child(&field_presenter.root()).child(&popup_list.root);
252
+
253
+ let control = Self { root, shared };
254
+ control.install_visual_subscriptions();
255
+ control.bind_events();
256
+ control.shared.sync_value_label();
257
+ control.shared.handle_theme_changed();
258
+ control.root.persist_state(persisted_value_adapter(
259
+ "dropdown-selected-index",
260
+ PersistedInt32Codec,
261
+ 1,
262
+ {
263
+ let shared = control.shared.clone();
264
+ move || {
265
+ let value = shared.selected_index_value.get();
266
+ if value >= 0 {
267
+ Some(value)
268
+ } else {
269
+ None
270
+ }
271
+ }
272
+ },
273
+ {
274
+ let shared = control.shared.clone();
275
+ move |value| {
276
+ shared.set_selected_index(value, true);
277
+ }
278
+ },
279
+ ));
280
+ control
281
+ }
282
+
283
+ fn ensure_scroll_hook() {
284
+ DROPDOWN_SCROLL_HOOK_REGISTERED.with(|registered| {
285
+ if registered.get() {
286
+ return;
287
+ }
288
+ registered.set(true);
289
+ event::register_scroll_hook(|| {
290
+ ACTIVE_DROPDOWN.with(|slot| {
291
+ let Some(shared) = slot.borrow().as_ref().and_then(Weak::upgrade) else {
292
+ return;
293
+ };
294
+ if !shared.is_trigger_visible_in_viewport() {
295
+ shared.close();
296
+ }
297
+ });
298
+ });
299
+ });
300
+ }
301
+
302
+ fn install_visual_subscriptions(&self) {
303
+ *self.shared.theme_guard.borrow_mut() = Some(subscribe({
304
+ let weak_shared = Rc::downgrade(&self.shared);
305
+ move |_theme| {
306
+ if let Some(shared) = weak_shared.upgrade() {
307
+ shared.handle_theme_changed();
308
+ }
309
+ }
310
+ }));
311
+ *self.shared.focus_visibility_guard.borrow_mut() = Some(focus_visibility::subscribe({
312
+ let weak_shared = Rc::downgrade(&self.shared);
313
+ move |_visible| {
314
+ if let Some(shared) = weak_shared.upgrade() {
315
+ shared.sync_focus_chrome();
316
+ }
317
+ }
318
+ }));
319
+ }
320
+
321
+ fn bind_events(&self) {
322
+ let shared = self.shared.clone();
323
+ self.root.on_pointer_enter(move |_event| {
324
+ if !shared.is_enabled() {
325
+ return;
326
+ }
327
+ shared.hovered_state.set(true);
328
+ shared.handle_theme_changed();
329
+ });
330
+
331
+ let shared = self.shared.clone();
332
+ self.root.on_pointer_leave(move |_event| {
333
+ shared.pointer_pressed_state.set(false);
334
+ shared.hovered_state.set(false);
335
+ shared.handle_theme_changed();
336
+ });
337
+
338
+ let shared = self.shared.clone();
339
+ self.root.on_pointer_down(move |_event| {
340
+ if !shared.is_enabled() {
341
+ return;
342
+ }
343
+ shared.pointer_pressed_state.set(true);
344
+ shared.handle_theme_changed();
345
+ });
346
+
347
+ let shared = self.shared.clone();
348
+ self.root.on_pointer_up(move |event| {
349
+ if !shared.is_enabled() || !shared.pointer_pressed_state.get() {
350
+ return;
351
+ }
352
+ shared.pointer_pressed_state.set(false);
353
+ if shared.open_state.get() {
354
+ shared.close();
355
+ } else {
356
+ shared.open();
357
+ }
358
+ shared.handle_theme_changed();
359
+ event.handled = true;
360
+ });
361
+
362
+ let shared = self.shared.clone();
363
+ self.root.on_key_down(move |event| {
364
+ if shared.handle_global_key_event(
365
+ KeyEventType::Down,
366
+ event.key.as_str(),
367
+ event.modifiers,
368
+ ) {
369
+ event.handled = true;
370
+ return;
371
+ }
372
+ if !shared.is_enabled() || event.modifiers != 0 {
373
+ return;
374
+ }
375
+ if !shared.open_state.get() && is_activation_key(event) {
376
+ shared.open();
377
+ event.handled = true;
378
+ return;
379
+ }
380
+ if !shared.open_state.get() && event.key == "ArrowUp" {
381
+ shared.open();
382
+ shared.move_highlight(-1);
383
+ event.handled = true;
384
+ }
385
+ });
386
+
387
+ let shared = self.shared.clone();
388
+ self.root.on_key_up(move |event| {
389
+ if shared.handle_global_key_event(KeyEventType::Up, event.key.as_str(), event.modifiers)
390
+ {
391
+ event.handled = true;
392
+ }
393
+ });
394
+
395
+ let shared = self.shared.clone();
396
+ self.root.on_focus_changed(move |event| {
397
+ shared.focused_state.set(event.focused);
398
+ if !event.focused && !shared.open_state.get() {
399
+ shared.pointer_pressed_state.set(false);
400
+ }
401
+ shared.handle_theme_changed();
402
+ });
403
+ }
404
+
405
+ pub fn selected_index(&self) -> i32 {
406
+ self.shared.selected_index_value.get()
407
+ }
408
+
409
+ pub fn items<I>(&self, items: I) -> &Self
410
+ where
411
+ I: IntoIterator<Item = DropdownItem>,
412
+ {
413
+ self.shared.clear_items();
414
+ self.shared.items_value.borrow_mut().extend(items);
415
+ let count = self.shared.items_value.borrow().len() as i32;
416
+ let selected = self.shared.selected_index_value.get();
417
+ if selected >= count {
418
+ self.shared
419
+ .selected_index_value
420
+ .set(if count > 0 { 0 } else { -1 });
421
+ } else if selected < 0 && count > 0 {
422
+ self.shared.selected_index_value.set(0);
423
+ }
424
+ self.shared.popup_list.refresh_panel_layout();
425
+ self.shared.sync_value_label();
426
+ self.shared.handle_theme_changed();
427
+ self
428
+ }
429
+
430
+ pub fn on_changed(
431
+ &self,
432
+ callback: impl Fn(DropdownChangedEventArgs<DropdownItem>) + 'static,
433
+ ) -> &Self {
434
+ *self.shared.changed_callback.borrow_mut() = Some(Rc::new(callback));
435
+ self
436
+ }
437
+
438
+ pub fn max_visible_items(&self, count: i32) -> &Self {
439
+ self.shared.popup_list.max_visible_items(count);
440
+ self
441
+ }
442
+
443
+ pub fn popup_width(&self, value: f32) -> &Self {
444
+ self.shared.popup_list.popup_width(value);
445
+ self
446
+ }
447
+
448
+ pub fn popup_panel_color(&self, color: u32) -> &Self {
449
+ self.shared.popup_panel_color_overridden.set(true);
450
+ self.shared.popup_panel_color_value.set(color);
451
+ self.shared.popup_list.panel_node.bg_color(color);
452
+ self
453
+ }
454
+
455
+ pub fn popup_panel_background_blur(&self, sigma: f32) -> &Self {
456
+ self.shared.popup_panel_background_blur_overridden.set(true);
457
+ if sigma < 0.0 {
458
+ logger::warn(
459
+ "Layout",
460
+ &format!("Dropdown.popupPanelBackgroundBlur() received {sigma}; clamping to 0.0."),
461
+ );
462
+ }
463
+ self.shared
464
+ .popup_panel_background_blur_sigma_value
465
+ .set(sigma.max(0.0));
466
+ self.shared
467
+ .popup_list
468
+ .panel_node
469
+ .background_blur(self.shared.popup_panel_background_blur_sigma_value.get());
470
+ self
471
+ }
472
+
473
+ pub fn sizing(&self, sizing: Option<DropdownSizing>) -> &Self {
474
+ self.shared.close();
475
+ self.shared.sizing_value.set(sizing);
476
+ if self.shared.uses_default_field_presenter() {
477
+ self.shared.replace_field_presenter(
478
+ create_field_presenter(self.shared.field_template_value.borrow().clone(), sizing),
479
+ create_chevron_presenter(
480
+ self.shared.chevron_template_value.borrow().clone(),
481
+ sizing,
482
+ ),
483
+ );
484
+ } else if self.shared.uses_default_chevron_presenter() {
485
+ let previous_presenter = self.shared.chevron_presenter.borrow().clone();
486
+ let next_presenter = create_chevron_presenter(
487
+ self.shared.chevron_template_value.borrow().clone(),
488
+ sizing,
489
+ );
490
+ *self.shared.chevron_presenter.borrow_mut() = next_presenter.clone();
491
+ self.shared
492
+ .field_presenter
493
+ .borrow()
494
+ .chevron_host()
495
+ .remove_child(&previous_presenter.root());
496
+ self.shared
497
+ .field_presenter
498
+ .borrow()
499
+ .chevron_host()
500
+ .child(&next_presenter.root());
501
+ previous_presenter.root().dispose();
502
+ }
503
+ self.shared.popup_list.sizing(sizing);
504
+ self.shared.sync_value_label();
505
+ self.shared.handle_theme_changed();
506
+ self
507
+ }
508
+
509
+ pub fn colors(&self, colors: Option<DropdownColors>) -> &Self {
510
+ self.shared.colors_value.set(colors);
511
+ self.shared.popup_list.colors(colors);
512
+ self.shared.handle_theme_changed();
513
+ self
514
+ }
515
+
516
+ pub fn field_template(&self, template: Option<Rc<dyn DropdownFieldTemplate>>) -> &Self {
517
+ self.shared.close();
518
+ *self.shared.field_template_value.borrow_mut() = template.clone();
519
+ let next_field_presenter = create_field_presenter(template, self.shared.sizing_value.get());
520
+ let next_chevron_presenter = create_chevron_presenter(
521
+ self.shared.chevron_template_value.borrow().clone(),
522
+ self.shared.sizing_value.get(),
523
+ );
524
+ self.shared
525
+ .replace_field_presenter(next_field_presenter, next_chevron_presenter);
526
+ self.shared.sync_value_label();
527
+ self.shared.handle_theme_changed();
528
+ self
529
+ }
530
+
531
+ pub fn chevron_template(&self, template: Option<Rc<dyn DropdownChevronTemplate>>) -> &Self {
532
+ self.shared.close();
533
+ *self.shared.chevron_template_value.borrow_mut() = template.clone();
534
+ let previous_presenter = self.shared.chevron_presenter.borrow().clone();
535
+ let next_presenter = create_chevron_presenter(template, self.shared.sizing_value.get());
536
+ *self.shared.chevron_presenter.borrow_mut() = next_presenter.clone();
537
+ self.shared
538
+ .field_presenter
539
+ .borrow()
540
+ .chevron_host()
541
+ .remove_child(&previous_presenter.root());
542
+ self.shared
543
+ .field_presenter
544
+ .borrow()
545
+ .chevron_host()
546
+ .child(&next_presenter.root());
547
+ previous_presenter.root().dispose();
548
+ self.shared.handle_theme_changed();
549
+ self
550
+ }
551
+
552
+ pub fn option_row_template(
553
+ &self,
554
+ template: Option<Rc<dyn DropdownOptionRowTemplate>>,
555
+ ) -> &Self {
556
+ self.shared.close();
557
+ *self.shared.option_row_template_value.borrow_mut() = template.clone();
558
+ self.shared.popup_list.option_row_template(template);
559
+ self
560
+ }
561
+
562
+ pub fn select_index(&self, index: i32) -> &Self {
563
+ self.shared.set_selected_index(index, false);
564
+ self
565
+ }
566
+
567
+ pub fn enabled(&self, enabled: bool) -> &Self {
568
+ self.root.enabled(enabled);
569
+ if !enabled {
570
+ self.shared.pointer_pressed_state.set(false);
571
+ self.shared.hovered_state.set(false);
572
+ self.shared.close();
573
+ }
574
+ self.shared.handle_theme_changed();
575
+ self
576
+ }
577
+ }
578
+
579
+ impl Node for Dropdown {
580
+ fn retained_node_ref(&self) -> NodeRef {
581
+ self.root.retained_node_ref()
582
+ }
583
+
584
+ fn build_self(&self) {
585
+ self.root.build_self();
586
+ }
587
+
588
+ fn dispose(&self) {
589
+ self.shared.close();
590
+ focus_adorner::hide_owner(&self.root);
591
+ *self.shared.theme_guard.borrow_mut() = None;
592
+ *self.shared.focus_visibility_guard.borrow_mut() = None;
593
+ self.shared.popup_list.dispose();
594
+ self.root.dispose();
595
+ }
596
+ }
597
+
598
+ impl HasFlexBoxRoot for Dropdown {
599
+ fn flex_box_root(&self) -> &FlexBox {
600
+ &self.root
601
+ }
602
+ }
603
+
604
+ impl DropdownShared {
605
+ fn is_enabled(&self) -> bool {
606
+ self.root
607
+ .upgrade()
608
+ .is_some_and(|root| root.retained_node_ref().is_enabled_for_routing())
609
+ }
610
+
611
+ fn popup_list_activate_index(&self, index: i32) {
612
+ if index < 0 || index >= self.items_value.borrow().len() as i32 {
613
+ return;
614
+ }
615
+ self.set_selected_index(index, true);
616
+ self.close();
617
+ }
618
+
619
+ fn handle_global_key_event(&self, event_type: KeyEventType, key: &str, modifiers: u32) -> bool {
620
+ if !self.open_state.get() || modifiers != 0 || event_type != KeyEventType::Down {
621
+ return false;
622
+ }
623
+ match key {
624
+ "Escape" => {
625
+ self.close();
626
+ true
627
+ }
628
+ "Enter" => {
629
+ self.select_highlighted();
630
+ true
631
+ }
632
+ "Home" => {
633
+ self.highlight_index(0);
634
+ true
635
+ }
636
+ "End" => {
637
+ self.highlight_index(self.items_value.borrow().len() as i32 - 1);
638
+ true
639
+ }
640
+ "ArrowDown" => {
641
+ self.move_highlight(1);
642
+ true
643
+ }
644
+ "ArrowUp" => {
645
+ self.move_highlight(-1);
646
+ true
647
+ }
648
+ _ => false,
649
+ }
650
+ }
651
+
652
+ fn set_selected_index(&self, index: i32, emit: bool) {
653
+ if index == -1 {
654
+ self.selected_index_value.set(-1);
655
+ self.highlighted_index_value.set(-1);
656
+ self.popup_list.set_highlighted_index(-1);
657
+ self.sync_value_label();
658
+ self.handle_theme_changed();
659
+ return;
660
+ }
661
+ let count = self.items_value.borrow().len() as i32;
662
+ if count == 0 {
663
+ if index != -1 {
664
+ logger::warn(
665
+ "Layout",
666
+ &format!(
667
+ "Dropdown.selectIndex() received {index} before any items were assigned."
668
+ ),
669
+ );
670
+ }
671
+ return;
672
+ }
673
+ let clamped_index = index.clamp(0, count - 1);
674
+ if clamped_index != index {
675
+ logger::warn(
676
+ "Layout",
677
+ &format!("Dropdown.selectIndex() received {index}; clamping to {clamped_index}."),
678
+ );
679
+ }
680
+ let changed = self.selected_index_value.get() != clamped_index;
681
+ self.selected_index_value.set(clamped_index);
682
+ self.highlighted_index_value.set(clamped_index);
683
+ self.popup_list.set_highlighted_index(clamped_index);
684
+ self.sync_value_label();
685
+ self.handle_theme_changed();
686
+ if emit && changed {
687
+ if let Some(root) = self.root.upgrade() {
688
+ root.request_semantic_announcement();
689
+ }
690
+ self.emit_selection_changed();
691
+ }
692
+ }
693
+
694
+ fn emit_selection_changed(&self) {
695
+ let selected_index = self.selected_index_value.get();
696
+ if selected_index < 0 {
697
+ return;
698
+ }
699
+ let Some(item) = self
700
+ .items_value
701
+ .borrow()
702
+ .get(selected_index as usize)
703
+ .cloned()
704
+ else {
705
+ return;
706
+ };
707
+ if let Some(callback) = self.changed_callback.borrow().clone() {
708
+ callback(DropdownChangedEventArgs {
709
+ item,
710
+ selected_index,
711
+ });
712
+ }
713
+ }
714
+
715
+ fn open(&self) {
716
+ if self.open_state.get() || self.items_value.borrow().is_empty() {
717
+ return;
718
+ }
719
+ let Some(root) = self.root.upgrade() else {
720
+ return;
721
+ };
722
+ if root.handle() == crate::node::NodeHandle::INVALID {
723
+ return;
724
+ }
725
+ let initial_highlight = if self.selected_index_value.get() >= 0 {
726
+ self.selected_index_value.get()
727
+ } else if !self.items_value.borrow().is_empty() {
728
+ 0
729
+ } else {
730
+ -1
731
+ };
732
+ self.popup_list.set_highlighted_index(initial_highlight);
733
+ self.highlighted_index_value
734
+ .set(self.popup_list.highlighted_index());
735
+ let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
736
+ return;
737
+ };
738
+ if !self.popup_list.open(
739
+ bounds[0],
740
+ bounds[1],
741
+ bounds[2],
742
+ bounds[3],
743
+ initial_highlight,
744
+ ) {
745
+ return;
746
+ }
747
+ self.highlighted_index_value
748
+ .set(self.popup_list.highlighted_index());
749
+ self.open_state.set(true);
750
+ ACTIVE_DROPDOWN.with(|slot| {
751
+ *slot.borrow_mut() = Some(self.self_weak.borrow().clone());
752
+ });
753
+ ui::set_semantic_expanded(root.handle().raw(), true, true);
754
+ root.request_semantic_announcement();
755
+ if self.key_filter_token.get() == 0 {
756
+ let weak_self = self.self_weak.borrow().clone();
757
+ let token = event::push_key_filter(move |event_type, key, modifiers| {
758
+ weak_self.upgrade().is_some_and(|shared| {
759
+ shared.handle_global_key_event(event_type, key, modifiers)
760
+ })
761
+ });
762
+ self.key_filter_token.set(token);
763
+ }
764
+ self.handle_theme_changed();
765
+ }
766
+
767
+ fn close(&self) {
768
+ if !self.open_state.get() && !self.popup_list.is_open() {
769
+ return;
770
+ }
771
+ self.popup_list.close();
772
+ self.open_state.set(false);
773
+ ACTIVE_DROPDOWN.with(|slot| {
774
+ let should_clear = slot
775
+ .borrow()
776
+ .as_ref()
777
+ .and_then(Weak::upgrade)
778
+ .and_then(|active| active.root.upgrade())
779
+ .and_then(|active_root| {
780
+ self.root
781
+ .upgrade()
782
+ .map(|root| active_root.handle() == root.handle())
783
+ })
784
+ .unwrap_or(false);
785
+ if should_clear {
786
+ slot.borrow_mut().take();
787
+ }
788
+ });
789
+ if let Some(root) = self.root.upgrade() {
790
+ ui::set_semantic_expanded(root.handle().raw(), true, false);
791
+ root.request_semantic_announcement();
792
+ }
793
+ let token = self.key_filter_token.replace(0);
794
+ if token != 0 {
795
+ event::remove_key_filter(token);
796
+ }
797
+ self.handle_theme_changed();
798
+ }
799
+
800
+ fn sync_value_label(&self) {
801
+ let selected_index = self.selected_index_value.get();
802
+ if let Some(item) = self.items_value.borrow().get(selected_index as usize) {
803
+ self.field_presenter
804
+ .borrow()
805
+ .value_node()
806
+ .text(item.label.clone());
807
+ if let Some(root) = self.root.upgrade() {
808
+ root.default_semantic_label(item.label.clone());
809
+ }
810
+ return;
811
+ }
812
+ self.field_presenter.borrow().value_node().text("");
813
+ if let Some(root) = self.root.upgrade() {
814
+ root.default_semantic_label("Dropdown");
815
+ }
816
+ }
817
+
818
+ fn highlight_index(&self, index: i32) {
819
+ self.popup_list.highlight_index(index);
820
+ self.highlighted_index_value
821
+ .set(self.popup_list.highlighted_index());
822
+ }
823
+
824
+ fn move_highlight(&self, delta: i32) {
825
+ if self.popup_list.highlighted_index() < 0 && self.selected_index_value.get() >= 0 {
826
+ self.popup_list
827
+ .set_highlighted_index(self.selected_index_value.get());
828
+ }
829
+ self.popup_list.move_highlight(delta);
830
+ self.highlighted_index_value
831
+ .set(self.popup_list.highlighted_index());
832
+ }
833
+
834
+ fn select_highlighted(&self) {
835
+ let highlighted = self.highlighted_index_value.get();
836
+ if highlighted < 0 || highlighted >= self.items_value.borrow().len() as i32 {
837
+ return;
838
+ }
839
+ self.set_selected_index(highlighted, true);
840
+ self.close();
841
+ }
842
+
843
+ fn clear_items(&self) {
844
+ self.close();
845
+ self.items_value.borrow_mut().clear();
846
+ self.selected_index_value.set(-1);
847
+ self.highlighted_index_value.set(-1);
848
+ self.popup_list.clear();
849
+ }
850
+
851
+ fn is_trigger_visible_in_viewport(&self) -> bool {
852
+ let Some(root) = self.root.upgrade() else {
853
+ return true;
854
+ };
855
+ let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
856
+ return true;
857
+ };
858
+ let x = bounds[0];
859
+ let y = bounds[1];
860
+ let width = bounds[2];
861
+ let height = bounds[3];
862
+ if width <= 0.0 || height <= 0.0 {
863
+ return true;
864
+ }
865
+ let right = x + width;
866
+ let bottom = y + height;
867
+ let mut current = root.retained_node_ref().parent();
868
+ while let Some(node) = current {
869
+ if node.node_type() == NodeType::ScrollView {
870
+ if let Some(scroll_bounds) = ui::get_bounds(node.handle().raw()) {
871
+ let sv_x = scroll_bounds[0];
872
+ let sv_y = scroll_bounds[1];
873
+ let sv_right = sv_x + scroll_bounds[2];
874
+ let sv_bottom = sv_y + scroll_bounds[3];
875
+ return right > sv_x && bottom > sv_y && x < sv_right && y < sv_bottom;
876
+ }
877
+ break;
878
+ }
879
+ current = node.parent();
880
+ }
881
+ let viewport_width = ui::get_viewport_width();
882
+ let viewport_height = ui::get_viewport_height();
883
+ right > 0.0 && bottom > 0.0 && x < viewport_width && y < viewport_height
884
+ }
885
+
886
+ fn handle_theme_changed(&self) {
887
+ let Some(root) = self.root.upgrade() else {
888
+ return;
889
+ };
890
+ let theme = current_theme();
891
+ if !self.popup_panel_color_overridden.get() {
892
+ self.popup_panel_color_value
893
+ .set(theme.context_menu.panel_background);
894
+ }
895
+ if !self.popup_panel_background_blur_overridden.get() {
896
+ self.popup_panel_background_blur_sigma_value
897
+ .set(DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA);
898
+ }
899
+ root.cursor(if self.is_enabled() {
900
+ CursorStyle::Pointer
901
+ } else {
902
+ CursorStyle::Default
903
+ });
904
+ root.corner_radius(0.0)
905
+ .border(0.0, 0x00000000)
906
+ .padding(0.0, 0.0, 0.0, 0.0)
907
+ .bg_color(0x00000000)
908
+ .opacity(if self.is_enabled() { 1.0 } else { 0.6 });
909
+
910
+ self.field_presenter.borrow().root().fill_width();
911
+ let selected_label = self
912
+ .items_value
913
+ .borrow()
914
+ .get(self.selected_index_value.get() as usize)
915
+ .map(|item| item.label.clone())
916
+ .unwrap_or_default();
917
+ self.field_presenter.borrow().apply(
918
+ theme.clone(),
919
+ &DropdownFieldVisualState::new(
920
+ self.open_state.get(),
921
+ self.focused_state.get(),
922
+ self.is_enabled(),
923
+ self.pointer_pressed_state.get(),
924
+ selected_label,
925
+ ),
926
+ self.colors_value.get(),
927
+ );
928
+ self.chevron_presenter.borrow().apply(
929
+ theme.clone(),
930
+ DropdownChevronVisualState::new(
931
+ self.open_state.get(),
932
+ self.hovered_state.get(),
933
+ self.is_enabled(),
934
+ ),
935
+ );
936
+ self.popup_list
937
+ .panel_node
938
+ .padding(
939
+ SELECTABLE_POPUP_LIST_PANEL_PADDING,
940
+ SELECTABLE_POPUP_LIST_PANEL_PADDING,
941
+ SELECTABLE_POPUP_LIST_PANEL_PADDING,
942
+ SELECTABLE_POPUP_LIST_PANEL_PADDING,
943
+ )
944
+ .corner_radius(theme.spacing.sm)
945
+ .bg_color(self.popup_panel_color_value.get())
946
+ .border(1.0, theme.context_menu.panel_border_color)
947
+ .background_blur(self.popup_panel_background_blur_sigma_value.get())
948
+ .drop_shadow(
949
+ theme.context_menu.panel_shadow_color,
950
+ 0.0,
951
+ theme.context_menu.shadow_offset_y,
952
+ theme.context_menu.shadow_blur,
953
+ theme.context_menu.shadow_spread,
954
+ );
955
+ self.popup_list.popup_scroll_box.bg_color(0x00000000);
956
+ self.popup_list.sync_option_visuals();
957
+ self.sync_focus_chrome();
958
+ }
959
+
960
+ fn sync_focus_chrome(&self) {
961
+ let Some(root) = self.root.upgrade() else {
962
+ return;
963
+ };
964
+ if self.focused_state.get()
965
+ && self.is_enabled()
966
+ && focus_visibility::keyboard_focus_visible()
967
+ {
968
+ focus_adorner::show_standard(&root, current_theme().spacing.sm);
969
+ } else {
970
+ focus_adorner::hide_owner(&root);
971
+ }
972
+ }
973
+
974
+ fn replace_field_presenter(
975
+ &self,
976
+ next_field_presenter: Rc<dyn DropdownFieldPresenter>,
977
+ next_chevron_presenter: Rc<dyn DropdownChevronPresenter>,
978
+ ) {
979
+ let Some(root) = self.root.upgrade() else {
980
+ return;
981
+ };
982
+ let previous_field_root = self.field_presenter.borrow().root();
983
+ *self.field_presenter.borrow_mut() = next_field_presenter.clone();
984
+ *self.chevron_presenter.borrow_mut() = next_chevron_presenter.clone();
985
+ next_field_presenter.root().fill_width();
986
+ next_field_presenter
987
+ .chevron_host()
988
+ .child(&next_chevron_presenter.root());
989
+ root.remove_child(&previous_field_root);
990
+ root.child(&next_field_presenter.root());
991
+ root.child(&self.popup_list.root);
992
+ previous_field_root.dispose();
993
+ }
994
+
995
+ fn uses_default_field_presenter(&self) -> bool {
996
+ if self.field_template_value.borrow().is_some() {
997
+ return false;
998
+ }
999
+ let template_set = get_control_templates();
1000
+ template_set.is_none() || template_set.is_some_and(|set| set.dropdown_field.is_none())
1001
+ }
1002
+
1003
+ fn uses_default_chevron_presenter(&self) -> bool {
1004
+ if self.chevron_template_value.borrow().is_some() {
1005
+ return false;
1006
+ }
1007
+ let template_set = get_control_templates();
1008
+ template_set.is_none() || template_set.is_some_and(|set| set.dropdown_chevron.is_none())
1009
+ }
1010
+ }