virtualizer_adapter/
controller.rs

1use crate::{
2    Easing, ScrollAnchor, Tween, VirtualizerKey, apply_anchor, capture_first_visible_anchor,
3};
4
5/// A framework-neutral controller that wraps a `virtualizer::Virtualizer` and provides common
6/// adapter workflows (anchoring, tween-driven scrolling).
7///
8/// This type does not hold any UI objects. Adapters drive it by calling:
9/// - `on_viewport_size` / `on_scroll` when UI events occur
10/// - `tick(now_ms)` each frame/timer tick (for tween scrolling and `is_scrolling` debouncing)
11///
12/// For UI scroll containers (e.g. DOM), you can use the returned offset from `tick()` to set the
13/// real scroll position, while keeping the virtualizer state in sync.
14///
15/// # Typical integration loop
16///
17/// - On UI scroll events: call `on_scroll(offset, now_ms)` (cancels any active tween).
18/// - On UI resize/layout: call `on_viewport_size(main)`.
19/// - Each frame/timer: call `tick(now_ms)`; if it returns `Some(offset)`, apply it to the real UI.
20#[derive(Clone, Debug)]
21pub struct Controller<K> {
22    v: virtualizer::Virtualizer<K>,
23    tween: Option<Tween>,
24}
25
26impl<K: VirtualizerKey> Controller<K> {
27    pub fn new(options: virtualizer::VirtualizerOptions<K>) -> Self {
28        Self {
29            v: virtualizer::Virtualizer::new(options),
30            tween: None,
31        }
32    }
33
34    pub fn from_virtualizer(v: virtualizer::Virtualizer<K>) -> Self {
35        Self { v, tween: None }
36    }
37
38    pub fn virtualizer(&self) -> &virtualizer::Virtualizer<K> {
39        &self.v
40    }
41
42    pub fn virtualizer_mut(&mut self) -> &mut virtualizer::Virtualizer<K> {
43        &mut self.v
44    }
45
46    pub fn into_virtualizer(self) -> virtualizer::Virtualizer<K> {
47        self.v
48    }
49
50    pub fn is_animating(&self) -> bool {
51        self.tween.is_some()
52    }
53
54    pub fn cancel_animation(&mut self) {
55        self.tween = None;
56    }
57
58    pub fn on_viewport_size(&mut self, viewport_main: u32) {
59        self.v.set_viewport_size(viewport_main);
60    }
61
62    /// Call this when the UI reports a scroll offset change (e.g. user wheel/drag).
63    ///
64    /// This cancels any active tween.
65    pub fn on_scroll(&mut self, scroll_offset: u64, now_ms: u64) {
66        self.cancel_animation();
67        self.v.apply_scroll_offset_event(scroll_offset, now_ms);
68    }
69
70    /// Advances the controller.
71    ///
72    /// - If a tween is active, updates `scroll_offset` and returns the new offset.
73    /// - Otherwise, runs `is_scrolling` debouncing and returns `None`.
74    pub fn tick(&mut self, now_ms: u64) -> Option<u64> {
75        let Some(tween) = self.tween else {
76            self.v.update_scrolling(now_ms);
77            return None;
78        };
79
80        let off = tween.sample(now_ms);
81        self.v.apply_scroll_offset_event_clamped(off, now_ms);
82
83        if tween.is_done(now_ms) {
84            self.tween = None;
85            self.v.set_is_scrolling(false);
86        }
87
88        Some(self.v.scroll_offset())
89    }
90
91    /// Computes and applies a scroll-to-index immediately (no animation).
92    ///
93    /// Returns the applied (clamped) offset.
94    pub fn scroll_to_index(&mut self, index: usize, align: virtualizer::Align, now_ms: u64) -> u64 {
95        let off = self.v.scroll_to_index_offset(index, align);
96        self.v.apply_scroll_offset_event_clamped(off, now_ms);
97        self.v.scroll_offset()
98    }
99
100    /// Applies a scroll-to-offset immediately (no animation).
101    ///
102    /// Returns the applied (clamped) offset.
103    pub fn scroll_to_offset(&mut self, offset: u64, now_ms: u64) -> u64 {
104        self.v.apply_scroll_offset_event_clamped(offset, now_ms);
105        self.v.scroll_offset()
106    }
107
108    /// Starts a tween to an index (adapter-driven).
109    ///
110    /// Returns the clamped target offset.
111    pub fn start_tween_to_index(
112        &mut self,
113        index: usize,
114        align: virtualizer::Align,
115        now_ms: u64,
116        duration_ms: u64,
117        easing: Easing,
118    ) -> u64 {
119        let to = self.v.scroll_to_index_offset(index, align);
120        self.start_tween_to_offset(to, now_ms, duration_ms, easing)
121    }
122
123    /// Starts a tween to an offset (adapter-driven).
124    ///
125    /// Returns the clamped target offset.
126    pub fn start_tween_to_offset(
127        &mut self,
128        offset: u64,
129        now_ms: u64,
130        duration_ms: u64,
131        easing: Easing,
132    ) -> u64 {
133        let to = self.v.clamp_scroll_offset(offset);
134        let from = self.v.scroll_offset();
135        self.tween = Some(Tween::new(from, to, now_ms, duration_ms, easing));
136        to
137    }
138
139    pub fn capture_first_visible_anchor(&self) -> Option<ScrollAnchor<K>> {
140        capture_first_visible_anchor(&self.v)
141    }
142
143    /// Captures an anchor for the item at a given offset in the viewport.
144    ///
145    /// For example, `offset_in_viewport = 0` anchors the item at the top of the viewport.
146    pub fn capture_anchor_at_offset_in_viewport(
147        &self,
148        offset_in_viewport: u64,
149    ) -> Option<ScrollAnchor<K>> {
150        let abs = self.v.scroll_offset().saturating_add(offset_in_viewport);
151        let item = self.v.virtual_item_keyed_for_offset(abs)?;
152        let offset_in_viewport = self.v.scroll_offset().saturating_sub(item.start);
153        Some(ScrollAnchor {
154            key: item.key,
155            offset_in_viewport,
156        })
157    }
158
159    /// Applies a previously captured anchor by adjusting the scroll offset.
160    ///
161    /// This cancels any active tween.
162    pub fn apply_anchor(
163        &mut self,
164        anchor: &ScrollAnchor<K>,
165        key_to_index: impl FnMut(&K) -> Option<usize>,
166    ) -> bool {
167        self.cancel_animation();
168        apply_anchor(&mut self.v, anchor, key_to_index)
169    }
170}