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');
}
}