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    #[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        window()
68            .expect("no global window exists")
69            .request_animation_frame(callback.as_ref().unchecked_ref())
70            .expect("failed to request animation frame");
71        callback.forget();
72    }
73
74    /// Schedules a viewport height measurement on the next animation frame for a specific container.
75    ///
76    /// Uses an atomic set guard keyed by `container_id` to ensure only one pending
77    /// measurement exists per container — if a callback for the same id hasn't
78    /// fired yet, subsequent calls are silently ignored. This prevents accumulating
79    /// redundant animation-frame callbacks when the component re-renders frequently.
80    ///
81    /// # Arguments
82    ///
83    /// - `&str` - The container element id.
84    pub(crate) fn schedule_measure_by_id(self, container_id: &str) {
85        if !PendingMeasureCell::get_mut_pending_measure().insert(container_id.to_string()) {
86            return;
87        }
88
89        let id: String = container_id.to_string();
90        let callback: Closure<dyn FnMut()> = Closure::wrap(Box::new(move || {
91            PendingMeasureCell::get_mut_pending_measure().remove(&id);
92            if let Some(element) = Self::try_get_container_by_id(&id) {
93                let html_element: HtmlElement = element.unchecked_into();
94                self.get_viewport_height().set(html_element.client_height());
95            }
96        }));
97        window()
98            .expect("no global window exists")
99            .request_animation_frame(callback.as_ref().unchecked_ref())
100            .expect("failed to request animation frame");
101        callback.forget();
102    }
103
104    /// Returns the virtual list container element by its default id.
105    ///
106    /// # Returns
107    ///
108    /// - `Option<Element>` - The container element, if found in the document.
109    pub fn try_get_container() -> Option<Element> {
110        window()
111            .expect("no global window exists")
112            .document()
113            .expect("should have a document")
114            .get_element_by_id(VIRTUAL_LIST_CONTAINER_ID)
115    }
116
117    /// Returns the virtual list container element by its id.
118    ///
119    /// # Arguments
120    ///
121    /// - `C: AsRef<str>` - The container element id.
122    ///
123    /// # Returns
124    ///
125    /// - `Option<Element>` - The container element, if found in the document.
126    pub fn try_get_container_by_id<C>(container_id: C) -> Option<Element>
127    where
128        C: AsRef<str>,
129    {
130        window()
131            .expect("no global window exists")
132            .document()
133            .expect("should have a document")
134            .get_element_by_id(container_id.as_ref())
135    }
136
137    /// Computes the range of visible item indices for the virtual list.
138    ///
139    /// Calculates the start and end indices based on the current scroll offset,
140    /// viewport height, fixed item height, and total item count. Includes an
141    /// overscan buffer to reduce blank areas during fast scrolling.
142    ///
143    /// # Arguments
144    ///
145    /// - `i32` - The current scroll offset in pixels.
146    /// - `i32` - The current viewport height in pixels.
147    /// - `usize` - The total number of items in the list.
148    /// - `i32` - The fixed height of each item in pixels.
149    /// - `usize` - The number of overscan items to render beyond the viewport.
150    ///
151    /// # Returns
152    ///
153    /// - `(usize, usize, usize, usize)` - A tuple of (visible_start, visible_end, render_start, render_end).
154    ///   visible_start/visible_end represent the actual visible range without overscan.
155    ///   render_start/render_end represent the rendering range including overscan.
156    pub(crate) fn compute_visible_range(
157        scroll_offset: i32,
158        viewport_height: i32,
159        total_count: usize,
160        item_height: i32,
161        overscan_count: usize,
162    ) -> (usize, usize, usize, usize) {
163        let visible_start: usize = (scroll_offset / item_height).max(0) as usize;
164        let visible_count: usize = if viewport_height > 0 {
165            let viewport_bottom: i32 = scroll_offset + viewport_height;
166            let visible_end: usize =
167                ((viewport_bottom + item_height - 1) / item_height).max(0) as usize;
168            visible_end - visible_start
169        } else {
170            VIRTUAL_LIST_DEFAULT_VISIBLE_COUNT
171        };
172        let visible_end: usize = (visible_start + visible_count).min(total_count);
173        let render_start: usize = visible_start.saturating_sub(overscan_count);
174        let render_end: usize = (visible_end + overscan_count).min(total_count);
175        (visible_start, visible_end, render_start, render_end)
176    }
177}