use crate::element::{Component, Element};
use crate::style::{Color, Modifier, Style};
#[derive(Debug, Clone)]
pub struct Tab {
pub label: String,
pub color: Option<Color>,
pub disabled: bool,
}
impl Tab {
pub fn new(label: impl Into<String>) -> Self {
Self {
label: label.into(),
color: None,
disabled: false,
}
}
#[must_use]
pub fn color(mut self, color: Color) -> Self {
self.color = Some(color);
self
}
#[must_use]
pub fn disabled(mut self) -> Self {
self.disabled = true;
self
}
}
impl<S: Into<String>> From<S> for Tab {
fn from(s: S) -> Self {
Tab::new(s)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TabDivider {
#[default]
Line,
Dot,
Space,
Slash,
Custom(char),
}
impl TabDivider {
pub fn char(&self) -> char {
match self {
TabDivider::Line => '│',
TabDivider::Dot => '•',
TabDivider::Space => ' ',
TabDivider::Slash => '/',
TabDivider::Custom(c) => *c,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TabStyle {
#[default]
Simple,
Boxed,
Underline,
}
#[derive(Debug, Clone)]
pub struct TabsProps {
pub tabs: Vec<Tab>,
pub selected: usize,
pub divider: TabDivider,
pub selected_color: Option<Color>,
pub selected_bg_color: Option<Color>,
pub unselected_color: Option<Color>,
pub disabled_color: Option<Color>,
pub divider_color: Option<Color>,
pub style: TabStyle,
pub padding: u8,
pub selected_bold: bool,
pub selected_underline: bool,
}
impl Default for TabsProps {
fn default() -> Self {
Self {
tabs: Vec::new(),
selected: 0,
divider: TabDivider::Line,
selected_color: Some(Color::Cyan),
selected_bg_color: None,
unselected_color: None,
disabled_color: Some(Color::DarkGray),
divider_color: Some(Color::DarkGray),
style: TabStyle::Simple,
padding: 1,
selected_bold: true,
selected_underline: false,
}
}
}
impl TabsProps {
pub fn new<I, T>(tabs: I) -> Self
where
I: IntoIterator<Item = T>,
T: Into<Tab>,
{
Self {
tabs: tabs.into_iter().map(Into::into).collect(),
..Default::default()
}
}
#[must_use]
pub fn selected(mut self, index: usize) -> Self {
self.selected = index;
self
}
#[must_use]
pub fn divider(mut self, divider: TabDivider) -> Self {
self.divider = divider;
self
}
#[must_use]
pub fn selected_color(mut self, color: Color) -> Self {
self.selected_color = Some(color);
self
}
#[must_use]
pub fn selected_bg_color(mut self, color: Color) -> Self {
self.selected_bg_color = Some(color);
self
}
#[must_use]
pub fn unselected_color(mut self, color: Color) -> Self {
self.unselected_color = Some(color);
self
}
#[must_use]
pub fn divider_color(mut self, color: Color) -> Self {
self.divider_color = Some(color);
self
}
#[must_use]
pub fn style(mut self, style: TabStyle) -> Self {
self.style = style;
self
}
#[must_use]
pub fn padding(mut self, padding: u8) -> Self {
self.padding = padding;
self
}
#[must_use]
pub fn selected_bold(mut self, bold: bool) -> Self {
self.selected_bold = bold;
self
}
#[must_use]
pub fn selected_underline(mut self, underline: bool) -> Self {
self.selected_underline = underline;
self
}
#[must_use]
pub fn no_divider(mut self) -> Self {
self.divider = TabDivider::Space;
self
}
pub fn selected_tab(&self) -> Option<&Tab> {
self.tabs.get(self.selected)
}
pub fn selected_label(&self) -> Option<&str> {
self.selected_tab().map(|t| t.label.as_str())
}
}
pub struct Tabs;
impl Component for Tabs {
type Props = TabsProps;
fn render(props: &Self::Props) -> Element {
if props.tabs.is_empty() {
return Element::text("");
}
let padding = " ".repeat(props.padding as usize);
let divider_char = props.divider.char();
let mut parts: Vec<String> = Vec::new();
for (i, tab) in props.tabs.iter().enumerate() {
let is_selected = i == props.selected;
if i > 0 {
parts.push(format!(" {} ", divider_char));
}
if is_selected {
parts.push(format!("[{}{}{}]", padding, tab.label, padding));
} else {
parts.push(format!(" {}{}{} ", padding, tab.label, padding));
}
}
let content = parts.join("");
let mut style = Style::new();
if let Some(color) = props.selected_color {
style = style.fg(color);
}
if props.selected_bold {
style = style.add_modifier(Modifier::BOLD);
}
Element::Text { content, style }
}
}
#[derive(Debug, Clone, Default)]
pub struct TabsState {
pub selected: usize,
pub tab_count: usize,
}
impl TabsState {
pub fn new(tab_count: usize) -> Self {
Self {
selected: 0,
tab_count,
}
}
pub fn next(&mut self) {
if self.tab_count > 0 {
self.selected = (self.selected + 1) % self.tab_count;
}
}
pub fn prev(&mut self) {
if self.tab_count > 0 {
self.selected = if self.selected == 0 {
self.tab_count - 1
} else {
self.selected - 1
};
}
}
pub fn first(&mut self) {
self.selected = 0;
}
pub fn last(&mut self) {
if self.tab_count > 0 {
self.selected = self.tab_count - 1;
}
}
pub fn select(&mut self, index: usize) {
if index < self.tab_count {
self.selected = index;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_tab_new() {
let tab = Tab::new("Home");
assert_eq!(tab.label, "Home");
assert!(tab.color.is_none());
assert!(!tab.disabled);
}
#[test]
fn test_tab_builder() {
let tab = Tab::new("Settings").color(Color::Blue).disabled();
assert_eq!(tab.label, "Settings");
assert_eq!(tab.color, Some(Color::Blue));
assert!(tab.disabled);
}
#[test]
fn test_tab_from_string() {
let tab: Tab = "Help".into();
assert_eq!(tab.label, "Help");
}
#[test]
fn test_tabs_props_new() {
let props = TabsProps::new(vec!["A", "B", "C"]);
assert_eq!(props.tabs.len(), 3);
assert_eq!(props.selected, 0);
}
#[test]
fn test_tabs_props_builder() {
let props = TabsProps::new(vec!["A", "B"])
.selected(1)
.selected_color(Color::Yellow)
.divider(TabDivider::Dot);
assert_eq!(props.selected, 1);
assert_eq!(props.selected_color, Some(Color::Yellow));
assert_eq!(props.divider, TabDivider::Dot);
}
#[test]
fn test_tabs_state_navigation() {
let mut state = TabsState::new(4);
assert_eq!(state.selected, 0);
state.next();
assert_eq!(state.selected, 1);
state.next();
state.next();
assert_eq!(state.selected, 3);
state.next(); assert_eq!(state.selected, 0);
state.prev(); assert_eq!(state.selected, 3);
state.first();
assert_eq!(state.selected, 0);
state.last();
assert_eq!(state.selected, 3);
}
#[test]
fn test_tabs_state_select() {
let mut state = TabsState::new(5);
state.select(3);
assert_eq!(state.selected, 3);
state.select(10); assert_eq!(state.selected, 3);
}
#[test]
fn test_tabs_render_empty() {
let props = TabsProps::new(Vec::<&str>::new());
let elem = Tabs::render(&props);
assert!(elem.is_text());
}
#[test]
fn test_tabs_render_basic() {
let props = TabsProps::new(vec!["A", "B", "C"]);
let elem = Tabs::render(&props);
assert!(elem.is_text());
}
#[test]
fn test_tabs_selected_label() {
let props = TabsProps::new(vec!["Home", "Settings"]).selected(1);
assert_eq!(props.selected_label(), Some("Settings"));
}
#[test]
fn test_tab_divider_chars() {
assert_eq!(TabDivider::Line.char(), '│');
assert_eq!(TabDivider::Dot.char(), '•');
assert_eq!(TabDivider::Space.char(), ' ');
assert_eq!(TabDivider::Slash.char(), '/');
assert_eq!(TabDivider::Custom('X').char(), 'X');
}
}