orbital-core-components 0.1.1

Themed Orbital UI components for Leptos applications
Documentation
use leptos::prelude::*;
use wasm_bindgen::JsCast;

use super::node::RichTreeNode;
use super::node_view::RichTreeNodeView;

pub(crate) const VIRTUALIZE_THRESHOLD: usize = 50;
const VIRTUAL_ROW_HEIGHT: f64 = 32.0;
const VIRTUAL_OVERSCAN: usize = 5;

#[component]
pub fn VirtualRichTreeChildren(nodes: Vec<RichTreeNode>) -> impl IntoView {
    let nodes = StoredValue::new(nodes);
    let scroll_top = RwSignal::new(0.0);

    let visible_range = Memo::new(move |_| {
        let all = nodes.get_value();
        let total = all.len();
        if total == 0 {
            return (0_usize, 0_usize, 0.0);
        }
        let top = scroll_top.get();
        let start = ((top / VIRTUAL_ROW_HEIGHT).floor() as usize).saturating_sub(VIRTUAL_OVERSCAN);
        let visible_count =
            ((400.0 / VIRTUAL_ROW_HEIGHT).ceil() as usize).saturating_add(VIRTUAL_OVERSCAN * 2);
        let end = (start + visible_count).min(total);
        (start, end, total as f64 * VIRTUAL_ROW_HEIGHT)
    });

    view! {
        <div
            class="orbital-tree-virtual-scroll"
            style="max-height: 400px; overflow: auto;"
            on:scroll=move |ev| {
                if let Some(el) = ev
                    .target()
                    .and_then(|target| target.dyn_into::<web_sys::HtmlElement>().ok())
                {
                    scroll_top.set(el.scroll_top() as f64);
                }
            }
        >
            <div style=move || {
                let (_, _, height) = visible_range.get();
                format!("height: {height}px; position: relative;")
            }>
                <div style=move || {
                    let (start, _, _) = visible_range.get();
                    format!(
                        "position: absolute; top: {}px; left: 0; right: 0;",
                        start as f64 * VIRTUAL_ROW_HEIGHT
                    )
                }>
                    <For
                        each=move || {
                            let (start, end, _) = visible_range.get();
                            nodes
                                .get_value()
                                .into_iter()
                                .enumerate()
                                .filter_map(|(index, node)| {
                                    (index >= start && index < end).then_some(node)
                                })
                                .collect::<Vec<_>>()
                        }
                        key=|child| child.id.clone()
                        children=move |child| view! { <RichTreeNodeView node=child /> }
                    />
                </div>
            </div>
        </div>
    }
}