use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq)]
pub struct RawNode<T> {
pub name: String,
pub full_path: PathBuf, pub children: Vec<RawNode<T>>,
pub payload: T,
pub is_dir: bool,
}
impl<T: Clone> RawNode<T> {
pub fn expand_all(&self, state: &mut TreeViewState) {
if self.is_dir {
state.expanded_paths.insert(self.full_path.clone());
for child in &self.children {
child.expand_all(state);
}
}
}
pub fn sort_recursive(&mut self) {
self.children.sort_by(|a, b| match (a.is_dir, b.is_dir) {
(true, false) => std::cmp::Ordering::Less,
(false, true) => std::cmp::Ordering::Greater,
_ => a.name.cmp(&b.name),
});
for child in &mut self.children {
child.sort_recursive();
}
}
pub fn find_and_act<F>(&mut self, target_path: &Path, action: &mut F) -> bool
where
F: FnMut(&mut Self),
{
if self.full_path == target_path {
action(self);
return true;
}
if target_path.starts_with(&self.full_path) {
for child in &mut self.children {
if child.find_and_act(target_path, action) {
return true;
}
}
}
false
}
pub fn apply_recursive<F>(&mut self, action: &F)
where
F: Fn(&mut Self),
{
action(self);
for child in &mut self.children {
child.apply_recursive(action);
}
}
}
impl<T: Clone + Default + std::ops::AddAssign> RawNode<T> {
pub fn from_path_list(custom_root: Option<String>, files: Vec<(Vec<String>, T)>) -> Vec<Self> {
let mut internal_root = RawNode {
name: String::new(),
full_path: PathBuf::new(),
children: Vec::new(),
payload: T::default(),
is_dir: true,
};
for (path_parts, payload) in files {
internal_root.insert_recursive(&path_parts, payload, Path::new(""));
}
internal_root.sort_recursive();
if let Some(root_name) = custom_root {
let wrapper = RawNode {
name: root_name.clone(),
full_path: PathBuf::from(root_name),
children: internal_root.children,
payload: internal_root.payload,
is_dir: true,
};
vec![wrapper]
} else {
internal_root.children
}
}
fn insert_recursive(&mut self, path_parts: &[String], payload: T, parent_path: &Path) {
self.payload += payload.clone();
if path_parts.is_empty() {
return;
}
let name = &path_parts[0];
let is_last = path_parts.len() == 1;
let current_path = parent_path.join(name);
let child_idx = if let Some(idx) = self.children.iter().position(|c| &c.name == name) {
idx
} else {
let new_node = RawNode {
name: name.clone(),
full_path: current_path.clone(),
children: Vec::new(),
payload: T::default(),
is_dir: !is_last,
};
self.children.push(new_node);
self.children.len() - 1
};
if is_last {
self.children[child_idx].payload = payload;
} else {
self.children[child_idx].insert_recursive(&path_parts[1..], payload, ¤t_path);
}
}
}
impl RawNode<crate::app::TorrentPreviewPayload> {
pub fn collect_priorities(
&self,
out: &mut std::collections::HashMap<usize, crate::app::FilePriority>,
) {
if let Some(idx) = self.payload.file_index {
out.insert(idx, self.payload.priority);
}
for child in &self.children {
child.collect_priorities(out);
}
}
}
type FilterRule<T> = Rc<dyn Fn(&RawNode<T>) -> bool>;
#[derive(Clone)]
pub struct TreeFilter<T> {
pub queries: Vec<String>,
pub node_rule: Option<FilterRule<T>>,
}
impl<T> Default for TreeFilter<T> {
fn default() -> Self {
Self {
queries: Vec::new(),
node_rule: None,
}
}
}
impl<T> TreeFilter<T> {
pub fn from_text(input: &str) -> Self {
let queries = input
.split_whitespace()
.filter(|s| !s.is_empty())
.map(|s| s.to_lowercase())
.collect();
Self {
queries,
node_rule: None,
}
}
pub fn new(input: &str, rule: impl Fn(&RawNode<T>) -> bool + 'static) -> Self {
let mut filter = Self::from_text(input);
filter.node_rule = Some(Rc::new(rule));
filter
}
pub fn matches(&self, node: &RawNode<T>) -> bool {
if let Some(rule) = &self.node_rule {
if !(rule)(node) {
return false;
}
}
if self.queries.is_empty() {
return true;
}
let name_lower = node.name.to_lowercase();
self.queries.iter().all(|q| name_lower.contains(q))
}
pub fn any_matches(&self, node: &RawNode<T>) -> bool {
if self.matches(node) {
return true;
}
node.children.iter().any(|child| self.any_matches(child))
}
}
#[derive(Debug, Clone, Default, PartialEq)]
pub struct TreeViewState {
pub cursor_path: Option<PathBuf>,
pub current_path: PathBuf,
pub expanded_paths: HashSet<PathBuf>,
pub selected_paths: HashSet<PathBuf>,
pub top_most_offset: usize,
}
impl TreeViewState {
pub fn new() -> Self {
Self::default()
}
}
#[derive(Debug, PartialEq)]
pub struct RenderItem<'a, T> {
pub node: &'a RawNode<T>,
pub path: PathBuf,
pub depth: usize,
pub is_last: bool,
pub is_expanded: bool,
pub is_selected: bool,
pub is_cursor: bool,
}
impl<'a, T> Clone for RenderItem<'a, T> {
fn clone(&self) -> Self {
Self {
node: self.node,
path: self.path.clone(),
depth: self.depth,
is_last: self.is_last,
is_expanded: self.is_expanded,
is_selected: self.is_selected,
is_cursor: self.is_cursor,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TreeAction {
Up,
Down,
Left,
Right,
}
pub struct TreeMathHelper;
impl TreeMathHelper {
pub fn get_visible_slice<'a, T>(
nodes: &'a [RawNode<T>],
state: &TreeViewState,
filter: TreeFilter<T>,
max_height: usize,
) -> Vec<RenderItem<'a, T>> {
let mut full_list = Vec::new();
Self::project_recursive(nodes, state, &filter, 0, &mut full_list);
let start = state.top_most_offset.min(full_list.len());
let end = (start + max_height).min(full_list.len());
if start < end {
full_list[start..end].to_vec()
} else {
Vec::new()
}
}
pub fn apply_action<T>(
state: &mut TreeViewState,
nodes: &[RawNode<T>],
action: TreeAction,
filter: TreeFilter<T>,
max_height: usize,
) -> bool {
let mut full_list = Vec::new();
Self::project_recursive(nodes, state, &filter, 0, &mut full_list);
Self::handle_action(state, &full_list, action, max_height)
}
fn project_recursive<'a, T>(
nodes: &'a [RawNode<T>],
state: &TreeViewState,
filter: &TreeFilter<T>,
depth: usize,
output: &mut Vec<RenderItem<'a, T>>,
) {
let is_searching = !filter.queries.is_empty();
let visible_nodes: Vec<_> = nodes
.iter()
.filter(|node| filter.any_matches(node))
.collect();
let len = visible_nodes.len();
for (i, node) in visible_nodes.into_iter().enumerate() {
let path = node.full_path.clone();
let expanded = if is_searching {
true
} else {
state.expanded_paths.contains(&path)
};
output.push(RenderItem {
node,
path: path.clone(),
depth,
is_last: i == len - 1,
is_expanded: expanded,
is_selected: state.selected_paths.contains(&path),
is_cursor: state.cursor_path.as_ref() == Some(&path),
});
if node.is_dir && expanded {
Self::project_recursive(&node.children, state, filter, depth + 1, output);
}
}
}
fn handle_action<T>(
state: &mut TreeViewState,
full_list: &[RenderItem<'_, T>],
action: TreeAction,
max_height: usize,
) -> bool {
if full_list.is_empty() {
return false;
}
let current_idx = state
.cursor_path
.as_ref()
.and_then(|path| full_list.iter().position(|item| &item.path == path))
.unwrap_or(0);
let mut new_idx = current_idx;
match action {
TreeAction::Up => new_idx = current_idx.saturating_sub(1),
TreeAction::Down => {
if current_idx < full_list.len() - 1 {
new_idx = current_idx + 1;
}
}
TreeAction::Right => {
let item = &full_list[current_idx];
if item.node.is_dir {
if !state.expanded_paths.contains(&item.path) {
state.expanded_paths.insert(item.path.clone());
} else if current_idx < full_list.len() - 1 {
let next_item = &full_list[current_idx + 1];
if next_item.depth > item.depth {
new_idx = current_idx + 1;
}
}
}
}
TreeAction::Left => {
let item = &full_list[current_idx];
if item.node.is_dir && state.expanded_paths.contains(&item.path) {
state.expanded_paths.remove(&item.path);
} else if item.depth > 0 {
let parent = full_list[0..current_idx]
.iter()
.rfind(|x| x.depth == item.depth - 1);
if let Some(p) = parent {
new_idx = full_list
.iter()
.position(|i| i.path == p.path)
.unwrap_or(current_idx);
}
}
}
}
state.cursor_path = Some(full_list[new_idx].path.clone());
let effective_height = max_height.max(1);
if new_idx < state.top_most_offset {
state.top_most_offset = new_idx;
} else if new_idx >= state.top_most_offset + effective_height {
state.top_most_offset = (new_idx + 1).saturating_sub(effective_height);
}
true
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, PartialEq)]
struct TestPayload {
progress: f64,
}
fn mock_complex_tree() -> Vec<RawNode<TestPayload>> {
vec![
RawNode {
name: "root1".to_string(),
full_path: PathBuf::from("root1"),
is_dir: true,
payload: TestPayload { progress: 0.0 },
children: vec![
RawNode {
name: "sub1".to_string(),
full_path: PathBuf::from("root1/sub1"),
is_dir: true,
payload: TestPayload { progress: 0.0 },
children: vec![
RawNode {
name: "leaf1".to_string(),
full_path: PathBuf::from("root1/sub1/leaf1"),
is_dir: false,
payload: TestPayload { progress: 1.0 },
children: vec![],
},
RawNode {
name: "leaf2".to_string(),
full_path: PathBuf::from("root1/sub1/leaf2"),
is_dir: false,
payload: TestPayload { progress: 1.0 },
children: vec![],
},
],
},
RawNode {
name: "leaf3".to_string(),
full_path: PathBuf::from("root1/leaf3"),
is_dir: false,
payload: TestPayload { progress: 1.0 },
children: vec![],
},
],
},
RawNode {
name: "root_leaf".to_string(),
full_path: PathBuf::from("root_leaf"),
is_dir: false,
payload: TestPayload { progress: 1.0 },
children: vec![],
},
]
}
#[test]
fn test_initial_state() {
let tree = mock_complex_tree();
let state = TreeViewState::default();
let list = TreeMathHelper::get_visible_slice(&tree, &state, TreeFilter::from_text(""), 10);
assert_eq!(list.len(), 2);
}
#[test]
fn test_scrolling_down_triggers_offset() {
let tree = mock_complex_tree();
let mut state = TreeViewState::default();
state.expanded_paths.insert(PathBuf::from("root1"));
let max_height = 2;
state.cursor_path = Some(PathBuf::from("root1"));
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Down,
TreeFilter::from_text(""),
max_height,
);
assert_eq!(state.top_most_offset, 0);
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Down,
TreeFilter::from_text(""),
max_height,
);
assert_eq!(state.top_most_offset, 1);
}
#[test]
fn test_scrolling_behavior_on_zoom_change() {
let tree = mock_complex_tree();
let mut state = TreeViewState::default();
state.expanded_paths.insert(PathBuf::from("root1"));
state.cursor_path = Some(PathBuf::from("root_leaf"));
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Left,
TreeFilter::from_text(""),
10,
);
assert_eq!(state.top_most_offset, 0);
}
#[test]
fn test_left_collapses_dir() {
let tree = mock_complex_tree();
let mut state = TreeViewState::default();
let path = PathBuf::from("root1");
state.expanded_paths.insert(path.clone());
state.cursor_path = Some(path.clone());
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Left,
TreeFilter::from_text(""),
10,
);
assert!(!state.expanded_paths.contains(&path));
}
#[test]
fn test_search_auto_expands_and_filters() {
let tree = mock_complex_tree();
let state = TreeViewState::default();
let list =
TreeMathHelper::get_visible_slice(&tree, &state, TreeFilter::from_text("leaf1"), 10);
assert_eq!(list.len(), 3);
assert!(list[0].is_expanded);
assert!(list[1].is_expanded);
assert_eq!(list[2].node.name, "leaf1");
}
#[test]
fn test_lazy_loading_simulation() {
let mut tree = vec![RawNode {
name: "photos".to_string(),
full_path: PathBuf::from("photos"),
is_dir: true,
payload: TestPayload { progress: 0.0 },
children: vec![],
}];
let mut state = TreeViewState {
cursor_path: Some(PathBuf::from("photos")),
..Default::default()
};
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Right,
TreeFilter::from_text(""),
10,
);
assert!(state.expanded_paths.contains(&PathBuf::from("photos")));
let visible =
TreeMathHelper::get_visible_slice(&tree, &state, TreeFilter::from_text(""), 10);
assert_eq!(visible.len(), 1);
tree[0].children.push(RawNode {
name: "vacation.jpg".to_string(),
full_path: PathBuf::from("photos/vacation.jpg"),
is_dir: false,
payload: TestPayload { progress: 1.0 },
children: vec![],
});
let visible_after_load =
TreeMathHelper::get_visible_slice(&tree, &state, TreeFilter::from_text(""), 10);
assert_eq!(visible_after_load.len(), 2);
assert_eq!(visible_after_load[1].node.name, "vacation.jpg");
}
#[test]
fn test_list_reordering_preserves_cursor() {
let mut tree = vec![
RawNode {
name: "Slow".into(),
full_path: PathBuf::from("Slow"),
is_dir: false,
payload: TestPayload { progress: 0.0 },
children: vec![],
},
RawNode {
name: "Fast".into(),
full_path: PathBuf::from("Fast"),
is_dir: false,
payload: TestPayload { progress: 0.0 },
children: vec![],
},
];
let state = TreeViewState {
cursor_path: Some(PathBuf::from("Fast")),
..Default::default()
};
tree.swap(0, 1);
let visible =
TreeMathHelper::get_visible_slice(&tree, &state, TreeFilter::from_text(""), 10);
assert_eq!(visible[0].node.name, "Fast");
assert!(visible[0].is_cursor);
assert!(!visible[1].is_cursor);
}
#[test]
fn test_expanding_actually_empty_directory() {
let tree = vec![RawNode {
name: "EmptyDir".into(),
full_path: PathBuf::from("EmptyDir"),
is_dir: true,
payload: TestPayload { progress: 0.0 },
children: vec![],
}];
let mut state = TreeViewState {
cursor_path: Some(PathBuf::from("EmptyDir")),
..Default::default()
};
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Right,
TreeFilter::from_text(""),
10,
);
assert!(state.expanded_paths.contains(&PathBuf::from("EmptyDir")));
let old_cursor = state.cursor_path.clone();
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Right,
TreeFilter::from_text(""),
10,
);
assert_eq!(state.cursor_path, old_cursor);
}
#[test]
fn test_smart_nav_right_descends_into_child() {
let tree = vec![RawNode {
name: "Root".into(),
full_path: PathBuf::from("Root"),
is_dir: true,
payload: TestPayload { progress: 0.0 },
children: vec![RawNode {
name: "Child".into(),
full_path: PathBuf::from("Root/Child"),
is_dir: false,
payload: TestPayload { progress: 0.0 },
children: vec![],
}],
}];
let mut state = TreeViewState::default();
state.expanded_paths.insert(PathBuf::from("Root"));
state.cursor_path = Some(PathBuf::from("Root"));
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Right,
TreeFilter::from_text(""),
10,
);
assert_eq!(state.cursor_path, Some(PathBuf::from("Root/Child")));
}
#[test]
fn test_smart_nav_left_jumps_to_parent() {
let tree = vec![RawNode {
name: "Root".into(),
full_path: PathBuf::from("Root"),
is_dir: true,
payload: TestPayload { progress: 0.0 },
children: vec![RawNode {
name: "Child".into(),
full_path: PathBuf::from("Root/Child"),
is_dir: false,
payload: TestPayload { progress: 0.0 },
children: vec![],
}],
}];
let mut state = TreeViewState::default();
state.expanded_paths.insert(PathBuf::from("Root"));
state.cursor_path = Some(PathBuf::from("Root/Child"));
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Left,
TreeFilter::from_text(""),
10,
);
assert_eq!(state.cursor_path, Some(PathBuf::from("Root")));
assert!(state.expanded_paths.contains(&PathBuf::from("Root")));
}
#[test]
fn test_selection_persists_after_collapse() {
let tree = vec![RawNode {
name: "Root".into(),
full_path: PathBuf::from("Root"),
is_dir: true,
payload: TestPayload { progress: 0.0 },
children: vec![RawNode {
name: "Child".into(),
full_path: PathBuf::from("Root/Child"),
is_dir: false,
payload: TestPayload { progress: 0.0 },
children: vec![],
}],
}];
let mut state = TreeViewState::default();
let child_path = PathBuf::from("Root/Child");
state.expanded_paths.insert(PathBuf::from("Root"));
state.selected_paths.insert(child_path.clone());
state.cursor_path = Some(PathBuf::from("Root"));
TreeMathHelper::apply_action(
&mut state,
&tree,
TreeAction::Left,
TreeFilter::from_text(""),
10,
);
assert!(!state.expanded_paths.contains(&PathBuf::from("Root")));
assert!(state.selected_paths.contains(&child_path));
}
}