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