use std::path::Path;
use iced::keyboard::{self, Modifiers, key::Named};
use super::DirectoryTree;
use super::drag::DragMsg;
use super::message::DirectoryTreeEvent;
use super::node::{TreeNode, VisibleRow};
use super::selection::SelectionMode;
impl DirectoryTree {
pub fn handle_key(
&self,
key: &keyboard::Key,
modifiers: Modifiers,
) -> Option<DirectoryTreeEvent> {
let keyboard::Key::Named(named) = key else {
return None;
};
let nav_mode = if modifiers.shift() {
SelectionMode::ExtendRange
} else {
SelectionMode::Replace
};
let rows = self.visible_rows();
match named {
Named::ArrowDown => self.move_selection(&rows, Direction::Next, nav_mode),
Named::ArrowUp => self.move_selection(&rows, Direction::Prev, nav_mode),
Named::Home => rows.first().map(|r| select_event(r, nav_mode)),
Named::End => rows.last().map(|r| select_event(r, nav_mode)),
Named::Enter => self.enter_action(),
Named::Space => self.toggle_active(),
Named::ArrowLeft => self.left_action(&rows),
Named::ArrowRight => self.right_action(),
Named::Escape if self.drag.is_some() => {
Some(DirectoryTreeEvent::Drag(DragMsg::Cancelled))
}
_ => None,
}
}
fn move_selection(
&self,
rows: &[VisibleRow<'_>],
dir: Direction,
mode: SelectionMode,
) -> Option<DirectoryTreeEvent> {
if rows.is_empty() {
return None;
}
let Some(current) = self.active_path.as_deref() else {
return match dir {
Direction::Next => rows.first().map(|r| select_event(r, mode)),
Direction::Prev => rows.last().map(|r| select_event(r, mode)),
};
};
let Some(idx) = rows.iter().position(|r| r.node.path == current) else {
return match dir {
Direction::Next => rows.first().map(|r| select_event(r, mode)),
Direction::Prev => rows.last().map(|r| select_event(r, mode)),
};
};
let next_idx = match dir {
Direction::Next => idx.saturating_add(1),
Direction::Prev => idx.checked_sub(1)?,
};
rows.get(next_idx).map(|r| select_event(r, mode))
}
fn enter_action(&self) -> Option<DirectoryTreeEvent> {
let path = self.active_path.as_deref()?;
let node = find(&self.root, path)?;
if node.is_dir {
Some(DirectoryTreeEvent::Toggled(path.to_path_buf()))
} else {
None
}
}
fn toggle_active(&self) -> Option<DirectoryTreeEvent> {
let path = self.active_path.as_deref()?;
let node = find(&self.root, path)?;
Some(DirectoryTreeEvent::Selected(
path.to_path_buf(),
node.is_dir,
SelectionMode::Toggle,
))
}
fn left_action(&self, rows: &[VisibleRow<'_>]) -> Option<DirectoryTreeEvent> {
let path = self.active_path.as_deref()?;
let node = find(&self.root, path)?;
if node.is_dir && node.is_expanded {
return Some(DirectoryTreeEvent::Toggled(path.to_path_buf()));
}
let current_idx = rows.iter().position(|r| r.node.path == path)?;
let current_depth = rows[current_idx].depth;
if current_depth == 0 {
return None;
}
let parent = rows[..current_idx]
.iter()
.rev()
.find(|r| r.depth < current_depth)?;
Some(select_event(parent, SelectionMode::Replace))
}
fn right_action(&self) -> Option<DirectoryTreeEvent> {
let path = self.active_path.as_deref()?;
let node = find(&self.root, path)?;
if !node.is_dir {
return None;
}
if !node.is_expanded {
return Some(DirectoryTreeEvent::Toggled(path.to_path_buf()));
}
let first = node.children.first()?;
Some(DirectoryTreeEvent::Selected(
first.path.clone(),
first.is_dir,
SelectionMode::Replace,
))
}
}
#[derive(Clone, Copy)]
enum Direction {
Next,
Prev,
}
fn find<'a>(node: &'a TreeNode, target: &Path) -> Option<&'a TreeNode> {
if node.path == target {
return Some(node);
}
if !target.starts_with(&node.path) {
return None;
}
for child in &node.children {
if let Some(hit) = find(child, target) {
return Some(hit);
}
}
None
}
fn select_event(row: &VisibleRow<'_>, mode: SelectionMode) -> DirectoryTreeEvent {
DirectoryTreeEvent::Selected(row.node.path.clone(), row.node.is_dir, mode)
}
#[cfg(test)]
mod tests;