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_handle: FireHandle = (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        })
77        .into();
78        App::batch(|| {
79            scroll_offset_signal.subscribe(move || {
80                App::batch(|| unsafe { fire_handle.fire() });
81            });
82            viewport_height_signal.subscribe(move || {
83                App::batch(|| unsafe { fire_handle.fire() });
84            });
85            unsafe { fire_handle.fire() }
86            range_watch_initialized.set(true);
87        });
88    }
89    let total_height: i32 = total_count as i32 * item_height;
90    let top_padding: i32 = render_start as i32 * item_height;
91    let children: Vec<VirtualNode> = (render_start..render_end)
92        .map(|index: usize| {
93            let item_node: VirtualNode = (item_renderer)(index);
94            html! {
95                div {
96                    key: index.to_string()
97                    style: format!("height: {item_height}px; box-sizing: border-box;")
98                    item_node
99                }
100            }
101        })
102        .collect();
103    html! {
104        div {
105            class: c_virtual_list_container()
106            id: container_id
107            onscroll: scroll_handler
108            div {
109                style: format!("position: relative; height: {total_height}px;")
110                div {
111                    style: format!("position: absolute; top: {top_padding}px; left: 0; right: 0;")
112                    children
113                }
114            }
115        }
116    }
117}