use ratatui::{
buffer::Buffer,
layout::Rect,
style::{Color, Style},
widgets::{Block, BorderType, Borders, Widget},
};
use crate::widgets::document_viewer::foundation::DocumentOutlineItem;
pub fn render_outline(
area: Rect,
buf: &mut Buffer,
outline: &[DocumentOutlineItem],
hovered: bool,
hovered_entry: Option<usize>,
) {
let Some(toc_area) = outline_overlay_area(area, outline.len(), hovered) else {
return;
};
render_outline_block(toc_area, buf);
let inner = outline_inner_area(toc_area);
if hovered {
render_expanded_entries(inner, buf, outline, hovered_entry);
} else {
render_compact_marker(inner, buf);
}
}
pub fn outline_entry_at_position(
x: u16,
y: u16,
area: Rect,
outline_len: usize,
hovered: bool,
) -> Option<usize> {
let toc_area = outline_overlay_area(area, outline_len, hovered)?;
let inner = outline_inner_area(toc_area);
if x < inner.x || x >= inner.x + inner.width || y < inner.y || y >= inner.y + inner.height {
return None;
}
let index = y.saturating_sub(inner.y) as usize;
(index < outline_len).then_some(index)
}
pub fn outline_overlay_area(area: Rect, item_count: usize, hovered: bool) -> Option<Rect> {
if area.width < 24 || area.height < 4 || item_count == 0 {
return None;
}
let width = if hovered {
area.width.clamp(20, 30).min(area.width.saturating_sub(4))
} else {
12.min(area.width.saturating_sub(4))
};
let wanted_height = if hovered { item_count as u16 + 2 } else { 4 };
let height = wanted_height.clamp(4, area.height.saturating_sub(1));
Some(Rect {
x: area.x + area.width.saturating_sub(width + 2),
y: area.y + 1,
width,
height,
})
}
fn outline_inner_area(area: Rect) -> Rect {
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.inner(area)
}
fn render_outline_block(area: Rect, buf: &mut Buffer) {
Block::default()
.title(" TOC ")
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(Color::DarkGray))
.render(area, buf);
}
fn render_expanded_entries(
area: Rect,
buf: &mut Buffer,
outline: &[DocumentOutlineItem],
hovered_entry: Option<usize>,
) {
for (row, item) in outline.iter().take(area.height as usize).enumerate() {
let prefix = " ".repeat(item.level.min(3));
let text = format!("{prefix}{}", item.title);
let style = if hovered_entry == Some(row) {
Style::default().fg(Color::Black).bg(Color::Cyan)
} else {
Style::default().fg(Color::Gray)
};
buf.set_stringn(
area.x,
area.y + row as u16,
text,
area.width as usize,
style,
);
}
}
fn render_compact_marker(area: Rect, buf: &mut Buffer) {
if area.is_empty() {
return;
}
buf.set_stringn(
area.x,
area.y,
"⠉⢙⣛⣛⣛⣛⣛⣛⣛⣛",
area.width as usize,
Style::default().fg(Color::DarkGray),
);
}