Skip to main content

euv_ui/component/virtual_list/hook/
impl.rs

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