use crate::app_theme::AppThemeColors;
use crate::content_tree::state::ContentTreeState;
use crate::keybinds::{ContentTreeAction, Keybinds};
use ratatui::{
Frame,
layout::{Alignment, Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{Block, List, ListItem, ListState, Paragraph},
};
fn get_tree_prefix(state: &ContentTreeState, visible: &[usize], p: usize) -> String {
let idx = visible[p];
let node = &state.nodes[idx];
let depth = node.depth;
if depth == 0 {
return String::new();
}
let mut prefix = String::new();
for l in 1..=depth {
let mut has_sibling_below = false;
for &next_idx in &visible[p + 1..] {
let next_depth = state.nodes[next_idx].depth;
if next_depth == l {
has_sibling_below = true;
break;
}
if next_depth < l {
break;
}
}
if l < depth {
if has_sibling_below {
prefix.push_str("│ ");
} else {
prefix.push_str(" ");
}
} else {
let is_first_child = if p > 0 {
state.nodes[visible[p - 1]].depth < depth
} else {
true
};
let is_last_child = !has_sibling_below;
if is_first_child && !is_last_child {
prefix.push_str("╭── ");
} else if is_last_child {
prefix.push_str("╰── ");
} else {
prefix.push_str("├── ");
}
}
}
prefix
}
pub fn draw_content_tree(
frame: &mut Frame,
area: Rect,
state: &ContentTreeState,
theme: &AppThemeColors,
keybinds: &Keybinds,
) {
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Min(0), Constraint::Length(1), ])
.split(area);
let title_area = chunks[0];
let main_area = chunks[1];
let hint_area = chunks[2];
crate::ui::draw_view_title_bar(
frame,
title_area,
&format!("CONTENT TREE — {}", state.note_title),
theme,
);
if state.load_error {
let err_p = Paragraph::new("Could not load note")
.style(Style::default().fg(theme.destructive))
.alignment(Alignment::Center);
frame.render_widget(err_p, main_area);
} else {
let content_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Ratio(45, 100), Constraint::Length(1), Constraint::Min(0), ])
.split(main_area);
let left_area = content_chunks[0];
let sep_area = content_chunks[1];
let right_area = content_chunks[2];
let visible = state.visible_indices();
let mut items = Vec::new();
for (p, &idx) in visible.iter().enumerate() {
if let Some(node) = state.nodes.get(idx) {
let prefix = get_tree_prefix(state, &visible, p);
let mut spans = Vec::new();
if !prefix.is_empty() {
spans.push(Span::styled(prefix, Style::default().fg(theme.muted)));
}
match &node.kind {
crate::content_tree::parse::NodeKind::Header { title, .. } => {
let arrow = if node.has_children {
if state.expanded.contains(&idx) {
"▼ "
} else {
"▶ "
}
} else {
" "
};
spans.push(Span::styled(
arrow,
Style::default()
.fg(theme.heading)
.add_modifier(Modifier::BOLD),
));
spans.push(Span::styled(
title.clone(),
Style::default()
.fg(theme.heading)
.add_modifier(Modifier::BOLD),
));
}
crate::content_tree::parse::NodeKind::ListItem { text } => {
spans.push(Span::styled("• ", Style::default().fg(theme.accent)));
spans.push(Span::styled(text.clone(), Style::default().fg(theme.text)));
}
crate::content_tree::parse::NodeKind::Paragraph { preview, .. } => {
spans.push(Span::styled(
preview.clone(),
Style::default().fg(theme.muted),
));
}
crate::content_tree::parse::NodeKind::CodeBlock { lang, .. } => {
spans.push(Span::styled(
format!("```{lang}"),
Style::default().fg(theme.muted),
));
}
}
items.push(ListItem::new(Line::from(spans)));
}
}
let list = List::new(items)
.block(Block::default().style(theme.bg_style()))
.highlight_style(
Style::default()
.fg(theme.highlight_fg)
.bg(theme.highlight_bg)
.add_modifier(Modifier::BOLD),
)
.highlight_symbol("> ");
let selected_pos = visible.iter().position(|&x| x == state.selected);
let mut list_state = ListState::default();
if let Some(pos) = selected_pos {
list_state.select(Some(pos));
}
frame.render_stateful_widget(list, left_area, &mut list_state);
crate::ui::draw_dim_vline(frame, sep_area, theme.muted);
if !state.nodes.is_empty() && state.selected < state.nodes.len() {
let node = &state.nodes[state.selected];
let full_text = node.full_text();
let right_block = Block::default()
.title(Span::styled(
" Full Content ",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
))
.style(theme.bg_style())
.padding(ratatui::widgets::Padding::new(2, 2, 1, 1));
let p = Paragraph::new(full_text)
.block(right_block)
.style(Style::default().fg(theme.text))
.wrap(ratatui::widgets::Wrap { trim: false });
frame.render_widget(p, right_area);
} else {
let right_block = Block::default()
.title(Span::styled(
" Full Content ",
Style::default()
.fg(theme.accent)
.add_modifier(Modifier::BOLD),
))
.style(theme.bg_style());
let p = Paragraph::new("").block(right_block);
frame.render_widget(p, right_area);
}
}
let move_keys = format!(
"{}/{} move",
keybinds.content_tree_keys_display(ContentTreeAction::MoveDown),
keybinds.content_tree_keys_display(ContentTreeAction::MoveUp)
);
let fold_keys = format!(
"{} fold",
keybinds.content_tree_keys_display(ContentTreeAction::ToggleCollapse)
);
let jump_keys = format!(
"{} jump",
keybinds.content_tree_keys_display(ContentTreeAction::Open)
);
let back_keys = format!(
"{} back",
keybinds.content_tree_keys_display(ContentTreeAction::Back)
);
let help_keys = format!(
"{} help",
keybinds.content_tree_keys_display(ContentTreeAction::Help)
);
let hint = format!("{move_keys} · {fold_keys} · {jump_keys} · {back_keys} · {help_keys}");
crate::ui::draw_status_bar(frame, hint_area, theme, None, &hint, None);
}