use ratatui::prelude::*;
use ratatui::widgets::{Block, Borders, Tabs};
use crate::tui::app::{App, ClickAction, Tab};
const TAB_PADDING_LEFT_WIDTH: u16 = 1;
const TAB_PADDING_RIGHT_WIDTH: u16 = 1;
const TAB_DIVIDER_WIDTH: u16 = 3;
pub fn render(frame: &mut Frame, app: &mut App, area: Rect) {
let is_very_narrow = app.is_very_narrow();
let visible_tabs: Vec<Tab> = Tab::all()
.iter()
.copied()
.filter(|t| app.is_tab_visible(*t))
.collect();
let titles: Vec<Line> = visible_tabs
.iter()
.map(|t| {
let name = tab_label(*t, is_very_narrow);
let style = if *t == app.current_tab {
Style::default()
.fg(app.theme.accent)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(app.theme.muted)
};
Line::from(Span::styled(name, style))
})
.collect();
let selected = visible_tabs
.iter()
.position(|t| *t == app.current_tab)
.unwrap_or(0);
let is_narrow = app.is_narrow();
let mut block = Block::default()
.borders(Borders::ALL)
.border_style(Style::default().fg(app.theme.border))
.title(Span::styled(
" tokmesh ",
Style::default()
.fg(app.theme.accent)
.add_modifier(Modifier::BOLD),
))
.title_alignment(Alignment::Left)
.style(Style::default().bg(app.theme.background));
if !is_narrow {
block = block.title_top(
Line::from(vec![
Span::styled(" | ", app.theme.subtle_text_style()),
Span::styled("GitHub ", app.theme.subtle_text_style()),
])
.right_aligned(),
);
}
let tabs = Tabs::new(titles)
.block(block)
.select(selected)
.highlight_style(
Style::default()
.fg(app.theme.accent)
.add_modifier(Modifier::BOLD),
)
.divider(Span::styled(" │ ", Style::default().fg(app.theme.border)));
frame.render_widget(tabs, area);
register_tab_click_areas(app, area);
}
fn register_tab_click_areas(app: &mut App, area: Rect) {
if area.width <= 2 || area.height <= 2 {
return;
}
let is_very_narrow = app.is_very_narrow();
let y = area.y.saturating_add(1);
let right = area.right().saturating_sub(1);
let mut x = area.x.saturating_add(1);
let visible_tabs: Vec<Tab> = Tab::all()
.iter()
.copied()
.filter(|t| app.is_tab_visible(*t))
.collect();
let tab_count = visible_tabs.len();
for (index, tab) in visible_tabs.into_iter().enumerate() {
let remaining_width = right.saturating_sub(x);
if remaining_width == 0 {
break;
}
let left_padding_width = TAB_PADDING_LEFT_WIDTH.min(remaining_width);
let remaining_width = remaining_width.saturating_sub(left_padding_width);
let title_width = tab_label_width(tab, is_very_narrow).min(remaining_width);
let remaining_width = remaining_width.saturating_sub(title_width);
let right_padding_width = TAB_PADDING_RIGHT_WIDTH.min(remaining_width);
let click_width = left_padding_width + title_width + right_padding_width;
app.add_click_area(Rect::new(x, y, click_width, 1), ClickAction::Tab(tab));
x = x.saturating_add(click_width);
let remaining_width = right.saturating_sub(x);
if remaining_width == 0 || index + 1 == tab_count {
break;
}
x = x.saturating_add(TAB_DIVIDER_WIDTH.min(remaining_width));
}
}
fn tab_label(tab: Tab, is_very_narrow: bool) -> &'static str {
if is_very_narrow {
tab.short_name()
} else {
tab.as_str()
}
}
fn tab_label_width(tab: Tab, is_very_narrow: bool) -> u16 {
Line::from(tab_label(tab, is_very_narrow)).width() as u16
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::{KeyModifiers, MouseButton, MouseEvent, MouseEventKind};
use ratatui::{backend::TestBackend, Terminal};
use crate::tui::app::TuiConfig;
fn make_app(width: u16) -> App {
let config = TuiConfig {
theme: "blue".to_string(),
refresh: 0,
sessions_path: None,
clients: None,
since: None,
until: None,
year: None,
initial_tab: None,
};
let mut app = App::new_with_cached_data(config, None).unwrap();
app.terminal_width = width;
app
}
fn render_header(app: &mut App, width: u16) -> Vec<Vec<String>> {
let backend = TestBackend::new(width, 3);
let mut terminal = Terminal::new(backend).unwrap();
terminal
.draw(|frame| render(frame, app, Rect::new(0, 0, width, 3)))
.unwrap();
terminal
.backend()
.buffer()
.content()
.chunks(width as usize)
.map(|row| row.iter().map(|cell| cell.symbol().to_string()).collect())
.collect()
}
fn rendered_label_column(rows: &[Vec<String>], label: &str) -> u16 {
let target: Vec<String> = label.chars().map(|ch| ch.to_string()).collect();
rows[1]
.windows(target.len())
.position(|window| window == target.as_slice())
.expect("expected label to be rendered") as u16
}
fn click_header(app: &mut App, column: u16) {
app.handle_mouse_event(MouseEvent {
kind: MouseEventKind::Down(MouseButton::Left),
column,
row: 1,
modifiers: KeyModifiers::NONE,
});
}
#[test]
fn daily_label_click_uses_rendered_normal_tab_position() {
let mut app = make_app(80);
let rows = render_header(&mut app, 80);
click_header(&mut app, rendered_label_column(&rows, "Daily"));
assert_eq!(app.current_tab, Tab::Daily);
}
#[test]
fn daily_short_label_click_uses_rendered_very_narrow_tab_position() {
let mut app = make_app(59);
let rows = render_header(&mut app, 59);
click_header(&mut app, rendered_label_column(&rows, "Day"));
assert_eq!(app.current_tab, Tab::Daily);
}
}