1use super::core::*;
2use super::*;
3use crate::animation::{animate_float, Animation, AnimationTiming};
4use crate::transitions::NodeTransitions;
5
6#[derive(Default)]
7struct ScrollViewAnimationState {
8 scroll_offset: Option<Animation>,
9}
10
11#[derive(Clone)]
12pub 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
19impl Default for ScrollView {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl ScrollView {
26 pub fn new() -> Self {
27 let core = Rc::new(RefCell::new(NodeCore::new(NodeKind::ScrollView)));
28 core.borrow_mut().handlers.pointer_down = Some(Rc::new(|_event: &mut PointerEventArgs| {}));
29 core.borrow_mut().scroll_routing = Some(ScrollRoutingState {
30 enabled_x: true,
31 enabled_y: true,
32 ..ScrollRoutingState::default()
33 });
34 Self {
35 core,
36 props: Rc::new(RefCell::new(ScrollViewProps {
37 width: None,
38 height: None,
39 enable_scroll_x: true,
40 enable_scroll_y: true,
41 friction: None,
42 smooth_scrolling: true,
43 scroll_offset: None,
44 content_size: None,
45 persist_scroll: true,
46 transitions: None,
47 })),
48 bound_scroll_state: Rc::new(RefCell::new(Some(ScrollState::new()))),
49 active_animations: Rc::new(RefCell::new(ScrollViewAnimationState::default())),
50 }
51 }
52
53 pub fn bind_scroll_state(&self, scroll_state: ScrollState) -> &Self {
54 *self.bound_scroll_state.borrow_mut() = Some(scroll_state.clone());
55 self.sync_bound_scroll_state_from_props();
56 let bound_scroll_state = self.bound_scroll_state.clone();
57 self.core.borrow_mut().handlers.scroll_changed = Some(Rc::new(
58 move |offset_x,
59 offset_y,
60 content_width,
61 content_height,
62 viewport_width,
63 viewport_height| {
64 if let Some(state) = bound_scroll_state.borrow().clone() {
65 state.set_offset_x(offset_x);
66 state.set_offset_y(offset_y);
67 state.set_content_width(content_width);
68 state.set_content_height(content_height);
69 state.set_viewport_width(viewport_width);
70 state.set_viewport_height(viewport_height);
71 }
72 },
73 ));
74 self
75 }
76
77 pub fn scroll_state(&self) -> ScrollState {
78 self.bound_scroll_state
79 .borrow()
80 .clone()
81 .expect("ScrollView always owns a scroll state")
82 }
83
84 pub fn is_vertical_scroll_enabled(&self) -> bool {
85 self.props.borrow().enable_scroll_y
86 }
87
88 pub fn is_horizontal_scroll_enabled(&self) -> bool {
89 self.props.borrow().enable_scroll_x
90 }
91
92 pub fn width(&self, width: f32, unit: Unit) -> &Self {
93 self.props.borrow_mut().width = Some((width, unit));
94 self.set_viewport_width_if_pixel(width, unit);
95 let mut core = self.core.borrow_mut();
96 core.behavior.fill_width = false;
97 core.behavior.fill_width_percent = None;
98 drop(core);
99 if self.has_built_handle() {
100 ui::set_width(self.handle().raw(), width, unit as u32);
101 self.notify_retained_layout_mutation();
102 }
103 self
104 }
105
106 pub fn width_len(&self, length: Length) -> &Self {
107 let (width, unit) = length;
108 self.width(width, unit)
109 }
110
111 pub fn height(&self, height: f32, unit: Unit) -> &Self {
112 self.props.borrow_mut().height = Some((height, unit));
113 self.set_viewport_height_if_pixel(height, unit);
114 let mut core = self.core.borrow_mut();
115 core.behavior.fill_height = false;
116 core.behavior.fill_height_percent = None;
117 drop(core);
118 if self.has_built_handle() {
119 ui::set_height(self.handle().raw(), height, unit as u32);
120 self.notify_retained_layout_mutation();
121 }
122 self
123 }
124
125 pub fn height_len(&self, length: Length) -> &Self {
126 let (height, unit) = length;
127 self.height(height, unit)
128 }
129
130 pub fn fill_width(&self) -> &Self {
131 self.props.borrow_mut().width = None;
132 let mut core = self.core.borrow_mut();
133 core.behavior.fill_width = true;
134 core.behavior.fill_width_percent = None;
135 drop(core);
136 if self.has_built_handle() {
137 ui::set_fill_width(self.handle().raw(), true);
138 self.notify_retained_layout_mutation();
139 }
140 self
141 }
142
143 pub fn fill_height(&self) -> &Self {
144 self.props.borrow_mut().height = None;
145 let mut core = self.core.borrow_mut();
146 core.behavior.fill_height = true;
147 core.behavior.fill_height_percent = None;
148 drop(core);
149 if self.has_built_handle() {
150 ui::set_fill_height(self.handle().raw(), true);
151 self.notify_retained_layout_mutation();
152 }
153 self
154 }
155
156 pub fn fill_size(&self) -> &Self {
157 self.fill_width();
158 self.fill_height();
159 self
160 }
161
162 pub fn fill_width_percent(&self, percent: f32) -> &Self {
163 self.props.borrow_mut().width = None;
164 let mut core = self.core.borrow_mut();
165 core.behavior.fill_width = false;
166 core.behavior.fill_width_percent = Some(percent);
167 drop(core);
168 if self.has_built_handle() {
169 ui::set_fill_width_percent(self.handle().raw(), percent);
170 self.notify_retained_layout_mutation();
171 }
172 self
173 }
174
175 pub fn fill_height_percent(&self, percent: f32) -> &Self {
176 self.props.borrow_mut().height = None;
177 let mut core = self.core.borrow_mut();
178 core.behavior.fill_height = false;
179 core.behavior.fill_height_percent = Some(percent);
180 drop(core);
181 if self.has_built_handle() {
182 ui::set_fill_height_percent(self.handle().raw(), percent);
183 self.notify_retained_layout_mutation();
184 }
185 self
186 }
187
188 pub fn min_width(&self, value: f32, unit: Unit) -> &Self {
189 self.core.borrow_mut().behavior.min_width = Some((value, unit));
190 if self.has_built_handle() {
191 ui::set_min_width(self.handle().raw(), value, unit as u32);
192 self.notify_retained_layout_mutation();
193 }
194 self
195 }
196
197 pub fn max_width(&self, value: f32, unit: Unit) -> &Self {
198 self.core.borrow_mut().behavior.max_width = Some((value, unit));
199 if self.has_built_handle() {
200 ui::set_max_width(self.handle().raw(), value, unit as u32);
201 self.notify_retained_layout_mutation();
202 }
203 self
204 }
205
206 pub fn min_height(&self, value: f32, unit: Unit) -> &Self {
207 self.core.borrow_mut().behavior.min_height = Some((value, unit));
208 if self.has_built_handle() {
209 ui::set_min_height(self.handle().raw(), value, unit as u32);
210 self.notify_retained_layout_mutation();
211 }
212 self
213 }
214
215 pub fn max_height(&self, value: f32, unit: Unit) -> &Self {
216 self.core.borrow_mut().behavior.max_height = Some((value, unit));
217 if self.has_built_handle() {
218 ui::set_max_height(self.handle().raw(), value, unit as u32);
219 self.notify_retained_layout_mutation();
220 }
221 self
222 }
223
224 pub fn flex_basis(&self, value: f32) -> &Self {
225 self.core.borrow_mut().behavior.flex_basis = Some(value);
226 if self.has_built_handle() {
227 ui::set_flex_basis(self.handle().raw(), value);
228 self.notify_retained_layout_mutation();
229 }
230 self
231 }
232
233 pub fn scroll_enabled(&self, enabled_x: bool, enabled_y: bool) -> &Self {
234 let mut props = self.props.borrow_mut();
235 props.enable_scroll_x = enabled_x;
236 props.enable_scroll_y = enabled_y;
237 self.retained_node_ref()
238 .set_scroll_routing_enabled(enabled_x, enabled_y);
239 if self.has_built_handle() {
240 ui::set_scroll_enabled(self.handle().raw(), enabled_x, enabled_y);
241 self.notify_retained_mutation();
242 }
243 self
244 }
245
246 pub fn scroll_enabled_x(&self, enabled: bool) -> &Self {
247 self.props.borrow_mut().enable_scroll_x = enabled;
248 let enabled_y = self
249 .retained_node_ref()
250 .scroll_routing_state()
251 .map(|state| state.enabled_y)
252 .unwrap_or(true);
253 self.retained_node_ref()
254 .set_scroll_routing_enabled(enabled, enabled_y);
255 if self.has_built_handle() {
256 ui::set_scroll_enabled(self.handle().raw(), enabled, enabled_y);
257 self.notify_retained_mutation();
258 }
259 self
260 }
261
262 pub fn scroll_enabled_y(&self, enabled: bool) -> &Self {
263 self.props.borrow_mut().enable_scroll_y = enabled;
264 let enabled_x = self
265 .retained_node_ref()
266 .scroll_routing_state()
267 .map(|state| state.enabled_x)
268 .unwrap_or(true);
269 self.retained_node_ref()
270 .set_scroll_routing_enabled(enabled_x, enabled);
271 if self.has_built_handle() {
272 ui::set_scroll_enabled(self.handle().raw(), enabled_x, enabled);
273 self.notify_retained_mutation();
274 }
275 self
276 }
277
278 pub fn friction(&self, friction: f32) -> &Self {
279 self.props.borrow_mut().friction = Some(friction);
280 if self.has_built_handle() {
281 ui::set_scroll_friction(self.handle().raw(), friction);
282 self.notify_retained_mutation();
283 }
284 self
285 }
286
287 pub fn smooth_scrolling(&self, smooth_scrolling: bool) -> &Self {
288 self.props.borrow_mut().smooth_scrolling = smooth_scrolling;
289 if self.has_built_handle() {
290 ui::set_smooth_scrolling(self.handle().raw(), smooth_scrolling);
291 }
292 self
293 }
294
295 pub fn scroll_offset(&self, offset_x: f32, offset_y: f32) -> &Self {
296 self.cancel_scroll_offset_transition();
297 if self.should_animate_scroll_offset(offset_x, offset_y) {
298 if let Some(timing) = self
299 .props
300 .borrow()
301 .transitions
302 .as_ref()
303 .and_then(NodeTransitions::scroll_offset_timing)
304 {
305 self.take_programmatic_scroll_ownership();
306 self.start_scroll_offset_animation(offset_x, offset_y, timing);
307 return self;
308 }
309 }
310 if self.has_built_handle() && self.current_scroll_offset() != (offset_x, offset_y) {
311 self.take_programmatic_scroll_ownership();
312 }
313 self.apply_animated_scroll_offset(offset_x, offset_y);
314 self
315 }
316
317 pub fn scroll_content_size(&self, width: f32, height: f32) -> &Self {
318 self.props.borrow_mut().content_size = Some((width, height));
319 self.set_content_size_metrics(width, height);
320 if self.has_built_handle() {
321 ui::set_scroll_content_size(self.handle().raw(), width, height);
322 self.notify_retained_mutation();
323 }
324 self
325 }
326
327 pub fn persist_scroll(&self, persist: bool) -> &Self {
328 self.props.borrow_mut().persist_scroll = persist;
329 self
330 }
331
332 pub fn set_runtime_scroll_offset(&self, offset_x: f32, offset_y: f32) {
333 self.cancel_scroll_offset_transition();
334 self.apply_animated_scroll_offset(offset_x, offset_y);
335 }
336
337 pub fn scroll_to(&self, offset_x: f32, offset_y: f32) -> &Self {
338 self.cancel_scroll_offset_transition();
339 if self.has_built_handle() && self.current_scroll_offset() != (offset_x, offset_y) {
340 self.take_programmatic_scroll_ownership();
341 }
342 self.apply_animated_scroll_offset(offset_x, offset_y);
343 self
344 }
345
346 pub fn scroll_to_animated(
347 &self,
348 offset_x: f32,
349 offset_y: f32,
350 timing: AnimationTiming,
351 ) -> &Self {
352 self.cancel_scroll_offset_transition();
353 if !self.has_built_handle() || self.current_scroll_offset() == (offset_x, offset_y) {
354 self.apply_animated_scroll_offset(offset_x, offset_y);
355 return self;
356 }
357 self.take_programmatic_scroll_ownership();
358 self.start_scroll_offset_animation(offset_x, offset_y, timing);
359 self
360 }
361
362 pub fn transitions(&self, transitions: Option<NodeTransitions>) -> &Self {
363 self.props.borrow_mut().transitions = transitions;
364 self
365 }
366
367 pub fn focusable(&self, enabled: bool, tab_index: i32) -> &Self {
368 if enabled {
369 self.retained_node_ref().require_interactive();
370 }
371 let mut core = self.core.borrow_mut();
372 core.behavior.focusable = Some((enabled, tab_index));
373 let interactive = core.behavior.enabled && core.behavior.inherited_enabled;
374 let handle = core.handle;
375 drop(core);
376 if handle != NodeHandle::INVALID {
377 ui::set_focusable(handle.raw(), interactive && enabled, tab_index);
378 self.notify_retained_mutation();
379 }
380 self
381 }
382
383 pub fn on_wheel(&self, handler: impl Fn(&mut WheelEventArgs) + 'static) -> &Self {
384 self.core.borrow_mut().handlers.wheel = Some(Rc::new(handler));
385 self.retained_node_ref().require_interactive();
386 self
387 }
388
389 pub fn on_pan_gesture(&self, handler: impl Fn(&mut GestureEventArgs) + 'static) -> &Self {
390 self.core.borrow_mut().handlers.pan_gesture = Some(Rc::new(handler));
391 self
392 }
393
394 pub fn on_pinch_gesture(&self, handler: impl Fn(&mut GestureEventArgs) + 'static) -> &Self {
395 self.core.borrow_mut().handlers.pinch_gesture = Some(Rc::new(handler));
396 self
397 }
398
399 pub fn long_press_options(&self, minimum_duration_ms: i32, movement_tolerance: f32) -> &Self {
400 let mut core = self.core.borrow_mut();
401 core.handlers.long_press_minimum_duration_ms = minimum_duration_ms.max(0);
402 core.handlers.long_press_movement_tolerance = movement_tolerance.max(0.0);
403 self
404 }
405
406 pub fn on_long_press(&self, handler: impl Fn(&mut LongPressEventArgs) + 'static) -> &Self {
407 self.core.borrow_mut().handlers.long_press = Some(Rc::new(handler));
408 self.retained_node_ref().require_interactive();
409 self
410 }
411
412 pub fn child<T: Node>(&self, child: &T) -> &Self {
413 self.append_child(child);
414 self
415 }
416
417 pub fn children<I, C>(&self, children: I) -> &Self
418 where
419 I: IntoIterator<Item = C>,
420 C: Into<Child>,
421 {
422 for child in children {
423 self.retained_node_ref()
424 .append_child_ref(&child.into().node_ref);
425 }
426 self
427 }
428
429 fn sync_bound_scroll_state_from_props(&self) {
430 let props = self.props.borrow();
431 let width = props.width;
432 let height = props.height;
433 let scroll_offset = props.scroll_offset;
434 let content_size = props.content_size;
435 drop(props);
436
437 if let Some((width, unit)) = width {
438 self.set_viewport_width_if_pixel(width, unit);
439 }
440 if let Some((height, unit)) = height {
441 self.set_viewport_height_if_pixel(height, unit);
442 }
443 if let Some((offset_x, offset_y)) = scroll_offset {
444 self.set_scroll_offset_metrics(offset_x, offset_y);
445 }
446 if let Some((width, height)) = content_size {
447 self.set_content_size_metrics(width, height);
448 }
449 }
450
451 fn set_viewport_width_if_pixel(&self, width: f32, unit: Unit) {
452 if unit != Unit::Pixel {
453 return;
454 }
455 if let Some(state) = self.bound_scroll_state.borrow().clone() {
456 state.set_viewport_width(width);
457 }
458 self.update_scroll_routing_metrics(|state| state.viewport_width = width);
459 }
460
461 fn set_viewport_height_if_pixel(&self, height: f32, unit: Unit) {
462 if unit != Unit::Pixel {
463 return;
464 }
465 if let Some(state) = self.bound_scroll_state.borrow().clone() {
466 state.set_viewport_height(height);
467 }
468 self.update_scroll_routing_metrics(|state| state.viewport_height = height);
469 }
470
471 fn set_content_size_metrics(&self, width: f32, height: f32) {
472 if let Some(state) = self.bound_scroll_state.borrow().clone() {
473 if width >= 0.0 {
474 state.set_content_width(width);
475 }
476 if height >= 0.0 {
477 state.set_content_height(height);
478 }
479 }
480 self.update_scroll_routing_metrics(|state| {
481 if width >= 0.0 {
482 state.content_width = width;
483 }
484 if height >= 0.0 {
485 state.content_height = height;
486 }
487 });
488 }
489
490 fn set_scroll_offset_metrics(&self, offset_x: f32, offset_y: f32) {
491 if let Some(state) = self.bound_scroll_state.borrow().clone() {
492 state.set_offset_x(offset_x);
493 state.set_offset_y(offset_y);
494 }
495 self.retained_node_ref()
496 .set_scroll_routing_offsets(offset_x, offset_y);
497 }
498
499 fn update_scroll_routing_metrics(&self, update: impl FnOnce(&mut ScrollRoutingState)) {
500 let node = self.retained_node_ref();
501 let Some(mut state) = node.scroll_routing_state() else {
502 return;
503 };
504 update(&mut state);
505 node.set_scroll_routing_metrics(
506 state.offset_x,
507 state.offset_y,
508 state.content_width,
509 state.content_height,
510 state.viewport_width,
511 state.viewport_height,
512 );
513 }
514
515 fn current_scroll_offset(&self) -> (f32, f32) {
516 self.props.borrow().scroll_offset.unwrap_or((0.0, 0.0))
517 }
518
519 fn apply_animated_scroll_offset(&self, offset_x: f32, offset_y: f32) {
520 self.props.borrow_mut().scroll_offset = Some((offset_x, offset_y));
521 self.set_scroll_offset_metrics(offset_x, offset_y);
522 if self.has_built_handle() {
523 self.prepare_programmatic_scroll(offset_x, offset_y);
524 ui::set_scroll_offset(self.handle().raw(), offset_x, offset_y);
525 self.notify_retained_mutation();
526 }
527 }
528
529 fn should_animate_scroll_offset(&self, offset_x: f32, offset_y: f32) -> bool {
530 self.has_built_handle()
531 && self
532 .props
533 .borrow()
534 .transitions
535 .as_ref()
536 .and_then(NodeTransitions::scroll_offset_timing)
537 .is_some()
538 && self.current_scroll_offset() != (offset_x, offset_y)
539 }
540
541 fn cancel_scroll_offset_transition(&self) {
542 if let Some(animation) = self.active_animations.borrow_mut().scroll_offset.take() {
543 animation.cancel();
544 }
545 }
546
547 fn start_scroll_offset_animation(&self, offset_x: f32, offset_y: f32, timing: AnimationTiming) {
548 let (from_x, from_y) = self.current_scroll_offset();
549 let weak_core = Rc::downgrade(&self.core);
550 let weak_props = Rc::downgrade(&self.props);
551 let weak_state = Rc::downgrade(&self.bound_scroll_state);
552 let animation = animate_float(0.0, 1.0, timing, move |progress| {
553 let Some(core) = weak_core.upgrade() else {
554 return;
555 };
556 let Some(props) = weak_props.upgrade() else {
557 return;
558 };
559 let next_x = from_x + ((offset_x - from_x) * progress);
560 let next_y = from_y + ((offset_y - from_y) * progress);
561 props.borrow_mut().scroll_offset = Some((next_x, next_y));
562 if let Some(bound) = weak_state.upgrade().and_then(|slot| slot.borrow().clone()) {
563 bound.set_offset_x(next_x);
564 bound.set_offset_y(next_y);
565 }
566 let node = NodeRef::from_core(core);
567 node.set_scroll_routing_offsets(next_x, next_y);
568 if node.handle() != NodeHandle::INVALID {
569 ui::set_scroll_offset(node.handle().raw(), next_x, next_y);
572 crate::frame_scheduler::mark_needs_commit();
573 }
574 });
575 self.active_animations.borrow_mut().scroll_offset = Some(animation);
576 }
577
578 fn take_programmatic_scroll_ownership(&self) {
579 ui::clear_momentum_scroll();
580 }
581
582 fn prepare_programmatic_scroll(&self, _offset_x: f32, _offset_y: f32) {
583 }
590}
591
592impl Node for ScrollView {
593 fn retained_node_ref(&self) -> NodeRef {
594 NodeRef::from_node(self.core.clone(), self.clone())
595 }
596
597 fn build_self(&self) {
598 apply_scroll_view_props(
599 self.handle(),
600 &self.props.borrow(),
601 self.core.borrow().behavior.clone(),
602 );
603 }
604}
605
606impl ThemeBindable for ScrollView {
607 fn theme_binding_node(&self) -> NodeRef {
608 self.retained_node_ref()
609 }
610
611 fn weak_theme_target(&self) -> Box<dyn Fn() -> Option<Self>> {
612 let core = Rc::downgrade(&self.core);
613 let props = Rc::downgrade(&self.props);
614 let bound_scroll_state = Rc::downgrade(&self.bound_scroll_state);
615 let active_animations = Rc::downgrade(&self.active_animations);
616 Box::new(move || {
617 Some(Self {
618 core: core.upgrade()?,
619 props: props.upgrade()?,
620 bound_scroll_state: bound_scroll_state.upgrade()?,
621 active_animations: active_animations.upgrade()?,
622 })
623 })
624 }
625}
626
627#[cfg(test)]
628mod tests {
629 use super::*;
630 use crate::ffi::{self, Call};
631
632 #[test]
633 fn scroll_to_takes_programmatic_scroll_ownership_before_setting_offset() {
634 ffi::test::reset();
635 let view = ScrollView::new();
636 view.build();
637 ffi::test::take_calls();
638
639 view.scroll_to(12.0, 34.0);
640
641 let calls = ffi::test::take_calls();
642 let clear_index = calls
643 .iter()
644 .position(|call| matches!(call, Call::ClearMomentumScroll))
645 .expect("programmatic scroll should clear momentum first");
646 let set_index = calls
647 .iter()
648 .position(|call| matches!(call, Call::SetScrollOffset { offset_x, offset_y, .. } if *offset_x == 12.0 && *offset_y == 34.0))
649 .expect("programmatic scroll should set native offset");
650 assert!(clear_index < set_index);
651 }
652
653 #[test]
654 fn animated_scroll_takes_programmatic_scroll_ownership_before_first_tick() {
655 ffi::test::reset();
656 crate::animation::reset_animations();
657 let view = ScrollView::new();
658 view.build();
659 ffi::test::take_calls();
660
661 view.scroll_to_animated(0.0, 80.0, AnimationTiming::new(100.0));
662
663 let calls = ffi::test::take_calls();
664 assert!(
665 calls
666 .iter()
667 .any(|call| matches!(call, Call::ClearMomentumScroll)),
668 "animated programmatic scroll should clear native momentum before ticking"
669 );
670 }
671
672 #[test]
673 fn smooth_wheel_scrolling_defaults_on_and_supports_retained_opt_out() {
674 ffi::test::reset();
675 let view = ScrollView::new();
676 view.build();
677
678 let calls = ffi::test::take_calls();
679 assert!(calls.iter().any(|call| matches!(
680 call,
681 Call::SetSmoothScrolling {
682 smooth_scrolling: true,
683 ..
684 }
685 )));
686
687 view.smooth_scrolling(false);
688 let calls = ffi::test::take_calls();
689 assert!(calls.iter().any(|call| matches!(
690 call,
691 Call::SetSmoothScrolling {
692 smooth_scrolling: false,
693 ..
694 }
695 )));
696 }
697}