Skip to main content

euv_ui/component/virtual_list/hook/
impl.rs

1use super::*;
2
3/// Encapsulated access to the global pending measurement set.
4impl PendingMeasureCell {
5    /// Returns a mutable reference to the set of pending container ids.
6    #[allow(static_mut_refs)]
7    fn get_mut_pending_measure() -> &'static mut HashSet<String> {
8        unsafe { &mut *PENDING_MEASURE_BY_ID.deref().get_0().get() }
9    }
10}
11
12/// Implementation of virtual list functionality.
13impl UseVirtualList {
14    /// Creates virtual list state signals for tracking scroll offset and viewport height.
15    ///
16    /// # Returns
17    ///
18    /// - `UseVirtualList` - The virtual list state containing scroll offset and viewport height signals.
19    pub fn use_scroll_state() -> UseVirtualList {
20        UseVirtualList::new(App::use_signal(|| 0), App::use_signal(|| 0))
21    }
22
23    /// Creates a scroll event handler that tracks the container scroll position and viewport height.
24    ///
25    /// Reads `scrollTop` and `clientHeight` from the scroll container element
26    /// referenced by `VIRTUAL_LIST_CONTAINER_ID` and updates the corresponding signals.
27    ///
28    /// # Returns
29    ///
30    /// - `Option<Rc<dyn Fn(Event)>>` - A scroll handler for the virtual list container.
31    pub fn on_scroll(self) -> Option<Rc<dyn Fn(Event)>> {
32        Some(Rc::new(move |_: Event| {
33            if let Some(container) = Self::try_get_container() {
34                let html_element: HtmlElement = container.unchecked_into();
35                self.get_scroll_offset().set(html_element.scroll_top());
36                self.get_viewport_height().set(html_element.client_height());
37            }
38        }))
39    }
40
41    /// Reads the container `clientHeight` and writes it to the viewport height signal.
42    ///
43    /// Should only be called when the DOM is already present (e.g. inside a resize
44    /// callback or after the first paint). For initial mount use
45    /// `schedule_measure` instead.
46    pub fn update_viewport_height(self) {
47        if let Some(container) = Self::try_get_container() {
48            let html_element: HtmlElement = container.unchecked_into();
49            self.get_viewport_height().set(html_element.client_height());
50        }
51    }
52
53    /// Schedules a viewport height measurement on the next animation frame.
54    ///
55    /// Uses an atomic guard to ensure only one measurement is pending at a time —
56    /// if a previous callback hasn't fired yet, subsequent calls are silently
57    /// ignored. This prevents accumulating redundant animation-frame callbacks
58    /// when the component re-renders frequently.
59    pub fn schedule_measure(self) {
60        if PENDING_MEASURE.swap(true, Ordering::Relaxed) {
61            return;
62        }
63        let callback: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
64            PENDING_MEASURE.store(false, Ordering::Relaxed);
65            self.update_viewport_height();
66        }));
67        let Some(window_value) = window() else {
68            PENDING_MEASURE.store(false, Ordering::Relaxed);
69            return;
70        };
71        let Ok(_) = window_value.request_animation_frame(callback.as_ref().unchecked_ref()) else {
72            PENDING_MEASURE.store(false, Ordering::Relaxed);
73            return;
74        };
75        callback.forget();
76    }
77
78    /// Schedules a viewport height measurement on the next animation frame for a specific container.
79    ///
80    /// Uses an atomic set guard keyed by `container_id` to ensure only one pending
81    /// measurement exists per container — if a callback for the same id hasn't
82    /// fired yet, subsequent calls are silently ignored. This prevents accumulating
83    /// redundant animation-frame callbacks when the component re-renders frequently.
84    ///
85    /// # Arguments
86    ///
87    /// - `&str` - The container element id.
88    pub(crate) fn schedule_measure_by_id(self, container_id: &str) {
89        if !PendingMeasureCell::get_mut_pending_measure().insert(container_id.to_string()) {
90            return;
91        }
92
93        let id: String = container_id.to_string();
94        let callback: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
95            PendingMeasureCell::get_mut_pending_measure().remove(&id);
96            if let Some(element) = Self::try_get_container_by_id(&id) {
97                let html_element: HtmlElement = element.unchecked_into();
98                self.get_viewport_height().set(html_element.client_height());
99            }
100        }));
101        let Some(window_value) = window() else {
102            PendingMeasureCell::get_mut_pending_measure().remove(container_id);
103            return;
104        };
105        let Ok(_) = window_value.request_animation_frame(callback.as_ref().unchecked_ref()) else {
106            PendingMeasureCell::get_mut_pending_measure().remove(container_id);
107            return;
108        };
109        callback.forget();
110    }
111
112    /// Returns the virtual list container element by its default id.
113    ///
114    /// # Returns
115    ///
116    /// - `Option<Element>` - The container element, if found in the document.
117    pub fn try_get_container() -> Option<Element> {
118        let window_value: Window = window()?;
119        let document_value: Document = window_value.document()?;
120        document_value.get_element_by_id(VIRTUAL_LIST_CONTAINER_ID)
121    }
122
123    /// Returns the virtual list container element by its id.
124    ///
125    /// # Arguments
126    ///
127    /// - `C: AsRef<str>` - The container element id.
128    ///
129    /// # Returns
130    ///
131    /// - `Option<Element>` - The container element, if found in the document.
132    pub fn try_get_container_by_id<C>(container_id: C) -> Option<Element>
133    where
134        C: AsRef<str>,
135    {
136        let window_value: Window = window()?;
137        let document_value: Document = window_value.document()?;
138        document_value.get_element_by_id(container_id.as_ref())
139    }
140
141    /// Computes the range of visible item indices for the virtual list.
142    ///
143    /// Calculates the start and end indices based on the current scroll offset,
144    /// viewport height, fixed item height, and total item count. Includes an
145    /// overscan buffer to reduce blank areas during fast scrolling.
146    ///
147    /// # Arguments
148    ///
149    /// - `i32` - The current scroll offset in pixels.
150    /// - `i32` - The current viewport height in pixels.
151    /// - `usize` - The total number of items in the list.
152    /// - `i32` - The fixed height of each item in pixels.
153    /// - `usize` - The number of overscan items to render beyond the viewport.
154    ///
155    /// # Returns
156    ///
157    /// - `(usize, usize, usize, usize)` - The first pair is the actual
158    ///   visible range without overscan; the second pair is the
159    ///   rendering range including overscan.
160    pub(crate) fn compute_visible_range(
161        scroll_offset: i32,
162        viewport_height: i32,
163        total_count: usize,
164        item_height: i32,
165        overscan_count: usize,
166    ) -> (usize, usize, usize, usize) {
167        let visible_start: usize = (scroll_offset / item_height).max(0) as usize;
168        let visible_count: usize = if viewport_height > 0 {
169            let viewport_bottom: i32 = scroll_offset + viewport_height;
170            let visible_end: usize =
171                ((viewport_bottom + item_height - 1) / item_height).max(0) as usize;
172            visible_end - visible_start
173        } else {
174            VIRTUAL_LIST_DEFAULT_VISIBLE_COUNT
175        };
176        let visible_end: usize = (visible_start + visible_count).min(total_count);
177        let render_start: usize = visible_start.saturating_sub(overscan_count);
178        let render_end: usize = (visible_end + overscan_count).min(total_count);
179        (visible_start, visible_end, render_start, render_end)
180    }
181}