shuire 0.1.1

Vim-like TUI git diff viewer
use std::collections::HashMap;

use ratatui::{
    Frame,
    layout::Rect,
    style::{Color, Modifier, Style},
    text::{Line, Span},
    widgets::{Block, Borders, Clear, Paragraph},
};
use unicode_width::UnicodeWidthStr;

use super::Component;
use crate::{action::Action, state::ThemeName, theme};

pub struct Toast {
    message: Option<String>,
    /// Remaining ticks before the toast auto-dismisses. Tick cadence is
    /// set by `App::tick_rate` (~4 Hz), so 12 gives ≈3 s of visibility.
    ticks: u8,
    theme_name: ThemeName,
    color_overrides: HashMap<String, Color>,
}

const VISIBILITY_TICKS: u8 = 12;

impl Toast {
    pub fn new(theme_name: ThemeName, color_overrides: HashMap<String, Color>) -> Self {
        Self {
            message: None,
            ticks: 0,
            theme_name,
            color_overrides,
        }
    }

    pub fn set_theme(&mut self, theme_name: ThemeName, color_overrides: HashMap<String, Color>) {
        self.theme_name = theme_name;
        self.color_overrides = color_overrides;
    }
}

impl Component for Toast {
    fn update(&mut self, action: Action) -> color_eyre::Result<Option<Action>> {
        match action {
            Action::Flash(msg) => {
                self.message = Some(msg);
                self.ticks = VISIBILITY_TICKS;
            }
            Action::Tick if self.ticks > 0 => {
                self.ticks -= 1;
                if self.ticks == 0 {
                    self.message = None;
                }
            }
            _ => {}
        }
        Ok(None)
    }

    fn draw(&mut self, f: &mut Frame, _area: Rect) -> color_eyre::Result<()> {
        let Some(ref msg) = self.message else {
            return Ok(());
        };
        let theme = theme::Theme::for_name_with_overrides(self.theme_name, &self.color_overrides);
        let area = f.area();
        let prefix = "";
        let content_w = prefix.width() + msg.width() + 1;
        let box_w = (content_w + 2).min(area.width.saturating_sub(2) as usize);
        let box_h: u16 = 3;
        if area.width < box_w as u16 + 2 || area.height < box_h + 2 {
            return Ok(());
        }
        let x = area.width.saturating_sub(box_w as u16 + 1);
        let y = area.height.saturating_sub(box_h + 1);
        let rect = Rect::new(x, y, box_w as u16, box_h);

        f.render_widget(Clear, rect);
        let block = Block::default()
            .borders(Borders::ALL)
            .border_type(theme.chrome.border_type())
            .border_style(Style::default().fg(theme.border_unfocused).bg(theme.bg_alt))
            .style(Style::default().bg(theme.bg_alt));
        let text = Line::from(vec![
            Span::styled(
                prefix.to_string(),
                Style::default().fg(theme.shuire).bg(theme.bg_alt),
            ),
            Span::styled(
                msg.to_string(),
                Style::default()
                    .fg(theme.context_fg)
                    .bg(theme.bg_alt)
                    .add_modifier(Modifier::BOLD),
            ),
        ]);
        let para = Paragraph::new(text).block(block);
        f.render_widget(para, rect);
        Ok(())
    }
}