Skip to main content

fui/node/
scroll_view.rs

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                // FUI-AS marks every programmatic set as a pending native-scroll ack while
570                // ownership is taken once before starting the animation.
571                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        // FUI-AS keeps a pending programmatic ack so its scroll callback can ignore the
584        // matching native echo. FUI-RS updates scroll metrics in the central event
585        // dispatcher and does not cancel transitions from that echo yet, so there is no
586        // local ack state to update here. Keep the call site split faithful so the ack
587        // state can be added without changing public behavior when scroll dispatch is
588        // ported to the exact FUI-AS ownership model.
589    }
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}