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::style::Style;
use crate::theme::ThemeTokens;
use crate::widgets::Widget;

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum BorderStyle {
    None,
    Plain,
    Rounded,
    Double,
    Thick,
    Custom {
        top_left: char,
        top_right: char,
        bottom_left: char,
        bottom_right: char,
        top: char,
        bottom: char,
        left: char,
        right: char,
    },
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Block<'a> {
    pub title: &'a str,
    pub title_right: Option<&'a str>,
    pub borders: BorderStyle,
    pub border_color: Color,
    pub bg: Option<Color>,
    pub style: Style,
    pub inner_margin: Rect,
}

impl<'a> Block<'a> {
    pub fn new(title: &'a str) -> Self {
        Self {
            title,
            title_right: None,
            borders: BorderStyle::Rounded,
            border_color: Color::rgb(48, 54, 61),
            bg: None,
            style: Style::new(),
            inner_margin: Rect::ZERO,
        }
    }

    pub fn bordered() -> Self {
        Self::new("").with_borders(BorderStyle::Plain)
    }

    pub fn inner_for_bordered(area: Rect) -> Rect {
        Rect::new(
            area.x.saturating_add(1),
            area.y.saturating_add(1),
            area.width.saturating_sub(2),
            area.height.saturating_sub(2),
        )
    }

    pub fn title(mut self, title: &'a str) -> Self {
        self.title = title;
        self
    }

    pub fn border_style(mut self, style: Style) -> Self {
        if let Some(fg) = style.fg {
            self.border_color = fg;
        }
        if let Some(bg) = style.bg {
            self.bg = Some(bg);
        }
        self.style = self.style.merge(&style);
        self
    }

    pub fn with_theme_tokens(mut self, tokens: ThemeTokens) -> Self {
        self.border_color = tokens.dim;
        self.bg = Some(tokens.panel);
        self.style = tokens.panel_style();
        self
    }

    pub fn with_borders(mut self, borders: BorderStyle) -> Self {
        self.borders = borders;
        self
    }

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

    pub fn with_bg(mut self, bg: Color) -> Self {
        self.bg = Some(bg);
        self
    }

    pub fn with_title_right(mut self, title: &'a str) -> Self {
        self.title_right = Some(title);
        self
    }

    pub fn with_inner_margin(mut self, margin: Rect) -> Self {
        self.inner_margin = margin;
        self
    }

    pub fn inner(&self, area: Rect) -> Rect {
        match self.borders {
            BorderStyle::None => area,
            _ => Rect::new(
                area.x.saturating_add(1),
                area.y.saturating_add(1),
                area.width.saturating_sub(2),
                area.height.saturating_sub(2),
            )
            .inner(self.inner_margin),
        }
    }

    fn chars_for_border(
        &self,
        style: &BorderStyle,
    ) -> (char, char, char, char, char, char, char, char) {
        match style {
            BorderStyle::None => (' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '),
            BorderStyle::Plain => ('', '', '', '', '', '', '', ''),
            BorderStyle::Rounded => ('', '', '', '', '', '', '', ''),
            BorderStyle::Double => ('', '', '', '', '', '', '', ''),
            BorderStyle::Thick => ('', '', '', '', '', '', '', ''),
            BorderStyle::Custom {
                top_left,
                top_right,
                bottom_left,
                bottom_right,
                top,
                bottom,
                left,
                right,
            } => (
                *top_left,
                *top_right,
                *bottom_left,
                *bottom_right,
                *top,
                *bottom,
                *left,
                *right,
            ),
        }
    }
}

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

    #[test]
    fn block_inner_defaults_to_one_cell_border_only() {
        let block = Block::new("title");
        assert_eq!(block.inner(Rect::new(0, 0, 10, 6)), Rect::new(1, 1, 8, 4));
    }

    #[test]
    fn block_builder_aliases_match_old_shape() {
        let block = Block::bordered()
            .title("demo")
            .border_style(Style::new().fg(Color::CYAN));
        assert_eq!(block.title, "demo");
        assert_eq!(block.borders, BorderStyle::Plain);
        assert_eq!(block.border_color, Color::CYAN);
    }

    #[test]
    fn block_inner_for_bordered_matches_one_cell_border() {
        assert_eq!(
            Block::inner_for_bordered(Rect::new(2, 3, 10, 6)),
            Rect::new(3, 4, 8, 4)
        );
    }
}

impl<'a> Widget for Block<'a> {
    fn render(&self, buffer: &mut Buffer, area: Rect) {
        if area.width < 2 || area.height < 2 {
            return;
        }

        if let Some(bg) = self.bg {
            buffer.fill(area, ' ', Color::WHITE, Some(bg));
        }

        if self.borders == BorderStyle::None {
            return;
        }

        let (tl, tr, bl, br, top, bot, l, r) = self.chars_for_border(&self.borders);

        for x in (area.x + 1) as usize..(area.right() - 1) as usize {
            buffer.set(
                x,
                area.y as usize,
                crate::core::buffer::Cell {
                    ch: top,
                    fg: self.border_color,
                    bg: self.bg,
                    bold: false,
                    italic: false,
                    underlined: false,
                },
            );
            buffer.set(
                x,
                (area.bottom() - 1) as usize,
                crate::core::buffer::Cell {
                    ch: bot,
                    fg: self.border_color,
                    bg: self.bg,
                    bold: false,
                    italic: false,
                    underlined: false,
                },
            );
        }

        for y in (area.y + 1) as usize..(area.bottom() - 1) as usize {
            buffer.set(
                area.x as usize,
                y,
                crate::core::buffer::Cell {
                    ch: l,
                    fg: self.border_color,
                    bg: self.bg,
                    bold: false,
                    italic: false,
                    underlined: false,
                },
            );
            buffer.set(
                (area.right() - 1) as usize,
                y,
                crate::core::buffer::Cell {
                    ch: r,
                    fg: self.border_color,
                    bg: self.bg,
                    bold: false,
                    italic: false,
                    underlined: false,
                },
            );
        }

        buffer.set(
            area.x as usize,
            area.y as usize,
            crate::core::buffer::Cell {
                ch: tl,
                fg: self.border_color,
                bg: self.bg,
                bold: true,
                italic: false,
                underlined: false,
            },
        );
        buffer.set(
            (area.right() - 1) as usize,
            area.y as usize,
            crate::core::buffer::Cell {
                ch: tr,
                fg: self.border_color,
                bg: self.bg,
                bold: true,
                italic: false,
                underlined: false,
            },
        );
        buffer.set(
            area.x as usize,
            (area.bottom() - 1) as usize,
            crate::core::buffer::Cell {
                ch: bl,
                fg: self.border_color,
                bg: self.bg,
                bold: true,
                italic: false,
                underlined: false,
            },
        );
        buffer.set(
            (area.right() - 1) as usize,
            (area.bottom() - 1) as usize,
            crate::core::buffer::Cell {
                ch: br,
                fg: self.border_color,
                bg: self.bg,
                bold: true,
                italic: false,
                underlined: false,
            },
        );

        if !self.title.is_empty() {
            let title_text = format!(" {} ", self.title);
            let max_title_w = area.width.saturating_sub(4) as usize;
            let display: String = title_text.chars().take(max_title_w).collect();
            let title_x = (area.x + 2) as usize;
            buffer.set_str_bold(
                title_x,
                area.y as usize,
                &display,
                self.border_color.brighten(0.25),
                self.bg,
            );
        }

        if let Some(right_title) = self.title_right {
            let rt = format!(" {} ", right_title);
            let max_title_w = area.width.saturating_sub(4) as usize;
            let display: String = rt.chars().take(max_title_w).collect();
            let rx = (area.right() as usize).saturating_sub(display.len() + 2);
            buffer.set_str_bold(
                rx,
                area.y as usize,
                &display,
                self.border_color.brighten(0.18),
                self.bg,
            );
        }
    }
}