use crate::{ error::Result, filetreeitems::FileTreeItems, tree_iter::TreeIterator, TreeItemInfo, }; use std::{collections::BTreeSet, path::Path, usize}; /// #[derive(Copy, Clone, Debug)] pub enum MoveSelection { Up, Down, Left, Right, Top, End, PageDown, PageUp, } #[derive(Debug, Clone, Copy)] pub struct VisualSelection { pub count: usize, pub index: usize, } /// wraps `FileTreeItems` as a datastore and adds selection functionality #[derive(Default)] pub struct FileTree { items: FileTreeItems, selection: Option, // caches the absolute selection translated to visual index visual_selection: Option, } impl FileTree { /// pub fn new( list: &[&Path], collapsed: &BTreeSet<&String>, ) -> Result { let mut new_self = Self { items: FileTreeItems::new(list, collapsed)?, selection: if list.is_empty() { None } else { Some(0) }, visual_selection: None, }; new_self.visual_selection = new_self.calc_visual_selection(); Ok(new_self) } /// pub const fn is_empty(&self) -> bool { self.items.file_count() == 0 } /// pub fn collapse_but_root(&mut self) { if !self.is_empty() { self.items.collapse(0, true); self.items.expand(0, false); } } /// iterates visible elements starting from `start_index_visual` pub fn iterate( &self, start_index_visual: usize, max_amount: usize, ) -> TreeIterator<'_> { let start = self .visual_index_to_absolute(start_index_visual) .unwrap_or_default(); TreeIterator::new( self.items.iterate(start, max_amount), self.selection, ) } /// pub const fn visual_selection(&self) -> Option<&VisualSelection> { self.visual_selection.as_ref() } /// pub fn selected_file(&self) -> Option<&TreeItemInfo> { self.selection.and_then(|index| { let item = &self.items.tree_items[index]; if item.kind().is_path() { None } else { Some(item.info()) } }) } /// pub fn collapse_recursive(&mut self) { if let Some(selection) = self.selection { self.items.collapse(selection, true); } } /// pub fn expand_recursive(&mut self) { if let Some(selection) = self.selection { self.items.expand(selection, true); } } /// pub fn move_selection(&mut self, dir: MoveSelection) -> bool { self.selection.map_or(false, |selection| { let new_index = match dir { MoveSelection::Up => { self.selection_updown(selection, true) } MoveSelection::Down => { self.selection_updown(selection, false) } MoveSelection::Left => self.selection_left(selection), MoveSelection::Right => { self.selection_right(selection) } MoveSelection::Top => { Self::selection_start(selection) } MoveSelection::End => self.selection_end(selection), MoveSelection::PageDown | MoveSelection::PageUp => { None } }; let changed_index = new_index.map(|i| i != selection).unwrap_or_default(); if changed_index { self.selection = new_index; self.visual_selection = self.calc_visual_selection(); } changed_index || new_index.is_some() }) } fn visual_index_to_absolute( &self, visual_index: usize, ) -> Option { self.items .iterate(0, self.items.len()) .enumerate() .find_map(|(i, (abs, _))| { if i == visual_index { Some(abs) } else { None } }) } fn calc_visual_selection(&self) -> Option { self.selection.map(|selection_absolute| { let mut count = 0; let mut visual_index = 0; for (index, _item) in self.items.iterate(0, self.items.len()) { if selection_absolute == index { visual_index = count; } count += 1; } VisualSelection { index: visual_index, count, } }) } const fn selection_start(current_index: usize) -> Option { if current_index == 0 { None } else { Some(0) } } fn selection_end(&self, current_index: usize) -> Option { let items_max = self.items.len().saturating_sub(1); let mut new_index = items_max; loop { if self.is_visible_index(new_index) { break; } if new_index == 0 { break; } new_index = new_index.saturating_sub(1); new_index = std::cmp::min(new_index, items_max); } if new_index == current_index { None } else { Some(new_index) } } fn selection_updown( &self, current_index: usize, up: bool, ) -> Option { let mut index = current_index; loop { index = { let new_index = if up { index.saturating_sub(1) } else { index.saturating_add(1) }; // when reaching usize bounds if new_index == index { break; } if new_index >= self.items.len() { break; } new_index }; if self.is_visible_index(index) { break; } } if index == current_index { None } else { Some(index) } } fn select_parent( &mut self, current_index: usize, ) -> Option { let indent = self.items.tree_items[current_index].info().indent(); let mut index = current_index; while let Some(selection) = self.selection_updown(index, true) { index = selection; if self.items.tree_items[index].info().indent() < indent { break; } } if index == current_index { None } else { Some(index) } } fn selection_left( &mut self, current_index: usize, ) -> Option { let item = &mut self.items.tree_items[current_index]; if item.kind().is_path() && !item.kind().is_path_collapsed() { self.items.collapse(current_index, false); return Some(current_index); } self.select_parent(current_index) } fn selection_right( &mut self, current_selection: usize, ) -> Option { let item = &mut self.items.tree_items[current_selection]; if item.kind().is_path() { if item.kind().is_path_collapsed() { self.items.expand(current_selection, false); return Some(current_selection); } return self.selection_updown(current_selection, false); } None } fn is_visible_index(&self, index: usize) -> bool { self.items .tree_items .get(index) .map(|item| item.info().is_visible()) .unwrap_or_default() } } #[cfg(test)] mod test { use crate::{FileTree, MoveSelection}; use pretty_assertions::assert_eq; use std::{collections::BTreeSet, path::Path}; #[test] fn test_selection() { let items = vec![ Path::new("a/b"), // ]; let mut tree = FileTree::new(&items, &BTreeSet::new()).unwrap(); assert!(tree.move_selection(MoveSelection::Down)); assert_eq!(tree.selection, Some(1)); assert!(!tree.move_selection(MoveSelection::Down)); assert_eq!(tree.selection, Some(1)); } #[test] fn test_selection_skips_collapsed() { let items = vec![ Path::new("a/b/c"), // Path::new("a/d"), // ]; //0 a/ //1 b/ //2 c //3 d let mut tree = FileTree::new(&items, &BTreeSet::new()).unwrap(); tree.items.collapse(1, false); tree.selection = Some(1); assert!(tree.move_selection(MoveSelection::Down)); assert_eq!(tree.selection, Some(3)); } #[test] fn test_selection_left_collapse() { let items = vec![ Path::new("a/b/c"), // Path::new("a/d"), // ]; //0 a/ //1 b/ //2 c //3 d let mut tree = FileTree::new(&items, &BTreeSet::new()).unwrap(); tree.selection = Some(1); //collapses 1 assert!(tree.move_selection(MoveSelection::Left)); // index will not change assert_eq!(tree.selection, Some(1)); assert!(tree.items.tree_items[1].kind().is_path_collapsed()); assert!(!tree.items.tree_items[2].info().is_visible()); } #[test] fn test_selection_left_parent() { let items = vec![ Path::new("a/b/c"), // Path::new("a/d"), // ]; //0 a/ //1 b/ //2 c //3 d let mut tree = FileTree::new(&items, &BTreeSet::new()).unwrap(); tree.selection = Some(2); assert!(tree.move_selection(MoveSelection::Left)); assert_eq!(tree.selection, Some(1)); assert!(tree.move_selection(MoveSelection::Left)); assert_eq!(tree.selection, Some(1)); assert!(tree.move_selection(MoveSelection::Left)); assert_eq!(tree.selection, Some(0)); } #[test] fn test_selection_right_expand() { let items = vec![ Path::new("a/b/c"), // Path::new("a/d"), // ]; //0 a/ //1 b/ //2 c //3 d let mut tree = FileTree::new(&items, &BTreeSet::new()).unwrap(); tree.items.collapse(1, false); tree.items.collapse(0, false); tree.selection = Some(0); assert!(tree.move_selection(MoveSelection::Right)); assert_eq!(tree.selection, Some(0)); assert!(!tree.items.tree_items[0].kind().is_path_collapsed()); assert!(tree.move_selection(MoveSelection::Right)); assert_eq!(tree.selection, Some(1)); assert!(tree.items.tree_items[1].kind().is_path_collapsed()); assert!(tree.move_selection(MoveSelection::Right)); assert_eq!(tree.selection, Some(1)); assert!(!tree.items.tree_items[1].kind().is_path_collapsed()); } #[test] fn test_selection_top() { let items = vec![ Path::new("a/b/c"), // Path::new("a/d"), // ]; //0 a/ //1 b/ //2 c //3 d let mut tree = FileTree::new(&items, &BTreeSet::new()).unwrap(); tree.selection = Some(3); assert!(tree.move_selection(MoveSelection::Top)); assert_eq!(tree.selection, Some(0)); } #[test] fn test_visible_selection() { let items = vec![ Path::new("a/b/c"), // Path::new("a/b/c2"), // Path::new("a/d"), // ]; //0 a/ //1 b/ //2 c //3 c2 //4 d let mut tree = FileTree::new(&items, &BTreeSet::new()).unwrap(); tree.selection = Some(1); assert!(tree.move_selection(MoveSelection::Left)); assert!(tree.move_selection(MoveSelection::Down)); let s = tree.visual_selection().unwrap(); assert_eq!(s.count, 3); assert_eq!(s.index, 2); } }