use crate::{prelude::*, style::Styled, widgets::Block};
const DEFAULT_HIGHLIGHT_STYLE: Style = Style::new().add_modifier(Modifier::REVERSED);
#[derive(Debug, Default, Clone, Eq, PartialEq, Hash)]
pub struct Tabs<'a> {
block: Option<Block<'a>>,
titles: Vec<Line<'a>>,
selected: usize,
style: Style,
highlight_style: Style,
divider: Span<'a>,
padding_left: Line<'a>,
padding_right: Line<'a>,
}
impl<'a> Tabs<'a> {
pub fn new<Iter>(titles: Iter) -> Self
where
Iter: IntoIterator,
Iter::Item: Into<Line<'a>>,
{
Self {
block: None,
titles: titles.into_iter().map(Into::into).collect(),
selected: 0,
style: Style::default(),
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
divider: Span::raw(symbols::line::VERTICAL),
padding_left: Line::from(" "),
padding_right: Line::from(" "),
}
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub const fn select(mut self, selected: usize) -> Self {
self.selected = selected;
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn style<S: Into<Style>>(mut self, style: S) -> Self {
self.style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn highlight_style<S: Into<Style>>(mut self, style: S) -> Self {
self.highlight_style = style.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn divider<T>(mut self, divider: T) -> Self
where
T: Into<Span<'a>>,
{
self.divider = divider.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn padding<T, U>(mut self, left: T, right: U) -> Self
where
T: Into<Line<'a>>,
U: Into<Line<'a>>,
{
self.padding_left = left.into();
self.padding_right = right.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn padding_left<T>(mut self, padding: T) -> Self
where
T: Into<Line<'a>>,
{
self.padding_left = padding.into();
self
}
#[must_use = "method moves the value of self and returns the modified value"]
pub fn padding_right<T>(mut self, padding: T) -> Self
where
T: Into<Line<'a>>,
{
self.padding_left = padding.into();
self
}
}
impl<'a> Styled for Tabs<'a> {
type Item = Self;
fn style(&self) -> Style {
self.style
}
fn set_style<S: Into<Style>>(self, style: S) -> Self::Item {
self.style(style)
}
}
impl Widget for Tabs<'_> {
fn render(self, area: Rect, buf: &mut Buffer) {
self.render_ref(area, buf);
}
}
impl WidgetRef for Tabs<'_> {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
buf.set_style(area, self.style);
self.block.render_ref(area, buf);
let inner = self.block.inner_if_some(area);
self.render_tabs(inner, buf);
}
}
impl Tabs<'_> {
fn render_tabs(&self, tabs_area: Rect, buf: &mut Buffer) {
if tabs_area.is_empty() {
return;
}
let mut x = tabs_area.left();
let titles_length = self.titles.len();
for (i, title) in self.titles.iter().enumerate() {
let last_title = titles_length - 1 == i;
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 {
break;
}
let pos = buf.set_line(x, tabs_area.top(), &self.padding_left, remaining_width);
x = pos.0;
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 {
break;
}
let pos = buf.set_line(x, tabs_area.top(), title, remaining_width);
if i == self.selected {
buf.set_style(
Rect {
x,
y: tabs_area.top(),
width: pos.0.saturating_sub(x),
height: 1,
},
self.highlight_style,
);
}
x = pos.0;
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 {
break;
}
let pos = buf.set_line(x, tabs_area.top(), &self.padding_right, remaining_width);
x = pos.0;
let remaining_width = tabs_area.right().saturating_sub(x);
if remaining_width == 0 || last_title {
break;
}
let pos = buf.set_span(x, tabs_area.top(), &self.divider, remaining_width);
x = pos.0;
}
}
}
impl<'a, Item> FromIterator<Item> for Tabs<'a>
where
Item: Into<Line<'a>>,
{
fn from_iter<Iter: IntoIterator<Item = Item>>(iter: Iter) -> Self {
Self::new(iter)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new() {
let titles = vec!["Tab1", "Tab2", "Tab3", "Tab4"];
let tabs = Tabs::new(titles.clone());
assert_eq!(
tabs,
Tabs {
block: None,
titles: vec![
Line::from("Tab1"),
Line::from("Tab2"),
Line::from("Tab3"),
Line::from("Tab4"),
],
selected: 0,
style: Style::default(),
highlight_style: DEFAULT_HIGHLIGHT_STYLE,
divider: Span::raw(symbols::line::VERTICAL),
padding_right: Line::from(" "),
padding_left: Line::from(" "),
}
);
}
#[test]
fn new_from_vec_of_str() {
Tabs::new(vec!["a", "b"]);
}
#[test]
fn collect() {
let tabs: Tabs = (0..5).map(|i| format!("Tab{i}")).collect();
assert_eq!(
tabs.titles,
vec![
Line::from("Tab0"),
Line::from("Tab1"),
Line::from("Tab2"),
Line::from("Tab3"),
Line::from("Tab4"),
],
);
}
#[track_caller]
fn test_case(tabs: Tabs, area: Rect, expected: &Buffer) {
let mut buffer = Buffer::empty(area);
tabs.render(area, &mut buffer);
assert_eq!(&buffer, expected);
}
#[test]
fn render_default() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_no_padding() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("", "");
let mut expected = Buffer::with_lines(["Tab1│Tab2│Tab3│Tab4 "]);
expected.set_style(Rect::new(0, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_more_padding() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).padding("---", "++");
let mut expected = Buffer::with_lines(["---Tab1++│---Tab2++│---Tab3++│"]);
expected.set_style(Rect::new(3, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_with_block() {
let tabs =
Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).block(Block::bordered().title("Tabs"));
let mut expected = Buffer::with_lines([
"┌Tabs────────────────────────┐",
"│ Tab1 │ Tab2 │ Tab3 │ Tab4 │",
"└────────────────────────────┘",
]);
expected.set_style(Rect::new(2, 1, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
test_case(tabs, Rect::new(0, 0, 30, 3), &expected);
}
#[test]
fn render_style() {
let tabs =
Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).style(Style::default().fg(Color::Red));
let mut expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 ".red()]);
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE.red());
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_select() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]);
let expected = Buffer::with_lines([Line::from(vec![
" ".into(),
"Tab1".reversed(),
" │ Tab2 │ Tab3 │ Tab4 ".into(),
])]);
test_case(tabs.clone().select(0), Rect::new(0, 0, 30, 1), &expected);
let expected = Buffer::with_lines([Line::from(vec![
" Tab1 │ ".into(),
"Tab2".reversed(),
" │ Tab3 │ Tab4 ".into(),
])]);
test_case(tabs.clone().select(1), Rect::new(0, 0, 30, 1), &expected);
let expected = Buffer::with_lines([Line::from(vec![
" Tab1 │ Tab2 │ Tab3 │ ".into(),
"Tab4".reversed(),
" ".into(),
])]);
test_case(tabs.clone().select(3), Rect::new(0, 0, 30, 1), &expected);
let expected = Buffer::with_lines([" Tab1 │ Tab2 │ Tab3 │ Tab4 "]);
test_case(tabs.clone().select(4), Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_style_and_selected() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"])
.style(Style::new().red())
.highlight_style(Style::new().underlined())
.select(0);
let expected = Buffer::with_lines([Line::from(vec![
" ".red(),
"Tab1".red().underlined(),
" │ Tab2 │ Tab3 │ Tab4 ".red(),
])]);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn render_divider() {
let tabs = Tabs::new(vec!["Tab1", "Tab2", "Tab3", "Tab4"]).divider("--");
let mut expected = Buffer::with_lines([" Tab1 -- Tab2 -- Tab3 -- Tab4 "]);
expected.set_style(Rect::new(1, 0, 4, 1), DEFAULT_HIGHLIGHT_STYLE);
test_case(tabs, Rect::new(0, 0, 30, 1), &expected);
}
#[test]
fn can_be_stylized() {
assert_eq!(
Tabs::new(vec![""])
.black()
.on_white()
.bold()
.not_italic()
.style,
Style::default()
.fg(Color::Black)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
.remove_modifier(Modifier::ITALIC)
);
}
}