@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,678 @@
1
+ use super::*;
2
+ use crate::bindings::ui;
3
+ use crate::controls::selection_area;
4
+ use crate::controls::SelectionArea;
5
+ use crate::frame_scheduler::mark_needs_commit;
6
+ use crate::signal::Subscription;
7
+ use std::cell::{Cell, RefCell};
8
+ use std::rc::Rc;
9
+
10
+ const FULL_SIZE: f32 = 100.0;
11
+ const DEFAULT_MAX_VISIBLE_ITEMS: i32 = 20;
12
+ const POOL_OVERSCAN_ITEMS: i32 = 2;
13
+ const MISSING_BIND_ITEM_MESSAGE: &str =
14
+ "VirtualList: item renderer not configured. Call .onBindItem() after construction.";
15
+
16
+ type VirtualListBinder = Rc<dyn Fn(&FlexBox, i32)>;
17
+
18
+ struct VirtualListInner {
19
+ root: FlexBox,
20
+ total_items: Cell<i32>,
21
+ item_height: f32,
22
+ bind_item: RefCell<VirtualListBinder>,
23
+ scroll_state: ScrollState,
24
+ scroll_box: ScrollBox,
25
+ top_spacer: FlexBox,
26
+ bottom_spacer: FlexBox,
27
+ pool_size: i32,
28
+ pool_rows: Vec<SelectionArea>,
29
+ pool_containers: Vec<FlexBox>,
30
+ pool_item_index_by_row: RefCell<Vec<i32>>,
31
+ subscriptions: RefCell<Vec<Subscription>>,
32
+ current_first_visible_index: Cell<i32>,
33
+ current_last_visible_index: Cell<i32>,
34
+ }
35
+
36
+ #[derive(Clone)]
37
+ pub struct VirtualList {
38
+ inner: Rc<VirtualListInner>,
39
+ }
40
+
41
+ impl VirtualList {
42
+ pub fn new(total_items: i32, item_height: f32) -> Self {
43
+ Self::with_max_visible(total_items, item_height, DEFAULT_MAX_VISIBLE_ITEMS)
44
+ }
45
+
46
+ pub fn with_max_visible(total_items: i32, item_height: f32, max_visible: i32) -> Self {
47
+ let total_items = total_items.max(0);
48
+ let item_height = if item_height > 0.0 { item_height } else { 1.0 };
49
+ let pool_size = if max_visible > 0 {
50
+ max_visible + POOL_OVERSCAN_ITEMS
51
+ } else {
52
+ POOL_OVERSCAN_ITEMS
53
+ };
54
+ let scroll_state = ScrollState::new();
55
+ let top_spacer = flex_box();
56
+ top_spacer
57
+ .width(FULL_SIZE, Unit::Percent)
58
+ .height(0.0, Unit::Pixel);
59
+ let bottom_spacer = flex_box();
60
+ bottom_spacer
61
+ .width(FULL_SIZE, Unit::Percent)
62
+ .height(0.0, Unit::Pixel);
63
+ let content = column();
64
+ content.width(FULL_SIZE, Unit::Percent).child(&top_spacer);
65
+
66
+ let mut pool_rows = Vec::new();
67
+ let mut pool_containers = Vec::new();
68
+ let mut pool_item_index_by_row = Vec::new();
69
+ for _ in 0..pool_size {
70
+ let container = flex_box();
71
+ container.fill_size();
72
+ let row_area = selection_area();
73
+ row_area
74
+ .width(FULL_SIZE, Unit::Percent)
75
+ .height(0.0, Unit::Pixel)
76
+ .child(&container);
77
+ content.child(&row_area);
78
+ pool_rows.push(row_area);
79
+ pool_containers.push(container);
80
+ pool_item_index_by_row.push(-1);
81
+ }
82
+ content.child(&bottom_spacer);
83
+
84
+ let scroll_box = ScrollBox::with_parts(scroll_state.clone(), ScrollView::new());
85
+ scroll_box
86
+ .scroll_enabled_x(false)
87
+ .scroll_enabled_y(true)
88
+ .scroll_offset(scroll_state.offset_x(), scroll_state.offset_y())
89
+ .scroll_content_size(-1.0, total_items as f32 * item_height)
90
+ .fill_size()
91
+ .child(&content);
92
+ scroll_state.set_content_height(total_items as f32 * item_height);
93
+
94
+ let root = column();
95
+ root.fill_size().child(&scroll_box);
96
+
97
+ let inner = Rc::new(VirtualListInner {
98
+ root,
99
+ total_items: Cell::new(total_items),
100
+ item_height,
101
+ bind_item: RefCell::new(Rc::new(|_, _| panic!("{MISSING_BIND_ITEM_MESSAGE}"))),
102
+ scroll_state,
103
+ scroll_box,
104
+ top_spacer,
105
+ bottom_spacer,
106
+ pool_size,
107
+ pool_rows,
108
+ pool_containers,
109
+ pool_item_index_by_row: RefCell::new(pool_item_index_by_row),
110
+ subscriptions: RefCell::new(Vec::new()),
111
+ current_first_visible_index: Cell::new(-1),
112
+ current_last_visible_index: Cell::new(-1),
113
+ });
114
+ let list = Self { inner };
115
+ list.attach_listeners();
116
+ list
117
+ }
118
+
119
+ pub fn scroll_state(&self) -> ScrollState {
120
+ self.inner.scroll_state.clone()
121
+ }
122
+
123
+ pub fn total_items(&self) -> i32 {
124
+ self.inner.total_items.get()
125
+ }
126
+
127
+ pub fn scroll_box(&self) -> ScrollBox {
128
+ self.inner.scroll_box.clone()
129
+ }
130
+
131
+ pub fn item_height(&self) -> f32 {
132
+ self.inner.item_height
133
+ }
134
+
135
+ pub fn total_content_height(&self) -> f32 {
136
+ self.total_items() as f32 * self.item_height()
137
+ }
138
+
139
+ pub fn first_visible_index(&self) -> i32 {
140
+ let current = self.inner.current_first_visible_index.get();
141
+ if current >= 0 {
142
+ current
143
+ } else {
144
+ 0
145
+ }
146
+ }
147
+
148
+ pub fn rendered_item_count(&self) -> i32 {
149
+ let first = self.inner.current_first_visible_index.get();
150
+ let last = self.inner.current_last_visible_index.get();
151
+ if first < 0 || last < first {
152
+ 0
153
+ } else {
154
+ last - first
155
+ }
156
+ }
157
+
158
+ pub fn node_id(&self, id: impl Into<String>) -> &Self {
159
+ self.inner.scroll_box.node_id(id);
160
+ self
161
+ }
162
+
163
+ pub fn persist_scroll(&self, flag: bool) -> &Self {
164
+ self.inner.scroll_box.persist_scroll(flag);
165
+ self
166
+ }
167
+
168
+ pub fn width(&self, value: f32, unit: Unit) -> &Self {
169
+ self.inner.root.width(value, unit);
170
+ if unit == Unit::Pixel {
171
+ self.inner.scroll_state.set_viewport_width(value);
172
+ }
173
+ self
174
+ }
175
+
176
+ pub fn height(&self, value: f32, unit: Unit) -> &Self {
177
+ self.inner.root.height(value, unit);
178
+ if unit == Unit::Pixel {
179
+ self.inner.scroll_state.set_viewport_height(value);
180
+ }
181
+ self
182
+ }
183
+
184
+ pub fn on_bind_item(&self, renderer: impl Fn(&FlexBox, i32) + 'static) -> &Self {
185
+ *self.inner.bind_item.borrow_mut() = Rc::new(renderer);
186
+ self.inner.current_first_visible_index.set(-1);
187
+ self.inner.current_last_visible_index.set(-1);
188
+ self.rebuild_visible_range(true);
189
+ self
190
+ }
191
+
192
+ #[allow(non_snake_case)]
193
+ pub fn onBindItem(&self, renderer: impl Fn(&FlexBox, i32) + 'static) -> &Self {
194
+ self.on_bind_item(renderer)
195
+ }
196
+
197
+ pub fn update_item_count(&self, next: i32) {
198
+ self.inner.total_items.set(next.max(0));
199
+ self.inner
200
+ .scroll_state
201
+ .set_content_height(self.total_content_height());
202
+ self.inner
203
+ .scroll_box
204
+ .scroll_content_size(-1.0, self.total_content_height());
205
+ self.inner.current_first_visible_index.set(-1);
206
+ self.inner.current_last_visible_index.set(-1);
207
+
208
+ let clamped_offset = self.max_offset_for_current_viewport();
209
+ if self.inner.scroll_state.offset_y() > clamped_offset {
210
+ self.inner
211
+ .scroll_box
212
+ .scroll_offset(self.inner.scroll_state.offset_x(), clamped_offset);
213
+ return;
214
+ }
215
+ self.rebuild_visible_range(true);
216
+ }
217
+
218
+ fn attach_listeners(&self) {
219
+ let weak = Rc::downgrade(&self.inner);
220
+ self.inner
221
+ .subscriptions
222
+ .borrow_mut()
223
+ .push(self.inner.scroll_state.subscribe_offset_y(move || {
224
+ if let Some(inner) = weak.upgrade() {
225
+ VirtualList { inner }.handle_scroll_offset_changed();
226
+ }
227
+ }));
228
+ let weak = Rc::downgrade(&self.inner);
229
+ self.inner.subscriptions.borrow_mut().push(
230
+ self.inner.scroll_state.subscribe_viewport_height(move || {
231
+ if let Some(inner) = weak.upgrade() {
232
+ VirtualList { inner }.handle_metrics_changed();
233
+ }
234
+ }),
235
+ );
236
+ let weak = Rc::downgrade(&self.inner);
237
+ self.inner.subscriptions.borrow_mut().push(
238
+ self.inner.scroll_state.subscribe_content_height(move || {
239
+ if let Some(inner) = weak.upgrade() {
240
+ VirtualList { inner }.handle_metrics_changed();
241
+ }
242
+ }),
243
+ );
244
+ }
245
+
246
+ fn handle_metrics_changed(&self) {
247
+ self.rebuild_visible_range(true);
248
+ }
249
+
250
+ fn handle_scroll_offset_changed(&self) {
251
+ self.rebuild_visible_range(true);
252
+ }
253
+
254
+ fn rebuild_visible_range(&self, commit: bool) {
255
+ let total_items = self.total_items();
256
+ if total_items <= 0 {
257
+ if self.inner.current_first_visible_index.get() == 0
258
+ && self.inner.current_last_visible_index.get() == 0
259
+ {
260
+ return;
261
+ }
262
+ self.inner.current_first_visible_index.set(0);
263
+ self.inner.current_last_visible_index.set(0);
264
+ self.inner.top_spacer.height(0.0, Unit::Pixel);
265
+ self.inner.bottom_spacer.height(0.0, Unit::Pixel);
266
+ for pool_index in 0..self.inner.pool_size as usize {
267
+ self.hide_pool_item(pool_index);
268
+ }
269
+ self.commit_if_built(commit);
270
+ return;
271
+ }
272
+
273
+ let mut first_visible_index =
274
+ (self.inner.scroll_state.offset_y() / self.inner.item_height).floor() as i32;
275
+ if first_visible_index < 0 {
276
+ first_visible_index = 0;
277
+ }
278
+ if first_visible_index > total_items {
279
+ first_visible_index = total_items;
280
+ }
281
+
282
+ let viewport_height = if self.inner.scroll_state.viewport_height() > 0.0 {
283
+ self.inner.scroll_state.viewport_height()
284
+ } else {
285
+ self.inner.item_height
286
+ };
287
+ let mut visible_count = (viewport_height / self.inner.item_height).ceil() as i32 + 1;
288
+ if visible_count < 1 {
289
+ visible_count = 1;
290
+ }
291
+ if visible_count > self.inner.pool_size {
292
+ visible_count = self.inner.pool_size;
293
+ }
294
+
295
+ let mut last_visible_index = first_visible_index + visible_count;
296
+ if last_visible_index > total_items {
297
+ last_visible_index = total_items;
298
+ }
299
+
300
+ if first_visible_index == self.inner.current_first_visible_index.get()
301
+ && last_visible_index == self.inner.current_last_visible_index.get()
302
+ {
303
+ return;
304
+ }
305
+
306
+ self.inner
307
+ .current_first_visible_index
308
+ .set(first_visible_index);
309
+ self.inner
310
+ .current_last_visible_index
311
+ .set(last_visible_index);
312
+
313
+ let top_spacer_height = first_visible_index as f32 * self.inner.item_height;
314
+ let mut bottom_spacer_height =
315
+ self.total_content_height() - (last_visible_index as f32 * self.inner.item_height);
316
+ if bottom_spacer_height < 0.0 {
317
+ bottom_spacer_height = 0.0;
318
+ }
319
+
320
+ self.inner.top_spacer.height(top_spacer_height, Unit::Pixel);
321
+ self.inner
322
+ .bottom_spacer
323
+ .height(bottom_spacer_height, Unit::Pixel);
324
+
325
+ let previous_item_index_by_row = self.inner.pool_item_index_by_row.borrow().clone();
326
+ let visible_items = (last_visible_index - first_visible_index) as usize;
327
+
328
+ for pool_index in 0..self.inner.pool_size as usize {
329
+ let previous_item_index = previous_item_index_by_row[pool_index];
330
+ if previous_item_index != -1
331
+ && (previous_item_index < first_visible_index
332
+ || previous_item_index >= last_visible_index)
333
+ {
334
+ self.clear_row_selection(pool_index);
335
+ }
336
+ }
337
+
338
+ for pool_index in 0..self.inner.pool_size as usize {
339
+ let row_area = &self.inner.pool_rows[pool_index];
340
+ if pool_index < visible_items {
341
+ let next_item_index = first_visible_index + pool_index as i32;
342
+ row_area.height(self.inner.item_height, Unit::Pixel);
343
+ let container = &self.inner.pool_containers[pool_index];
344
+ self.render_item(container, next_item_index);
345
+ self.inner.pool_item_index_by_row.borrow_mut()[pool_index] = next_item_index;
346
+ } else {
347
+ self.hide_pool_item(pool_index);
348
+ }
349
+ }
350
+
351
+ for pool_index in 0..visible_items {
352
+ let next_item_index = first_visible_index + pool_index as i32;
353
+ let previous_pool_index =
354
+ Self::find_pool_index_for_item(&previous_item_index_by_row, next_item_index);
355
+ if previous_pool_index != -1 && previous_pool_index as usize != pool_index {
356
+ self.retarget_row_selection(previous_pool_index as usize, pool_index);
357
+ }
358
+ }
359
+
360
+ self.commit_if_built(commit);
361
+ }
362
+
363
+ fn hide_pool_item(&self, pool_index: usize) {
364
+ if self.inner.pool_item_index_by_row.borrow()[pool_index] != -1 {
365
+ self.clear_row_selection(pool_index);
366
+ self.inner.pool_item_index_by_row.borrow_mut()[pool_index] = -1;
367
+ }
368
+ self.inner.pool_rows[pool_index].height(0.0, Unit::Pixel);
369
+ self.clear_item_node(&self.inner.pool_containers[pool_index].retained_node_ref());
370
+ }
371
+
372
+ fn clear_row_selection(&self, pool_index: usize) {
373
+ if !self.has_built_handle() {
374
+ return;
375
+ }
376
+ self.clear_selection_node(&self.inner.pool_rows[pool_index].retained_node_ref());
377
+ }
378
+
379
+ fn retarget_row_selection(&self, from_pool_index: usize, to_pool_index: usize) {
380
+ if !self.has_built_handle() {
381
+ return;
382
+ }
383
+ let mut from_texts = Vec::new();
384
+ let mut to_texts = Vec::new();
385
+ self.collect_text_nodes(
386
+ &self.inner.pool_rows[from_pool_index].retained_node_ref(),
387
+ &mut from_texts,
388
+ );
389
+ self.collect_text_nodes(
390
+ &self.inner.pool_rows[to_pool_index].retained_node_ref(),
391
+ &mut to_texts,
392
+ );
393
+ let pair_count = from_texts.len().min(to_texts.len());
394
+ for index in 0..pair_count {
395
+ let from_text = from_texts[index];
396
+ let to_text = to_texts[index];
397
+ if from_text != NodeHandle::INVALID
398
+ && to_text != NodeHandle::INVALID
399
+ && from_text != to_text
400
+ {
401
+ ui::retarget_selection(from_text.raw(), to_text.raw());
402
+ }
403
+ }
404
+ }
405
+
406
+ fn collect_text_nodes(&self, node: &NodeRef, out: &mut Vec<NodeHandle>) {
407
+ if node.text_content_for_routing().is_some() {
408
+ out.push(node.handle());
409
+ return;
410
+ }
411
+ for child in node.children() {
412
+ self.collect_text_nodes(&child, out);
413
+ }
414
+ }
415
+
416
+ fn clear_selection_node(&self, node: &NodeRef) {
417
+ if node.text_content_for_routing().is_some() {
418
+ let handle = node.handle();
419
+ if handle != NodeHandle::INVALID {
420
+ ui::clear_selection(handle.raw());
421
+ }
422
+ return;
423
+ }
424
+ for child in node.children() {
425
+ self.clear_selection_node(&child);
426
+ }
427
+ }
428
+
429
+ fn clear_item_node(&self, node: &NodeRef) {
430
+ node.set_semantic_label_for_routing(Some(String::new()));
431
+ let handle = node.handle();
432
+ if handle != NodeHandle::INVALID {
433
+ ui::set_semantic_label(handle.raw(), "");
434
+ }
435
+ if node.text_content_for_routing().is_some() {
436
+ node.set_text_content_for_routing(Some(String::new()));
437
+ if handle != NodeHandle::INVALID {
438
+ ui::set_text(handle.raw(), "");
439
+ }
440
+ }
441
+ for child in node.children() {
442
+ self.clear_item_node(&child);
443
+ }
444
+ }
445
+
446
+ fn commit_if_built(&self, commit: bool) {
447
+ if !commit || !self.has_built_handle() {
448
+ return;
449
+ }
450
+ mark_needs_commit();
451
+ }
452
+
453
+ fn render_item(&self, container: &FlexBox, index: i32) {
454
+ let binder = self.inner.bind_item.borrow().clone();
455
+ binder(container, index);
456
+ }
457
+
458
+ fn max_offset_for_current_viewport(&self) -> f32 {
459
+ let viewport_height = if self.inner.scroll_state.viewport_height() > 0.0 {
460
+ self.inner.scroll_state.viewport_height()
461
+ } else {
462
+ 0.0
463
+ };
464
+ let max_offset = self.total_content_height() - viewport_height;
465
+ if max_offset > 0.0 {
466
+ max_offset
467
+ } else {
468
+ 0.0
469
+ }
470
+ }
471
+
472
+ fn find_pool_index_for_item(previous_item_index_by_row: &[i32], item_index: i32) -> i32 {
473
+ previous_item_index_by_row
474
+ .iter()
475
+ .position(|candidate| *candidate == item_index)
476
+ .map(|index| index as i32)
477
+ .unwrap_or(-1)
478
+ }
479
+ }
480
+
481
+ impl Node for VirtualList {
482
+ fn retained_node_ref(&self) -> NodeRef {
483
+ let list = self.clone();
484
+ self.inner
485
+ .root
486
+ .retained_node_ref()
487
+ .with_build_callback(move || list.build())
488
+ }
489
+
490
+ fn build(&self) {
491
+ self.ensure_handle();
492
+ self.inner.root.build_self();
493
+ self.build_children();
494
+ if self.inner.current_first_visible_index.get() < 0 && self.total_items() > 0 {
495
+ self.rebuild_visible_range(false);
496
+ }
497
+ ui::set_selection_area_barrier(self.handle().raw(), true);
498
+ }
499
+
500
+ fn build_self(&self) {
501
+ self.inner.root.build_self();
502
+ }
503
+
504
+ fn dispose(&self) {
505
+ self.inner.subscriptions.borrow_mut().clear();
506
+ self.inner.root.dispose();
507
+ }
508
+ }
509
+
510
+ impl HasFlexBoxRoot for VirtualList {
511
+ fn flex_box_root(&self) -> &FlexBox {
512
+ &self.inner.root
513
+ }
514
+ }
515
+
516
+ #[cfg(test)]
517
+ mod tests {
518
+ use super::*;
519
+ use crate::ffi::{self, Call};
520
+ use crate::prelude::*;
521
+ use std::collections::HashMap;
522
+
523
+ fn tracked_bind_virtual_list_item(
524
+ rendered_indices: &Rc<RefCell<Vec<i32>>>,
525
+ labels: &Rc<RefCell<HashMap<usize, TextNode>>>,
526
+ container: &FlexBox,
527
+ index: i32,
528
+ ) {
529
+ rendered_indices.borrow_mut().push(index);
530
+ let key = std::ptr::from_ref(container) as usize;
531
+ let existing = { labels.borrow().get(&key).cloned() };
532
+ let label = if let Some(existing) = existing {
533
+ existing
534
+ } else {
535
+ let label = text("");
536
+ container.child(&label);
537
+ labels.borrow_mut().insert(key, label.clone());
538
+ label
539
+ };
540
+ let text_value = format!("Item {}", index);
541
+ label.text(&text_value);
542
+ label.semantic_label(&text_value);
543
+ }
544
+
545
+ fn static_bind_virtual_list_item(
546
+ labels: &Rc<RefCell<HashMap<usize, TextNode>>>,
547
+ container: &FlexBox,
548
+ index: i32,
549
+ ) {
550
+ let key = std::ptr::from_ref(container) as usize;
551
+ let existing = { labels.borrow().get(&key).cloned() };
552
+ let label = if let Some(existing) = existing {
553
+ existing
554
+ } else {
555
+ let label = text("");
556
+ container.child(&label);
557
+ labels.borrow_mut().insert(key, label.clone());
558
+ label
559
+ };
560
+ label.text(format!("row {}", index));
561
+ }
562
+
563
+ #[test]
564
+ fn binds_only_the_visible_window_for_a_fixed_height_list() {
565
+ ffi::test::reset();
566
+ let rendered_indices = Rc::new(RefCell::new(Vec::new()));
567
+ let labels = Rc::new(RefCell::new(HashMap::new()));
568
+ let list = VirtualList::new(10_000, 20.0);
569
+ let captured_indices = rendered_indices.clone();
570
+ let captured_labels = labels.clone();
571
+ list.onBindItem(move |container, index| {
572
+ tracked_bind_virtual_list_item(&captured_indices, &captured_labels, container, index);
573
+ });
574
+ list.width(180.0, Unit::Pixel);
575
+ list.height(100.0, Unit::Pixel);
576
+
577
+ list.build();
578
+
579
+ let rendered = rendered_indices.borrow();
580
+ let last_window_start = rendered.len() - 6;
581
+ assert_eq!(rendered[last_window_start], 0);
582
+ assert_eq!(rendered[last_window_start + 5], 5);
583
+ assert_eq!(list.first_visible_index(), 0);
584
+ assert_eq!(list.rendered_item_count(), 6);
585
+ list.dispose();
586
+ }
587
+
588
+ #[test]
589
+ fn rebinds_pooled_rows_without_recreating_nodes_when_scroll_offset_changes() {
590
+ ffi::test::reset();
591
+ let rendered_indices = Rc::new(RefCell::new(Vec::new()));
592
+ let labels = Rc::new(RefCell::new(HashMap::new()));
593
+ let list = VirtualList::new(10_000, 20.0);
594
+ let captured_indices = rendered_indices.clone();
595
+ let captured_labels = labels.clone();
596
+ list.onBindItem(move |container, index| {
597
+ tracked_bind_virtual_list_item(&captured_indices, &captured_labels, container, index);
598
+ });
599
+ list.width(180.0, Unit::Pixel);
600
+ list.height(100.0, Unit::Pixel);
601
+ Application::mount(list.clone());
602
+
603
+ rendered_indices.borrow_mut().clear();
604
+ ffi::test::reset();
605
+ list.scroll_state().set_offset_y(60.0);
606
+
607
+ let rendered = rendered_indices.borrow();
608
+ assert_eq!(rendered.len(), 6);
609
+ assert_eq!(rendered[0], 3);
610
+ assert_eq!(rendered[5], 8);
611
+ assert_eq!(list.first_visible_index(), 3);
612
+ let calls = ffi::test::take_calls();
613
+ assert!(!calls
614
+ .iter()
615
+ .any(|call| matches!(call, Call::CreateNode { .. })));
616
+ assert!(!calls
617
+ .iter()
618
+ .any(|call| matches!(call, Call::DeleteNode { .. })));
619
+ assert!(calls
620
+ .iter()
621
+ .any(|call| matches!(call, Call::SetText { text, .. } if text == "Item 3")));
622
+ assert!(calls.iter().any(|call| {
623
+ matches!(
624
+ call,
625
+ Call::SetHeight {
626
+ value,
627
+ unit_enum,
628
+ ..
629
+ } if *value == 20.0 && *unit_enum == Unit::Pixel as u32
630
+ )
631
+ }));
632
+ Application::unmount();
633
+ }
634
+
635
+ #[test]
636
+ fn tracks_content_height_and_clamps_the_visible_window_when_item_count_changes() {
637
+ ffi::test::reset();
638
+ let labels = Rc::new(RefCell::new(HashMap::new()));
639
+ let list = VirtualList::new(10_000, 24.0);
640
+ let captured_labels = labels.clone();
641
+ list.onBindItem(move |container, index| {
642
+ static_bind_virtual_list_item(&captured_labels, container, index);
643
+ });
644
+ list.height(120.0, Unit::Pixel);
645
+
646
+ assert_eq!(list.scroll_state().content_height(), 240_000.0);
647
+ assert_eq!(list.rendered_item_count(), 6);
648
+
649
+ list.update_item_count(5);
650
+
651
+ assert_eq!(list.scroll_state().content_height(), 120.0);
652
+ assert_eq!(list.rendered_item_count(), 5);
653
+ list.dispose();
654
+ }
655
+
656
+ #[test]
657
+ fn supports_owner_captured_renderers_with_on_bind_item() {
658
+ ffi::test::reset();
659
+ let rendered_indices = Rc::new(RefCell::new(Vec::new()));
660
+ let labels = Rc::new(RefCell::new(HashMap::new()));
661
+ let list = VirtualList::new(10_000, 20.0);
662
+ let captured_indices = rendered_indices.clone();
663
+ let captured_labels = labels.clone();
664
+ list.onBindItem(move |container, index| {
665
+ tracked_bind_virtual_list_item(&captured_indices, &captured_labels, container, index);
666
+ });
667
+ list.width(180.0, Unit::Pixel);
668
+ list.height(100.0, Unit::Pixel);
669
+
670
+ list.build();
671
+
672
+ let rendered = rendered_indices.borrow();
673
+ assert!(rendered.len() >= 6);
674
+ assert_eq!(rendered[0], 0);
675
+ assert_eq!(rendered[rendered.len() - 1], 5);
676
+ list.dispose();
677
+ }
678
+ }
@@ -0,0 +1,36 @@
1
+ #[cfg(all(debug_assertions, target_arch = "wasm32"))]
2
+ use crate::logger;
3
+ #[cfg(all(debug_assertions, target_arch = "wasm32"))]
4
+ use std::panic;
5
+ #[cfg(all(debug_assertions, target_arch = "wasm32"))]
6
+ use std::sync::Once;
7
+
8
+ #[cfg(all(debug_assertions, target_arch = "wasm32"))]
9
+ static INSTALL_PANIC_HOOK: Once = Once::new();
10
+
11
+ #[cfg(all(debug_assertions, target_arch = "wasm32"))]
12
+ pub(crate) fn install() {
13
+ INSTALL_PANIC_HOOK.call_once(|| {
14
+ panic::set_hook(Box::new(|info| {
15
+ let mut message = String::from("Rust panic");
16
+ if let Some(location) = info.location() {
17
+ message.push_str(" at ");
18
+ message.push_str(location.file());
19
+ message.push(':');
20
+ message.push_str(&location.line().to_string());
21
+ }
22
+
23
+ if let Some(payload) = info.payload().downcast_ref::<&str>() {
24
+ message.push_str(": ");
25
+ message.push_str(payload);
26
+ } else if let Some(payload) = info.payload().downcast_ref::<String>() {
27
+ message.push_str(": ");
28
+ message.push_str(payload);
29
+ }
30
+ logger::error("Panic", &message);
31
+ }));
32
+ });
33
+ }
34
+
35
+ #[cfg(any(not(debug_assertions), not(target_arch = "wasm32")))]
36
+ pub(crate) fn install() {}