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}