scrin 0.1.80

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::buffer::Buffer;
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::overlays::{Overlay, OverlayPosition, Transition};
use std::time::Duration;

#[derive(Debug, Clone, PartialEq)]
pub enum ToastKind {
    Info,
    Success,
    Warning,
    Error,
    Copy,
}

impl ToastKind {
    pub fn icon(&self) -> &str {
        match self {
            ToastKind::Info => "",
            ToastKind::Success => "",
            ToastKind::Warning => "",
            ToastKind::Error => "",
            ToastKind::Copy => "📋",
        }
    }

    pub fn color(&self, theme_color: Color) -> Color {
        match self {
            ToastKind::Info => theme_color,
            ToastKind::Success => Color::rgb(63, 185, 80),
            ToastKind::Warning => Color::rgb(210, 153, 34),
            ToastKind::Error => Color::rgb(248, 81, 73),
            ToastKind::Copy => Color::rgb(88, 166, 255),
        }
    }
}

#[derive(Debug, Clone)]
pub struct Toast {
    pub message: String,
    pub kind: ToastKind,
    pub position: OverlayPosition,
    pub visible: bool,
    pub opacity: f32,
    pub lifetime: Duration,
    pub elapsed: Duration,
    pub transition: Transition,
    pub transition_progress: f32,
    pub max_width: u16,
}

impl Toast {
    pub fn new(message: &str, kind: ToastKind) -> Self {
        Self {
            message: message.to_string(),
            kind,
            position: OverlayPosition::BottomCenter,
            visible: true,
            opacity: 1.0,
            lifetime: Duration::from_secs(3),
            elapsed: Duration::ZERO,
            transition: Transition::Fade,
            transition_progress: 1.0,
            max_width: 50,
        }
    }

    pub fn with_position(mut self, pos: OverlayPosition) -> Self {
        self.position = pos;
        self
    }

    pub fn with_lifetime(mut self, dur: Duration) -> Self {
        self.lifetime = dur;
        self
    }

    pub fn with_transition(mut self, t: Transition) -> Self {
        self.transition = t;
        self
    }

    pub fn with_max_width(mut self, w: u16) -> Self {
        self.max_width = w;
        self
    }

    pub fn is_expired(&self) -> bool {
        self.elapsed >= self.lifetime
    }

    fn render_toast(&self, buffer: &mut Buffer, area: Rect) {
        let icon = self.kind.icon();
        let color = self.kind.color(Color::rgb(88, 166, 255));
        let text = format!(" {} {} ", icon, self.message);
        let display_text: String = text.chars().take(self.max_width as usize).collect();
        let w = display_text.len() as u16 + 4;
        let h = 3;
        let pos = self.position_in_area(area, w, h);
        let toast_rect = Rect::new(pos.0, pos.1, w, h);

        let bg = Color::rgb(22, 27, 34);
        let border = color;
        buffer.fill(toast_rect, ' ', Color::WHITE, Some(bg));

        for x in toast_rect.x..toast_rect.right() {
            buffer.set(
                x as usize,
                toast_rect.y as usize,
                crate::core::buffer::Cell {
                    ch: '',
                    fg: border.dim(0.3),
                    bg: Some(bg),
                    bold: false,
                    italic: false,
                    underlined: false,
                },
            );
            buffer.set(
                x as usize,
                toast_rect.bottom() as usize - 1,
                crate::core::buffer::Cell {
                    ch: '',
                    fg: border.dim(0.3),
                    bg: Some(bg),
                    bold: false,
                    italic: false,
                    underlined: false,
                },
            );
        }

        buffer.set(
            toast_rect.x as usize,
            toast_rect.y as usize,
            crate::core::buffer::Cell {
                ch: '',
                fg: border,
                bg: Some(bg),
                bold: true,
                italic: false,
                underlined: false,
            },
        );
        buffer.set(
            toast_rect.right() as usize - 1,
            toast_rect.y as usize,
            crate::core::buffer::Cell {
                ch: '',
                fg: border,
                bg: Some(bg),
                bold: true,
                italic: false,
                underlined: false,
            },
        );
        buffer.set(
            toast_rect.x as usize,
            toast_rect.bottom() as usize - 1,
            crate::core::buffer::Cell {
                ch: '',
                fg: border,
                bg: Some(bg),
                bold: true,
                italic: false,
                underlined: false,
            },
        );
        buffer.set(
            toast_rect.right() as usize - 1,
            toast_rect.bottom() as usize - 1,
            crate::core::buffer::Cell {
                ch: '',
                fg: border,
                bg: Some(bg),
                bold: true,
                italic: false,
                underlined: false,
            },
        );

        let text_x = toast_rect.x as usize + 2;
        let text_y = toast_rect.y as usize + 1;
        buffer.set_str(text_x, text_y, &display_text, color, Some(bg));
    }

    fn position_in_area(&self, area: Rect, w: u16, h: u16) -> (u16, u16) {
        match self.position {
            OverlayPosition::TopLeft => (area.x + 1, area.y + 1),
            OverlayPosition::TopCenter => (area.x + (area.width.saturating_sub(w)) / 2, area.y + 1),
            OverlayPosition::TopRight => (area.right().saturating_sub(w + 1), area.y + 1),
            OverlayPosition::CenterLeft => {
                (area.x + 1, area.y + (area.height.saturating_sub(h)) / 2)
            }
            OverlayPosition::Center => (
                area.x + (area.width.saturating_sub(w)) / 2,
                area.y + (area.height.saturating_sub(h)) / 2,
            ),
            OverlayPosition::CenterRight => (
                area.right().saturating_sub(w + 1),
                area.y + (area.height.saturating_sub(h)) / 2,
            ),
            OverlayPosition::BottomLeft => (area.x + 1, area.bottom().saturating_sub(h + 1)),
            OverlayPosition::BottomCenter => (
                area.x + (area.width.saturating_sub(w)) / 2,
                area.bottom().saturating_sub(h + 1),
            ),
            OverlayPosition::BottomRight => (
                area.right().saturating_sub(w + 1),
                area.bottom().saturating_sub(h + 1),
            ),
        }
    }
}

impl Overlay for Toast {
    fn is_visible(&self) -> bool {
        self.visible && !self.is_expired()
    }

    fn show(&mut self) {
        self.visible = true;
        self.elapsed = Duration::ZERO;
        self.transition_progress = 0.0;
    }

    fn hide(&mut self) {
        self.visible = false;
    }

    fn toggle(&mut self) {
        if self.visible {
            self.hide();
        } else {
            self.show();
        }
    }

    fn update(&mut self, delta: Duration) {
        self.elapsed += delta;
        let fade_start = self.lifetime.saturating_sub(Duration::from_secs(1));
        if self.elapsed > fade_start {
            let fade_elapsed = self.elapsed - fade_start;
            let fade_duration = self.lifetime - fade_start;
            if !fade_duration.is_zero() {
                self.opacity =
                    1.0 - (fade_elapsed.as_secs_f32() / fade_duration.as_secs_f32()).min(1.0);
            }
        }
        if self.transition == Transition::Fade && self.transition_progress < 1.0 {
            self.transition_progress =
                (self.transition_progress + delta.as_secs_f32() * 3.0).min(1.0);
        }
    }

    fn render(&self, buffer: &mut Buffer, area: Rect) {
        if self.is_visible() {
            self.render_toast(buffer, area);
        }
    }

    fn position(&self, screen: Rect) -> Rect {
        screen
    }
}

pub fn copy_toast(message: &str) -> Toast {
    Toast::new(message, ToastKind::Copy)
        .with_position(OverlayPosition::BottomCenter)
        .with_lifetime(Duration::from_secs(2))
        .with_transition(Transition::Fade)
}