use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
text::{Line, Span},
widgets::Paragraph,
Frame,
};
use crate::{app::Tab, theme};
pub fn render(frame: &mut Frame<'_>, area: Rect, current: Tab) {
let cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(7), Constraint::Min(0)])
.split(area);
let brand = Line::from(vec![Span::styled("DECK ", theme::green_hi())]);
frame.render_widget(Paragraph::new(brand), cols[0]);
let key_for = |i: usize, tab: Tab| -> String {
if i < Tab::PRIMARY_COUNT {
return if i == 9 {
"[0] ".to_string()
} else {
format!("[{}] ", i + 1)
};
}
match tab.letter_shortcut() {
Some(letter) => format!("[{letter}] "),
None => String::new(),
}
};
let tabs: Vec<(String, &'static str)> = Tab::all()
.iter()
.enumerate()
.map(|(i, t)| (key_for(i, *t), t.label()))
.collect();
let widths: Vec<usize> = tabs.iter().map(|e| e.0.len() + e.1.len() + 1).collect();
let total_width: usize = widths.iter().sum();
let avail = cols[1].width as usize;
let all = Tab::all();
let current_idx = all
.iter()
.position(|t| *t == current)
.unwrap_or(0);
let mut spans = Vec::new();
let push_tab = |spans: &mut Vec<Span<'_>>, i: usize, current_idx: usize| {
let key = tabs[i].0.clone();
let label = tabs[i].1;
if i == current_idx {
spans.push(Span::styled(key, theme::green()));
spans.push(Span::styled(label, theme::green_hi()));
} else {
spans.push(Span::styled(key, theme::chrome()));
spans.push(Span::styled(label, theme::dim()));
}
spans.push(Span::raw(" "));
};
if total_width <= avail {
for i in 0..tabs.len() {
push_tab(&mut spans, i, current_idx);
}
frame.render_widget(Paragraph::new(Line::from(spans)), cols[1]);
return;
}
let (start, end) = scroll_window_horizontal(&widths, avail, current_idx);
if start > 0 {
spans.push(Span::styled(format!("<{start} "), theme::dim()));
}
for i in start..end {
push_tab(&mut spans, i, current_idx);
}
let hidden_after = all.len().saturating_sub(end);
if hidden_after > 0 {
spans.push(Span::styled(format!("+{hidden_after}"), theme::dim()));
}
frame.render_widget(Paragraph::new(Line::from(spans)), cols[1]);
}
fn scroll_window_horizontal(widths: &[usize], avail: usize, current_idx: usize) -> (usize, usize) {
let n = widths.len();
if n == 0 || avail == 0 {
return (0, 0);
}
const LEFT_CHIP: usize = 4;
const RIGHT_CHIP: usize = 3;
let mut left_reserve = 0usize;
let mut right_reserve = 0usize;
let cur = current_idx.min(n - 1);
let (mut start, mut end) = (cur, cur);
for _ in 0..3 {
let viewport = avail.saturating_sub(left_reserve + right_reserve);
if viewport == 0 {
return (cur, cur + 1);
}
let cur_w = widths[cur];
if cur_w > viewport {
return (cur, cur + 1);
}
let mut used = cur_w;
start = cur;
end = cur + 1;
loop {
let can_right = end < n && used + widths[end] <= viewport;
let can_left = start > 0 && used + widths[start - 1] <= viewport;
if !can_right && !can_left {
break;
}
if can_right && (!can_left || widths[end] <= widths[start - 1]) {
used += widths[end];
end += 1;
} else if can_left {
start -= 1;
used += widths[start];
} else {
break;
}
}
let need_left = if start > 0 { LEFT_CHIP } else { 0 };
let need_right = if end < n { RIGHT_CHIP } else { 0 };
if need_left == left_reserve && need_right == right_reserve {
return (start, end);
}
left_reserve = need_left;
right_reserve = need_right;
}
(start, end)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn fits_returns_full_range() {
let widths = vec![10, 10, 10, 10];
let (start, end) = scroll_window_horizontal(&widths, 40, 2);
assert_eq!((start, end), (0, 4));
}
#[test]
fn current_at_head_window_starts_at_zero() {
let widths = vec![10, 10, 10, 10, 10];
let (start, end) = scroll_window_horizontal(&widths, 25, 0);
assert_eq!(start, 0);
assert!(end > 0);
assert!(end <= 3);
}
#[test]
fn current_in_middle_window_contains_it() {
let widths = vec![10, 10, 10, 10, 10];
let (start, end) = scroll_window_horizontal(&widths, 25, 2);
assert!(
start <= 2 && 2 < end,
"window {start}..{end} missing cursor"
);
}
#[test]
fn current_at_tail_window_ends_at_n() {
let widths = vec![10, 10, 10, 10, 10];
let (_start, end) = scroll_window_horizontal(&widths, 25, 4);
assert_eq!(end, 5);
}
#[test]
fn zero_avail_yields_empty_window() {
let widths = vec![10, 10, 10];
let (start, end) = scroll_window_horizontal(&widths, 0, 1);
assert_eq!((start, end), (0, 0));
}
#[test]
fn single_wide_tab_clamps_to_cursor() {
let widths = vec![5, 50, 5];
let (start, end) = scroll_window_horizontal(&widths, 20, 1);
assert_eq!((start, end), (1, 2));
}
}