@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,597 @@
1
+ use super::dropdown_option_row_presenter::{
2
+ create_default_dropdown_option_row_presenter, DropdownOptionRowPresenter,
3
+ DropdownOptionRowTemplate, DropdownOptionRowVisualState,
4
+ };
5
+ use crate::controls::control_template_set::get_control_templates;
6
+ use crate::controls::{DropdownColors, DropdownSizing};
7
+ use crate::ffi::{CursorStyle, FlexDirection, HandleValue, PositionType, SemanticRole, Unit};
8
+ use crate::logger;
9
+ use crate::node::{
10
+ flex_box, portal, scroll_box, FlexBox, FlexBoxSurface, Node, NodeHandle, ScrollBarVisibility,
11
+ ScrollBox,
12
+ };
13
+ use crate::popup_presenter::PopupPresenter;
14
+ use crate::theme::current_theme;
15
+ use std::cell::{Cell, RefCell};
16
+ use std::rc::Rc;
17
+
18
+ const PANEL_EDGE_PADDING: f32 = 8.0;
19
+ const OPTION_HEIGHT: f32 = 34.0;
20
+ pub(crate) const SELECTABLE_POPUP_LIST_PANEL_PADDING: f32 = 4.0;
21
+ const UNLIMITED_VISIBLE_ITEMS: i32 = 0;
22
+
23
+ #[derive(Clone)]
24
+ pub(crate) struct SelectablePopupListOwner {
25
+ pub(crate) item_count: Rc<dyn Fn() -> i32>,
26
+ pub(crate) item_label: Rc<dyn Fn(i32) -> String>,
27
+ pub(crate) item_selected: Rc<dyn Fn(i32) -> bool>,
28
+ pub(crate) enabled: Rc<dyn Fn() -> bool>,
29
+ pub(crate) highlight_index: Rc<dyn Fn(i32)>,
30
+ pub(crate) activate_index: Rc<dyn Fn(i32)>,
31
+ pub(crate) pointer_down: Rc<dyn Fn(i32)>,
32
+ pub(crate) pointer_up: Rc<dyn Fn(i32)>,
33
+ }
34
+
35
+ fn create_option_row_presenter(
36
+ template: Option<Rc<dyn DropdownOptionRowTemplate>>,
37
+ sizing: Option<DropdownSizing>,
38
+ ) -> Rc<dyn DropdownOptionRowPresenter> {
39
+ if let Some(template) = template {
40
+ return template.create(sizing);
41
+ }
42
+ let template_set = get_control_templates();
43
+ let app_template = template_set.and_then(|set| set.dropdown_option_row);
44
+ if let Some(app_template) = app_template {
45
+ app_template.create(sizing)
46
+ } else {
47
+ create_default_dropdown_option_row_presenter(sizing)
48
+ }
49
+ }
50
+
51
+ #[derive(Clone)]
52
+ pub(crate) struct SelectablePopupListOptionNode {
53
+ root: FlexBox,
54
+ presenter: Rc<RefCell<Rc<dyn DropdownOptionRowPresenter>>>,
55
+ owner: SelectablePopupListOwner,
56
+ slot_index: Rc<Cell<i32>>,
57
+ current_label: Rc<RefCell<String>>,
58
+ }
59
+
60
+ impl SelectablePopupListOptionNode {
61
+ pub(crate) fn new(
62
+ owner: SelectablePopupListOwner,
63
+ slot_index: i32,
64
+ template: Option<Rc<dyn DropdownOptionRowTemplate>>,
65
+ sizing: Option<DropdownSizing>,
66
+ ) -> Self {
67
+ let presenter = create_option_row_presenter(template, sizing);
68
+ let root = flex_box();
69
+ root.semantic_role(SemanticRole::ListItem)
70
+ .width(100.0, Unit::Percent)
71
+ .cursor(CursorStyle::Pointer)
72
+ .focusable(false, 0)
73
+ .interactive(true)
74
+ .child(&presenter.root());
75
+ let control = Self {
76
+ root,
77
+ presenter: Rc::new(RefCell::new(presenter.clone())),
78
+ owner,
79
+ slot_index: Rc::new(Cell::new(slot_index)),
80
+ current_label: Rc::new(RefCell::new(String::new())),
81
+ };
82
+ control.sync_presenter_layout();
83
+ control.bind_events();
84
+ control.bind_presenter_pointer_events(&presenter);
85
+ control
86
+ }
87
+
88
+ fn bind_events(&self) {
89
+ let owner = self.owner.clone();
90
+ let slot_index = self.slot_index.clone();
91
+ self.root.on_pointer_enter(move |event| {
92
+ (owner.highlight_index)(slot_index.get());
93
+ event.handled = true;
94
+ });
95
+ let owner = self.owner.clone();
96
+ let slot_index = self.slot_index.clone();
97
+ self.root.on_pointer_down(move |event| {
98
+ event.capture_pointer();
99
+ (owner.pointer_down)(slot_index.get());
100
+ event.handled = true;
101
+ });
102
+ let owner = self.owner.clone();
103
+ let slot_index = self.slot_index.clone();
104
+ self.root.on_pointer_up(move |event| {
105
+ event.release_pointer_capture();
106
+ (owner.pointer_up)(slot_index.get());
107
+ (owner.activate_index)(slot_index.get());
108
+ event.handled = true;
109
+ });
110
+ let owner = self.owner.clone();
111
+ let slot_index = self.slot_index.clone();
112
+ self.root.on_pointer_cancel(move |event| {
113
+ event.release_pointer_capture();
114
+ (owner.pointer_up)(slot_index.get());
115
+ event.handled = true;
116
+ });
117
+ }
118
+
119
+ fn bind_presenter_pointer_events(&self, presenter: &Rc<dyn DropdownOptionRowPresenter>) {
120
+ presenter.root().cursor(CursorStyle::Pointer);
121
+ presenter.label_node().cursor(CursorStyle::Pointer);
122
+ self.bind_nested_pointer_events(&presenter.root());
123
+ self.bind_nested_pointer_events(&presenter.label_node());
124
+ }
125
+
126
+ fn bind_nested_pointer_events<T>(&self, node: &T)
127
+ where
128
+ T: Node,
129
+ {
130
+ let owner = self.owner.clone();
131
+ let slot_index = self.slot_index.clone();
132
+ node.on_pointer_enter(move |event| {
133
+ (owner.highlight_index)(slot_index.get());
134
+ event.handled = true;
135
+ });
136
+ let owner = self.owner.clone();
137
+ let slot_index = self.slot_index.clone();
138
+ node.on_pointer_down(move |event| {
139
+ event.capture_pointer();
140
+ (owner.pointer_down)(slot_index.get());
141
+ event.handled = true;
142
+ });
143
+ let owner = self.owner.clone();
144
+ let slot_index = self.slot_index.clone();
145
+ node.on_pointer_up(move |event| {
146
+ event.release_pointer_capture();
147
+ (owner.pointer_up)(slot_index.get());
148
+ (owner.activate_index)(slot_index.get());
149
+ event.handled = true;
150
+ });
151
+ let owner = self.owner.clone();
152
+ let slot_index = self.slot_index.clone();
153
+ node.on_pointer_cancel(move |event| {
154
+ event.release_pointer_capture();
155
+ (owner.pointer_up)(slot_index.get());
156
+ event.handled = true;
157
+ });
158
+ }
159
+
160
+ pub(crate) fn root(&self) -> FlexBox {
161
+ self.root.clone()
162
+ }
163
+
164
+ pub(crate) fn row_height(&self) -> f32 {
165
+ self.presenter.borrow().metrics().height
166
+ }
167
+
168
+ pub(crate) fn label(&self, label: impl Into<String>) {
169
+ let label = label.into();
170
+ *self.current_label.borrow_mut() = label.clone();
171
+ self.root.semantic_label(label.clone());
172
+ self.presenter.borrow().label_node().text(label);
173
+ }
174
+
175
+ pub(crate) fn template(
176
+ &self,
177
+ template: Option<Rc<dyn DropdownOptionRowTemplate>>,
178
+ sizing: Option<DropdownSizing>,
179
+ ) {
180
+ let previous_presenter = self.presenter.borrow().clone();
181
+ let next_presenter = create_option_row_presenter(template, sizing);
182
+ *self.presenter.borrow_mut() = next_presenter.clone();
183
+ self.root.remove_child(&previous_presenter.root());
184
+ self.root.child(&next_presenter.root());
185
+ previous_presenter.root().dispose();
186
+ next_presenter
187
+ .label_node()
188
+ .text(self.current_label.borrow().clone());
189
+ self.bind_presenter_pointer_events(&next_presenter);
190
+ self.sync_presenter_layout();
191
+ }
192
+
193
+ pub(crate) fn apply_theme(
194
+ &self,
195
+ highlighted: bool,
196
+ selected: bool,
197
+ enabled: bool,
198
+ colors: Option<DropdownColors>,
199
+ ) {
200
+ self.root.semantic_selected(selected);
201
+ self.root.semantic_disabled(!enabled);
202
+ self.presenter.borrow().apply(
203
+ current_theme(),
204
+ DropdownOptionRowVisualState::new(highlighted, selected, enabled),
205
+ colors,
206
+ );
207
+ }
208
+
209
+ fn sync_presenter_layout(&self) {
210
+ let presenter = self.presenter.borrow();
211
+ self.root.height(presenter.metrics().height, Unit::Pixel);
212
+ presenter.root().fill_size();
213
+ }
214
+ }
215
+
216
+ #[derive(Clone)]
217
+ pub(crate) struct SelectablePopupList {
218
+ pub(crate) root: FlexBox,
219
+ pub(crate) panel_node: FlexBox,
220
+ pub(crate) popup_presenter: PopupPresenter,
221
+ pub(crate) popup_scroll_box: ScrollBox,
222
+ pub(crate) options_host: FlexBox,
223
+ owner: SelectablePopupListOwner,
224
+ option_nodes: Rc<RefCell<Vec<SelectablePopupListOptionNode>>>,
225
+ option_row_template_value: Rc<RefCell<Option<Rc<dyn DropdownOptionRowTemplate>>>>,
226
+ sizing_value: Rc<Cell<Option<DropdownSizing>>>,
227
+ colors_value: Rc<Cell<Option<DropdownColors>>>,
228
+ highlighted_index_value: Rc<Cell<i32>>,
229
+ max_visible_items_value: Rc<Cell<i32>>,
230
+ popup_width_value: Rc<Cell<f32>>,
231
+ }
232
+
233
+ impl SelectablePopupList {
234
+ pub(crate) fn new(owner: SelectablePopupListOwner) -> Self {
235
+ let root = portal();
236
+ root.position_type(PositionType::Absolute)
237
+ .position(0.0, 0.0)
238
+ .width(100.0, Unit::Percent)
239
+ .height(100.0, Unit::Percent);
240
+ let popup_scroll_box = scroll_box();
241
+ popup_scroll_box
242
+ .scroll_enabled_x(false)
243
+ .scroll_enabled_y(true)
244
+ .horizontal_scrollbar_visibility(ScrollBarVisibility::Never)
245
+ .vertical_scrollbar_visibility(ScrollBarVisibility::Auto);
246
+ let options_host = flex_box();
247
+ options_host
248
+ .flex_direction(FlexDirection::Column)
249
+ .semantic_role(SemanticRole::List)
250
+ .semantic_label("Dropdown options");
251
+ let panel_node = flex_box();
252
+ panel_node
253
+ .position_type(PositionType::Absolute)
254
+ .flex_direction(FlexDirection::Column);
255
+ let popup_presenter =
256
+ PopupPresenter::new_with_semantic_scope(root.clone(), panel_node.clone(), None);
257
+ popup_scroll_box.child(&options_host);
258
+ panel_node.child(&popup_scroll_box);
259
+ Self {
260
+ root,
261
+ panel_node,
262
+ popup_presenter,
263
+ popup_scroll_box,
264
+ options_host,
265
+ owner,
266
+ option_nodes: Rc::new(RefCell::new(Vec::new())),
267
+ option_row_template_value: Rc::new(RefCell::new(None)),
268
+ sizing_value: Rc::new(Cell::new(None)),
269
+ colors_value: Rc::new(Cell::new(None)),
270
+ highlighted_index_value: Rc::new(Cell::new(-1)),
271
+ max_visible_items_value: Rc::new(Cell::new(UNLIMITED_VISIBLE_ITEMS)),
272
+ popup_width_value: Rc::new(Cell::new(0.0)),
273
+ }
274
+ }
275
+
276
+ pub(crate) fn is_open(&self) -> bool {
277
+ self.popup_presenter.is_open()
278
+ }
279
+
280
+ pub(crate) fn highlighted_index(&self) -> i32 {
281
+ self.highlighted_index_value.get()
282
+ }
283
+
284
+ pub(crate) fn max_visible_items(&self, count: i32) {
285
+ if count <= 0 {
286
+ logger::warn(
287
+ "Layout",
288
+ &format!(
289
+ "Dropdown.maxVisibleItems() received {count}; using unlimited visible items."
290
+ ),
291
+ );
292
+ }
293
+ self.max_visible_items_value.set(if count > 0 {
294
+ count
295
+ } else {
296
+ UNLIMITED_VISIBLE_ITEMS
297
+ });
298
+ self.refresh_panel_layout();
299
+ }
300
+
301
+ pub(crate) fn popup_width(&self, value: f32) {
302
+ if value <= 0.0 {
303
+ logger::warn(
304
+ "Layout",
305
+ &format!("Dropdown.popupWidth() received {value}; clamping to 0.0."),
306
+ );
307
+ }
308
+ self.popup_width_value.set(value.max(0.0));
309
+ self.refresh_panel_layout();
310
+ }
311
+
312
+ pub(crate) fn sizing(&self, sizing: Option<DropdownSizing>) {
313
+ self.sizing_value.set(sizing);
314
+ let template = self.option_row_template_value.borrow().clone();
315
+ let nodes = self.option_nodes.borrow();
316
+ for node in nodes.iter() {
317
+ node.template(template.clone(), self.sizing_value.get());
318
+ }
319
+ drop(nodes);
320
+ self.refresh_panel_layout();
321
+ }
322
+
323
+ pub(crate) fn colors(&self, colors: Option<DropdownColors>) {
324
+ self.colors_value.set(colors);
325
+ self.sync_option_visuals();
326
+ }
327
+
328
+ pub(crate) fn option_row_template(&self, template: Option<Rc<dyn DropdownOptionRowTemplate>>) {
329
+ *self.option_row_template_value.borrow_mut() = template.clone();
330
+ let nodes = self.option_nodes.borrow();
331
+ for node in nodes.iter() {
332
+ node.template(template.clone(), self.sizing_value.get());
333
+ }
334
+ drop(nodes);
335
+ self.refresh_panel_layout();
336
+ self.sync_option_visuals();
337
+ }
338
+
339
+ pub(crate) fn open(
340
+ &self,
341
+ trigger_x: f32,
342
+ trigger_y: f32,
343
+ trigger_width: f32,
344
+ trigger_height: f32,
345
+ initial_highlight_index: i32,
346
+ ) -> bool {
347
+ if self.is_open()
348
+ || (self.owner.item_count)() == 0
349
+ || self.root.handle() == NodeHandle::INVALID
350
+ || self.root.handle().raw() == HandleValue::Invalid as u64
351
+ {
352
+ return false;
353
+ }
354
+ self.ensure_option_nodes();
355
+ self.rebuild_panel();
356
+ self.set_highlighted_index(initial_highlight_index);
357
+ self.position_panel(trigger_x, trigger_y, trigger_width, trigger_height);
358
+ true
359
+ }
360
+
361
+ pub(crate) fn refresh_open(
362
+ &self,
363
+ trigger_x: f32,
364
+ trigger_y: f32,
365
+ trigger_width: f32,
366
+ trigger_height: f32,
367
+ highlighted_index: i32,
368
+ ) {
369
+ if !self.is_open() {
370
+ return;
371
+ }
372
+ self.ensure_option_nodes();
373
+ self.rebuild_panel();
374
+ let count = (self.owner.item_count)();
375
+ let mut next_highlight = highlighted_index;
376
+ if next_highlight >= count {
377
+ next_highlight = if count > 0 { count - 1 } else { -1 };
378
+ }
379
+ if next_highlight < 0 && count > 0 {
380
+ next_highlight = 0;
381
+ }
382
+ self.set_highlighted_index(next_highlight);
383
+ self.position_panel(trigger_x, trigger_y, trigger_width, trigger_height);
384
+ }
385
+
386
+ pub(crate) fn close(&self) {
387
+ self.popup_presenter.hide();
388
+ }
389
+
390
+ pub(crate) fn dispose(&self) {
391
+ self.popup_presenter.dispose();
392
+ }
393
+
394
+ pub(crate) fn clear(&self) {
395
+ self.close();
396
+ self.highlighted_index_value.set(-1);
397
+ }
398
+
399
+ pub(crate) fn set_highlighted_index(&self, index: i32) {
400
+ self.highlighted_index_value.set(index);
401
+ self.sync_option_visuals();
402
+ self.ensure_highlighted_visible();
403
+ }
404
+
405
+ pub(crate) fn highlight_index(&self, index: i32) {
406
+ let count = (self.owner.item_count)();
407
+ if index < 0 || index >= count || self.highlighted_index_value.get() == index {
408
+ if index < 0 || index >= count {
409
+ logger::warn(
410
+ "Layout",
411
+ &format!(
412
+ "Dropdown.highlightIndex() received {index} outside the available item range."
413
+ ),
414
+ );
415
+ }
416
+ return;
417
+ }
418
+ self.highlighted_index_value.set(index);
419
+ self.sync_option_visuals();
420
+ self.ensure_highlighted_visible();
421
+ }
422
+
423
+ pub(crate) fn move_highlight(&self, delta: i32) {
424
+ let count = (self.owner.item_count)();
425
+ if count == 0 {
426
+ return;
427
+ }
428
+ let mut next_index = self.highlighted_index_value.get();
429
+ if next_index < 0 {
430
+ next_index = 0;
431
+ }
432
+ next_index += delta;
433
+ if next_index < 0 {
434
+ next_index = count - 1;
435
+ } else if next_index >= count {
436
+ next_index = 0;
437
+ }
438
+ self.highlight_index(next_index);
439
+ }
440
+
441
+ pub(crate) fn refresh_panel_layout(&self) {
442
+ let count = (self.owner.item_count)();
443
+ self.options_host
444
+ .width(100.0, Unit::Percent)
445
+ .height(count as f32 * self.resolve_option_row_height(), Unit::Pixel);
446
+ self.popup_scroll_box.width(100.0, Unit::Percent).height(
447
+ (self.resolve_viewport_clamped_panel_outer_height()
448
+ - (SELECTABLE_POPUP_LIST_PANEL_PADDING * 2.0))
449
+ .max(0.0),
450
+ Unit::Pixel,
451
+ );
452
+ if self.is_open() {
453
+ self.ensure_highlighted_visible();
454
+ }
455
+ }
456
+
457
+ pub(crate) fn position_panel(
458
+ &self,
459
+ trigger_x: f32,
460
+ trigger_y: f32,
461
+ trigger_width: f32,
462
+ trigger_height: f32,
463
+ ) {
464
+ let popup_width = self.resolve_popup_width(trigger_width);
465
+ let panel_height = self.resolve_viewport_clamped_panel_outer_height();
466
+ self.panel_node
467
+ .width(popup_width, Unit::Pixel)
468
+ .height(panel_height, Unit::Pixel);
469
+ self.popup_scroll_box.width(100.0, Unit::Percent).height(
470
+ (panel_height - (SELECTABLE_POPUP_LIST_PANEL_PADDING * 2.0)).max(0.0),
471
+ Unit::Pixel,
472
+ );
473
+ self.popup_presenter.show_anchored(
474
+ trigger_x,
475
+ trigger_y,
476
+ trigger_width,
477
+ trigger_height,
478
+ popup_width,
479
+ panel_height,
480
+ );
481
+ }
482
+
483
+ pub(crate) fn sync_option_visuals(&self) {
484
+ self.ensure_option_nodes();
485
+ let count = (self.owner.item_count)();
486
+ let nodes = self.option_nodes.borrow();
487
+ for (index, node) in nodes.iter().take(count as usize).enumerate() {
488
+ let index = index as i32;
489
+ node.apply_theme(
490
+ index == self.highlighted_index_value.get(),
491
+ (self.owner.item_selected)(index),
492
+ (self.owner.enabled)(),
493
+ self.colors_value.get(),
494
+ );
495
+ }
496
+ }
497
+
498
+ fn rebuild_panel(&self) {
499
+ {
500
+ let nodes = self.option_nodes.borrow();
501
+ for node in nodes.iter() {
502
+ self.options_host.remove_child(&node.root());
503
+ }
504
+ }
505
+ let count = (self.owner.item_count)();
506
+ let nodes = self.option_nodes.borrow();
507
+ for index in 0..count {
508
+ let node = &nodes[index as usize];
509
+ node.label((self.owner.item_label)(index));
510
+ self.options_host.child(&node.root());
511
+ }
512
+ drop(nodes);
513
+ self.refresh_panel_layout();
514
+ }
515
+
516
+ fn ensure_option_nodes(&self) {
517
+ let count = (self.owner.item_count)();
518
+ while self.option_nodes.borrow().len() < count as usize {
519
+ let slot_index = self.option_nodes.borrow().len() as i32;
520
+ let node = SelectablePopupListOptionNode::new(
521
+ self.owner.clone(),
522
+ slot_index,
523
+ self.option_row_template_value.borrow().clone(),
524
+ self.sizing_value.get(),
525
+ );
526
+ self.option_nodes.borrow_mut().push(node);
527
+ }
528
+ }
529
+
530
+ fn resolve_option_row_height(&self) -> f32 {
531
+ let nodes = self.option_nodes.borrow();
532
+ if nodes.is_empty() {
533
+ if let Some(sizing) = self.sizing_value.get() {
534
+ if sizing.has_option_height() && self.option_row_template_value.borrow().is_none() {
535
+ return sizing.option_height_px();
536
+ }
537
+ }
538
+ return OPTION_HEIGHT;
539
+ }
540
+ nodes[0].row_height()
541
+ }
542
+
543
+ fn resolve_visible_item_count(&self) -> i32 {
544
+ let count = (self.owner.item_count)();
545
+ let max_visible = self.max_visible_items_value.get();
546
+ if max_visible <= 0 || count <= max_visible {
547
+ count
548
+ } else {
549
+ max_visible
550
+ }
551
+ }
552
+
553
+ fn resolve_panel_outer_height(&self) -> f32 {
554
+ self.resolve_visible_item_count() as f32 * self.resolve_option_row_height()
555
+ + (SELECTABLE_POPUP_LIST_PANEL_PADDING * 2.0)
556
+ }
557
+
558
+ fn resolve_viewport_clamped_panel_outer_height(&self) -> f32 {
559
+ let max_height = (crate::bindings::ui::get_viewport_height() - (PANEL_EDGE_PADDING * 2.0))
560
+ .max(PANEL_EDGE_PADDING);
561
+ self.resolve_panel_outer_height().min(max_height)
562
+ }
563
+
564
+ fn resolve_popup_width(&self, trigger_width: f32) -> f32 {
565
+ let width = self.popup_width_value.get();
566
+ if width > 0.0 {
567
+ width
568
+ } else {
569
+ trigger_width
570
+ }
571
+ }
572
+
573
+ fn ensure_highlighted_visible(&self) {
574
+ let highlighted_index = self.highlighted_index_value.get();
575
+ if !self.is_open() || highlighted_index < 0 {
576
+ return;
577
+ }
578
+ let visible_height = (self.resolve_viewport_clamped_panel_outer_height()
579
+ - (SELECTABLE_POPUP_LIST_PANEL_PADDING * 2.0))
580
+ .max(0.0);
581
+ if visible_height <= 0.0 {
582
+ return;
583
+ }
584
+ let row_height = self.resolve_option_row_height();
585
+ let item_top = highlighted_index as f32 * row_height;
586
+ let item_bottom = item_top + row_height;
587
+ let state = self.popup_scroll_box.scroll_state();
588
+ let mut next_offset = state.offset_y();
589
+ if item_top < next_offset {
590
+ next_offset = item_top;
591
+ } else if item_bottom > next_offset + visible_height {
592
+ next_offset = item_bottom - visible_height;
593
+ }
594
+ self.popup_scroll_box
595
+ .set_runtime_scroll_offset(0.0, next_offset);
596
+ }
597
+ }