use crossterm::event::KeyCode;
use crate::{
Component,
Event,
Focusable,
InputResult,
RenderError,
Rendered,
theme::{
Palette,
Style,
Theme,
stylize,
},
};
pub struct Tabs {
items: Vec<String>,
active: usize,
focused: bool,
}
impl Tabs {
pub fn new(items: Vec<impl Into<String>>) -> Self {
Self {
items: items.into_iter().map(Into::into).collect(),
active: 0,
focused: false,
}
}
pub fn active(&self) -> usize {
self.active
}
pub fn set_active(&mut self, index: usize) {
self.active = index.min(self.items.len().saturating_sub(1));
}
}
impl Focusable for Tabs {
fn focused(&self) -> bool {
self.focused
}
fn set_focused(&mut self, focused: bool) {
self.focused = focused;
}
}
impl Component for Tabs {
fn render(&self, _width: u16) -> Result<Rendered, RenderError> {
let theme = Theme::current();
let accent_style = Style::new().fg(theme.accent()).bold();
let inactive_style = Style::new().fg(theme.text_secondary());
let mut line = String::new();
if self.focused {
line.push('│');
line.push(' ');
}
for (i, item) in self.items.iter().enumerate() {
if i == self.active {
let text = format!(" [{}] ", item);
line.push_str(&stylize(&text, &accent_style));
} else {
let text = format!(" {} ", item);
line.push_str(&stylize(&text, &inactive_style));
}
}
Ok(Rendered {
lines: vec![line],
cursor: None,
images: Vec::new(),
})
}
fn handle_input(&mut self, event: &Event) -> InputResult {
use crossterm::event::KeyModifiers;
if let Event::Key(key) = event {
match key.code {
| KeyCode::Right => {
if self.active + 1 < self.items.len() {
self.active += 1;
}
InputResult::Handled
},
| KeyCode::Left => {
if self.active > 0 {
self.active -= 1;
}
InputResult::Handled
},
| KeyCode::Char('l') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
if self.active + 1 < self.items.len() {
self.active += 1;
}
InputResult::Handled
},
| KeyCode::Char('h') if !key.modifiers.contains(KeyModifiers::CONTROL) => {
if self.active > 0 {
self.active -= 1;
}
InputResult::Handled
},
| _ => InputResult::Ignored,
}
} else {
InputResult::Ignored
}
}
fn as_focusable(&self) -> Option<&dyn Focusable> {
Some(self)
}
fn as_focusable_mut(&mut self) -> Option<&mut dyn Focusable> {
Some(self)
}
}
#[cfg(test)]
mod tests {
use crossterm::event::KeyCode;
use super::*;
#[test]
fn tabs_new() {
let tabs = Tabs::new(vec!["a", "b", "c"]);
assert_eq!(tabs.active(), 0);
assert_eq!(tabs.items.len(), 3);
}
#[test]
fn tabs_set_active() {
let mut tabs = Tabs::new(vec!["a", "b", "c"]);
tabs.set_active(1);
assert_eq!(tabs.active(), 1);
tabs.set_active(10);
assert_eq!(tabs.active(), 2);
}
#[test]
fn tabs_focusable() {
let mut tabs = Tabs::new(vec!["a", "b"]);
assert!(!tabs.focused());
tabs.set_focused(true);
assert!(tabs.focused());
}
#[test]
fn tabs_render_unfocused() {
Theme::with(Theme::Light, || {
let tabs = Tabs::new(vec!["Tab 1", "Tab 2"]);
let rendered = tabs.render(80).unwrap();
assert_eq!(rendered.lines.len(), 1);
assert!(!rendered.lines[0].starts_with('│'));
});
}
#[test]
fn tabs_render_focused() {
Theme::with(Theme::Light, || {
let mut tabs = Tabs::new(vec!["Tab 1", "Tab 2"]);
tabs.set_focused(true);
let rendered = tabs.render(80).unwrap();
assert!(rendered.lines[0].starts_with('│'));
});
}
#[test]
fn tabs_handle_input_right() {
let mut tabs = Tabs::new(vec!["a", "b", "c"]);
tabs.set_focused(true);
let result = tabs.handle_input(&Event::Key(KeyCode::Right.into()));
assert_eq!(result, InputResult::Handled);
assert_eq!(tabs.active(), 1);
}
#[test]
fn tabs_handle_input_left() {
let mut tabs = Tabs::new(vec!["a", "b", "c"]);
tabs.set_focused(true);
tabs.set_active(2);
let result = tabs.handle_input(&Event::Key(KeyCode::Left.into()));
assert_eq!(result, InputResult::Handled);
assert_eq!(tabs.active(), 1);
}
#[test]
fn tabs_handle_input_h_l() {
let mut tabs = Tabs::new(vec!["a", "b", "c"]);
tabs.set_focused(true);
tabs.set_active(1);
let result = tabs.handle_input(&Event::Key(KeyCode::Char('h').into()));
assert_eq!(result, InputResult::Handled);
assert_eq!(tabs.active(), 0);
let result = tabs.handle_input(&Event::Key(KeyCode::Char('l').into()));
assert_eq!(result, InputResult::Handled);
assert_eq!(tabs.active(), 1);
}
#[test]
fn tabs_handle_input_clamps() {
let mut tabs = Tabs::new(vec!["a", "b"]);
tabs.set_focused(true);
tabs.set_active(1);
let result = tabs.handle_input(&Event::Key(KeyCode::Right.into()));
assert_eq!(result, InputResult::Handled);
assert_eq!(tabs.active(), 1);
tabs.set_active(0);
let result = tabs.handle_input(&Event::Key(KeyCode::Left.into()));
assert_eq!(result, InputResult::Handled);
assert_eq!(tabs.active(), 0); }
}