use retroglyph_core::{Color, Rect, Style};
use super::Widget;
use crate::Surface;
use crate::Theme;
use crate::draw::fill_rect;
use crate::text::truncate as truncate_to_cols;
#[derive(Clone, Copy, Debug)]
pub struct Tabs<'a> {
titles: &'a [&'a str],
selected: Option<usize>,
style: Style,
selected_style: Style,
column_spacing: u16,
divider: Option<char>,
}
impl<'a> Tabs<'a> {
#[must_use]
pub fn new(titles: &'a [&'a str]) -> Self {
Self {
titles,
selected: None,
style: Style::new().fg(Color::Rgb {
r: 170,
g: 175,
b: 190,
}),
selected_style: Style::new().fg(Color::BRIGHT_WHITE).bg(Color::Rgb {
r: 40,
g: 60,
b: 90,
}),
column_spacing: 1,
divider: None,
}
}
#[must_use]
pub const fn select(mut self, index: Option<usize>) -> Self {
self.selected = index;
self
}
#[must_use]
pub const fn style(mut self, style: Style) -> Self {
self.style = style;
self
}
#[must_use]
pub const fn selected_style(mut self, style: Style) -> Self {
self.selected_style = style;
self
}
#[must_use]
pub const fn column_spacing(mut self, spacing: u16) -> Self {
self.column_spacing = spacing;
self
}
#[must_use]
pub const fn divider(mut self, divider: Option<char>) -> Self {
self.divider = divider;
self
}
#[must_use]
pub fn theme(self, theme: Theme) -> Self {
self.theme_on(theme, theme.panel_bg)
}
#[must_use]
pub fn theme_on(mut self, theme: Theme, bg: Color) -> Self {
self.style = Style::new().fg(theme.dim).bg(bg);
self.selected_style = Style::new().fg(theme.accent).bg(bg);
self
}
}
impl Widget for Tabs<'_> {
fn render(&self, area: Rect, surface: &mut Surface<'_>) {
if area.width() == 0 || area.height() == 0 {
return;
}
let y = area.top();
let mut x = area.left();
for (index, &title) in self.titles.iter().enumerate() {
if x >= area.right() {
break;
}
let avail = (area.right() - x) as usize;
let text = truncate_to_cols(title, avail);
let style = if Some(index) == self.selected {
self.selected_style
} else {
self.style
};
#[allow(clippy::cast_possible_truncation)]
let text_width = text.chars().count() as u16;
if Some(index) == self.selected && text_width > 0 {
fill_rect(
surface,
Rect::new(x, y, text_width, 1),
' ',
Style::new().bg(style.background()),
);
}
surface.print((x, y), text, style);
x = x.saturating_add(text_width);
if index + 1 < self.titles.len() {
if let Some(divider) = self.divider {
let mid = x + self.column_spacing / 2;
if mid < area.right() {
surface.put((mid, y), divider, Style::new());
}
}
x = x.saturating_add(self.column_spacing);
}
}
}
}
#[cfg(test)]
mod tests {
use retroglyph_core::{Grid, Pos};
use super::*;
#[test]
fn draws_every_title_left_to_right() {
let area = Rect::new(0, 0, 20, 1);
let titles = ["One", "Two"];
let mut grid = Grid::new(20, 1);
Tabs::new(&titles).render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'O');
assert_eq!(grid[Pos::new(4, 0)].glyph(), 'T');
}
#[test]
fn highlights_the_selected_tab() {
let area = Rect::new(0, 0, 20, 1);
let titles = ["One", "Two"];
let mut grid = Grid::new(20, 1);
Tabs::new(&titles)
.select(Some(1))
.render(area, &mut Surface::new(&mut grid, area, 0));
let selected_bg = grid[Pos::new(4, 0)].style().background();
let plain_bg = grid[Pos::new(0, 0)].style().background();
assert_ne!(selected_bg, plain_bg);
}
#[test]
fn nothing_highlighted_when_unselected() {
let area = Rect::new(0, 0, 20, 1);
let titles = ["One", "Two"];
let mut grid = Grid::new(20, 1);
Tabs::new(&titles).render(area, &mut Surface::new(&mut grid, area, 0));
let bg0 = grid[Pos::new(0, 0)].style().background();
let bg1 = grid[Pos::new(4, 0)].style().background();
assert_eq!(bg0, bg1);
}
#[test]
fn column_spacing_can_be_overridden() {
let area = Rect::new(0, 0, 20, 1);
let titles = ["A", "B"];
let mut grid = Grid::new(20, 1);
Tabs::new(&titles)
.column_spacing(3)
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'A');
assert_eq!(grid[Pos::new(4, 0)].glyph(), 'B');
}
#[test]
fn divider_renders_between_tabs_when_set() {
let area = Rect::new(0, 0, 20, 1);
let titles = ["A", "B"];
let mut grid = Grid::new(20, 1);
Tabs::new(&titles)
.column_spacing(3)
.divider(Some('|'))
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(2, 0)].glyph(), '|');
}
#[test]
fn no_divider_by_default() {
let area = Rect::new(0, 0, 20, 1);
let titles = ["A", "B"];
let mut grid = Grid::new(20, 1);
Tabs::new(&titles).render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(1, 0)].glyph(), ' ');
}
#[test]
fn stops_drawing_past_the_area_width_without_panicking() {
let area = Rect::new(0, 0, 4, 1);
let titles = ["Alpha", "Bravo", "Charlie"];
let mut grid = Grid::new(4, 1);
Tabs::new(&titles).render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), 'A');
}
#[test]
fn style_can_be_overridden() {
use retroglyph_core::Color;
let area = Rect::new(0, 0, 20, 1);
let titles = ["One"];
let custom = Style::new().fg(Color::RED);
let mut grid = Grid::new(20, 1);
Tabs::new(&titles)
.style(custom)
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Color::RED);
}
#[test]
fn selected_style_can_be_overridden() {
use retroglyph_core::Color;
let area = Rect::new(0, 0, 20, 1);
let titles = ["One"];
let custom = Style::new().fg(Color::GREEN).bg(Color::BLUE);
let mut grid = Grid::new(20, 1);
Tabs::new(&titles)
.selected_style(custom)
.select(Some(0))
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Color::GREEN);
assert_eq!(grid[Pos::new(0, 0)].style().background(), Color::BLUE);
}
#[test]
fn zero_width_is_a_no_op() {
let area = Rect::new(0, 0, 0, 1);
let titles = ["One"];
let mut grid = Grid::new(1, 1);
Tabs::new(&titles).render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].glyph(), ' ');
}
#[test]
fn theme_maps_named_roles_onto_style_and_selected_style() {
let area = Rect::new(0, 0, 20, 1);
let titles = ["One", "Two"];
let mut grid = Grid::new(20, 1);
Tabs::new(&titles)
.theme(Theme::DARK)
.select(Some(1))
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(grid[Pos::new(0, 0)].style().foreground(), Theme::DARK.dim);
assert_eq!(
grid[Pos::new(0, 0)].style().background(),
Theme::DARK.panel_bg
);
assert_eq!(
grid[Pos::new(4, 0)].style().foreground(),
Theme::DARK.accent
);
assert_eq!(
grid[Pos::new(4, 0)].style().background(),
Theme::DARK.panel_bg
);
}
#[test]
fn theme_on_uses_the_given_backdrop_instead_of_panel_bg() {
use retroglyph_core::Color;
let area = Rect::new(0, 0, 20, 1);
let titles = ["One"];
let mut grid = Grid::new(20, 1);
Tabs::new(&titles)
.theme_on(Theme::DARK, Color::Default)
.select(Some(0))
.render(area, &mut Surface::new(&mut grid, area, 0));
assert_eq!(
grid[Pos::new(0, 0)].style().foreground(),
Theme::DARK.accent
);
assert_eq!(grid[Pos::new(0, 0)].style().background(), Color::Default);
}
}