yew_hooks/hooks/
use_virtual_list.rs1use std::rc::Rc;
2use wasm_bindgen::prelude::*;
3use yew::prelude::*;
4
5#[derive(Clone, PartialEq)]
7pub struct VirtualListItem<T> {
8 pub data: T,
10 pub index: usize,
12 pub top: f64,
14 pub height: f64,
16}
17
18pub struct UseVirtualListHandle<T> {
20 pub visible_items: Vec<VirtualListItem<T>>,
22 pub total_height: f64,
24 pub start_index: usize,
26 pub end_index: usize,
28 pub scroll_to: Rc<dyn Fn(usize)>,
30}
31
32impl<T> Clone for UseVirtualListHandle<T>
33where
34 T: Clone,
35{
36 fn clone(&self) -> Self {
37 Self {
38 visible_items: self.visible_items.clone(),
39 total_height: self.total_height,
40 start_index: self.start_index,
41 end_index: self.end_index,
42 scroll_to: self.scroll_to.clone(),
43 }
44 }
45}
46
47impl<T> PartialEq for UseVirtualListHandle<T>
48where
49 T: PartialEq,
50{
51 fn eq(&self, other: &Self) -> bool {
52 self.visible_items == other.visible_items
53 && self.total_height == other.total_height
54 && self.start_index == other.start_index
55 && self.end_index == other.end_index
56 }
57}
58
59#[hook]
117pub fn use_virtual_list<T>(
118 items: Vec<T>,
119 item_height: fn(usize) -> f64,
120 container: NodeRef,
121 wrapper: NodeRef,
122 overscan: usize,
123) -> UseVirtualListHandle<T>
124where
125 T: Clone + PartialEq + 'static,
126{
127 let scroll_position = use_state(|| 0.0);
128 let container_height = use_state(|| 0.0);
129 let handle = use_state(|| UseVirtualListHandle {
130 visible_items: vec![],
131 total_height: 0.0,
132 start_index: 0,
133 end_index: 0,
134 scroll_to: Rc::new(|_| {}),
135 });
136
137 {
138 let items = items.clone();
139 let scroll_top_val = *scroll_position;
140 let container_height_val = *container_height;
141 let handle_clone = handle.clone();
142 let wrapper_clone = wrapper.clone();
143 let scroll_position_clone = scroll_position.clone();
144 let container_clone = container.clone();
145 use_effect_with(
146 (items, container_height_val, scroll_top_val, overscan),
147 move |(items, container_height, scroll_top, overscan)| {
148 let heights: Vec<f64> = (0..items.len()).map(item_height).collect();
149 let total_height = heights.iter().sum::<f64>();
150 let mut cumulative = 0.0;
151 let mut start_index = 0;
152 for (i, &h) in heights.iter().enumerate() {
153 if cumulative + h > *scroll_top {
154 start_index = i;
155 break;
156 }
157 cumulative += h;
158 }
159 let start_cum = cumulative;
160 let mut end_index = start_index;
161 let mut current_cum = start_cum;
162 while current_cum < *scroll_top + *container_height && end_index < items.len() {
163 current_cum += heights[end_index];
164 end_index += 1;
165 }
166 end_index = end_index.min(items.len());
167 let visible_start = start_index;
168 let visible_end = end_index;
169 let render_start = visible_start.saturating_sub(*overscan);
170 let render_end = (visible_end + *overscan).min(items.len());
171 let visible_items = (render_start..render_end)
172 .map(|index| {
173 let top = heights[0..index].iter().sum::<f64>();
174 VirtualListItem {
175 data: items[index].clone(),
176 index,
177 top,
178 height: heights[index],
179 }
180 })
181 .collect();
182 let scroll_to = {
183 let heights = heights.clone();
184 let st_setter = scroll_position_clone.clone();
185 let container = container_clone.clone();
186 Rc::new(move |index: usize| {
187 if index < heights.len() {
188 let top = heights[0..index].iter().sum::<f64>();
189 st_setter.set(top);
190 if let Some(c) = container.get() {
191 if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
192 e.set_scroll_top(top as i32);
193 }
194 }
195 }
196 })
197 };
198 let new_handle = UseVirtualListHandle {
199 visible_items,
200 total_height,
201 start_index: visible_start,
202 end_index: visible_end.saturating_sub(1),
203 scroll_to,
204 };
205 handle_clone.set(new_handle.clone());
206 if let Some(w) = wrapper_clone.get() {
208 if let Some(e) = w.dyn_ref::<web_sys::HtmlElement>() {
209 let _ = e
210 .style()
211 .set_property("height", &format!("{}px", total_height));
212 let _ = e.style().set_property("position", "relative");
213 }
214 }
215 },
216 );
217 }
218
219 {
220 let container_clone = container.clone();
221 let scroll_position_clone = scroll_position.clone();
222 let container_height_clone = container_height.clone();
223 use_effect_with(container_clone, move |container| {
224 if let Some(c) = container.get() {
225 let c = c.clone();
226 if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
227 container_height_clone.set(e.client_height() as f64);
228 scroll_position_clone.set(e.scroll_top() as f64);
229 let scroll_top_inner = scroll_position_clone.clone();
230 let c_clone = c.clone();
231 let closure = Closure::wrap(Box::new(move |_: web_sys::Event| {
232 if let Some(e) = c_clone.dyn_ref::<web_sys::HtmlElement>() {
233 scroll_top_inner.set(e.scroll_top() as f64);
234 }
235 }) as Box<dyn FnMut(_)>);
236 let _ = e.add_event_listener_with_callback(
237 "scroll",
238 closure.as_ref().unchecked_ref(),
239 );
240 closure.forget();
241 }
242 }
243 || {}
244 });
245 }
246
247 (*handle).clone()
248}