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}