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}