use crate::app::App;
use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
};
use unicode_width::UnicodeWidthStr as _;
const MAX_LABEL_WIDTH: usize = 24;
const PAD_EACH_SIDE: usize = 1;
fn truncate_label(label: &str) -> String {
use unicode_width::UnicodeWidthChar as _;
let display_width = label.width();
if display_width <= MAX_LABEL_WIDTH {
return label.to_owned();
}
let limit = MAX_LABEL_WIDTH - 1;
let mut out = String::new();
let mut used = 0usize;
for ch in label.chars() {
let w = ch.width().unwrap_or(0);
if used + w > limit {
break;
}
out.push(ch);
used += w;
}
out.push('…');
out
}
fn label_cell_width(label: &str) -> u16 {
crate::cast::u16_sat(label.width() + PAD_EACH_SIDE * 2)
}
fn visible_window(
widths: &[u16],
active_idx: usize,
available_width: u16,
overflow_reserve: u16,
) -> (usize, usize) {
let n = widths.len();
if n == 0 {
return (0, 0);
}
let mut start = active_idx;
let mut end = active_idx + 1;
let mut used: u16 = widths[active_idx];
loop {
let mut expanded = false;
if start > 0 {
let extra = widths[start - 1];
let reserve_l = if start > 1 { overflow_reserve } else { 0 };
let reserve_r = if end < n { overflow_reserve } else { 0 };
if used.saturating_add(extra).saturating_add(reserve_l).saturating_add(reserve_r)
<= available_width
{
start -= 1;
used = used.saturating_add(extra);
expanded = true;
}
}
if end < n {
let extra = widths[end];
let reserve_l = if start > 0 { overflow_reserve } else { 0 };
let reserve_r = if end + 1 < n { overflow_reserve } else { 0 };
if used.saturating_add(extra).saturating_add(reserve_l).saturating_add(reserve_r)
<= available_width
{
end += 1;
used = used.saturating_add(extra);
expanded = true;
}
}
if !expanded {
break;
}
}
(start, end)
}
pub fn draw(f: &mut Frame, app: &App, area: Rect) {
const OVERFLOW_MAX: u16 = 5;
if app.tabs.is_empty() {
return;
}
let p = &app.palette;
let n = app.tabs.len();
let active_idx = app.tabs.active_index().unwrap_or(0);
let labels: Vec<String> = app
.tabs
.tabs
.iter()
.enumerate()
.map(|(i, tab)| {
let num = if i < 9 { format!("{}", i + 1) } else { " ".to_string() };
let name = truncate_label(&tab.repo);
let badge = match tab.needs_action_count {
Some(count) if count > 0 => format!(" [{count}]"),
_ => String::new(),
};
format!(" {num}: {name}{badge} ")
})
.collect();
let widths: Vec<u16> = labels.iter().map(|l| label_cell_width(l)).collect();
let (start, end) = visible_window(&widths, active_idx, area.width, OVERFLOW_MAX);
let hidden_before = start;
let hidden_after = n.saturating_sub(end);
let mut spans: Vec<Span> = Vec::new();
for (i, tab) in app.tabs.tabs.iter().enumerate().skip(start).take(end - start) {
let is_active = i == active_idx;
let base_style = if is_active {
Style::default().fg(p.on_accent_fg).bg(p.accent).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(p.dim).bg(p.status_bar_bg)
};
let badge_style = if is_active {
base_style
} else {
Style::default().fg(p.needs_action).bg(p.status_bar_bg).add_modifier(Modifier::BOLD)
};
let num = if i < 9 { format!("{}", i + 1) } else { " ".to_string() };
let name = truncate_label(&tab.repo);
let base_text = format!(" {num}: {name}");
spans.push(Span::styled(base_text, base_style));
match tab.needs_action_count {
Some(n) if n > 0 => {
spans.push(Span::styled(format!(" [{n}]"), badge_style));
spans.push(Span::styled(" ", base_style));
}
_ => {
spans.push(Span::styled(" ", base_style));
}
}
}
if hidden_before > 0 {
spans.insert(
0,
Span::styled(
format!(" +{hidden_before} "),
Style::default().fg(p.accent_alt).bg(p.status_bar_bg),
),
);
}
if hidden_after > 0 {
spans.push(Span::styled(
format!(" +{hidden_after} "),
Style::default().fg(p.accent_alt).bg(p.status_bar_bg),
));
}
let line = Line::from(spans);
let paragraph = Paragraph::new(line).style(Style::default().bg(p.status_bar_bg));
f.render_widget(paragraph, area);
}