use ratatui::{
layout::{Constraint, Direction, Layout, Rect},
style::{Color, Style},
widgets::{Block, Borders},
Frame,
};
use super::widgets::{TreeNode, TreeState, TreeWidget};
pub fn centered_rect(percent_x: u16, percent_y: u16, area: Rect) -> Rect {
let vertical = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Percentage((100 - percent_y) / 2),
Constraint::Percentage(percent_y),
Constraint::Percentage((100 - percent_y) / 2),
])
.split(area);
Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Percentage((100 - percent_x) / 2),
Constraint::Percentage(percent_x),
Constraint::Percentage((100 - percent_x) / 2),
])
.split(vertical[1])[1]
}
pub fn render_tree_pane(
frame: &mut Frame,
area: Rect,
roots: &[TreeNode],
state: &mut TreeState,
title: &str,
) -> Rect {
let chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Percentage(50), Constraint::Percentage(50)])
.split(area);
state.viewport_height = chunks[0].height.saturating_sub(2) as usize;
let tree = TreeWidget::new(roots, state).block(
Block::default()
.borders(Borders::ALL)
.title(format!(" {title} "))
.border_style(Style::default().fg(Color::Cyan)),
);
frame.render_widget(tree, chunks[0]);
chunks[1]
}
fn node_matches(node: &TreeNode, q: &str) -> bool {
node.label.to_lowercase().contains(q)
|| node
.value
.as_deref()
.map(|v| v.to_lowercase().contains(q))
.unwrap_or(false)
}
pub fn search_and_reveal(roots: &mut [TreeNode], state: &mut TreeState, query: &str) -> bool {
let q = query.trim().to_lowercase();
if q.is_empty() {
return false;
}
fn expand_to(node: &mut TreeNode, q: &str) -> bool {
if node_matches(node, q) {
return true;
}
for c in &mut node.children {
if expand_to(c, q) {
node.expanded = true;
return true;
}
}
false
}
let mut found = false;
for r in roots.iter_mut() {
if expand_to(r, &q) {
found = true;
break;
}
}
if !found {
return false;
}
if let Some(idx) = roots
.iter()
.flat_map(|r| r.flatten(true))
.position(|n| node_matches(n, &q))
{
state.selected = idx;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn centered_rect_is_centered_and_sized() {
let area = Rect::new(0, 0, 100, 100);
let r = centered_rect(60, 40, area);
assert_eq!(r.width, 60);
assert_eq!(r.height, 40);
assert_eq!(r.x, 20);
assert_eq!(r.y, 30);
}
fn tree() -> Vec<TreeNode> {
let mut root = TreeNode::new("settings", "section");
let mut group = TreeNode::new("skill", "section");
group.add_child(TreeNode::with_value(
"dest",
"~/skills",
"setting:string:skill.dest",
));
root.add_child(group);
vec![root]
}
#[test]
fn search_reveals_and_selects_match() {
let mut roots = tree();
let mut state = TreeState::new();
assert!(search_and_reveal(&mut roots, &mut state, "dest"));
let visible: Vec<_> = roots.iter().flat_map(|r| r.flatten(true)).collect();
assert_eq!(visible[state.selected].label, "dest");
}
#[test]
fn search_matches_on_value_too() {
let mut roots = tree();
let mut state = TreeState::new();
assert!(search_and_reveal(&mut roots, &mut state, "skills"));
}
#[test]
fn blank_or_missing_query_returns_false() {
let mut roots = tree();
let mut state = TreeState::new();
assert!(!search_and_reveal(&mut roots, &mut state, " "));
assert!(!search_and_reveal(&mut roots, &mut state, "nonexistent"));
}
}