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}