use ratatui::{
Frame,
layout::Rect,
style::{Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
};
use unicode_width::UnicodeWidthStr;
use super::widgets::truncate;
use crate::app::{App, Nav, SidebarAction, SidebarRow, Tone};
pub fn draw(f: &mut Frame, app: &mut App, area: Rect) {
let th = app.theme;
app.sidebar_rows = app.sidebar_layout();
app.sidebar_area = area;
let inner = Rect {
width: area.width.saturating_sub(1),
..area
};
for y in area.y..area.y + area.height {
f.buffer_mut()[(area.x + area.width.saturating_sub(1), y)]
.set_symbol("\u{2502}")
.set_style(Style::default().fg(th.border));
}
let title = Line::from(vec![
Span::styled(" \u{25c6} ", Style::default().fg(th.accent)),
Span::styled(
"Linear",
Style::default().fg(th.text).add_modifier(Modifier::BOLD),
),
]);
f.render_widget(Paragraph::new(title), Rect { height: 1, ..inner });
let list = Rect {
y: inner.y + 2,
height: inner.height.saturating_sub(2),
..inner
};
app.sidebar_area = list;
let height = list.height as usize;
if app.sidebar_index < app.sidebar_offset {
app.sidebar_offset = app.sidebar_index;
} else if height > 0 && app.sidebar_index >= app.sidebar_offset + height {
app.sidebar_offset = app.sidebar_index + 1 - height;
}
app.sidebar_offset = app
.sidebar_offset
.min(app.sidebar_rows.len().saturating_sub(height));
let width = list.width as usize;
let exact = app
.sidebar_rows
.iter()
.any(|r| matches!(r, SidebarRow::Item(i) if i.nav() == Some(app.nav)));
let fallback = match app.nav {
Nav::View(_) if !exact => Some(Nav::Views),
_ => None,
};
let lines: Vec<Line> = app
.sidebar_rows
.iter()
.enumerate()
.skip(app.sidebar_offset)
.take(height)
.map(|(index, row)| match row {
SidebarRow::Gap => Line::from(""),
SidebarRow::Header(text) => Line::from(Span::styled(
format!(" {text}"),
Style::default().fg(th.muted).add_modifier(Modifier::BOLD),
)),
SidebarRow::Item(item) => {
let active =
item.nav() == Some(app.nav) || (item.nav().is_some() && item.nav() == fallback);
let cursor = app.sidebar_focus && index == app.sidebar_index;
let bg = if cursor {
Some(th.selection_bg)
} else if active {
Some(th.surface)
} else {
None
};
let indent = " ".repeat(item.depth as usize);
let chevron = match item.expanded {
Some(true) => "\u{25be} ",
Some(false) => "\u{25b8} ",
None => " ",
};
let (text, icon_default) = match item.tone {
Tone::Subtle => (th.muted, th.muted),
Tone::Normal => (th.text_dim, th.text_dim),
Tone::Strong => (th.text, th.text_dim),
};
let icon_style = Style::default().fg(item.color.unwrap_or(icon_default));
let mut label_style =
Style::default().fg(if active || cursor { th.text } else { text });
if active || cursor || item.action == SidebarAction::SwitchTeam {
label_style = label_style.add_modifier(Modifier::BOLD);
}
let trailing = item
.trailing
.as_ref()
.map(|t| format!("{t} "))
.unwrap_or_default();
let lead = format!(" {indent}{chevron}");
let fixed = lead.width() + 2 + trailing.width();
let label = truncate(&item.label, width.saturating_sub(fixed));
let pad = width.saturating_sub(fixed + label.width());
let mut spans = vec![
Span::styled(lead, Style::default().fg(th.muted)),
Span::styled(format!("{} ", item.icon), icon_style),
Span::styled(label, label_style),
Span::raw(" ".repeat(pad)),
Span::styled(trailing, Style::default().fg(th.muted)),
];
if let Some(bg) = bg {
for span in &mut spans {
span.style = span.style.bg(bg);
}
}
Line::from(spans)
}
})
.collect();
f.render_widget(Paragraph::new(lines), list);
}