use crate::app::App;
use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
};
use unicode_width::UnicodeWidthStr as _;
const MAX_NAME_WIDTH: usize = 20;
const MIN_NAME_WIDTH: usize = 6;
const CLOSE_WIDTH: u16 = 3;
fn truncate_name(name: &str) -> String {
let display_width = name.width();
if display_width <= MAX_NAME_WIDTH {
return name.to_string();
}
if let Some(dot) = name.rfind('.') {
let ext = &name[dot..]; let stem = &name[..dot];
let ext_w = ext.width();
let available_stem = MAX_NAME_WIDTH.saturating_sub(1 + ext_w).max(1);
if available_stem < MIN_NAME_WIDTH.saturating_sub(ext_w) {
return collect_cells(name, MAX_NAME_WIDTH.saturating_sub(1)) + "…";
}
let stem_part = collect_cells(stem, available_stem);
format!("{stem_part}…{ext}")
} else {
collect_cells(name, MAX_NAME_WIDTH.saturating_sub(1)) + "…"
}
}
fn collect_cells(s: &str, limit: usize) -> String {
use unicode_width::UnicodeWidthChar as _;
let mut out = String::new();
let mut used = 0usize;
for ch in s.chars() {
let w = ch.width().unwrap_or(0);
if used + w > limit {
break;
}
out.push(ch);
used += w;
}
out
}
fn label_display_width(label: &str) -> u16 {
label.width() as u16
}
pub fn draw(f: &mut Frame, app: &mut App, area: Rect) {
app.tab_bar_rects.clear();
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_name(&tab.view.file_name);
format!(" {num}: {name} ")
})
.collect();
let label_widths: Vec<u16> = labels.iter().map(|l| label_display_width(l)).collect();
let widths: Vec<u16> = label_widths.iter().map(|w| w + CLOSE_WIDTH).collect();
const OVERFLOW_MAX: u16 = 5;
let (start, end) = visible_window(&widths, active_idx, area.width, OVERFLOW_MAX);
let hidden_before = start;
let hidden_after = n.saturating_sub(end);
let overflow_before_w: u16 = if hidden_before > 0 {
format!(" +{hidden_before} ").width() as u16
} else {
0
};
let mut spans: Vec<Span> = Vec::new();
let mut x_cursor = area.x + overflow_before_w;
for i in start..end {
let tab_id = app.tabs.tabs[i].id;
let label = &labels[i];
let lw = label_widths[i];
let style = if i == active_idx {
Style::default()
.fg(p.selection_fg)
.bg(p.accent)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(p.dim).bg(p.status_bar_bg)
};
let close_style = if i == active_idx {
Style::default().fg(p.dim).bg(p.accent)
} else {
Style::default().fg(p.dim).bg(p.status_bar_bg)
};
let close_label = " × ";
spans.push(Span::styled(label.clone(), style));
spans.push(Span::styled(close_label, close_style));
app.tab_bar_rects.push((
tab_id,
Rect {
x: x_cursor,
y: area.y,
width: lw,
height: 1,
},
));
app.tab_close_rects.push((
tab_id,
Rect {
x: x_cursor + lw,
y: area.y,
width: CLOSE_WIDTH,
height: 1,
},
));
x_cursor += lw + CLOSE_WIDTH;
}
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);
}
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)
}