scrin 0.1.82

A terminal UI toolkit with panes, widgets, overlays, animations, and Aisling-powered effects/loaders.
Documentation
use crate::core::buffer::{Buffer, Cell};
use crate::core::color::Color;
use crate::core::rect::Rect;
use crate::sanitize;
use crate::style::Style;
use crate::widgets::Widget;

#[derive(Debug, Clone)]
pub struct Tabs<'a> {
    pub titles: &'a [&'a str],
    pub selected: usize,
    pub style: Style,
    pub highlight_style: Style,
    pub divider: &'a str,
}

impl<'a> Tabs<'a> {
    pub fn new(titles: &'a [&'a str]) -> Self {
        Self {
            titles,
            selected: 0,
            style: Style::new().fg(Color::rgb(139, 148, 158)),
            highlight_style: Style::new().fg(Color::rgb(88, 166, 255)).bold(),
            divider: "",
        }
    }

    pub fn with_selected(mut self, selected: usize) -> Self {
        self.selected = selected;
        self
    }

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

    pub fn with_highlight_style(mut self, style: Style) -> Self {
        self.highlight_style = style;
        self
    }

    pub fn select_next(&mut self) {
        if self.titles.is_empty() {
            return;
        }
        self.selected = (self.selected + 1) % self.titles.len();
    }

    pub fn select_prev(&mut self) {
        if self.titles.is_empty() {
            return;
        }
        self.selected = if self.selected == 0 {
            self.titles.len() - 1
        } else {
            self.selected - 1
        };
    }
}

fn render_styled_str(
    buffer: &mut Buffer,
    x: usize,
    y: usize,
    s: &str,
    style: Style,
    max_width: usize,
) -> usize {
    let mut col = 0;
    for ch in s.chars() {
        let width = sanitize::char_display_width(ch);
        if width == 0 {
            continue;
        }
        if col + width > max_width {
            break;
        }
        let mut cell = Cell::new(ch, style.fg_or_default(), style.bg);
        cell.set_style(style);
        buffer.set(x + col, y, cell);
        col += width;
    }
    col
}

impl<'a> Widget for Tabs<'a> {
    fn render(&self, buffer: &mut Buffer, area: Rect) {
        if area.is_empty() || self.titles.is_empty() {
            return;
        }

        let mut x = area.x as usize;
        let y = area.y as usize;
        let right = area.right() as usize;
        for (i, title) in self.titles.iter().enumerate() {
            if x >= right {
                break;
            }
            let is_selected = i == self.selected;
            let title_style = if is_selected {
                self.highlight_style
            } else {
                self.style
            };
            x += render_styled_str(buffer, x, y, title, title_style, right.saturating_sub(x));

            if i < self.titles.len() - 1 && x < right {
                x += render_styled_str(
                    buffer,
                    x,
                    y,
                    self.divider,
                    self.style,
                    right.saturating_sub(x),
                );
            }
        }
    }
}

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

    #[test]
    fn tabs_render_applies_selected_and_plain_styles() {
        let tabs = Tabs::new(&["A", "B"])
            .with_selected(1)
            .with_style(Style::new().fg(Color::RED))
            .with_highlight_style(Style::new().fg(Color::GREEN).bold());
        let mut buf = Buffer::new(12, 1);

        tabs.render(&mut buf, Rect::new(0, 0, 12, 1));

        let first = buf.get(0, 0).unwrap();
        assert_eq!(first.ch, 'A');
        assert_eq!(first.fg, Color::RED);
        assert!(!first.bold);

        let selected = buf.get(4, 0).unwrap();
        assert_eq!(selected.ch, 'B');
        assert_eq!(selected.fg, Color::GREEN);
        assert!(selected.bold);
    }

    #[test]
    fn tabs_selection_helpers_ignore_empty_titles() {
        let titles: [&str; 0] = [];
        let mut tabs = Tabs::new(&titles);
        let mut buf = Buffer::new(4, 1);

        tabs.select_next();
        tabs.select_prev();
        tabs.render(&mut buf, Rect::new(0, 0, 4, 1));

        assert_eq!(tabs.selected, 0);
        assert_eq!(buf.to_plain_string(), "    ");
    }

    #[test]
    fn tabs_render_respects_offset_area_and_truncates() {
        let tabs = Tabs::new(&["ABCDE"]);
        let mut buf = Buffer::new(8, 1);

        tabs.render(&mut buf, Rect::new(2, 0, 3, 1));

        assert_eq!(buf.to_plain_string(), "  ABC   ");
    }

    #[test]
    fn tabs_render_advances_by_display_width() {
        let tabs = Tabs::new(&["中A"]);
        let mut buf = Buffer::new(4, 1);

        tabs.render(&mut buf, Rect::new(0, 0, 4, 1));

        assert_eq!(buf.get(0, 0).unwrap().ch, '');
        assert!(buf.is_skip(1, 0));
        assert_eq!(buf.get(2, 0).unwrap().ch, 'A');
    }
}