@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,557 @@
1
+ use super::core::*;
2
+ use super::*;
3
+ use crate::animation::{animate_float, Animation, AnimationTiming};
4
+ use crate::transitions::NodeTransitions;
5
+
6
+ #[derive(Default)]
7
+ struct ScrollViewAnimationState {
8
+ scroll_offset: Option<Animation>,
9
+ }
10
+
11
+ #[derive(Clone)]
12
+ pub struct ScrollView {
13
+ core: Rc<RefCell<NodeCore>>,
14
+ props: Rc<RefCell<ScrollViewProps>>,
15
+ bound_scroll_state: Rc<RefCell<Option<ScrollState>>>,
16
+ active_animations: Rc<RefCell<ScrollViewAnimationState>>,
17
+ }
18
+
19
+ impl ScrollView {
20
+ pub fn new() -> Self {
21
+ let core = Rc::new(RefCell::new(NodeCore::new(NodeKind::ScrollView)));
22
+ core.borrow_mut().handlers.pointer_down = Some(Rc::new(|_event: &mut PointerEventArgs| {}));
23
+ core.borrow_mut().scroll_routing = Some(ScrollRoutingState {
24
+ enabled_x: true,
25
+ enabled_y: true,
26
+ ..ScrollRoutingState::default()
27
+ });
28
+ Self {
29
+ core,
30
+ props: Rc::new(RefCell::new(ScrollViewProps {
31
+ width: None,
32
+ height: None,
33
+ bg_color: None,
34
+ padding: None,
35
+ enable_scroll_x: true,
36
+ enable_scroll_y: true,
37
+ friction: None,
38
+ smooth_scrolling: true,
39
+ scroll_offset: None,
40
+ content_size: None,
41
+ persist_scroll: true,
42
+ transitions: None,
43
+ })),
44
+ bound_scroll_state: Rc::new(RefCell::new(None)),
45
+ active_animations: Rc::new(RefCell::new(ScrollViewAnimationState::default())),
46
+ }
47
+ }
48
+
49
+ pub fn bind_scroll_state(&self, scroll_state: ScrollState) -> &Self {
50
+ *self.bound_scroll_state.borrow_mut() = Some(scroll_state.clone());
51
+ self.sync_bound_scroll_state_from_props();
52
+ let bound_scroll_state = self.bound_scroll_state.clone();
53
+ self.core.borrow_mut().handlers.scroll_changed = Some(Rc::new(
54
+ move |offset_x,
55
+ offset_y,
56
+ content_width,
57
+ content_height,
58
+ viewport_width,
59
+ viewport_height| {
60
+ if let Some(state) = bound_scroll_state.borrow().clone() {
61
+ state.set_offset_x(offset_x);
62
+ state.set_offset_y(offset_y);
63
+ state.set_content_width(content_width);
64
+ state.set_content_height(content_height);
65
+ state.set_viewport_width(viewport_width);
66
+ state.set_viewport_height(viewport_height);
67
+ }
68
+ },
69
+ ));
70
+ self
71
+ }
72
+
73
+ pub fn width(&self, width: f32, unit: Unit) -> &Self {
74
+ self.props.borrow_mut().width = Some((width, unit));
75
+ self.set_viewport_width_if_pixel(width, unit);
76
+ let mut core = self.core.borrow_mut();
77
+ core.behavior.fill_width = false;
78
+ core.behavior.fill_width_percent = None;
79
+ self
80
+ }
81
+
82
+ pub fn width_len(&self, length: Length) -> &Self {
83
+ let (width, unit) = length;
84
+ self.width(width, unit)
85
+ }
86
+
87
+ pub fn height(&self, height: f32, unit: Unit) -> &Self {
88
+ self.props.borrow_mut().height = Some((height, unit));
89
+ self.set_viewport_height_if_pixel(height, unit);
90
+ let mut core = self.core.borrow_mut();
91
+ core.behavior.fill_height = false;
92
+ core.behavior.fill_height_percent = None;
93
+ self
94
+ }
95
+
96
+ pub fn height_len(&self, length: Length) -> &Self {
97
+ let (height, unit) = length;
98
+ self.height(height, unit)
99
+ }
100
+
101
+ pub fn fill_width(&self) -> &Self {
102
+ self.props.borrow_mut().width = None;
103
+ let mut core = self.core.borrow_mut();
104
+ core.behavior.fill_width = true;
105
+ core.behavior.fill_width_percent = None;
106
+ self
107
+ }
108
+
109
+ pub fn fill_height(&self) -> &Self {
110
+ self.props.borrow_mut().height = None;
111
+ let mut core = self.core.borrow_mut();
112
+ core.behavior.fill_height = true;
113
+ core.behavior.fill_height_percent = None;
114
+ self
115
+ }
116
+
117
+ pub fn fill_size(&self) -> &Self {
118
+ self.fill_width();
119
+ self.fill_height();
120
+ self
121
+ }
122
+
123
+ pub fn bg_color(&self, color: u32) -> &Self {
124
+ self.props.borrow_mut().bg_color = Some(color);
125
+ self
126
+ }
127
+
128
+ pub fn padding(&self, left: f32, top: f32, right: f32, bottom: f32) -> &Self {
129
+ self.props.borrow_mut().padding = Some((left, top, right, bottom));
130
+ self
131
+ }
132
+
133
+ pub fn scroll_enabled(&self, enabled_x: bool, enabled_y: bool) -> &Self {
134
+ let mut props = self.props.borrow_mut();
135
+ props.enable_scroll_x = enabled_x;
136
+ props.enable_scroll_y = enabled_y;
137
+ self.retained_node_ref()
138
+ .set_scroll_routing_enabled(enabled_x, enabled_y);
139
+ self
140
+ }
141
+
142
+ pub fn scroll_enabled_x(&self, enabled: bool) -> &Self {
143
+ self.props.borrow_mut().enable_scroll_x = enabled;
144
+ let enabled_y = self
145
+ .retained_node_ref()
146
+ .scroll_routing_state()
147
+ .map(|state| state.enabled_y)
148
+ .unwrap_or(true);
149
+ self.retained_node_ref()
150
+ .set_scroll_routing_enabled(enabled, enabled_y);
151
+ self
152
+ }
153
+
154
+ pub fn scroll_enabled_y(&self, enabled: bool) -> &Self {
155
+ self.props.borrow_mut().enable_scroll_y = enabled;
156
+ let enabled_x = self
157
+ .retained_node_ref()
158
+ .scroll_routing_state()
159
+ .map(|state| state.enabled_x)
160
+ .unwrap_or(true);
161
+ self.retained_node_ref()
162
+ .set_scroll_routing_enabled(enabled_x, enabled);
163
+ self
164
+ }
165
+
166
+ pub fn scroll_friction(&self, friction: f32) -> &Self {
167
+ self.props.borrow_mut().friction = Some(friction);
168
+ self
169
+ }
170
+
171
+ pub fn smooth_scrolling(&self, smooth_scrolling: bool) -> &Self {
172
+ self.props.borrow_mut().smooth_scrolling = smooth_scrolling;
173
+ if self.has_built_handle() {
174
+ ui::set_smooth_scrolling(self.handle().raw(), smooth_scrolling);
175
+ }
176
+ self
177
+ }
178
+
179
+ pub fn scroll_offset(&self, offset_x: f32, offset_y: f32) -> &Self {
180
+ self.cancel_scroll_offset_transition();
181
+ if self.should_animate_scroll_offset(offset_x, offset_y) {
182
+ if let Some(timing) = self
183
+ .props
184
+ .borrow()
185
+ .transitions
186
+ .as_ref()
187
+ .and_then(NodeTransitions::scroll_offset_timing)
188
+ {
189
+ self.take_programmatic_scroll_ownership();
190
+ self.start_scroll_offset_animation(offset_x, offset_y, timing);
191
+ return self;
192
+ }
193
+ }
194
+ if self.has_built_handle() && self.current_scroll_offset() != (offset_x, offset_y) {
195
+ self.take_programmatic_scroll_ownership();
196
+ }
197
+ self.apply_animated_scroll_offset(offset_x, offset_y);
198
+ self
199
+ }
200
+
201
+ pub fn content_size(&self, width: f32, height: f32) -> &Self {
202
+ self.props.borrow_mut().content_size = Some((width, height));
203
+ self.set_content_size_metrics(width, height);
204
+ self
205
+ }
206
+
207
+ pub fn scroll_content_size(&self, width: f32, height: f32) -> &Self {
208
+ self.content_size(width, height)
209
+ }
210
+
211
+ pub fn persist_scroll(&self, persist: bool) -> &Self {
212
+ self.props.borrow_mut().persist_scroll = persist;
213
+ self
214
+ }
215
+
216
+ pub fn set_runtime_scroll_offset(&self, offset_x: f32, offset_y: f32) {
217
+ self.cancel_scroll_offset_transition();
218
+ self.apply_animated_scroll_offset(offset_x, offset_y);
219
+ }
220
+
221
+ pub fn scroll_to(&self, offset_x: f32, offset_y: f32) -> &Self {
222
+ self.cancel_scroll_offset_transition();
223
+ if self.has_built_handle() && self.current_scroll_offset() != (offset_x, offset_y) {
224
+ self.take_programmatic_scroll_ownership();
225
+ }
226
+ self.apply_animated_scroll_offset(offset_x, offset_y);
227
+ self
228
+ }
229
+
230
+ pub fn scroll_to_animated(
231
+ &self,
232
+ offset_x: f32,
233
+ offset_y: f32,
234
+ timing: AnimationTiming,
235
+ ) -> &Self {
236
+ self.cancel_scroll_offset_transition();
237
+ if !self.has_built_handle() || self.current_scroll_offset() == (offset_x, offset_y) {
238
+ self.apply_animated_scroll_offset(offset_x, offset_y);
239
+ return self;
240
+ }
241
+ self.take_programmatic_scroll_ownership();
242
+ self.start_scroll_offset_animation(offset_x, offset_y, timing);
243
+ self
244
+ }
245
+
246
+ pub fn transitions(&self, transitions: Option<NodeTransitions>) -> &Self {
247
+ self.props.borrow_mut().transitions = transitions;
248
+ self
249
+ }
250
+
251
+ pub fn focusable(&self, enabled: bool, tab_index: i32) -> &Self {
252
+ let mut core = self.core.borrow_mut();
253
+ core.behavior.focusable = Some((enabled, tab_index));
254
+ let interactive = core.behavior.enabled && core.behavior.inherited_enabled;
255
+ let handle = core.handle;
256
+ drop(core);
257
+ if handle != NodeHandle::INVALID {
258
+ ui::set_focusable(handle.raw(), interactive && enabled, tab_index);
259
+ self.notify_retained_mutation();
260
+ }
261
+ self
262
+ }
263
+
264
+ pub fn on_wheel(&self, handler: impl Fn(&mut WheelEventArgs) + 'static) -> &Self {
265
+ self.core.borrow_mut().handlers.wheel = Some(Rc::new(handler));
266
+ self.retained_node_ref().require_interactive();
267
+ self
268
+ }
269
+
270
+ pub fn on_pan_gesture(&self, handler: impl Fn(&mut GestureEventArgs) + 'static) -> &Self {
271
+ self.core.borrow_mut().handlers.pan_gesture = Some(Rc::new(handler));
272
+ self
273
+ }
274
+
275
+ pub fn on_pinch_gesture(&self, handler: impl Fn(&mut GestureEventArgs) + 'static) -> &Self {
276
+ self.core.borrow_mut().handlers.pinch_gesture = Some(Rc::new(handler));
277
+ self
278
+ }
279
+
280
+ pub fn long_press_options(&self, minimum_duration_ms: i32, movement_tolerance: f32) -> &Self {
281
+ let mut core = self.core.borrow_mut();
282
+ core.handlers.long_press_minimum_duration_ms = minimum_duration_ms.max(0);
283
+ core.handlers.long_press_movement_tolerance = movement_tolerance.max(0.0);
284
+ self
285
+ }
286
+
287
+ pub fn on_long_press(&self, handler: impl Fn(&mut LongPressEventArgs) + 'static) -> &Self {
288
+ self.core.borrow_mut().handlers.long_press = Some(Rc::new(handler));
289
+ self.retained_node_ref().require_interactive();
290
+ self
291
+ }
292
+
293
+ pub fn child<T: Node>(&self, child: &T) -> &Self {
294
+ self.append_child(child);
295
+ self
296
+ }
297
+
298
+ pub fn children<I, C>(&self, children: I) -> &Self
299
+ where
300
+ I: IntoIterator<Item = C>,
301
+ C: Into<Child>,
302
+ {
303
+ for child in children {
304
+ self.retained_node_ref()
305
+ .append_child_ref(&child.into().node_ref);
306
+ }
307
+ self
308
+ }
309
+
310
+ fn sync_bound_scroll_state_from_props(&self) {
311
+ let props = self.props.borrow();
312
+ let width = props.width;
313
+ let height = props.height;
314
+ let scroll_offset = props.scroll_offset;
315
+ let content_size = props.content_size;
316
+ drop(props);
317
+
318
+ if let Some((width, unit)) = width {
319
+ self.set_viewport_width_if_pixel(width, unit);
320
+ }
321
+ if let Some((height, unit)) = height {
322
+ self.set_viewport_height_if_pixel(height, unit);
323
+ }
324
+ if let Some((offset_x, offset_y)) = scroll_offset {
325
+ self.set_scroll_offset_metrics(offset_x, offset_y);
326
+ }
327
+ if let Some((width, height)) = content_size {
328
+ self.set_content_size_metrics(width, height);
329
+ }
330
+ }
331
+
332
+ fn set_viewport_width_if_pixel(&self, width: f32, unit: Unit) {
333
+ if unit != Unit::Pixel {
334
+ return;
335
+ }
336
+ if let Some(state) = self.bound_scroll_state.borrow().clone() {
337
+ state.set_viewport_width(width);
338
+ }
339
+ self.update_scroll_routing_metrics(|state| state.viewport_width = width);
340
+ }
341
+
342
+ fn set_viewport_height_if_pixel(&self, height: f32, unit: Unit) {
343
+ if unit != Unit::Pixel {
344
+ return;
345
+ }
346
+ if let Some(state) = self.bound_scroll_state.borrow().clone() {
347
+ state.set_viewport_height(height);
348
+ }
349
+ self.update_scroll_routing_metrics(|state| state.viewport_height = height);
350
+ }
351
+
352
+ fn set_content_size_metrics(&self, width: f32, height: f32) {
353
+ if let Some(state) = self.bound_scroll_state.borrow().clone() {
354
+ if width >= 0.0 {
355
+ state.set_content_width(width);
356
+ }
357
+ if height >= 0.0 {
358
+ state.set_content_height(height);
359
+ }
360
+ }
361
+ self.update_scroll_routing_metrics(|state| {
362
+ if width >= 0.0 {
363
+ state.content_width = width;
364
+ }
365
+ if height >= 0.0 {
366
+ state.content_height = height;
367
+ }
368
+ });
369
+ }
370
+
371
+ fn set_scroll_offset_metrics(&self, offset_x: f32, offset_y: f32) {
372
+ if let Some(state) = self.bound_scroll_state.borrow().clone() {
373
+ state.set_offset_x(offset_x);
374
+ state.set_offset_y(offset_y);
375
+ }
376
+ self.retained_node_ref()
377
+ .set_scroll_routing_offsets(offset_x, offset_y);
378
+ }
379
+
380
+ fn update_scroll_routing_metrics(&self, update: impl FnOnce(&mut ScrollRoutingState)) {
381
+ let node = self.retained_node_ref();
382
+ let Some(mut state) = node.scroll_routing_state() else {
383
+ return;
384
+ };
385
+ update(&mut state);
386
+ node.set_scroll_routing_metrics(
387
+ state.offset_x,
388
+ state.offset_y,
389
+ state.content_width,
390
+ state.content_height,
391
+ state.viewport_width,
392
+ state.viewport_height,
393
+ );
394
+ }
395
+
396
+ fn current_scroll_offset(&self) -> (f32, f32) {
397
+ self.props.borrow().scroll_offset.unwrap_or((0.0, 0.0))
398
+ }
399
+
400
+ fn apply_animated_scroll_offset(&self, offset_x: f32, offset_y: f32) {
401
+ self.props.borrow_mut().scroll_offset = Some((offset_x, offset_y));
402
+ self.set_scroll_offset_metrics(offset_x, offset_y);
403
+ if self.has_built_handle() {
404
+ self.prepare_programmatic_scroll(offset_x, offset_y);
405
+ ui::set_scroll_offset(self.handle().raw(), offset_x, offset_y);
406
+ self.notify_retained_mutation();
407
+ }
408
+ }
409
+
410
+ fn should_animate_scroll_offset(&self, offset_x: f32, offset_y: f32) -> bool {
411
+ self.has_built_handle()
412
+ && self
413
+ .props
414
+ .borrow()
415
+ .transitions
416
+ .as_ref()
417
+ .and_then(NodeTransitions::scroll_offset_timing)
418
+ .is_some()
419
+ && self.current_scroll_offset() != (offset_x, offset_y)
420
+ }
421
+
422
+ fn cancel_scroll_offset_transition(&self) {
423
+ if let Some(animation) = self.active_animations.borrow_mut().scroll_offset.take() {
424
+ animation.cancel();
425
+ }
426
+ }
427
+
428
+ fn start_scroll_offset_animation(&self, offset_x: f32, offset_y: f32, timing: AnimationTiming) {
429
+ let (from_x, from_y) = self.current_scroll_offset();
430
+ let weak_core = Rc::downgrade(&self.core);
431
+ let weak_props = Rc::downgrade(&self.props);
432
+ let weak_state = Rc::downgrade(&self.bound_scroll_state);
433
+ let animation = animate_float(0.0, 1.0, timing, move |progress| {
434
+ let Some(core) = weak_core.upgrade() else {
435
+ return;
436
+ };
437
+ let Some(props) = weak_props.upgrade() else {
438
+ return;
439
+ };
440
+ let next_x = from_x + ((offset_x - from_x) * progress);
441
+ let next_y = from_y + ((offset_y - from_y) * progress);
442
+ props.borrow_mut().scroll_offset = Some((next_x, next_y));
443
+ if let Some(bound) = weak_state.upgrade().and_then(|slot| slot.borrow().clone()) {
444
+ bound.set_offset_x(next_x);
445
+ bound.set_offset_y(next_y);
446
+ }
447
+ let node = NodeRef::from_core(core);
448
+ node.set_scroll_routing_offsets(next_x, next_y);
449
+ if node.handle() != NodeHandle::INVALID {
450
+ // FUI-AS marks every programmatic set as a pending native-scroll ack while
451
+ // ownership is taken once before starting the animation.
452
+ ui::set_scroll_offset(node.handle().raw(), next_x, next_y);
453
+ crate::frame_scheduler::mark_needs_commit();
454
+ }
455
+ });
456
+ self.active_animations.borrow_mut().scroll_offset = Some(animation);
457
+ }
458
+
459
+ fn take_programmatic_scroll_ownership(&self) {
460
+ ui::clear_momentum_scroll();
461
+ }
462
+
463
+ fn prepare_programmatic_scroll(&self, _offset_x: f32, _offset_y: f32) {
464
+ // FUI-AS keeps a pending programmatic ack so its scroll callback can ignore the
465
+ // matching native echo. FUI-RS updates scroll metrics in the central event
466
+ // dispatcher and does not cancel transitions from that echo yet, so there is no
467
+ // local ack state to update here. Keep the call site split faithful so the ack
468
+ // state can be added without changing public behavior when scroll dispatch is
469
+ // ported to the exact FUI-AS ownership model.
470
+ }
471
+ }
472
+
473
+ #[cfg(test)]
474
+ mod tests {
475
+ use super::*;
476
+ use crate::ffi::{self, Call};
477
+
478
+ #[test]
479
+ fn scroll_to_takes_programmatic_scroll_ownership_before_setting_offset() {
480
+ ffi::test::reset();
481
+ let view = ScrollView::new();
482
+ view.build();
483
+ ffi::test::take_calls();
484
+
485
+ view.scroll_to(12.0, 34.0);
486
+
487
+ let calls = ffi::test::take_calls();
488
+ let clear_index = calls
489
+ .iter()
490
+ .position(|call| matches!(call, Call::ClearMomentumScroll))
491
+ .expect("programmatic scroll should clear momentum first");
492
+ let set_index = calls
493
+ .iter()
494
+ .position(|call| matches!(call, Call::SetScrollOffset { offset_x, offset_y, .. } if *offset_x == 12.0 && *offset_y == 34.0))
495
+ .expect("programmatic scroll should set native offset");
496
+ assert!(clear_index < set_index);
497
+ }
498
+
499
+ #[test]
500
+ fn animated_scroll_takes_programmatic_scroll_ownership_before_first_tick() {
501
+ ffi::test::reset();
502
+ crate::animation::reset_animations();
503
+ let view = ScrollView::new();
504
+ view.build();
505
+ ffi::test::take_calls();
506
+
507
+ view.scroll_to_animated(0.0, 80.0, AnimationTiming::new(100.0));
508
+
509
+ let calls = ffi::test::take_calls();
510
+ assert!(
511
+ calls
512
+ .iter()
513
+ .any(|call| matches!(call, Call::ClearMomentumScroll)),
514
+ "animated programmatic scroll should clear native momentum before ticking"
515
+ );
516
+ }
517
+
518
+ #[test]
519
+ fn smooth_wheel_scrolling_defaults_on_and_supports_retained_opt_out() {
520
+ ffi::test::reset();
521
+ let view = ScrollView::new();
522
+ view.build();
523
+
524
+ let calls = ffi::test::take_calls();
525
+ assert!(calls.iter().any(|call| matches!(
526
+ call,
527
+ Call::SetSmoothScrolling {
528
+ smooth_scrolling: true,
529
+ ..
530
+ }
531
+ )));
532
+
533
+ view.smooth_scrolling(false);
534
+ let calls = ffi::test::take_calls();
535
+ assert!(calls.iter().any(|call| matches!(
536
+ call,
537
+ Call::SetSmoothScrolling {
538
+ smooth_scrolling: false,
539
+ ..
540
+ }
541
+ )));
542
+ }
543
+ }
544
+
545
+ impl Node for ScrollView {
546
+ fn retained_node_ref(&self) -> NodeRef {
547
+ NodeRef::from_node(self.core.clone(), self.clone())
548
+ }
549
+
550
+ fn build_self(&self) {
551
+ apply_scroll_view_props(
552
+ self.handle(),
553
+ &self.props.borrow(),
554
+ self.core.borrow().behavior.clone(),
555
+ );
556
+ }
557
+ }