textual 1.0.0-dev

A reactive TUI framework inspired by the Python Textual library
Documentation
use std::time::{Duration, Instant};

#[derive(Debug, Clone)]
pub(super) struct InputChrome {
    focused: bool,
    mouse_down: bool,
    cursor_visible: bool,
    cursor_blink_next_at: Option<Instant>,
    app_active: bool,
    user_classes: Vec<String>,
    classes: Vec<String>,
    focused_classes: Vec<String>,
}

impl InputChrome {
    const CURSOR_BLINK_PERIOD: Duration = Duration::from_millis(500);

    pub(super) fn new() -> Self {
        let mut out = Self {
            focused: false,
            mouse_down: false,
            cursor_visible: false,
            cursor_blink_next_at: None,
            app_active: true,
            user_classes: Vec::new(),
            classes: Vec::new(),
            focused_classes: Vec::new(),
        };
        out.rebuild_classes();
        out
    }

    fn next_blink_deadline() -> Instant {
        let now = Instant::now();
        now.checked_add(Self::CURSOR_BLINK_PERIOD).unwrap_or(now)
    }

    fn rebuild_classes(&mut self) {
        let mut classes = vec!["input".to_string()];
        classes.extend(self.user_classes.iter().cloned());
        let mut focused_classes = classes.clone();
        focused_classes.push("focused".to_string());
        self.classes = classes;
        self.focused_classes = focused_classes;
    }

    pub(super) fn add_user_class(&mut self, class: String) {
        self.user_classes.push(class);
        self.rebuild_classes();
    }

    pub(super) fn set_class(&mut self, class: &str, enabled: bool) {
        if enabled {
            if !self.user_classes.iter().any(|c| c == class) {
                self.user_classes.push(class.to_string());
            }
        } else {
            self.user_classes.retain(|c| c != class);
        }
        self.rebuild_classes();
    }

    pub(super) fn style_classes(&self) -> &[String] {
        if self.focused {
            &self.focused_classes
        } else {
            &self.classes
        }
    }

    pub(super) fn set_focus(&mut self, focused: bool) {
        self.focused = focused;
        if !focused {
            self.mouse_down = false;
            self.cursor_visible = false;
            self.cursor_blink_next_at = None;
            return;
        }
        self.reset_blink();
    }

    pub(super) fn has_focus(&self) -> bool {
        self.focused
    }

    pub(super) fn set_mouse_down(&mut self, down: bool) {
        self.mouse_down = down;
    }

    pub(super) fn is_mouse_down(&self) -> bool {
        self.mouse_down
    }

    pub(super) fn is_active(&self) -> bool {
        self.mouse_down
    }

    pub(super) fn cursor_visible(&self) -> bool {
        self.cursor_visible
    }

    pub(super) fn reset_blink(&mut self) {
        if !self.focused || !self.app_active {
            return;
        }
        self.cursor_visible = true;
        self.cursor_blink_next_at = Some(Self::next_blink_deadline());
    }

    pub(super) fn handle_app_focus(&mut self, active: bool) {
        self.app_active = active;
        if !active {
            self.cursor_visible = false;
            self.cursor_blink_next_at = None;
            return;
        }
        self.reset_blink();
    }

    pub(super) fn handle_tick(&mut self, now: Instant) -> bool {
        if !self.focused || !self.app_active {
            return false;
        }
        let Some(next_at) = self.cursor_blink_next_at else {
            return false;
        };
        if now < next_at {
            return false;
        }
        self.cursor_visible = !self.cursor_visible;
        self.cursor_blink_next_at = now.checked_add(Self::CURSOR_BLINK_PERIOD).or(Some(now));
        true
    }
}