Skip to main content

euv_ui/component/virtual_list/view/
fn.rs

1use crate::*;
2
3/// A high-performance virtual list component for rendering large datasets.
4///
5/// Only renders the visible items plus an overscan buffer, keeping DOM node count
6/// constant regardless of total list size. Supports multiple instances on the same page
7/// through unique container ids.
8///
9/// # Arguments
10///
11/// - `VirtualNode<EuvVirtualListProps>` - The props node containing configuration, item renderer, and optional callbacks.
12///
13/// # Returns
14///
15/// - `VirtualNode` - The virtual list container with windowed rendering.
16#[component]
17pub fn euv_virtual_list(node: VirtualNode<EuvVirtualListProps>) -> VirtualNode {
18    let EuvVirtualListProps {
19        config,
20        item_renderer,
21        on_scroll,
22        on_visible_range_change,
23    } = node.try_get_props().unwrap_or_default();
24    let state: UseVirtualList = UseVirtualList::use_scroll_state();
25    let container_id: String = config.id.clone();
26    let total_count: usize = config.total_count;
27    let item_height: i32 = config.item_height;
28    let overscan_count: usize = config.overscan_count;
29    let viewport_height: i32 = state.get_viewport_height().get();
30    if viewport_height == 0 {
31        state.schedule_measure_by_id(&container_id);
32    }
33    let scroll_handler: Option<Rc<dyn Fn(Event)>> = {
34        let state: UseVirtualList = state;
35        let container_id: String = container_id.clone();
36        let on_scroll: Option<VirtualListScrollHandler> = on_scroll;
37        Some(Rc::new(move |_: Event| {
38            if let Some(element) = UseVirtualList::try_get_container_by_id(&container_id) {
39                let html_element: HtmlElement = element.unchecked_into();
40                let scroll_offset: i32 = html_element.scroll_top();
41                state.get_scroll_offset().set(scroll_offset);
42                if let Some(ref callback) = on_scroll {
43                    callback(scroll_offset);
44                }
45            }
46        }))
47    };
48    let scroll_offset: i32 = state.get_scroll_offset().get();
49    let (_, _, render_start, render_end): (usize, usize, usize, usize) =
50        UseVirtualList::compute_visible_range(
51            scroll_offset,
52            viewport_height,
53            total_count,
54            item_height,
55            overscan_count,
56        );
57    let range_callback: Option<VirtualListRangeHandler> = on_visible_range_change.clone();
58    let range_watch_initialized: Signal<bool> = App::use_signal(|| false);
59    if !range_watch_initialized.get() {
60        let scroll_offset_signal: Signal<i32> = state.get_scroll_offset();
61        let viewport_height_signal: Signal<i32> = state.get_viewport_height();
62        let fire_addr: usize = Box::leak(Box::new(Box::new(move || {
63            let scroll_offset: i32 = scroll_offset_signal.get();
64            let viewport_height: i32 = viewport_height_signal.get();
65            if let Some(ref callback) = range_callback {
66                let (visible_start, visible_end, _, _): (usize, usize, usize, usize) =
67                    UseVirtualList::compute_visible_range(
68                        scroll_offset,
69                        viewport_height,
70                        total_count,
71                        item_height,
72                        overscan_count,
73                    );
74                callback((visible_start, visible_end));
75            }
76        }) as Box<dyn FnMut()>)) as *mut Box<dyn FnMut()> as usize;
77        App::batch(|| {
78            scroll_offset_signal.subscribe(move || {
79                App::batch(|| unsafe { (&mut *(fire_addr as *mut Box<dyn FnMut()>))() });
80            });
81            viewport_height_signal.subscribe(move || {
82                App::batch(|| unsafe { (&mut *(fire_addr as *mut Box<dyn FnMut()>))() });
83            });
84            unsafe { (&mut *(fire_addr as *mut Box<dyn FnMut()>))() }
85            range_watch_initialized.set(true);
86        });
87    }
88    let total_height: i32 = total_count as i32 * item_height;
89    let top_padding: i32 = render_start as i32 * item_height;
90    let children: Vec<VirtualNode> = (render_start..render_end)
91        .map(|index: usize| {
92            let item_node: VirtualNode = (item_renderer)(index);
93            html! {
94                div {
95                    key: index.to_string()
96                    style: format!("height: {item_height}px; box-sizing: border-box;")
97                    item_node
98                }
99            }
100        })
101        .collect();
102    html! {
103        div {
104            class: c_virtual_list_container()
105            id: container_id
106            onscroll: scroll_handler
107            div {
108                style: format!("position: relative; height: {total_height}px;")
109                div {
110                    style: format!("position: absolute; top: {top_padding}px; left: 0; right: 0;")
111                    children
112                }
113            }
114        }
115    }
116}