@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,1574 @@
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_option_row_presenter::DropdownOptionRowTemplate;
7
+ use super::internal::selectable_popup_list::{
8
+ SelectablePopupList, SelectablePopupListOwner, SELECTABLE_POPUP_LIST_PANEL_PADDING,
9
+ };
10
+ use super::internal::text_input_presenter::{
11
+ TextInputPresenter, TextInputTemplate, TextInputVisualState,
12
+ };
13
+ use super::{DropdownColors, DropdownSizing, TextInput, TextInputColors};
14
+ use crate::bindings::ui;
15
+ use crate::event::{self, TextChangedEventArgs};
16
+ use crate::ffi::{
17
+ AlignItems, CursorStyle, FlexDirection, KeyEventType, KeyModifier, NodeType, SemanticRole, Unit,
18
+ };
19
+ use crate::focus_adorner;
20
+ use crate::focus_visibility;
21
+ use crate::logger;
22
+ use crate::node::{
23
+ row, FlexBox, FlexBoxSurface, HasFlexBoxRoot, Node, NodeRef, TextCore, WeakFlexBox,
24
+ };
25
+ use crate::signal::SubscriptionGuard;
26
+ use crate::theme::{current_theme, subscribe, Theme};
27
+ use crate::{app, frame_scheduler};
28
+ use std::cell::{Cell, RefCell};
29
+ use std::rc::{Rc, Weak};
30
+
31
+ const DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA: f32 = 10.0;
32
+
33
+ fn strings_equal_ignore_case(left: &str, right: &str) -> bool {
34
+ left.to_lowercase() == right.to_lowercase()
35
+ }
36
+
37
+ fn string_contains_ignore_case(value: &str, query: &str) -> bool {
38
+ value.to_lowercase().contains(&query.to_lowercase())
39
+ }
40
+
41
+ fn string_starts_with_ignore_case(value: &str, query: &str) -> bool {
42
+ value.to_lowercase().starts_with(&query.to_lowercase())
43
+ }
44
+
45
+ fn create_chevron_presenter(
46
+ template: Option<Rc<dyn DropdownChevronTemplate>>,
47
+ sizing: Option<DropdownSizing>,
48
+ ) -> Rc<dyn DropdownChevronPresenter> {
49
+ if let Some(template) = template {
50
+ return template.create(sizing);
51
+ }
52
+ if let Some(template) = get_control_templates().and_then(|set| set.dropdown_chevron) {
53
+ return template.create(sizing);
54
+ }
55
+ create_default_dropdown_chevron_presenter(sizing)
56
+ }
57
+
58
+ fn resolve_text_input_colors(
59
+ colors: Option<DropdownColors>,
60
+ theme: &Theme,
61
+ ) -> Option<TextInputColors> {
62
+ let colors = colors?;
63
+ let input_colors = TextInputColors::new();
64
+ if colors.has_background() {
65
+ input_colors.background(colors.background_color());
66
+ }
67
+ if colors.has_text_primary() {
68
+ input_colors.text_primary(colors.text_primary_color());
69
+ }
70
+ if colors.has_placeholder() {
71
+ input_colors.placeholder(colors.placeholder_color());
72
+ }
73
+ if colors.has_border() {
74
+ input_colors.border(colors.border_color());
75
+ }
76
+ if colors.has_accent() {
77
+ input_colors
78
+ .accent(colors.accent_color())
79
+ .caret(colors.accent_color());
80
+ } else {
81
+ input_colors.caret(theme.colors.accent);
82
+ }
83
+ Some(input_colors)
84
+ }
85
+
86
+ #[derive(Clone, Default)]
87
+ struct ComboBoxEditorPresenter {
88
+ host: RefCell<Option<FlexBox>>,
89
+ editor_host: RefCell<Option<TextCore>>,
90
+ placeholder_host: RefCell<Option<FlexBox>>,
91
+ }
92
+
93
+ impl TextInputPresenter for ComboBoxEditorPresenter {
94
+ fn bind(&self, host: FlexBox, editor_host: TextCore, placeholder_host: FlexBox) {
95
+ *self.host.borrow_mut() = Some(host);
96
+ *self.editor_host.borrow_mut() = Some(editor_host);
97
+ *self.placeholder_host.borrow_mut() = Some(placeholder_host);
98
+ }
99
+
100
+ fn apply(&self, _theme: Theme, state: &TextInputVisualState, _colors: Option<TextInputColors>) {
101
+ let Some(host) = self.host.borrow().clone() else {
102
+ return;
103
+ };
104
+ let Some(editor_host) = self.editor_host.borrow().clone() else {
105
+ return;
106
+ };
107
+ let Some(placeholder_host) = self.placeholder_host.borrow().clone() else {
108
+ return;
109
+ };
110
+ let editable_cursor = if state.enabled {
111
+ CursorStyle::Text
112
+ } else {
113
+ CursorStyle::Default
114
+ };
115
+ host.bg_color(0x00000000)
116
+ .corner_radius(0.0)
117
+ .border(0.0, 0x00000000)
118
+ .padding(0.0, 0.0, 0.0, 0.0)
119
+ .align_items(AlignItems::Center)
120
+ .cursor(editable_cursor)
121
+ .opacity(if state.enabled { 1.0 } else { 0.6 });
122
+ editor_host.cursor(editable_cursor);
123
+ placeholder_host
124
+ .position(0.0, 0.0)
125
+ .width(100.0, Unit::Percent)
126
+ .cursor(editable_cursor);
127
+ }
128
+ }
129
+
130
+ #[derive(Clone)]
131
+ struct ComboBoxEditorTemplate;
132
+
133
+ impl TextInputTemplate for ComboBoxEditorTemplate {
134
+ fn create(&self) -> Rc<dyn TextInputPresenter> {
135
+ Rc::new(ComboBoxEditorPresenter::default())
136
+ }
137
+ }
138
+
139
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
140
+ pub enum ComboBoxFilterMode {
141
+ None,
142
+ StartsWith,
143
+ Contains,
144
+ }
145
+
146
+ #[derive(Clone, Copy, Debug, PartialEq, Eq)]
147
+ pub enum ComboBoxCommitMode {
148
+ KeepText,
149
+ RevertToSelection,
150
+ SelectExactMatch,
151
+ }
152
+
153
+ #[derive(Clone, Debug, PartialEq, Eq)]
154
+ pub struct ComboBoxItem {
155
+ pub value: String,
156
+ }
157
+
158
+ impl ComboBoxItem {
159
+ pub fn new(value: impl Into<String>) -> Self {
160
+ Self {
161
+ value: value.into(),
162
+ }
163
+ }
164
+
165
+ pub fn from_value(value: impl Into<String>) -> Self {
166
+ Self::new(value)
167
+ }
168
+ }
169
+
170
+ #[derive(Clone)]
171
+ pub struct ComboBox {
172
+ root: FlexBox,
173
+ shared: Rc<ComboBoxShared>,
174
+ }
175
+
176
+ struct ComboBoxShared {
177
+ self_weak: RefCell<Weak<ComboBoxShared>>,
178
+ root: WeakFlexBox,
179
+ editor: TextInput,
180
+ chevron_host: FlexBox,
181
+ chevron_template_value: RefCell<Option<Rc<dyn DropdownChevronTemplate>>>,
182
+ sizing_value: Cell<Option<DropdownSizing>>,
183
+ colors_value: Cell<Option<DropdownColors>>,
184
+ chevron_presenter: RefCell<Rc<dyn DropdownChevronPresenter>>,
185
+ popup_list: SelectablePopupList,
186
+ items_value: RefCell<Vec<ComboBoxItem>>,
187
+ filtered_indices: RefCell<Vec<i32>>,
188
+ open_state: Cell<bool>,
189
+ popup_pointer_pressed_state: Cell<bool>,
190
+ pointer_pressed_state: Cell<bool>,
191
+ hovered_state: Cell<bool>,
192
+ focused_state: Cell<bool>,
193
+ wrapper_focused_state: Cell<bool>,
194
+ editor_focused_state: Cell<bool>,
195
+ deferred_blur_close_pending_state: Cell<bool>,
196
+ allow_custom_value: Cell<bool>,
197
+ auto_complete_value: Cell<bool>,
198
+ open_on_focus_value: Cell<bool>,
199
+ stays_open_on_edit_value: Cell<bool>,
200
+ filter_mode_value: Cell<ComboBoxFilterMode>,
201
+ commit_mode_value: Cell<ComboBoxCommitMode>,
202
+ key_filter_token: Cell<u32>,
203
+ selected_index_value: Cell<i32>,
204
+ committed_selected_index_value: Cell<i32>,
205
+ highlighted_index_value: Cell<i32>,
206
+ text_value: RefCell<String>,
207
+ popup_panel_color_value: Cell<u32>,
208
+ popup_panel_background_blur_sigma_value: Cell<f32>,
209
+ popup_panel_color_overridden: Cell<bool>,
210
+ popup_panel_background_blur_overridden: Cell<bool>,
211
+ suppress_editor_changed: Cell<bool>,
212
+ last_auto_complete_text_value: RefCell<String>,
213
+ changed_callback:
214
+ RefCell<Option<Rc<dyn Fn(crate::controls::ComboBoxChangedEventArgs<ComboBoxItem>)>>>,
215
+ text_changed_callback: RefCell<Option<Rc<dyn Fn(TextChangedEventArgs)>>>,
216
+ theme_guard: RefCell<Option<SubscriptionGuard>>,
217
+ focus_visibility_guard: RefCell<Option<SubscriptionGuard>>,
218
+ }
219
+
220
+ thread_local! {
221
+ static ACTIVE_COMBOBOX: RefCell<Option<Weak<ComboBoxShared>>> = const { RefCell::new(None) };
222
+ static COMBOBOX_SCROLL_HOOK_REGISTERED: Cell<bool> = const { Cell::new(false) };
223
+ }
224
+
225
+ impl Default for ComboBox {
226
+ fn default() -> Self {
227
+ Self::new()
228
+ }
229
+ }
230
+
231
+ impl ComboBox {
232
+ pub fn new() -> Self {
233
+ Self::with_text("")
234
+ }
235
+
236
+ pub fn with_text(text: impl Into<String>) -> Self {
237
+ Self::with_initial_text(text.into())
238
+ }
239
+
240
+ fn with_initial_text(text: String) -> Self {
241
+ Self::ensure_scroll_hook();
242
+ let root = row();
243
+ root.semantic_role(SemanticRole::ComboBox)
244
+ .focusable(true, 0)
245
+ .interactive(true)
246
+ .cursor(CursorStyle::Text)
247
+ .flex_direction(FlexDirection::Row)
248
+ .align_items(AlignItems::Center)
249
+ .reflect_semantic_disabled_from_enabled()
250
+ .default_semantic_label("Combo box");
251
+
252
+ let editor = TextInput::new();
253
+ editor
254
+ .text(text.clone())
255
+ .template(Some(Rc::new(ComboBoxEditorTemplate)));
256
+ editor.fill_width();
257
+
258
+ let chevron_presenter = create_chevron_presenter(None, None);
259
+ let chevron_host = row();
260
+ chevron_host
261
+ .width(32.0, Unit::Pixel)
262
+ .height(100.0, Unit::Percent)
263
+ .align_items(AlignItems::Center)
264
+ .justify_content(crate::ffi::JustifyContent::Center)
265
+ .child(&chevron_presenter.root());
266
+
267
+ let weak_root = root.downgrade();
268
+ let shared_slot: Rc<RefCell<Option<Weak<ComboBoxShared>>>> = Rc::new(RefCell::new(None));
269
+ let owner = SelectablePopupListOwner {
270
+ item_count: {
271
+ let shared_slot = shared_slot.clone();
272
+ Rc::new(move || {
273
+ shared_slot
274
+ .borrow()
275
+ .as_ref()
276
+ .and_then(Weak::upgrade)
277
+ .map(|shared| shared.filtered_indices.borrow().len() as i32)
278
+ .unwrap_or(0)
279
+ })
280
+ },
281
+ item_label: {
282
+ let shared_slot = shared_slot.clone();
283
+ Rc::new(move |index| {
284
+ let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) else {
285
+ return String::new();
286
+ };
287
+ let filtered = shared.filtered_indices.borrow();
288
+ let Some(source_index) = filtered.get(index as usize).copied() else {
289
+ return String::new();
290
+ };
291
+ let value = shared
292
+ .items_value
293
+ .borrow()
294
+ .get(source_index as usize)
295
+ .map(|item| item.value.clone())
296
+ .unwrap_or_default();
297
+ value
298
+ })
299
+ },
300
+ item_selected: {
301
+ let shared_slot = shared_slot.clone();
302
+ Rc::new(move |index| {
303
+ let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) else {
304
+ return false;
305
+ };
306
+ let filtered = shared.filtered_indices.borrow();
307
+ filtered.get(index as usize).is_some_and(|source_index| {
308
+ *source_index == shared.selected_index_value.get()
309
+ })
310
+ })
311
+ },
312
+ enabled: {
313
+ let weak_root = weak_root.clone();
314
+ Rc::new(move || {
315
+ weak_root
316
+ .upgrade()
317
+ .is_some_and(|root| root.retained_node_ref().is_enabled_for_routing())
318
+ })
319
+ },
320
+ highlight_index: {
321
+ let shared_slot = shared_slot.clone();
322
+ Rc::new(move |index| {
323
+ if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
324
+ shared.highlight_index(index);
325
+ }
326
+ })
327
+ },
328
+ activate_index: {
329
+ let shared_slot = shared_slot.clone();
330
+ Rc::new(move |index| {
331
+ if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
332
+ shared.popup_list_activate_index(index);
333
+ }
334
+ })
335
+ },
336
+ pointer_down: {
337
+ let shared_slot = shared_slot.clone();
338
+ Rc::new(move |_index| {
339
+ if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
340
+ shared.popup_pointer_pressed_state.set(true);
341
+ }
342
+ })
343
+ },
344
+ pointer_up: {
345
+ let shared_slot = shared_slot.clone();
346
+ Rc::new(move |_index| {
347
+ if let Some(shared) = shared_slot.borrow().as_ref().and_then(Weak::upgrade) {
348
+ shared.popup_list_pointer_up();
349
+ }
350
+ })
351
+ },
352
+ };
353
+ let popup_list = SelectablePopupList::new(owner);
354
+ let shared = Rc::new(ComboBoxShared {
355
+ self_weak: RefCell::new(Weak::new()),
356
+ root: weak_root,
357
+ editor: editor.clone(),
358
+ chevron_host: chevron_host.clone(),
359
+ chevron_template_value: RefCell::new(None),
360
+ sizing_value: Cell::new(None),
361
+ colors_value: Cell::new(None),
362
+ chevron_presenter: RefCell::new(chevron_presenter.clone()),
363
+ popup_list: popup_list.clone(),
364
+ items_value: RefCell::new(Vec::new()),
365
+ filtered_indices: RefCell::new(Vec::new()),
366
+ open_state: Cell::new(false),
367
+ popup_pointer_pressed_state: Cell::new(false),
368
+ pointer_pressed_state: Cell::new(false),
369
+ hovered_state: Cell::new(false),
370
+ focused_state: Cell::new(false),
371
+ wrapper_focused_state: Cell::new(false),
372
+ editor_focused_state: Cell::new(false),
373
+ deferred_blur_close_pending_state: Cell::new(false),
374
+ allow_custom_value: Cell::new(true),
375
+ auto_complete_value: Cell::new(false),
376
+ open_on_focus_value: Cell::new(false),
377
+ stays_open_on_edit_value: Cell::new(true),
378
+ filter_mode_value: Cell::new(ComboBoxFilterMode::Contains),
379
+ commit_mode_value: Cell::new(ComboBoxCommitMode::KeepText),
380
+ key_filter_token: Cell::new(0),
381
+ selected_index_value: Cell::new(-1),
382
+ committed_selected_index_value: Cell::new(-1),
383
+ highlighted_index_value: Cell::new(-1),
384
+ text_value: RefCell::new(text),
385
+ popup_panel_color_value: Cell::new(0x00000000),
386
+ popup_panel_background_blur_sigma_value: Cell::new(DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA),
387
+ popup_panel_color_overridden: Cell::new(false),
388
+ popup_panel_background_blur_overridden: Cell::new(false),
389
+ suppress_editor_changed: Cell::new(false),
390
+ last_auto_complete_text_value: RefCell::new(String::new()),
391
+ changed_callback: RefCell::new(None),
392
+ text_changed_callback: RefCell::new(None),
393
+ theme_guard: RefCell::new(None),
394
+ focus_visibility_guard: RefCell::new(None),
395
+ });
396
+ *shared.self_weak.borrow_mut() = Rc::downgrade(&shared);
397
+ *shared_slot.borrow_mut() = Some(Rc::downgrade(&shared));
398
+ shared.rebuild_filtered_indices();
399
+
400
+ popup_list.popup_presenter.overlay_node().on_click({
401
+ let weak_shared = Rc::downgrade(&shared);
402
+ move |_event| {
403
+ if let Some(shared) = weak_shared.upgrade() {
404
+ shared.close();
405
+ }
406
+ }
407
+ });
408
+
409
+ root.child(&editor)
410
+ .child(&chevron_host)
411
+ .child(&popup_list.root);
412
+
413
+ let control = Self { root, shared };
414
+ control.install_visual_subscriptions();
415
+ control.install_effective_enabled_subscription();
416
+ control.bind_events();
417
+ control.shared.sync_semantic_label();
418
+ control.shared.handle_theme_changed();
419
+ control
420
+ }
421
+
422
+ fn ensure_scroll_hook() {
423
+ COMBOBOX_SCROLL_HOOK_REGISTERED.with(|registered| {
424
+ if registered.get() {
425
+ return;
426
+ }
427
+ registered.set(true);
428
+ event::register_scroll_hook(|| {
429
+ ACTIVE_COMBOBOX.with(|slot| {
430
+ let Some(shared) = slot.borrow().as_ref().and_then(Weak::upgrade) else {
431
+ return;
432
+ };
433
+ if !shared.is_trigger_visible_in_viewport() {
434
+ shared.close();
435
+ }
436
+ });
437
+ });
438
+ });
439
+ }
440
+
441
+ fn install_visual_subscriptions(&self) {
442
+ *self.shared.theme_guard.borrow_mut() = Some(subscribe({
443
+ let weak_shared = Rc::downgrade(&self.shared);
444
+ move |_theme| {
445
+ if let Some(shared) = weak_shared.upgrade() {
446
+ shared.handle_theme_changed();
447
+ }
448
+ }
449
+ }));
450
+ *self.shared.focus_visibility_guard.borrow_mut() = Some(focus_visibility::subscribe({
451
+ let weak_shared = Rc::downgrade(&self.shared);
452
+ move |_visible| {
453
+ if let Some(shared) = weak_shared.upgrade() {
454
+ shared.sync_focus_chrome();
455
+ }
456
+ }
457
+ }));
458
+ }
459
+
460
+ fn install_effective_enabled_subscription(&self) {
461
+ let weak_shared = Rc::downgrade(&self.shared);
462
+ self.root
463
+ .retained_node_ref()
464
+ .on_effective_enabled_changed(Rc::new(move |_enabled| {
465
+ let Some(shared) = weak_shared.upgrade() else {
466
+ return;
467
+ };
468
+ shared.editor.enabled(shared.is_enabled());
469
+ if !shared.is_enabled() {
470
+ shared.pointer_pressed_state.set(false);
471
+ shared.hovered_state.set(false);
472
+ shared.close();
473
+ }
474
+ shared.handle_theme_changed();
475
+ }));
476
+ }
477
+
478
+ fn bind_events(&self) {
479
+ let shared = self.shared.clone();
480
+ self.root.on_pointer_enter(move |_event| {
481
+ if !shared.is_enabled() {
482
+ return;
483
+ }
484
+ shared.hovered_state.set(true);
485
+ shared.handle_theme_changed();
486
+ });
487
+
488
+ let shared = self.shared.clone();
489
+ self.root.on_pointer_leave(move |_event| {
490
+ shared.pointer_pressed_state.set(false);
491
+ shared.hovered_state.set(false);
492
+ shared.handle_theme_changed();
493
+ });
494
+
495
+ let shared = self.shared.clone();
496
+ self.root.on_pointer_down(move |_event| {
497
+ if !shared.is_enabled() {
498
+ return;
499
+ }
500
+ shared.pointer_pressed_state.set(true);
501
+ shared.editor.focus_now();
502
+ shared.handle_theme_changed();
503
+ });
504
+
505
+ let shared = self.shared.clone();
506
+ self.root.on_pointer_up(move |event| {
507
+ if !shared.is_enabled() || !shared.pointer_pressed_state.get() {
508
+ return;
509
+ }
510
+ shared.pointer_pressed_state.set(false);
511
+ shared.toggle_open();
512
+ shared.handle_theme_changed();
513
+ event.handled = true;
514
+ });
515
+
516
+ let shared = self.shared.clone();
517
+ self.root.on_key_down(move |event| {
518
+ if shared.handle_global_key_event(
519
+ KeyEventType::Down,
520
+ event.key.as_str(),
521
+ event.modifiers,
522
+ ) {
523
+ event.handled = true;
524
+ return;
525
+ }
526
+ if !shared.is_enabled() || event.modifiers != 0 {
527
+ return;
528
+ }
529
+ if !shared.open_state.get()
530
+ && (event.key == "Enter" || event.key == " " || event.key == "ArrowDown")
531
+ {
532
+ shared.open();
533
+ event.handled = true;
534
+ return;
535
+ }
536
+ if !shared.open_state.get() && event.key == "ArrowUp" {
537
+ shared.open();
538
+ shared.move_highlight(-1);
539
+ event.handled = true;
540
+ }
541
+ });
542
+
543
+ let shared = self.shared.clone();
544
+ self.root.on_key_up(move |event| {
545
+ if shared.handle_global_key_event(KeyEventType::Up, event.key.as_str(), event.modifiers)
546
+ {
547
+ event.handled = true;
548
+ }
549
+ });
550
+
551
+ let shared = self.shared.clone();
552
+ self.root.on_focus_changed(move |event| {
553
+ shared.wrapper_focused_state.set(event.focused);
554
+ if !event.focused && !shared.open_state.get() {
555
+ shared.pointer_pressed_state.set(false);
556
+ }
557
+ shared.sync_focused_state();
558
+ });
559
+
560
+ let weak_shared = Rc::downgrade(&self.shared);
561
+ self.shared.editor.on_changed(move |event| {
562
+ let Some(shared) = weak_shared.upgrade() else {
563
+ return;
564
+ };
565
+ shared.handle_editor_text_changed(event.text);
566
+ });
567
+
568
+ let weak_shared = Rc::downgrade(&self.shared);
569
+ self.shared.editor.on_focus_changed(move |event| {
570
+ let Some(shared) = weak_shared.upgrade() else {
571
+ return;
572
+ };
573
+ shared.handle_editor_focus_changed(event.focused);
574
+ });
575
+
576
+ let weak_shared = Rc::downgrade(&self.shared);
577
+ self.shared.editor.editor_node().on_key_down(move |event| {
578
+ let Some(shared) = weak_shared.upgrade() else {
579
+ return;
580
+ };
581
+ if shared.handle_editor_key_down(event.key.as_str(), event.modifiers) {
582
+ event.handled = true;
583
+ }
584
+ });
585
+ self.shared.editor.editor_node().editor_command_keys(true);
586
+
587
+ let weak_shared = Rc::downgrade(&self.shared);
588
+ self.shared.chevron_host.on_click(move |event| {
589
+ let Some(shared) = weak_shared.upgrade() else {
590
+ return;
591
+ };
592
+ if !shared.is_enabled() {
593
+ return;
594
+ }
595
+ shared.toggle_from_chevron();
596
+ event.handled = true;
597
+ });
598
+
599
+ let weak_shared = Rc::downgrade(&self.shared);
600
+ self.shared.chevron_host.on_pointer_enter(move |_event| {
601
+ let Some(shared) = weak_shared.upgrade() else {
602
+ return;
603
+ };
604
+ shared.hovered_state.set(true);
605
+ shared.handle_theme_changed();
606
+ });
607
+
608
+ let weak_shared = Rc::downgrade(&self.shared);
609
+ self.shared.chevron_host.on_pointer_leave(move |_event| {
610
+ let Some(shared) = weak_shared.upgrade() else {
611
+ return;
612
+ };
613
+ shared.pointer_pressed_state.set(false);
614
+ shared.hovered_state.set(false);
615
+ shared.handle_theme_changed();
616
+ });
617
+
618
+ let weak_shared = Rc::downgrade(&self.shared);
619
+ self.shared.chevron_host.on_pointer_down(move |_event| {
620
+ let Some(shared) = weak_shared.upgrade() else {
621
+ return;
622
+ };
623
+ shared.pointer_pressed_state.set(true);
624
+ shared.focus_editor_from_chevron();
625
+ shared.handle_theme_changed();
626
+ });
627
+
628
+ let weak_shared = Rc::downgrade(&self.shared);
629
+ self.shared.chevron_host.on_pointer_up(move |_event| {
630
+ let Some(shared) = weak_shared.upgrade() else {
631
+ return;
632
+ };
633
+ shared.pointer_pressed_state.set(false);
634
+ shared.handle_theme_changed();
635
+ });
636
+ }
637
+
638
+ pub fn selected_index(&self) -> i32 {
639
+ self.shared.selected_index_value.get()
640
+ }
641
+
642
+ pub fn value(&self) -> String {
643
+ self.shared.text_value.borrow().clone()
644
+ }
645
+
646
+ pub fn filtered_count(&self) -> usize {
647
+ self.shared.filtered_indices.borrow().len()
648
+ }
649
+
650
+ pub fn highlighted_index(&self) -> i32 {
651
+ self.shared.highlighted_index_value.get()
652
+ }
653
+
654
+ pub fn is_open(&self) -> bool {
655
+ self.shared.open_state.get()
656
+ }
657
+
658
+ pub fn items<I, S>(&self, items: I) -> &Self
659
+ where
660
+ I: IntoIterator<Item = S>,
661
+ S: Into<String>,
662
+ {
663
+ self.shared.close();
664
+ let mut slot = self.shared.items_value.borrow_mut();
665
+ slot.clear();
666
+ slot.extend(items.into_iter().map(ComboBoxItem::new));
667
+ drop(slot);
668
+ self.shared.sync_selection_from_text();
669
+ self.shared.rebuild_filtered_indices();
670
+ self.shared.popup_list.refresh_panel_layout();
671
+ self.shared.sync_option_visuals();
672
+ self.shared.sync_semantic_label();
673
+ self
674
+ }
675
+
676
+ pub fn text(&self, value: impl Into<String>) -> &Self {
677
+ self.shared.set_text(value.into(), false);
678
+ self
679
+ }
680
+
681
+ pub fn placeholder(&self, value: impl Into<String>) -> &Self {
682
+ self.shared.editor.placeholder(value);
683
+ self.shared.sync_semantic_label();
684
+ self
685
+ }
686
+
687
+ pub fn allow_custom(&self, flag: bool) -> &Self {
688
+ self.shared.allow_custom_value.set(flag);
689
+ self.shared.sync_selection_from_text();
690
+ self
691
+ }
692
+
693
+ pub fn auto_complete(&self, flag: bool) -> &Self {
694
+ self.shared.auto_complete_value.set(flag);
695
+ self
696
+ }
697
+
698
+ pub fn filter_mode(&self, mode: ComboBoxFilterMode) -> &Self {
699
+ self.shared.filter_mode_value.set(mode);
700
+ self.shared.rebuild_filtered_indices();
701
+ self.shared.refresh_popup_after_filter();
702
+ self.shared.sync_option_visuals();
703
+ self
704
+ }
705
+
706
+ pub fn commit_mode(&self, mode: ComboBoxCommitMode) -> &Self {
707
+ self.shared.commit_mode_value.set(mode);
708
+ self
709
+ }
710
+
711
+ pub fn open_on_focus(&self, flag: bool) -> &Self {
712
+ self.shared.open_on_focus_value.set(flag);
713
+ self
714
+ }
715
+
716
+ pub fn stays_open_on_edit(&self, flag: bool) -> &Self {
717
+ self.shared.stays_open_on_edit_value.set(flag);
718
+ self
719
+ }
720
+
721
+ pub fn max_visible_items(&self, count: i32) -> &Self {
722
+ self.shared.popup_list.max_visible_items(count);
723
+ self
724
+ }
725
+
726
+ pub fn popup_width(&self, value: f32) -> &Self {
727
+ self.shared.popup_list.popup_width(value);
728
+ self
729
+ }
730
+
731
+ pub fn popup_panel_color(&self, color: u32) -> &Self {
732
+ self.shared.popup_panel_color_overridden.set(true);
733
+ self.shared.popup_panel_color_value.set(color);
734
+ self.shared.popup_list.panel_node.bg_color(color);
735
+ self
736
+ }
737
+
738
+ pub fn popup_panel_background_blur(&self, sigma: f32) -> &Self {
739
+ self.shared.popup_panel_background_blur_overridden.set(true);
740
+ if sigma < 0.0 {
741
+ logger::warn(
742
+ "Layout",
743
+ &format!("ComboBox.popupPanelBackgroundBlur() received {sigma}; clamping to 0.0."),
744
+ );
745
+ }
746
+ self.shared
747
+ .popup_panel_background_blur_sigma_value
748
+ .set(sigma.max(0.0));
749
+ self.shared
750
+ .popup_list
751
+ .panel_node
752
+ .background_blur(self.shared.popup_panel_background_blur_sigma_value.get());
753
+ self
754
+ }
755
+
756
+ pub fn sizing(&self, sizing: Option<DropdownSizing>) -> &Self {
757
+ self.shared.sizing_value.set(sizing);
758
+ let previous_presenter = self.shared.chevron_presenter.borrow().clone();
759
+ let next_presenter =
760
+ create_chevron_presenter(self.shared.chevron_template_value.borrow().clone(), sizing);
761
+ *self.shared.chevron_presenter.borrow_mut() = next_presenter.clone();
762
+ self.shared
763
+ .chevron_host
764
+ .remove_child(&previous_presenter.root());
765
+ self.shared.chevron_host.child(&next_presenter.root());
766
+ previous_presenter.root().dispose();
767
+ self.shared.popup_list.sizing(sizing);
768
+ self.shared.handle_theme_changed();
769
+ self
770
+ }
771
+
772
+ pub fn colors(&self, colors: Option<DropdownColors>) -> &Self {
773
+ self.shared.colors_value.set(colors);
774
+ self.shared.popup_list.colors(colors);
775
+ self.shared.handle_theme_changed();
776
+ self
777
+ }
778
+
779
+ pub fn chevron_template(&self, template: Option<Rc<dyn DropdownChevronTemplate>>) -> &Self {
780
+ *self.shared.chevron_template_value.borrow_mut() = template.clone();
781
+ let previous_presenter = self.shared.chevron_presenter.borrow().clone();
782
+ let next_presenter = create_chevron_presenter(template, self.shared.sizing_value.get());
783
+ *self.shared.chevron_presenter.borrow_mut() = next_presenter.clone();
784
+ self.shared
785
+ .chevron_host
786
+ .remove_child(&previous_presenter.root());
787
+ self.shared.chevron_host.child(&next_presenter.root());
788
+ previous_presenter.root().dispose();
789
+ self.shared.handle_theme_changed();
790
+ self
791
+ }
792
+
793
+ pub fn option_row_template(
794
+ &self,
795
+ template: Option<Rc<dyn DropdownOptionRowTemplate>>,
796
+ ) -> &Self {
797
+ self.shared.close();
798
+ self.shared.popup_list.option_row_template(template);
799
+ self
800
+ }
801
+
802
+ pub fn select_index(&self, index: i32) -> &Self {
803
+ self.shared.set_selected_index(index, false);
804
+ self
805
+ }
806
+
807
+ pub fn on_changed(
808
+ &self,
809
+ callback: impl Fn(crate::controls::ComboBoxChangedEventArgs<ComboBoxItem>) + 'static,
810
+ ) -> &Self {
811
+ *self.shared.changed_callback.borrow_mut() = Some(Rc::new(callback));
812
+ self
813
+ }
814
+
815
+ pub fn on_text_changed(&self, callback: impl Fn(TextChangedEventArgs) + 'static) -> &Self {
816
+ *self.shared.text_changed_callback.borrow_mut() = Some(Rc::new(callback));
817
+ self
818
+ }
819
+
820
+ pub fn focus_now(&self) -> &Self {
821
+ if self.root.handle() != crate::node::NodeHandle::INVALID {
822
+ ui::request_focus(self.root.handle().raw());
823
+ }
824
+ self
825
+ }
826
+
827
+ pub fn enabled(&self, enabled: bool) -> &Self {
828
+ self.root.enabled(enabled);
829
+ if !enabled {
830
+ self.shared.pointer_pressed_state.set(false);
831
+ self.shared.hovered_state.set(false);
832
+ self.shared.close();
833
+ }
834
+ self.shared.handle_theme_changed();
835
+ self
836
+ }
837
+ }
838
+
839
+ impl HasFlexBoxRoot for ComboBox {
840
+ fn flex_box_root(&self) -> &FlexBox {
841
+ &self.root
842
+ }
843
+ }
844
+
845
+ impl Node for ComboBox {
846
+ fn retained_node_ref(&self) -> NodeRef {
847
+ self.root.retained_node_ref()
848
+ }
849
+
850
+ fn build_self(&self) {
851
+ self.root.build_self();
852
+ }
853
+
854
+ fn dispose(&self) {
855
+ self.shared.close();
856
+ focus_adorner::hide_owner(&self.root);
857
+ *self.shared.theme_guard.borrow_mut() = None;
858
+ *self.shared.focus_visibility_guard.borrow_mut() = None;
859
+ self.shared.popup_list.dispose();
860
+ self.root.dispose();
861
+ }
862
+ }
863
+
864
+ impl ComboBoxShared {
865
+ fn is_enabled(&self) -> bool {
866
+ self.root
867
+ .upgrade()
868
+ .is_some_and(|root| root.retained_node_ref().is_enabled_for_routing())
869
+ }
870
+
871
+ fn handle_global_key_event(&self, event_type: KeyEventType, key: &str, modifiers: u32) -> bool {
872
+ if !self.open_state.get() || modifiers != 0 || event_type != KeyEventType::Down {
873
+ return false;
874
+ }
875
+ match key {
876
+ "Escape" => {
877
+ self.close();
878
+ true
879
+ }
880
+ "Enter" => {
881
+ self.select_highlighted();
882
+ true
883
+ }
884
+ "Home" => {
885
+ self.highlight_index(0);
886
+ true
887
+ }
888
+ "End" => {
889
+ self.highlight_index(self.filtered_indices.borrow().len() as i32 - 1);
890
+ true
891
+ }
892
+ "ArrowDown" => {
893
+ self.move_highlight(1);
894
+ true
895
+ }
896
+ "ArrowUp" => {
897
+ self.move_highlight(-1);
898
+ true
899
+ }
900
+ _ => false,
901
+ }
902
+ }
903
+
904
+ fn toggle_from_chevron(&self) {
905
+ if !self.is_enabled() {
906
+ return;
907
+ }
908
+ self.focus_editor_from_chevron();
909
+ self.toggle_open();
910
+ self.handle_theme_changed();
911
+ }
912
+
913
+ fn focus_editor_from_chevron(&self) {
914
+ self.editor.caret_to_end();
915
+ self.editor.focus_now();
916
+ self.editor.caret_to_end();
917
+ }
918
+
919
+ fn toggle_open(&self) {
920
+ if self.open_state.get() {
921
+ self.close();
922
+ } else {
923
+ self.open();
924
+ }
925
+ }
926
+
927
+ fn handle_editor_key_down(&self, key: &str, modifiers: u32) -> bool {
928
+ if !self.is_enabled() {
929
+ return false;
930
+ }
931
+ if modifiers != 0 {
932
+ return Self::is_text_navigation_key(key, modifiers);
933
+ }
934
+ match key {
935
+ "ArrowDown" => {
936
+ if !self.open_state.get() {
937
+ self.open();
938
+ } else {
939
+ self.move_highlight(1);
940
+ }
941
+ true
942
+ }
943
+ "ArrowUp" => {
944
+ if !self.open_state.get() {
945
+ self.open();
946
+ }
947
+ self.move_highlight(-1);
948
+ true
949
+ }
950
+ "Enter" if self.open_state.get() => {
951
+ self.select_highlighted();
952
+ true
953
+ }
954
+ "Escape" if self.open_state.get() => {
955
+ self.close();
956
+ true
957
+ }
958
+ _ => Self::is_text_navigation_key(key, modifiers),
959
+ }
960
+ }
961
+
962
+ fn is_text_navigation_key(key: &str, modifiers: u32) -> bool {
963
+ let non_shift_modifiers = modifiers
964
+ & ((KeyModifier::Ctrl as u32) | (KeyModifier::Alt as u32) | (KeyModifier::Meta as u32));
965
+ non_shift_modifiers == 0
966
+ && matches!(
967
+ key,
968
+ "ArrowLeft"
969
+ | "ArrowRight"
970
+ | "ArrowUp"
971
+ | "ArrowDown"
972
+ | "Home"
973
+ | "End"
974
+ | "PageUp"
975
+ | "PageDown"
976
+ )
977
+ }
978
+
979
+ fn handle_editor_text_changed(&self, value: String) {
980
+ if self.suppress_editor_changed.get() {
981
+ return;
982
+ }
983
+ let mut next_value = value.clone();
984
+ let mut completion_selection: Option<(u32, u32)> = None;
985
+ let deleting_text = value.len() < self.text_value.borrow().len();
986
+ let should_auto_complete = self.auto_complete_value.get()
987
+ && !value.is_empty()
988
+ && !deleting_text
989
+ && value != *self.last_auto_complete_text_value.borrow();
990
+ self.last_auto_complete_text_value.borrow_mut().clear();
991
+ if should_auto_complete {
992
+ let auto_complete_index = self.find_auto_complete_match(&value);
993
+ if auto_complete_index >= 0 {
994
+ let completed_value = self.items_value.borrow()[auto_complete_index as usize]
995
+ .value
996
+ .clone();
997
+ if completed_value.len() > value.len() {
998
+ let selection_start = value.chars().count() as u32;
999
+ let selection_end = completed_value.chars().count() as u32;
1000
+ next_value = completed_value;
1001
+ completion_selection = Some((selection_start, selection_end));
1002
+ *self.last_auto_complete_text_value.borrow_mut() = value.clone();
1003
+ }
1004
+ }
1005
+ }
1006
+ if let Some((selection_start, selection_end)) = completion_selection {
1007
+ self.suppress_editor_changed.set(true);
1008
+ self.editor.text(next_value.clone());
1009
+ self.editor.selection_range(selection_start, selection_end);
1010
+ self.suppress_editor_changed.set(false);
1011
+ }
1012
+ *self.text_value.borrow_mut() = next_value.clone();
1013
+ self.sync_selection_from_text();
1014
+ self.rebuild_filtered_indices();
1015
+ if self.filtered_indices.borrow().is_empty() {
1016
+ self.close();
1017
+ } else if self.stays_open_on_edit_value.get() {
1018
+ self.highlighted_index_value.set(0);
1019
+ if self.open_state.get() {
1020
+ self.refresh_open_popup();
1021
+ } else {
1022
+ self.open();
1023
+ }
1024
+ }
1025
+ self.refresh_popup_after_filter();
1026
+ self.sync_option_visuals();
1027
+ self.sync_semantic_label();
1028
+ self.emit_text_changed(next_value);
1029
+ }
1030
+
1031
+ fn handle_editor_focus_changed(&self, focused: bool) {
1032
+ self.editor_focused_state.set(focused);
1033
+ if focused && self.open_on_focus_value.get() {
1034
+ self.open();
1035
+ }
1036
+ if !focused && !self.open_state.get() {
1037
+ self.commit_current_text();
1038
+ self.pointer_pressed_state.set(false);
1039
+ }
1040
+ self.sync_focused_state();
1041
+ }
1042
+
1043
+ fn sync_focused_state(&self) {
1044
+ let next_focused = self.wrapper_focused_state.get() || self.editor_focused_state.get();
1045
+ if !next_focused && !self.popup_pointer_pressed_state.get() {
1046
+ self.schedule_deferred_blur_close();
1047
+ }
1048
+ if self.focused_state.get() == next_focused {
1049
+ return;
1050
+ }
1051
+ self.focused_state.set(next_focused);
1052
+ self.handle_theme_changed();
1053
+ }
1054
+
1055
+ fn schedule_deferred_blur_close(&self) {
1056
+ if self.deferred_blur_close_pending_state.get() {
1057
+ return;
1058
+ }
1059
+ self.deferred_blur_close_pending_state.set(true);
1060
+ let weak = self.self_weak.borrow().clone();
1061
+ app::after_next_commit(move || {
1062
+ if let Some(shared) = weak.upgrade() {
1063
+ shared.fire_deferred_blur_close();
1064
+ }
1065
+ });
1066
+ frame_scheduler::mark_needs_commit();
1067
+ }
1068
+
1069
+ fn fire_deferred_blur_close(&self) {
1070
+ self.deferred_blur_close_pending_state.set(false);
1071
+ let next_focused = self.wrapper_focused_state.get() || self.editor_focused_state.get();
1072
+ if !next_focused && !self.popup_pointer_pressed_state.get() {
1073
+ self.pointer_pressed_state.set(false);
1074
+ self.close();
1075
+ }
1076
+ }
1077
+
1078
+ fn set_text(&self, value: String, emit: bool) {
1079
+ if *self.text_value.borrow() == value {
1080
+ return;
1081
+ }
1082
+ *self.text_value.borrow_mut() = value.clone();
1083
+ self.suppress_editor_changed.set(true);
1084
+ self.editor.text(value.clone());
1085
+ self.suppress_editor_changed.set(false);
1086
+ self.sync_selection_from_text();
1087
+ self.rebuild_filtered_indices();
1088
+ self.refresh_popup_after_filter();
1089
+ self.sync_option_visuals();
1090
+ self.sync_semantic_label();
1091
+ if emit {
1092
+ self.emit_text_changed(value);
1093
+ }
1094
+ }
1095
+
1096
+ fn set_selected_index(&self, index: i32, emit: bool) {
1097
+ if index == -1 {
1098
+ self.selected_index_value.set(-1);
1099
+ self.committed_selected_index_value.set(-1);
1100
+ self.highlighted_index_value.set(-1);
1101
+ self.popup_list.set_highlighted_index(-1);
1102
+ self.sync_semantic_label();
1103
+ return;
1104
+ }
1105
+ let count = self.items_value.borrow().len() as i32;
1106
+ if count == 0 {
1107
+ if index != -1 {
1108
+ logger::warn(
1109
+ "Layout",
1110
+ &format!(
1111
+ "ComboBox.selectIndex() received {index} before any items were assigned."
1112
+ ),
1113
+ );
1114
+ }
1115
+ return;
1116
+ }
1117
+ let clamped_index = index.clamp(0, count - 1);
1118
+ if clamped_index != index {
1119
+ logger::warn(
1120
+ "Layout",
1121
+ &format!("ComboBox.selectIndex() received {index}; clamping to {clamped_index}."),
1122
+ );
1123
+ }
1124
+ let changed = self.selected_index_value.get() != clamped_index;
1125
+ self.selected_index_value.set(clamped_index);
1126
+ self.committed_selected_index_value.set(clamped_index);
1127
+ let item = self.items_value.borrow()[clamped_index as usize].clone();
1128
+ self.set_text(item.value, false);
1129
+ self.editor.caret_to_end();
1130
+ self.rebuild_filtered_indices();
1131
+ let visible_index = self.find_visible_index_for_source_index(clamped_index);
1132
+ self.highlighted_index_value.set(visible_index);
1133
+ self.popup_list.set_highlighted_index(visible_index);
1134
+ self.sync_semantic_label();
1135
+ if emit && changed {
1136
+ if let Some(root) = self.root.upgrade() {
1137
+ root.request_semantic_announcement();
1138
+ }
1139
+ self.emit_selection_changed();
1140
+ }
1141
+ }
1142
+
1143
+ fn sync_selection_from_text(&self) {
1144
+ let exact_index = self.find_exact_text_match(&self.text_value.borrow());
1145
+ if exact_index >= 0 {
1146
+ self.selected_index_value.set(exact_index);
1147
+ return;
1148
+ }
1149
+ if self.allow_custom_value.get() {
1150
+ self.selected_index_value.set(-1);
1151
+ }
1152
+ }
1153
+
1154
+ fn commit_current_text(&self) {
1155
+ match self.commit_mode_value.get() {
1156
+ ComboBoxCommitMode::KeepText => {}
1157
+ ComboBoxCommitMode::SelectExactMatch => {
1158
+ let exact_index = self.find_exact_text_match(&self.text_value.borrow());
1159
+ if exact_index >= 0 {
1160
+ self.set_selected_index(exact_index, true);
1161
+ }
1162
+ }
1163
+ ComboBoxCommitMode::RevertToSelection => {
1164
+ let committed = self.committed_selected_index_value.get();
1165
+ if committed >= 0 && committed < self.items_value.borrow().len() as i32 {
1166
+ let value = self.items_value.borrow()[committed as usize].value.clone();
1167
+ self.set_text(value, true);
1168
+ self.editor.caret_to_end();
1169
+ self.selected_index_value.set(committed);
1170
+ }
1171
+ }
1172
+ }
1173
+ }
1174
+
1175
+ fn emit_selection_changed(&self) {
1176
+ let selected_index = self.selected_index_value.get();
1177
+ if selected_index < 0 {
1178
+ return;
1179
+ }
1180
+ let Some(item) = self
1181
+ .items_value
1182
+ .borrow()
1183
+ .get(selected_index as usize)
1184
+ .cloned()
1185
+ else {
1186
+ return;
1187
+ };
1188
+ if let Some(callback) = self.changed_callback.borrow().clone() {
1189
+ callback(crate::controls::ComboBoxChangedEventArgs {
1190
+ item,
1191
+ selected_index,
1192
+ });
1193
+ }
1194
+ }
1195
+
1196
+ fn emit_text_changed(&self, value: String) {
1197
+ if let Some(callback) = self.text_changed_callback.borrow().clone() {
1198
+ callback(TextChangedEventArgs { text: value });
1199
+ }
1200
+ }
1201
+
1202
+ fn open(&self) {
1203
+ let Some(root) = self.root.upgrade() else {
1204
+ return;
1205
+ };
1206
+ if self.open_state.get()
1207
+ || self.filtered_indices.borrow().is_empty()
1208
+ || root.handle() == crate::node::NodeHandle::INVALID
1209
+ {
1210
+ return;
1211
+ }
1212
+ let initial_highlight = if self.selected_index_value.get() >= 0 {
1213
+ self.find_visible_index_for_source_index(self.selected_index_value.get())
1214
+ } else {
1215
+ 0
1216
+ };
1217
+ self.popup_list.set_highlighted_index(initial_highlight);
1218
+ self.highlighted_index_value
1219
+ .set(self.popup_list.highlighted_index());
1220
+ let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
1221
+ return;
1222
+ };
1223
+ if !self.popup_list.open(
1224
+ bounds[0],
1225
+ bounds[1],
1226
+ bounds[2],
1227
+ bounds[3],
1228
+ initial_highlight,
1229
+ ) {
1230
+ return;
1231
+ }
1232
+ self.highlighted_index_value
1233
+ .set(self.popup_list.highlighted_index());
1234
+ self.open_state.set(true);
1235
+ ACTIVE_COMBOBOX.with(|slot| {
1236
+ *slot.borrow_mut() = Some(self.self_weak.borrow().clone());
1237
+ });
1238
+ ui::set_semantic_expanded(root.handle().raw(), true, true);
1239
+ root.request_semantic_announcement();
1240
+ if self.key_filter_token.get() == 0 {
1241
+ let weak_self = self.self_weak.borrow().clone();
1242
+ let token = event::push_key_filter(move |event_type, key, modifiers| {
1243
+ weak_self.upgrade().is_some_and(|shared| {
1244
+ shared.handle_global_key_event(event_type, key, modifiers)
1245
+ })
1246
+ });
1247
+ self.key_filter_token.set(token);
1248
+ }
1249
+ self.handle_theme_changed();
1250
+ }
1251
+
1252
+ fn close(&self) {
1253
+ if !self.open_state.get() && !self.popup_list.is_open() {
1254
+ return;
1255
+ }
1256
+ self.deferred_blur_close_pending_state.set(false);
1257
+ self.popup_pointer_pressed_state.set(false);
1258
+ self.popup_list.close();
1259
+ self.open_state.set(false);
1260
+ ACTIVE_COMBOBOX.with(|slot| {
1261
+ let should_clear = slot
1262
+ .borrow()
1263
+ .as_ref()
1264
+ .and_then(Weak::upgrade)
1265
+ .and_then(|active| active.root.upgrade())
1266
+ .and_then(|active_root| {
1267
+ self.root
1268
+ .upgrade()
1269
+ .map(|root| active_root.handle() == root.handle())
1270
+ })
1271
+ .unwrap_or(false);
1272
+ if should_clear {
1273
+ slot.borrow_mut().take();
1274
+ }
1275
+ });
1276
+ if let Some(root) = self.root.upgrade() {
1277
+ ui::set_semantic_expanded(root.handle().raw(), true, false);
1278
+ root.request_semantic_announcement();
1279
+ }
1280
+ let token = self.key_filter_token.replace(0);
1281
+ if token != 0 {
1282
+ event::remove_key_filter(token);
1283
+ }
1284
+ self.commit_current_text();
1285
+ self.handle_theme_changed();
1286
+ }
1287
+
1288
+ fn is_trigger_visible_in_viewport(&self) -> bool {
1289
+ let Some(root) = self.root.upgrade() else {
1290
+ return true;
1291
+ };
1292
+ let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
1293
+ return true;
1294
+ };
1295
+ let x = bounds[0];
1296
+ let y = bounds[1];
1297
+ let width = bounds[2];
1298
+ let height = bounds[3];
1299
+ if width <= 0.0 || height <= 0.0 {
1300
+ return true;
1301
+ }
1302
+ let right = x + width;
1303
+ let bottom = y + height;
1304
+ let mut current = root.retained_node_ref().parent();
1305
+ while let Some(node) = current {
1306
+ if node.node_type() == NodeType::ScrollView {
1307
+ if let Some(scroll_bounds) = ui::get_bounds(node.handle().raw()) {
1308
+ let sv_x = scroll_bounds[0];
1309
+ let sv_y = scroll_bounds[1];
1310
+ let sv_right = sv_x + scroll_bounds[2];
1311
+ let sv_bottom = sv_y + scroll_bounds[3];
1312
+ return right > sv_x && bottom > sv_y && x < sv_right && y < sv_bottom;
1313
+ }
1314
+ break;
1315
+ }
1316
+ current = node.parent();
1317
+ }
1318
+ let viewport_width = ui::get_viewport_width();
1319
+ let viewport_height = ui::get_viewport_height();
1320
+ right > 0.0 && bottom > 0.0 && x < viewport_width && y < viewport_height
1321
+ }
1322
+
1323
+ fn highlight_index(&self, index: i32) {
1324
+ self.popup_list.highlight_index(index);
1325
+ self.highlighted_index_value
1326
+ .set(self.popup_list.highlighted_index());
1327
+ }
1328
+
1329
+ fn move_highlight(&self, delta: i32) {
1330
+ self.popup_list.move_highlight(delta);
1331
+ self.highlighted_index_value
1332
+ .set(self.popup_list.highlighted_index());
1333
+ }
1334
+
1335
+ fn refresh_popup_after_filter(&self) {
1336
+ if self.open_state.get() {
1337
+ self.refresh_open_popup();
1338
+ } else {
1339
+ self.popup_list.refresh_panel_layout();
1340
+ }
1341
+ }
1342
+
1343
+ fn refresh_open_popup(&self) {
1344
+ let Some(root) = self.root.upgrade() else {
1345
+ self.popup_list.refresh_panel_layout();
1346
+ return;
1347
+ };
1348
+ let Some(bounds) = ui::get_bounds(root.handle().raw()) else {
1349
+ self.popup_list.refresh_panel_layout();
1350
+ return;
1351
+ };
1352
+ self.popup_list.refresh_open(
1353
+ bounds[0],
1354
+ bounds[1],
1355
+ bounds[2],
1356
+ bounds[3],
1357
+ self.highlighted_index_value.get(),
1358
+ );
1359
+ self.highlighted_index_value
1360
+ .set(self.popup_list.highlighted_index());
1361
+ }
1362
+
1363
+ fn select_highlighted(&self) {
1364
+ let highlighted = self.highlighted_index_value.get();
1365
+ let source_index = {
1366
+ let filtered = self.filtered_indices.borrow();
1367
+ if highlighted < 0 || highlighted >= filtered.len() as i32 {
1368
+ return;
1369
+ }
1370
+ filtered[highlighted as usize]
1371
+ };
1372
+ if highlighted < 0 {
1373
+ return;
1374
+ }
1375
+ self.set_selected_index(source_index, true);
1376
+ self.close();
1377
+ }
1378
+
1379
+ fn rebuild_filtered_indices(&self) {
1380
+ let mut filtered = self.filtered_indices.borrow_mut();
1381
+ filtered.clear();
1382
+ for (index, item) in self.items_value.borrow().iter().enumerate() {
1383
+ if self.should_include_item(item) {
1384
+ filtered.push(index as i32);
1385
+ }
1386
+ }
1387
+ if self.highlighted_index_value.get() >= filtered.len() as i32 {
1388
+ self.highlighted_index_value.set(if filtered.is_empty() {
1389
+ -1
1390
+ } else {
1391
+ filtered.len() as i32 - 1
1392
+ });
1393
+ }
1394
+ }
1395
+
1396
+ fn should_include_item(&self, item: &ComboBoxItem) -> bool {
1397
+ let text = self.text_value.borrow();
1398
+ if self.filter_mode_value.get() == ComboBoxFilterMode::None || text.is_empty() {
1399
+ return true;
1400
+ }
1401
+ if self.filter_mode_value.get() == ComboBoxFilterMode::StartsWith {
1402
+ return string_starts_with_ignore_case(&item.value, &text);
1403
+ }
1404
+ string_contains_ignore_case(&item.value, &text)
1405
+ }
1406
+
1407
+ fn find_auto_complete_match(&self, text: &str) -> i32 {
1408
+ for (index, item) in self.items_value.borrow().iter().enumerate() {
1409
+ if string_starts_with_ignore_case(&item.value, text) {
1410
+ return index as i32;
1411
+ }
1412
+ }
1413
+ -1
1414
+ }
1415
+
1416
+ fn find_exact_text_match(&self, text: &str) -> i32 {
1417
+ for (index, item) in self.items_value.borrow().iter().enumerate() {
1418
+ if strings_equal_ignore_case(&item.value, text) {
1419
+ return index as i32;
1420
+ }
1421
+ }
1422
+ -1
1423
+ }
1424
+
1425
+ fn find_visible_index_for_source_index(&self, source_index: i32) -> i32 {
1426
+ for (index, visible) in self.filtered_indices.borrow().iter().enumerate() {
1427
+ if *visible == source_index {
1428
+ return index as i32;
1429
+ }
1430
+ }
1431
+ if self.filtered_indices.borrow().is_empty() {
1432
+ -1
1433
+ } else {
1434
+ 0
1435
+ }
1436
+ }
1437
+
1438
+ fn handle_theme_changed(&self) {
1439
+ let Some(root) = self.root.upgrade() else {
1440
+ return;
1441
+ };
1442
+ let theme = current_theme();
1443
+ if !self.popup_panel_color_overridden.get() {
1444
+ self.popup_panel_color_value
1445
+ .set(theme.context_menu.panel_background);
1446
+ }
1447
+ if !self.popup_panel_background_blur_overridden.get() {
1448
+ self.popup_panel_background_blur_sigma_value
1449
+ .set(DEFAULT_PANEL_BACKGROUND_BLUR_SIGMA);
1450
+ }
1451
+ let sizing = self.sizing_value.get();
1452
+ let field_height = sizing
1453
+ .filter(|value| value.has_field_height())
1454
+ .map(|value| value.field_height_px())
1455
+ .unwrap_or(32.0);
1456
+ let chevron_box_size = sizing
1457
+ .filter(|value| value.has_chevron_box_size())
1458
+ .map(|value| value.chevron_box_size_px())
1459
+ .unwrap_or(32.0);
1460
+ let field_font_size = sizing
1461
+ .filter(|value| value.has_field_font_size())
1462
+ .map(|value| value.field_font_size_px())
1463
+ .unwrap_or(theme.fonts.size_body);
1464
+ let field_border_width = 2.0;
1465
+ let field_content_height = (field_height - (field_border_width * 2.0)).max(0.0);
1466
+ root.cursor(if self.is_enabled() {
1467
+ CursorStyle::Text
1468
+ } else {
1469
+ CursorStyle::Default
1470
+ })
1471
+ .corner_radius(0.0)
1472
+ .border(0.0, 0x00000000)
1473
+ .padding(0.0, 0.0, 0.0, 0.0)
1474
+ .bg_color(0x00000000)
1475
+ .opacity(if self.is_enabled() { 1.0 } else { 0.6 });
1476
+ let colors = self.colors_value.get();
1477
+ let field_background = colors
1478
+ .filter(|value| value.has_background())
1479
+ .map(|value| value.background_color())
1480
+ .unwrap_or(theme.colors.surface);
1481
+ let field_border_color = colors
1482
+ .filter(|value| value.has_border())
1483
+ .map(|value| value.border_color())
1484
+ .unwrap_or(theme.colors.border);
1485
+ root.height(field_height, Unit::Pixel)
1486
+ .corner_radius(theme.spacing.sm)
1487
+ .border(field_border_width, field_border_color)
1488
+ .padding(16.0, 0.0, 8.0, 0.0)
1489
+ .bg_color(field_background);
1490
+ self.editor
1491
+ .height(field_content_height, Unit::Pixel)
1492
+ .font_size(field_font_size)
1493
+ .line_height(field_content_height)
1494
+ .colors(resolve_text_input_colors(colors, &theme));
1495
+ self.chevron_host
1496
+ .width(chevron_box_size, Unit::Pixel)
1497
+ .height(field_content_height, Unit::Pixel)
1498
+ .align_items(AlignItems::Center)
1499
+ .justify_content(crate::ffi::JustifyContent::Center)
1500
+ .cursor(if self.is_enabled() {
1501
+ CursorStyle::Pointer
1502
+ } else {
1503
+ CursorStyle::Default
1504
+ });
1505
+ self.chevron_presenter.borrow().apply(
1506
+ theme.clone(),
1507
+ DropdownChevronVisualState::new(
1508
+ self.open_state.get(),
1509
+ self.hovered_state.get(),
1510
+ self.is_enabled(),
1511
+ ),
1512
+ );
1513
+ self.popup_list
1514
+ .panel_node
1515
+ .padding(
1516
+ SELECTABLE_POPUP_LIST_PANEL_PADDING,
1517
+ SELECTABLE_POPUP_LIST_PANEL_PADDING,
1518
+ SELECTABLE_POPUP_LIST_PANEL_PADDING,
1519
+ SELECTABLE_POPUP_LIST_PANEL_PADDING,
1520
+ )
1521
+ .corner_radius(theme.spacing.sm)
1522
+ .bg_color(self.popup_panel_color_value.get())
1523
+ .border(1.0, theme.context_menu.panel_border_color)
1524
+ .background_blur(self.popup_panel_background_blur_sigma_value.get())
1525
+ .drop_shadow(
1526
+ theme.context_menu.panel_shadow_color,
1527
+ 0.0,
1528
+ theme.context_menu.shadow_offset_y,
1529
+ theme.context_menu.shadow_blur,
1530
+ theme.context_menu.shadow_spread,
1531
+ );
1532
+ self.popup_list.popup_scroll_box.bg_color(0x00000000);
1533
+ self.sync_option_visuals();
1534
+ self.sync_focus_chrome();
1535
+ }
1536
+
1537
+ fn sync_option_visuals(&self) {
1538
+ self.popup_list.sync_option_visuals();
1539
+ }
1540
+
1541
+ fn sync_semantic_label(&self) {
1542
+ if let Some(root) = self.root.upgrade() {
1543
+ if !self.text_value.borrow().is_empty() {
1544
+ root.default_semantic_label(self.text_value.borrow().clone());
1545
+ } else {
1546
+ root.default_semantic_label("Combo box");
1547
+ }
1548
+ }
1549
+ }
1550
+
1551
+ fn sync_focus_chrome(&self) {
1552
+ let Some(root) = self.root.upgrade() else {
1553
+ return;
1554
+ };
1555
+ if self.focused_state.get()
1556
+ && self.is_enabled()
1557
+ && focus_visibility::keyboard_focus_visible()
1558
+ {
1559
+ focus_adorner::show_standard(&root, current_theme().spacing.sm);
1560
+ } else {
1561
+ focus_adorner::hide_owner(&root);
1562
+ }
1563
+ }
1564
+
1565
+ fn popup_list_activate_index(&self, index: i32) {
1566
+ self.highlight_index(index);
1567
+ self.select_highlighted();
1568
+ }
1569
+
1570
+ fn popup_list_pointer_up(&self) {
1571
+ self.popup_pointer_pressed_state.set(false);
1572
+ self.sync_focused_state();
1573
+ }
1574
+ }