use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
};
use unicode_width::UnicodeWidthStr;
use super::style;
use crate::theme::Skin;
const SEPARATOR: &str = " \u{2502} ";
fn brand_label(brand: &str) -> String {
format!(" {brand} ")
}
fn tab_label(key: &str, label: &str) -> String {
format!("{key} {label}")
}
pub fn height(brand: &str, tabs: &[(&str, &str)], width: usize) -> u16 {
pack(brand, tabs, width).len().max(1) as u16
}
pub fn render(
frame: &mut Frame,
area: Rect,
skin: &Skin,
brand: &str,
tabs: &[(&str, &str)],
active: usize,
) {
let palette = &skin.palette;
let brand_width = brand_label(brand).width();
let rows = pack(brand, tabs, area.width as usize);
let lines: Vec<Line> = rows
.iter()
.enumerate()
.map(|(row_index, segments)| {
let mut spans: Vec<Span> = Vec::new();
if row_index == 0 {
spans.push(Span::styled(
brand_label(brand),
style::fg(palette.accent).add_modifier(Modifier::BOLD),
));
} else {
spans.push(Span::raw(" ".repeat(brand_width)));
}
for (position, &index) in segments.iter().enumerate() {
if position > 0 {
spans.push(Span::styled(
SEPARATOR,
style::secondary(palette),
));
}
let (key, label) = tabs[index];
let tab_style = if index == active {
Style::default().add_modifier(Modifier::BOLD)
} else {
style::secondary(palette)
};
spans.push(Span::styled(format!("{key} "), tab_style));
spans.push(Span::styled(label.to_string(), tab_style));
}
Line::from(spans)
})
.collect();
frame.render_widget(Paragraph::new(lines), area);
}
fn pack(brand: &str, tabs: &[(&str, &str)], width: usize) -> Vec<Vec<usize>> {
let brand_width = brand_label(brand).width();
let separator_width = SEPARATOR.width();
let mut rows: Vec<Vec<usize>> = Vec::new();
let mut current: Vec<usize> = Vec::new();
let mut used = brand_width;
for (index, (key, label)) in tabs.iter().enumerate() {
let segment_width = tab_label(key, label).width();
if !current.is_empty() && used + separator_width + segment_width > width
{
rows.push(std::mem::take(&mut current));
used = brand_width;
}
if !current.is_empty() {
used += separator_width;
}
current.push(index);
used += segment_width;
}
if !current.is_empty() {
rows.push(current);
}
if rows.is_empty() {
rows.push(Vec::new());
}
rows
}
#[cfg(test)]
mod tests {
use super::*;
const TABS: &[(&str, &str)] =
&[("1", "Summary"), ("2", "Tasks"), ("3", "Gallery")];
#[test]
fn everything_fits_on_one_row_when_wide() {
let rows = pack("app", TABS, 200);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0], vec![0, 1, 2]);
}
#[test]
fn a_narrow_width_wraps_onto_further_rows() {
let rows = pack("app", TABS, 30);
assert!(rows.len() > 1, "expected a wrap, got {rows:?}");
let flat: Vec<usize> = rows.concat();
assert_eq!(flat, vec![0, 1, 2], "every tab must appear exactly once");
}
#[test]
fn a_tab_too_wide_for_the_row_still_gets_one() {
let rows = pack("brand", &[("1", "an extremely long tab label")], 4);
assert_eq!(rows.len(), 1);
assert_eq!(rows[0], vec![0]);
}
#[test]
fn no_tabs_yields_a_single_empty_row() {
let rows = pack("app", &[], 80);
assert_eq!(rows.len(), 1);
assert!(rows[0].is_empty());
}
#[test]
fn the_reported_height_matches_the_packed_rows() {
for width in [200, 40, 30, 20, 12] {
let rows = pack("app", TABS, width);
assert_eq!(
height("app", TABS, width) as usize,
rows.len(),
"width {width}"
);
}
}
}