scrin 0.1.83

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, Transition};
use std::time::Duration;

#[derive(Debug, Clone, PartialEq)]
pub enum ModalStyle {
    Dialog,
    Confirm,
    Prompt,
    FullScreen,
}

#[derive(Debug, Clone)]
pub struct Modal {
    pub title: String,
    pub content: String,
    pub style: ModalStyle,
    pub visible: bool,
    pub opacity: f32,
    pub transition: Transition,
    pub transition_progress: f32,
    pub border_color: Color,
    pub title_color: Color,
    pub content_color: Color,
    pub bg: Color,
    pub selected_index: usize,
    pub options: Vec<String>,
}

impl Modal {
    pub fn new(title: &str, content: &str) -> Self {
        Self {
            title: title.to_string(),
            content: content.to_string(),
            style: ModalStyle::Dialog,
            visible: false,
            opacity: 1.0,
            transition: Transition::Fade,
            transition_progress: 0.0,
            border_color: Color::rgb(88, 166, 255),
            title_color: Color::rgb(201, 209, 217),
            content_color: Color::rgb(139, 148, 158),
            bg: Color::rgb(22, 27, 34),
            selected_index: 0,
            options: Vec::new(),
        }
    }

    pub fn with_style(mut self, style: ModalStyle) -> Self {
        self.style = style;
        self
    }

    pub fn with_options(mut self, options: Vec<String>) -> Self {
        self.options = options;
        self
    }

    pub fn with_border_color(mut self, color: Color) -> Self {
        self.border_color = color;
        self
    }

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

    pub fn select_next(&mut self) {
        if !self.options.is_empty() {
            self.selected_index = (self.selected_index + 1) % self.options.len();
        }
    }

    pub fn select_prev(&mut self) {
        if !self.options.is_empty() {
            self.selected_index = if self.selected_index == 0 {
                self.options.len() - 1
            } else {
                self.selected_index - 1
            };
        }
    }

    pub fn selected_option(&self) -> Option<&str> {
        self.options.get(self.selected_index).map(|s| s.as_str())
    }

    fn render_modal(&self, buffer: &mut Buffer, area: Rect) {
        let (w, h) = match self.style {
            ModalStyle::Dialog => (40.min(area.width), 12.min(area.height)),
            ModalStyle::Confirm => (36.min(area.width), 8.min(area.height)),
            ModalStyle::Prompt => (50.min(area.width), 6.min(area.height)),
            ModalStyle::FullScreen => (area.width, area.height),
        };
        let x = area.x + (area.width.saturating_sub(w)) / 2;
        let y = area.y + (area.height.saturating_sub(h)) / 2;
        let modal_rect = Rect::new(x, y, w, h);

        buffer.fill(modal_rect, ' ', Color::WHITE, Some(self.bg));

        for dx in 1..w.saturating_sub(1) {
            buffer.set(
                (x + dx) as usize,
                y as usize,
                crate::core::buffer::Cell {
                    ch: '',
                    fg: self.border_color,
                    bg: Some(self.bg),
                    bold: false,
                    italic: false,
                    underlined: false,
                },
            );
            buffer.set(
                (x + dx) as usize,
                (y + h - 1) as usize,
                crate::core::buffer::Cell {
                    ch: '',
                    fg: self.border_color,
                    bg: Some(self.bg),
                    bold: false,
                    italic: false,
                    underlined: false,
                },
            );
        }

        buffer.set(
            x as usize,
            y as usize,
            crate::core::buffer::Cell {
                ch: '',
                fg: self.border_color,
                bg: Some(self.bg),
                bold: true,
                italic: false,
                underlined: false,
            },
        );
        buffer.set(
            (x + w - 1) as usize,
            y as usize,
            crate::core::buffer::Cell {
                ch: '',
                fg: self.border_color,
                bg: Some(self.bg),
                bold: true,
                italic: false,
                underlined: false,
            },
        );
        buffer.set(
            x as usize,
            (y + h - 1) as usize,
            crate::core::buffer::Cell {
                ch: '',
                fg: self.border_color,
                bg: Some(self.bg),
                bold: true,
                italic: false,
                underlined: false,
            },
        );
        buffer.set(
            (x + w - 1) as usize,
            (y + h - 1) as usize,
            crate::core::buffer::Cell {
                ch: '',
                fg: self.border_color,
                bg: Some(self.bg),
                bold: true,
                italic: false,
                underlined: false,
            },
        );

        let title_text = format!(" {} ", self.title);
        let title_display: String = title_text.chars().take((w - 4) as usize).collect();
        let title_x = x + (w.saturating_sub(title_display.len() as u16 + 4)) / 2;
        buffer.set_str(
            title_x as usize,
            y as usize,
            &title_display,
            self.title_color,
            Some(self.bg),
        );

        let content_display: String = self.content.chars().take((w - 4) as usize).collect();
        buffer.set_str(
            (x + 2) as usize,
            (y + 2) as usize,
            &content_display,
            self.content_color,
            Some(self.bg),
        );

        if !self.options.is_empty() {
            for (i, option) in self.options.iter().enumerate() {
                let opt_y = (y + 4 + i as u16).min(y + h - 2);
                let selected = i == self.selected_index;
                let prefix = if selected { " > " } else { "   " };
                let opt_display: String = option.chars().take((w - 6) as usize).collect();
                let color = if selected {
                    self.border_color
                } else {
                    self.content_color
                };
                let text = format!("{}{}", prefix, opt_display);
                buffer.set_str(
                    (x + 2) as usize,
                    opt_y as usize,
                    &text,
                    color,
                    Some(self.bg),
                );
            }
        }
    }
}

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

    fn show(&mut self) {
        self.visible = true;
        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) {
        if self.transition == Transition::Fade && self.transition_progress < 1.0 {
            self.transition_progress =
                (self.transition_progress + delta.as_secs_f32() * 4.0).min(1.0);
        }
    }

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

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