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