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
#[cfg(feature = "image")]
use std::collections::hash_map::DefaultHasher;
#[cfg(feature = "image")]
use std::hash::{Hash, Hasher};
#[cfg(any(test, feature = "image"))]
use std::time::Duration;
#[cfg(feature = "image")]
use web_time::Instant;

#[cfg(feature = "image")]
use crate::backend::ratatui_backend::image_support;
use crate::core::component::Component;
use crate::core::node::NodeKind;
#[cfg(feature = "image")]
use crate::style::Rect;

use super::{AppRunner, spinner_frame_for_speed};

impl<C: Component> AppRunner<C> {
    pub(crate) fn update_spinner_frames(&mut self) {
        let spinner_ids = self.core.tree.spinner_ids().to_vec();

        for id in spinner_ids {
            match &mut self.core.tree.node_mut(id).kind {
                NodeKind::Spinner(node) if node.auto_frame => {
                    node.frame = spinner_frame_for_speed(self.animation.spinner_frame, node.speed);
                }
                NodeKind::DraggableTabBar(node) => {
                    for tab in std::sync::Arc::make_mut(&mut node.tabs) {
                        if let Some(spinner) = tab
                            .leading
                            .as_mut()
                            .and_then(|content| content.spinner_mut())
                            && spinner.auto_frame
                        {
                            spinner.spinner.frame = Some(spinner_frame_for_speed(
                                self.animation.spinner_frame,
                                spinner.spinner.speed,
                            ));
                        }
                    }
                }
                NodeKind::List(node) => {
                    for item in std::sync::Arc::make_mut(&mut node.items) {
                        if let Some(spinner) = item.status.as_mut().and_then(|s| s.spinner_mut())
                            && spinner.auto_frame
                        {
                            spinner.frame = spinner_frame_for_speed(
                                self.animation.spinner_frame,
                                spinner.speed,
                            );
                        }
                        if let Some(spinner) = item.gutter.as_mut().and_then(|g| g.spinner_mut())
                            && spinner.auto_frame
                        {
                            spinner.frame = spinner_frame_for_speed(
                                self.animation.spinner_frame,
                                spinner.speed,
                            );
                        }
                    }
                }
                _ => {}
            }
        }
    }

    #[cfg(test)]
    pub(crate) fn update_animated_widgets(&mut self, dt: Duration) -> (bool, bool, bool) {
        crate::app::animation::tick_animated_widgets(&mut self.core.tree, dt)
    }

    #[cfg(test)]
    pub(crate) fn update_smooth_scrolls(&mut self, dt: Duration) -> (bool, bool, bool) {
        crate::app::animation::tick_smooth_scrolls(&mut self.core.tree, dt)
    }

    #[cfg(feature = "image")]
    pub(crate) fn update_image_frames(&mut self, delta_ms: u32) -> bool {
        if self.surface.is_inline() {
            return false;
        }

        let Some(viewport) = self.image_animation_viewport() else {
            return false;
        };

        let image_ids = self.core.tree.animated_image_ids().to_vec();

        let mut any_advanced = false;
        for id in image_ids {
            if self
                .core
                .tree
                .node(id)
                .rect
                .intersection(&viewport)
                .is_empty()
            {
                continue;
            }
            if let NodeKind::Image(node) = &mut self.core.tree.node_mut(id).kind
                && node.tick_animation(delta_ms)
            {
                any_advanced = true;
            }
        }

        any_advanced
    }

    #[cfg(feature = "image")]
    pub(crate) fn next_image_frame_due_in_ms(&self) -> Option<u32> {
        if self.surface.is_inline() {
            return None;
        }

        let viewport = self.image_animation_viewport()?;

        self.core
            .tree
            .animated_image_ids()
            .iter()
            .filter_map(|&id| {
                let node = self.core.tree.node(id);
                if node.rect.intersection(&viewport).is_empty() {
                    return None;
                }
                match &node.kind {
                    NodeKind::Image(image) => image.next_frame_due_in_ms(),
                    _ => None,
                }
            })
            .min()
    }

    #[cfg(feature = "image")]
    pub(crate) fn image_animations_suspended(&self) -> bool {
        self.animation
            .image_animation_suspend_until
            .is_some_and(|deadline| Instant::now() < deadline)
    }

    #[cfg(feature = "image")]
    pub(crate) fn suspend_image_animations_for(&mut self, duration: Duration) {
        let now = Instant::now();
        let requested_deadline = now + duration;
        self.animation.image_animation_suspend_until = Some(
            self.animation
                .image_animation_suspend_until
                .map(|current| current.max(requested_deadline))
                .unwrap_or(requested_deadline),
        );
        self.animation.last_image_tick = now;
    }

    #[cfg(feature = "image")]
    pub(crate) fn refresh_image_layout_suspension(&mut self) {
        if self.surface.is_inline() {
            self.animation.last_image_layout_hash = None;
            self.animation.image_animation_suspend_until = None;
            self.animation.last_image_tick = Instant::now();
            return;
        }

        let new_hash = self.animated_image_layout_hash();
        if self.animation.last_image_layout_hash != new_hash {
            self.animation.last_image_layout_hash = new_hash;
            if new_hash.is_some() {
                let pause = Duration::from_millis(super::image_layout_stabilize_ms() as u64);
                self.suspend_image_animations_for(pause);
                image_support::suspend_image_rendering_for(pause);
            } else {
                self.animation.image_animation_suspend_until = None;
                self.animation.last_image_tick = Instant::now();
            }
        }
    }

    #[cfg(feature = "image")]
    fn animated_image_layout_hash(&self) -> Option<u64> {
        let mut hasher = DefaultHasher::new();
        let mut found = false;

        for &id in self.core.tree.animated_image_ids() {
            let node = self.core.tree.node(id);
            let NodeKind::Image(image) = &node.kind else {
                continue;
            };
            found = true;
            image.source_hash.hash(&mut hasher);
            node.rect.hash(&mut hasher);
        }

        found.then_some(hasher.finish())
    }

    #[cfg(feature = "image")]
    fn image_animation_viewport(&self) -> Option<Rect> {
        if !self.core.tree.is_valid(self.core.tree.root) {
            return None;
        }
        Some(self.core.tree.node(self.core.tree.root).rect)
    }
}