nightshade_api/web/
virtual_list.rs1use leptos::html;
4use leptos::prelude::*;
5use wasm_bindgen::JsCast;
6
7use crate::web::visible_range;
8
9#[component]
14pub fn VirtualList<F>(
15 #[prop(into)] count: Signal<usize>,
16 #[prop(default = 32.0)] item_height: f64,
17 #[prop(default = 360.0)] height: f64,
18 #[prop(default = 8)] overscan: usize,
19 render: F,
20) -> impl IntoView
21where
22 F: Fn(usize) -> AnyView + 'static,
23{
24 let scroll_top = RwSignal::new(0.0);
25 let viewport_height = RwSignal::new(height);
26 let render = StoredValue::new_local(render);
27 let wrap_ref = NodeRef::<html::Div>::new();
28
29 let on_scroll = move |event: web_sys::Event| {
30 if let Some(element) = event
31 .target()
32 .and_then(|target| target.dyn_into::<web_sys::HtmlElement>().ok())
33 {
34 scroll_top.set(element.scroll_top() as f64);
35 viewport_height.set(element.client_height() as f64);
36 }
37 };
38
39 Effect::new(move |_| {
40 if let Some(element) = wrap_ref.get() {
41 viewport_height.set(element.client_height() as f64);
42 }
43 });
44
45 view! {
46 <div
47 class="nightshade-virtual-list"
48 node_ref=wrap_ref
49 style=format!("height:{height}px")
50 on:scroll=on_scroll
51 >
52 {move || {
53 let total = count.get();
54 let (start, end) = visible_range(
55 scroll_top.get(),
56 viewport_height.get(),
57 item_height,
58 overscan,
59 total,
60 );
61 let top_pad = start as f64 * item_height;
62 let bottom_pad = total.saturating_sub(end) as f64 * item_height;
63 let rows = (start..end)
64 .map(|index| {
65 let content = render.with_value(|render| render(index));
66 view! {
67 <div
68 class="nightshade-virtual-row"
69 style=format!("height:{item_height}px")
70 >
71 {content}
72 </div>
73 }
74 })
75 .collect_view();
76 view! {
77 <div style=format!("height:{top_pad}px")></div>
78 {rows}
79 <div style=format!("height:{bottom_pad}px")></div>
80 }
81 }}
82 </div>
83 }
84}