use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::Paragraph,
};
use super::{Overflow, Row, Sidebar, split_for_hbar};
use crate::{chrome, input, nav, scroll, style, text, theme::Skin};
impl Sidebar {
pub fn render(&self, frame: &mut Frame, area: Rect, skin: &Skin) {
let inner = self.render_frame(frame, area, skin);
let list_area = self.render_filter_line(frame, inner, skin);
self.render_list(frame, list_area, skin);
}
fn render_frame(&self, frame: &mut Frame, area: Rect, skin: &Skin) -> Rect {
if let Some(decor) = &self.decor {
let badge = self.filtered_items().count().to_string();
return chrome::framed_decor(frame, area, skin, decor, &badge);
}
let block = chrome::menu_panel(skin);
let inner = block.inner(area);
frame.render_widget(block, area);
inner
}
fn render_filter_line(
&self,
frame: &mut Frame,
inner: Rect,
skin: &Skin,
) -> Rect {
if !self.filtering {
return inner;
}
let split = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(1), Constraint::Min(0)])
.split(inner);
let palette = &skin.palette;
let mut query = vec![Span::styled("/", style::fg(palette.accent))];
query.extend(input::query_spans(
&self.filter,
palette,
(split[0].width as usize).saturating_sub(1),
));
frame.render_widget(Paragraph::new(Line::from(query)), split[0]);
split[1]
}
fn render_list(&self, frame: &mut Frame, area: Rect, skin: &Skin) {
let rows = self.rows();
let max_width = rows.iter().map(Self::row_width).max().unwrap_or(0);
let has_scrollbar = rows.len() > area.height as usize;
let content_width =
(area.width as usize).saturating_sub(usize::from(has_scrollbar));
let overflowing =
self.overflow == Overflow::Scroll && max_width > content_width;
let (body, hbar) = split_for_hbar(area, overflowing);
let height = body.height as usize;
self.viewport.set(height.max(1));
let selected_row = self.selected_row(&rows);
let offset = self.scroll_offset(&rows, selected_row, height);
self.offset.set(offset);
let h_offset = self.clamp_h_offset(max_width, content_width);
let lines: Vec<Line> = rows
.iter()
.enumerate()
.skip(offset)
.take(height)
.map(|(index, row)| {
self.render_row(
row,
index == selected_row,
content_width,
h_offset,
skin,
)
})
.collect();
frame.render_widget(Paragraph::new(lines), body);
scroll::render_scrollbar(
frame,
body,
skin,
nav::ScrollView {
total: rows.len(),
offset,
viewport: height,
},
);
if let Some(hbar) = hbar {
scroll::render_hscrollbar(
frame,
hbar,
skin,
nav::ScrollView {
total: max_width,
offset: h_offset,
viewport: content_width,
},
);
}
}
fn render_row(
&self,
row: &Row,
selected: bool,
width: usize,
h_offset: usize,
skin: &Skin,
) -> Line<'static> {
let palette = &skin.palette;
match row {
Row::Header(title) => Line::from(Span::styled(
self.clip(title, h_offset, width),
style::dim().add_modifier(Modifier::BOLD),
)),
Row::Item(item) => {
let pointer = if selected { skin.glyphs.pointer } else { " " };
let full = format!("{pointer} {}", item.label);
let clipped = self.clip(&full, h_offset, width);
if selected {
let bar = text::pad_end(&clipped, width);
let style = style::fg(palette.accent)
.add_modifier(Modifier::BOLD)
.bg(style::to_ratatui(palette.selection));
Line::from(Span::styled(bar, style))
} else {
Line::from(Span::styled(clipped, Style::default()))
}
}
}
}
fn clip(&self, text: &str, h_offset: usize, width: usize) -> String {
match self.overflow {
Overflow::Truncate => text::truncate(text, width),
Overflow::Scroll => text::window(text, h_offset, width),
}
}
}