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::app::focus_service::{self, OverlayKey};
use crate::app::input::focus::FocusDirection;
use crate::clipboard::ClipboardHandle;
use crate::core::component::Component;
use crate::core::event::MouseButton;
use crate::core::node::OverlayRoot;

use super::AppRunner;

impl<C: Component> AppRunner<C> {
    pub(crate) fn handle_overlay_click(&mut self, button: MouseButton, x: u16, y: u16) -> bool {
        let overlays: Vec<_> = self.core.tree.overlay_roots().to_vec();
        for overlay in overlays.iter().rev() {
            if !self.core.tree.is_valid(overlay.id) {
                continue;
            }
            let rect = self.core.tree.node(overlay.id).rect;
            let inside = rect.contains(x as i16, y as i16);

            if inside {
                if button == MouseButton::Right
                    && let Some(text) = overlay.copy_text.as_deref()
                {
                    self.copy_overlay_text(overlay, text);
                    return true;
                }

                if let (Some(text), Some(copy_zone)) =
                    (overlay.copy_text.as_deref(), overlay.copy_zone)
                    && button == MouseButton::Left
                    && copy_zone.contains(x as i16, y as i16)
                {
                    self.copy_overlay_text(overlay, text);
                    return true;
                }

                if button == MouseButton::Left && overlay.dismiss_policy.dismiss_on_click_inside() {
                    return self.dismiss_overlay(overlay);
                }
                return false;
            }

            if button == MouseButton::Left && overlay.dismiss_policy.dismiss_on_click_outside() {
                return self.dismiss_overlay(overlay);
            }

            if overlay.captures_focus {
                return true;
            }
        }

        false
    }

    fn copy_overlay_text(&self, overlay: &OverlayRoot, text: &str) {
        let clipboard = ClipboardHandle::new(self.clipboard.clone(), self.clipboard_config.clone());
        if let Err(err) = clipboard.copy(text) {
            self.clipboard.report_error(err);
            return;
        }
        if let Some(id) = overlay.overlay_id {
            let duration =
                Duration::from_millis(self.clipboard_config.copy_feedback_duration_ms as u64);
            self.core
                .overlay_manager
                .borrow_mut()
                .trigger_copy_feedback(id, duration);
        }
    }

    pub(crate) fn handle_overlay_escape(&mut self) -> bool {
        let overlays: Vec<_> = self.core.tree.overlay_roots().to_vec();
        for overlay in overlays.iter().rev() {
            if overlay.dismiss_policy.dismiss_on_escape() {
                return self.dismiss_overlay(overlay);
            }
        }

        overlays.iter().any(|overlay| overlay.captures_focus)
    }

    pub(crate) fn dismiss_overlay(&mut self, overlay: &OverlayRoot) -> bool {
        let dismissed = if let Some(id) = overlay.overlay_id {
            self.core.overlay_manager.borrow_mut().dismiss(id)
        } else if let Some(cb) = &overlay.on_dismiss {
            cb.emit(());
            true
        } else {
            false
        };

        if dismissed && overlay.captures_focus {
            self.restore_focus_from_stack(OverlayKey::of(overlay));
        }
        dismissed
    }

    pub(crate) fn restore_focus_from_stack(&mut self, overlay: OverlayKey) {
        let restored = focus_service::restore_focus_from_stack(
            &self.core.tree,
            &mut self.focus.refs(),
            overlay,
        );
        if restored {
            self.animation.reset_blink();
        }
    }

    pub(crate) fn ensure_overlay_focus(&mut self) {
        let moved = focus_service::ensure_overlay_focus(&self.core.tree, &mut self.focus.refs());
        if moved {
            self.animation.reset_blink();
        }
    }

    pub(crate) fn top_capturing_overlay_is_empty(&self) -> bool {
        focus_service::top_capturing_overlay_is_empty(&self.core.tree)
    }

    pub(crate) fn focus_overlay_next(&mut self) -> bool {
        self.focus_overlay_step(FocusDirection::Next)
    }

    pub(crate) fn focus_overlay_prev(&mut self) -> bool {
        self.focus_overlay_step(FocusDirection::Prev)
    }

    fn focus_overlay_step(&mut self, direction: FocusDirection) -> bool {
        let before = self.focus.focused;
        let handled =
            focus_service::overlay_step(&self.core.tree, &mut self.focus.refs(), direction);
        if handled && self.focus.focused != before {
            self.animation.reset_blink();
        }
        handled
    }
}