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 start_index = start_index.saturating_sub(*overscan);
168 let end_index = (end_index + *overscan).min(items.len());
169 let visible_items = (start_index..end_index)
170 .map(|index| {
171 let top = heights[0..index].iter().sum::<f64>();
172 VirtualListItem {
173 data: items[index].clone(),
174 index,
175 top,
176 height: heights[index],
177 }
178 })
179 .collect();
180 let scroll_to = {
181 let heights = heights.clone();
182 let st_setter = scroll_position_clone.clone();
183 let container = container_clone.clone();
184 Rc::new(move |index: usize| {
185 if index < heights.len() {
186 let top = heights[0..index].iter().sum::<f64>();
187 st_setter.set(top);
188 if let Some(c) = container.get() {
189 if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
190 e.set_scroll_top(top as i32);
191 }
192 }
193 }
194 })
195 };
196 let new_handle = UseVirtualListHandle {
197 visible_items,
198 total_height,
199 start_index,
200 end_index,
201 scroll_to,
202 };
203 handle_clone.set(new_handle.clone());
204 if let Some(w) = wrapper_clone.get() {
206 if let Some(e) = w.dyn_ref::<web_sys::HtmlElement>() {
207 let _ = e
208 .style()
209 .set_property("height", &format!("{}px", total_height));
210 let _ = e.style().set_property("position", "relative");
211 }
212 }
213 },
214 );
215 }
216
217 {
218 let container_clone = container.clone();
219 let scroll_position_clone = scroll_position.clone();
220 let container_height_clone = container_height.clone();
221 use_effect_with(container_clone, move |container| {
222 if let Some(c) = container.get() {
223 let c = c.clone();
224 if let Some(e) = c.dyn_ref::<web_sys::HtmlElement>() {
225 container_height_clone.set(e.client_height() as f64);
226 scroll_position_clone.set(e.scroll_top() as f64);
227 let scroll_top_inner = scroll_position_clone.clone();
228 let c_clone = c.clone();
229 let closure = Closure::wrap(Box::new(move |_: web_sys::Event| {
230 if let Some(e) = c_clone.dyn_ref::<web_sys::HtmlElement>() {
231 scroll_top_inner.set(e.scroll_top() as f64);
232 }
233 }) as Box<dyn FnMut(_)>);
234 let _ = e.add_event_listener_with_callback(
235 "scroll",
236 closure.as_ref().unchecked_ref(),
237 );
238 closure.forget();
239 }
240 }
241 || {}
242 });
243 }
244
245 (*handle).clone()
246}