pub(crate) mod config;
pub(crate) mod drag;
pub(crate) mod error;
pub(crate) mod executor;
pub(crate) mod icon;
pub(crate) mod keyboard;
pub(crate) mod message;
pub(crate) mod node;
pub(crate) mod search;
pub(crate) mod selection;
pub(crate) mod update;
pub(crate) mod view;
pub(crate) mod walker;
use std::path::PathBuf;
use std::sync::Arc;
use self::{
config::{DirectoryFilter, TreeConfig},
executor::{ScanExecutor, ThreadExecutor},
node::{TreeCache, TreeNode},
};
pub struct DirectoryTree {
pub(crate) root: TreeNode,
pub(crate) config: TreeConfig,
pub(crate) cache: TreeCache,
pub(crate) generation: u64,
pub(crate) selected_paths: Vec<std::path::PathBuf>,
pub(crate) active_path: Option<std::path::PathBuf>,
pub(crate) anchor_path: Option<std::path::PathBuf>,
pub(crate) drag: Option<drag::DragState>,
pub(crate) prefetching_paths: std::collections::HashSet<std::path::PathBuf>,
pub(crate) search: Option<search::SearchState>,
pub(crate) executor: Arc<dyn ScanExecutor>,
}
impl DirectoryTree {
pub fn new(root: PathBuf) -> Self {
let root_node = TreeNode::new_root(root.clone());
Self {
root: root_node,
config: TreeConfig {
root_path: root,
filter: DirectoryFilter::default(),
max_depth: None,
prefetch_per_parent: 0,
},
cache: TreeCache::default(),
generation: 0,
selected_paths: Vec::new(),
active_path: None,
anchor_path: None,
drag: None,
prefetching_paths: std::collections::HashSet::new(),
search: None,
executor: Arc::new(ThreadExecutor),
}
}
pub fn with_filter(mut self, filter: DirectoryFilter) -> Self {
self.set_filter(filter);
self
}
pub fn with_max_depth(mut self, depth: u32) -> Self {
self.config.max_depth = Some(depth);
self
}
pub fn with_prefetch_limit(mut self, limit: usize) -> Self {
self.config.prefetch_per_parent = limit;
self
}
pub fn with_executor(mut self, executor: Arc<dyn ScanExecutor>) -> Self {
self.executor = executor;
self
}
pub fn set_filter(&mut self, filter: DirectoryFilter) {
if self.config.filter == filter {
return;
}
self.config.filter = filter;
rebuild_from_cache(&mut self.root, &self.cache, filter);
self.sync_selection_flags();
self.recompute_search_visibility();
}
pub fn root_path(&self) -> &std::path::Path {
&self.config.root_path
}
pub fn filter(&self) -> DirectoryFilter {
self.config.filter
}
pub fn max_depth(&self) -> Option<u32> {
self.config.max_depth
}
pub fn selected_path(&self) -> Option<&std::path::Path> {
self.active_path.as_deref()
}
pub fn selected_paths(&self) -> &[std::path::PathBuf] {
&self.selected_paths
}
pub fn anchor_path(&self) -> Option<&std::path::Path> {
self.anchor_path.as_deref()
}
pub fn is_selected(&self, path: &std::path::Path) -> bool {
self.selected_paths.iter().any(|p| p == path)
}
pub fn is_dragging(&self) -> bool {
self.drag.is_some()
}
pub fn drop_target(&self) -> Option<&std::path::Path> {
self.drag.as_ref().and_then(|d| d.hover.as_deref())
}
pub fn drag_sources(&self) -> &[std::path::PathBuf] {
self.drag.as_ref().map_or(&[], |d| d.sources.as_slice())
}
pub fn set_search_query(&mut self, query: impl Into<String>) {
let q: String = query.into();
if q.is_empty() {
self.search = None;
return;
}
self.search = Some(search::SearchState::new(q));
self.recompute_search_visibility();
}
pub fn clear_search(&mut self) {
self.search = None;
}
pub fn search_query(&self) -> Option<&str> {
self.search.as_ref().map(|s| s.query.as_str())
}
pub fn is_searching(&self) -> bool {
self.search.is_some()
}
pub fn search_match_count(&self) -> usize {
self.search.as_ref().map_or(0, |s| s.match_count)
}
pub(crate) fn recompute_search_visibility(&mut self) {
let Some(state) = self.search.as_mut() else {
return;
};
let mut visible: std::collections::HashSet<std::path::PathBuf> =
std::collections::HashSet::new();
let mut match_count: usize = 0;
let _ = walk_for_search(
&self.root,
&state.query_lower,
&mut visible,
&mut match_count,
);
state.visible_paths = visible;
state.match_count = match_count;
}
pub(crate) fn visible_rows(&self) -> Vec<node::VisibleRow<'_>> {
match &self.search {
None => self.root.visible_rows(),
Some(state) => {
let mut out = Vec::new();
collect_search_visible(&self.root, 0, &state.visible_paths, &mut out);
out
}
}
}
}
fn collect_search_visible<'a>(
node: &'a TreeNode,
depth: u32,
visible: &std::collections::HashSet<std::path::PathBuf>,
out: &mut Vec<node::VisibleRow<'a>>,
) {
if !visible.contains(&node.path) {
return;
}
out.push(node::VisibleRow { node, depth });
for child in &node.children {
collect_search_visible(child, depth + 1, visible, out);
}
}
fn walk_for_search(
node: &TreeNode,
query_lower: &str,
visible: &mut std::collections::HashSet<std::path::PathBuf>,
match_count: &mut usize,
) -> bool {
let mut subtree_has_match = false;
for child in &node.children {
if walk_for_search(child, query_lower, visible, match_count) {
subtree_has_match = true;
}
}
let self_matches = search::matches_query(&node.path, query_lower);
if self_matches {
*match_count += 1;
}
if self_matches || subtree_has_match {
visible.insert(node.path.clone());
true
} else {
false
}
}
impl DirectoryTree {
pub(crate) fn sync_selection_flags(&mut self) {
self.root.clear_selection();
let paths: Vec<std::path::PathBuf> = self.selected_paths.clone();
for p in &paths {
if let Some(node) = self.root.find_mut(p) {
node.is_selected = true;
}
}
}
}
fn rebuild_from_cache(node: &mut TreeNode, cache: &node::TreeCache, filter: DirectoryFilter) {
if node.is_dir && node.is_loaded {
if let Some(cached) = cache.get(&node.path) {
let mut previous: std::collections::HashMap<PathBuf, TreeNode> = node
.children
.drain(..)
.map(|c| (c.path.clone(), c))
.collect();
node.children = cached
.raw
.iter()
.filter(|e| e.passes(filter))
.map(|e| {
previous
.remove(&e.path)
.unwrap_or_else(|| TreeNode::from_entry(e))
})
.collect();
} else {
}
}
for child in &mut node.children {
rebuild_from_cache(child, cache, filter);
}
}