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