scrin 0.1.74

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 crate::widgets::block::{Block, BorderStyle};
use crate::widgets::paragraph::Paragraph;
use crate::widgets::Widget;
use std::time::Duration;

#[derive(Debug, Clone, PartialEq)]
pub enum PopupStyle {
    Floating,
    Drawer,
    Tooltip,
    Panel,
}

#[derive(Debug, Clone)]
pub struct Popup {
    pub title: String,
    pub content: String,
    pub style: PopupStyle,
    pub visible: bool,
    pub position: PopupPosition,
    pub width: u16,
    pub height: u16,
    pub min_width: u16,
    pub min_height: u16,
    pub max_width: u16,
    pub max_height: u16,
    pub border_color: Color,
    pub bg: Color,
    pub opacity: f32,
    pub transition: Transition,
    pub transition_progress: f32,
    pub auto_close_on_resize: bool,
    pub close_on_esc: bool,
    pub close_on_click_outside: bool,
}

#[derive(Debug, Clone, Copy, PartialEq)]
pub enum PopupPosition {
    TopLeft,
    TopCenter,
    TopRight,
    CenterLeft,
    Center,
    CenterRight,
    BottomLeft,
    BottomCenter,
    BottomRight,
    FollowCursor(u16, u16),
}

impl Popup {
    pub fn new(title: &str, content: &str) -> Self {
        Self {
            title: title.to_string(),
            content: content.to_string(),
            style: PopupStyle::Floating,
            visible: false,
            position: PopupPosition::Center,
            width: 40,
            height: 12,
            min_width: 10,
            min_height: 3,
            max_width: 80,
            max_height: 40,
            border_color: Color::rgb(88, 166, 255),
            bg: Color::rgb(22, 27, 34),
            opacity: 1.0,
            transition: Transition::Fade,
            transition_progress: 0.0,
            auto_close_on_resize: true,
            close_on_esc: true,
            close_on_click_outside: false,
        }
    }

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

    pub fn set_style(&mut self, style: PopupStyle) {
        match style {
            PopupStyle::Floating => {
                self.border_color = Color::rgb(88, 166, 255);
            }
            PopupStyle::Drawer => {
                self.border_color = Color::rgb(63, 185, 80);
            }
            PopupStyle::Tooltip => {
                self.border_color = Color::rgb(255, 178, 72);
            }
            PopupStyle::Panel => {
                self.border_color = Color::rgb(248, 81, 73);
            }
        }
        self.style = style;
    }

    pub fn with_size(mut self, w: u16, h: u16) -> Self {
        self.width = w;
        self.height = h;
        self
    }

    pub fn with_min_size(mut self, w: u16, h: u16) -> Self {
        self.min_width = w;
        self.min_height = h;
        self
    }

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

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

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

    pub fn with_auto_close_on_resize(mut self, v: bool) -> Self {
        self.auto_close_on_resize = v;
        self
    }

    pub fn adapt_to_terminal(&mut self, cols: u16, rows: u16) {
        let max_w = cols.saturating_sub(4);
        let max_h = rows.saturating_sub(4);
        self.width = self
            .width
            .min(max_w)
            .max(self.min_width)
            .min(self.max_width);
        self.height = self
            .height
            .min(max_h)
            .max(self.min_height)
            .min(self.max_height);
        if self.auto_close_on_resize && (cols < 20 || rows < 8) {
            self.visible = false;
        }
    }

    fn calc_position(&self, screen: Rect) -> (u16, u16) {
        match self.position {
            PopupPosition::TopLeft => (screen.x + 1, screen.y + 1),
            PopupPosition::TopCenter => (
                screen.x + (screen.width.saturating_sub(self.width)) / 2,
                screen.y + 1,
            ),
            PopupPosition::TopRight => {
                (screen.right().saturating_sub(self.width + 1), screen.y + 1)
            }
            PopupPosition::CenterLeft => (
                screen.x + 1,
                screen.y + (screen.height.saturating_sub(self.height)) / 2,
            ),
            PopupPosition::Center => (
                screen.x + (screen.width.saturating_sub(self.width)) / 2,
                screen.y + (screen.height.saturating_sub(self.height)) / 2,
            ),
            PopupPosition::CenterRight => (
                screen.right().saturating_sub(self.width + 1),
                screen.y + (screen.height.saturating_sub(self.height)) / 2,
            ),
            PopupPosition::BottomLeft => (
                screen.x + 1,
                screen.bottom().saturating_sub(self.height + 1),
            ),
            PopupPosition::BottomCenter => (
                screen.x + (screen.width.saturating_sub(self.width)) / 2,
                screen.bottom().saturating_sub(self.height + 1),
            ),
            PopupPosition::BottomRight => (
                screen.right().saturating_sub(self.width + 1),
                screen.bottom().saturating_sub(self.height + 1),
            ),
            PopupPosition::FollowCursor(cx, cy) => {
                let x = cx.min(screen.right().saturating_sub(self.width));
                let y = cy.min(screen.bottom().saturating_sub(self.height));
                (x, y)
            }
        }
    }

    fn render_floating(&self, buffer: &mut Buffer, area: Rect) {
        let (x, y) = self.calc_position(area);
        let rect = Rect::new(x, y, self.width, self.height);

        let block = Block::new(&self.title)
            .with_borders(match self.style {
                PopupStyle::Floating => BorderStyle::Rounded,
                PopupStyle::Drawer => BorderStyle::Double,
                PopupStyle::Tooltip => BorderStyle::Plain,
                PopupStyle::Panel => BorderStyle::Thick,
            })
            .with_border_color(self.border_color)
            .with_bg(self.bg);
        block.render(buffer, rect);

        let inner = block.inner(rect);
        let p = Paragraph::new(&self.content);
        p.render(buffer, inner);
    }
}

impl Overlay for Popup {
    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.visible {
            self.render_floating(buffer, area);
        }
    }

    fn position(&self, screen: Rect) -> Rect {
        let (x, y) = self.calc_position(screen);
        Rect::new(x, y, self.width, self.height)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_popup_creation() {
        let p = Popup::new("Test", "content");
        assert!(!p.visible);
        assert_eq!(p.width, 40);
    }

    #[test]
    fn test_popup_toggle() {
        let mut p = Popup::new("Test", "content");
        p.toggle();
        assert!(p.visible);
        p.toggle();
        assert!(!p.visible);
    }

    #[test]
    fn test_popup_adapt_to_terminal() {
        let mut p = Popup::new("T", "c").with_size(60, 30);
        p.adapt_to_terminal(30, 15);
        assert!(p.width <= 26);
        assert!(p.height <= 11);
    }

    #[test]
    fn test_popup_adapt_close_on_small_terminal() {
        let mut p = Popup::new("T", "c").with_auto_close_on_resize(true);
        p.show();
        p.adapt_to_terminal(10, 5);
        assert!(!p.visible);
    }

    #[test]
    fn test_popup_calc_position_center() {
        let p = Popup::new("T", "c")
            .with_size(20, 5)
            .with_position(PopupPosition::Center);
        let screen = Rect::new(0, 0, 80, 24);
        let pos = p.calc_position(screen);
        assert_eq!(pos, (30, 9));
    }
}