operad 6.1.0

A cross-platform GUI library for Rust.
Documentation
use super::*;

#[derive(Debug, Clone, Copy, PartialEq)]
pub struct VirtualListSpec {
    pub row_count: usize,
    pub row_height: f32,
    pub viewport_height: f32,
    pub scroll_offset: f32,
    pub overscan: usize,
}

impl VirtualListSpec {
    pub fn visible_range(self) -> Range<usize> {
        if self.row_count == 0 || self.row_height <= f32::EPSILON {
            return 0..0;
        }
        let first = (self.scroll_offset.max(0.0) / self.row_height).floor() as usize;
        let visible = (self.viewport_height / self.row_height).ceil() as usize + 1;
        let start = first.saturating_sub(self.overscan).min(self.row_count);
        let end = (first + visible + self.overscan).min(self.row_count);
        start..end
    }

    pub fn total_height(self) -> f32 {
        self.row_count as f32 * self.row_height
    }
}

pub fn virtual_list(
    document: &mut UiDocument,
    parent: UiNodeId,
    name: impl Into<String>,
    spec: VirtualListSpec,
    mut build_row: impl FnMut(&mut UiDocument, UiNodeId, usize),
) -> UiNodeId {
    let name = name.into();
    let list = scroll_area(
        document,
        parent,
        name.clone(),
        ScrollAxes::VERTICAL,
        LayoutStyle::from_taffy_style(Style {
            display: Display::Flex,
            flex_direction: FlexDirection::Column,
            size: TaffySize {
                width: Dimension::percent(1.0),
                height: length(spec.viewport_height),
            },
            ..Default::default()
        }),
    );
    document.node_mut(list).accessibility = Some(
        AccessibilityMeta::new(AccessibilityRole::List)
            .label(name.clone())
            .value(format!("{} items", spec.row_count)),
    );
    if let Some(scroll) = &mut document.nodes[list.0].scroll {
        scroll.offset.y = spec.scroll_offset.max(0.0);
    }
    let range = spec.visible_range();
    let top = range.start as f32 * spec.row_height;
    if top > 0.0 {
        document.add_child(list, spacer(format!("{name}.top_spacer"), top));
    }
    for row in range.clone() {
        build_row(document, list, row);
    }
    let bottom = (spec.row_count.saturating_sub(range.end)) as f32 * spec.row_height;
    if bottom > 0.0 {
        document.add_child(list, spacer(format!("{name}.bottom_spacer"), bottom));
    }
    list
}

fn spacer(name: impl Into<String>, height: f32) -> UiNode {
    UiNode::container(
        name,
        UiNodeStyle {
            layout: LayoutStyle::from_taffy_style(Style {
                size: TaffySize {
                    width: Dimension::percent(1.0),
                    height: length(height),
                },
                flex_shrink: 0.0,
                ..Default::default()
            })
            .style,
            ..Default::default()
        },
    )
}