use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph}
};
use crate::tui::app::App;
const TAB_ICONS: [char; 20] = [
'\u{25A3}', '\u{25A4}', '\u{25A6}', '\u{2638}', '\u{25A7}', '\u{25D1}', '\u{25A9}', '\u{25C9}', '\u{25A8}', '\u{25C8}', '\u{25A5}', '\u{25E2}', '\u{2756}', '\u{25A0}', '\u{25E7}', '\u{2726}', '\u{25C6}', '\u{2605}', '\u{25C7}', '\u{25CE}' ];
pub struct ResourceTabsWidget {
enabled: bool
}
impl ResourceTabsWidget {
#[must_use]
pub const fn new(enabled: bool) -> Self {
Self {
enabled
}
}
fn visible_window(widths: &[u16], active: usize, budget: u16) -> (usize, usize) {
if widths.is_empty() {
return (0, 0);
}
let active = active.min(widths.len() - 1);
let mut start = active;
let mut end = active + 1;
let mut used = widths[active];
if used > budget {
return (start, end);
}
loop {
let grow_right = end < widths.len() && used.saturating_add(widths[end]) <= budget;
if grow_right {
used += widths[end];
end += 1;
}
let grow_left = start > 0 && used.saturating_add(widths[start - 1]) <= budget;
if grow_left {
start -= 1;
used += widths[start];
}
if !grow_right && !grow_left {
break;
}
}
(start, end)
}
fn build_tab_bar(app: &App, width: u16) -> Line<'static> {
let palette = app.theme.palette();
let tabs = app.visible_tabs();
let len = tabs.len();
let active_idx = tabs.iter().position(|t| *t == app.active_tab).unwrap_or(0);
let labels: Vec<(String, String, u16)> = tabs
.iter()
.map(|tab| {
let idx = tab.index();
let head = format!(" {} {} ", TAB_ICONS[idx], tab.display_name());
let tail = format!("{} ", app.tab_count(*tab));
let w =
u16::try_from(head.chars().count() + tail.chars().count()).unwrap_or(u16::MAX);
(head, tail, w)
})
.collect();
let widths: Vec<u16> = labels.iter().map(|l| l.2).collect();
let position = format!(" {}/{} ", active_idx + 1, len);
let position_w = u16::try_from(position.chars().count()).unwrap_or(0);
let reserve = position_w.saturating_add(4);
let budget = width.saturating_sub(reserve);
let (start, end) = Self::visible_window(&widths, active_idx, budget);
let mut spans: Vec<Span<'static>> = Vec::new();
let mut used: u16 = 0;
if start > 0 {
spans.push(Span::styled("\u{2039} ", Style::default().fg(palette.dim)));
used = used.saturating_add(2);
}
for (i, (head, tail, w)) in labels.into_iter().enumerate().take(end).skip(start) {
if i == active_idx {
let pill = Style::default()
.fg(palette.bg)
.bg(palette.tab_active)
.add_modifier(Modifier::BOLD);
spans.push(Span::styled(head, pill));
spans.push(Span::styled(tail, pill));
} else {
spans.push(Span::styled(
head,
Style::default().fg(palette.tab_inactive)
));
spans.push(Span::styled(tail, Style::default().fg(palette.dim)));
}
used = used.saturating_add(w);
}
if end < len {
spans.push(Span::styled(" \u{203A}", Style::default().fg(palette.dim)));
used = used.saturating_add(2);
}
let consumed = used.saturating_add(position_w);
if width > consumed {
spans.push(Span::raw(" ".repeat(usize::from(width - consumed))));
}
spans.push(Span::styled(
position,
Style::default()
.fg(palette.header)
.add_modifier(Modifier::BOLD)
));
Line::from(spans)
}
}
impl crate::tui::widgets::Widget for ResourceTabsWidget {
fn id(&self) -> &'static str {
"resource_tabs"
}
fn name(&self) -> &'static str {
"Resource Tabs"
}
fn enabled(&self) -> bool {
self.enabled
}
fn toggle(&mut self) {
self.enabled = !self.enabled;
}
fn render(&self, frame: &mut Frame, area: Rect, app: &App) {
let palette = app.theme.palette();
let border_color = if app.focus == crate::tui::app::Focus::ResourceTabs {
palette.accent
} else {
palette.border
};
let tab_bar = Self::build_tab_bar(app, area.width);
let paragraph = Paragraph::new(tab_bar).block(
Block::default()
.borders(Borders::BOTTOM)
.border_style(Style::default().fg(border_color))
);
frame.render_widget(paragraph, area);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tui::app::ResourceTab;
#[test]
fn all_tabs_fit_when_budget_is_large() {
let widths = [10u16, 10, 10, 10];
let (start, end) = ResourceTabsWidget::visible_window(&widths, 0, 1000);
assert_eq!((start, end), (0, 4));
}
#[test]
fn active_tab_is_always_inside_window() {
let widths = [12u16; 20];
let budget = 30;
for active in 0..widths.len() {
let (start, end) = ResourceTabsWidget::visible_window(&widths, active, budget);
assert!(
start <= active && active < end,
"active {active} not in window"
);
}
}
#[test]
fn window_never_exceeds_budget() {
let widths = [12u16; 20];
let budget = 40;
let (start, end) = ResourceTabsWidget::visible_window(&widths, 7, budget);
let total: u16 = widths[start..end].iter().sum();
assert!(total <= budget);
}
#[test]
fn oversized_active_tab_still_renders_alone() {
let widths = [5u16, 100, 5];
let (start, end) = ResourceTabsWidget::visible_window(&widths, 1, 10);
assert_eq!((start, end), (1, 2));
}
#[test]
fn empty_widths_yield_empty_window() {
let widths: [u16; 0] = [];
assert_eq!(ResourceTabsWidget::visible_window(&widths, 0, 50), (0, 0));
}
#[test]
fn icon_table_covers_every_tab() {
assert_eq!(TAB_ICONS.len(), ResourceTab::names().len());
}
}