tui-lipan 0.2.0

Opinionated, component-based TUI framework for Rust - declarative components, reconciliation, layout engine, focus, overlays, and rich widgets on top of ratatui.
Documentation
use std::time::Duration;

use crate::core::node::{NodeKind, NodeTree};
use crate::widgets::{ScrollEvent, ScrollMetrics, ScrollWheelBehavior, calc_scroll_view_window};

pub(crate) fn tick_tree_animations(tree: &mut NodeTree, dt: Duration) -> (bool, bool, bool) {
    let (changed, needs_paint, needs_layout) = tick_animated_widgets(tree, dt);
    let (scroll_changed, scroll_needs_paint, scroll_needs_layout) = tick_smooth_scrolls(tree, dt);
    (
        changed || scroll_changed,
        needs_paint || scroll_needs_paint,
        needs_layout || scroll_needs_layout,
    )
}

pub(crate) fn tick_animated_widgets(tree: &mut NodeTree, dt: Duration) -> (bool, bool, bool) {
    let animated_ids = tree.animated_widget_ids().to_vec();

    let mut changed = false;
    let mut needs_paint = false;
    let mut needs_layout = false;

    for id in animated_ids {
        if let NodeKind::Animated(node) = &mut tree.node_mut(id).kind {
            let result = node.tick(dt);
            changed |= result.changed;
            needs_paint |= result.paint_dirty;
            needs_layout |= result.layout_dirty;
        }
    }

    tree.refresh_animated_widget_activity();

    (changed, needs_paint, needs_layout)
}

pub(crate) fn tick_smooth_scrolls(tree: &mut NodeTree, dt: Duration) -> (bool, bool, bool) {
    let animated_ids = tree.animated_scroll_ids().to_vec();

    let mut changed = false;
    let mut needs_paint = false;
    let mut needs_layout = false;

    for id in animated_ids {
        if !tree.is_valid(id) {
            continue;
        }

        let rect = tree.node(id).rect;
        let key = tree.node(id).key.clone();
        let mut scroll_view_wheel_event = None;
        match &mut tree.node_mut(id).kind {
            NodeKind::ScrollView(node) => {
                let result = node.smooth_scroll.tick(dt, node.max_offset);
                let _still_animating = result.still_animating;
                if result.changed {
                    let next = node.smooth_scroll.current_offset(node.max_offset);
                    node.offset = next;
                    node.scroll_offset = next.min(u16::MAX as usize) as u16;
                    node.scroll_override = Some(next);
                    changed = true;
                    needs_layout = true;
                }

                if !node.scroll_wheel
                    || matches!(node.scroll_wheel_behavior, ScrollWheelBehavior::Immediate)
                {
                    if node.wheel_scroll.is_animating() {
                        node.wheel_scroll.cancel_at(node.offset);
                    }
                } else if let ScrollWheelBehavior::Smooth(config) = node.scroll_wheel_behavior {
                    let result = node.wheel_scroll.tick(dt, node.max_offset, config);
                    let _still_animating = result.still_animating;
                    if result.changed {
                        let next = node.wheel_scroll.current_offset(node.max_offset);
                        node.offset = next;
                        node.scroll_offset = next.min(u16::MAX as usize) as u16;
                        node.scroll_override = Some(next);
                        node.scroll_handler_dirty = true;
                        let window = calc_scroll_view_window(
                            next,
                            node.content_height as usize,
                            node.viewport_height as usize,
                            node.show_scroll_indicators,
                        );
                        scroll_view_wheel_event = Some((
                            next,
                            ScrollMetrics {
                                len: node.content_height as usize,
                                visible: window.visible_rows,
                                max_offset: window.max_offset,
                            },
                            node.on_scroll_to.clone(),
                            node.on_scroll.clone(),
                        ));
                        changed = true;
                        needs_layout = true;
                    }
                }
            }
            NodeKind::DocumentView(node) => {
                let inner = rect.inner(node.border, node.padding);
                let visible = node.content_layout(inner).content_height as usize;
                let max_offset = node.total_visual_lines.saturating_sub(visible);
                let result = node.smooth_scroll.tick(dt, max_offset);
                let _still_animating = result.still_animating;
                if result.changed {
                    let next = node.smooth_scroll.current_offset(max_offset);
                    node.scroll_offset = next;
                    node.scroll_override = Some(next);
                    changed = true;
                    needs_paint = true;
                }
            }
            NodeKind::TextArea(node) => {
                let h_scrollbar_over_border = node.h_scrollbar
                    && matches!(
                        node.h_scrollbar_variant,
                        crate::style::ScrollbarVariant::Integrated
                    )
                    && node.border;
                let visible = node.geometry.content_viewport_h(h_scrollbar_over_border) as usize;
                let max_offset = node.visual_lines_count.saturating_sub(visible);
                let result = node.smooth_scroll.tick(dt, max_offset);
                let _still_animating = result.still_animating;
                if result.changed {
                    let next = node.smooth_scroll.current_offset(max_offset);
                    node.scroll_offset = next;
                    node.scroll_override = Some(next);
                    changed = true;
                    needs_paint = true;
                }
            }
            _ => {}
        }

        if let Some((next, metrics, on_scroll_to, on_scroll)) = scroll_view_wheel_event {
            if let Some(key) = key {
                tree.scroll_input_offset_by_key.insert(key, next);
            }
            if let Some(cb) = on_scroll_to.as_ref() {
                cb.emit(next);
            } else if let Some(cb) = on_scroll.as_ref() {
                cb.emit(ScrollEvent {
                    offset: next,
                    metrics,
                });
            }
        }
    }

    tree.refresh_animated_scroll_activity();

    (changed, needs_paint, needs_layout)
}