keyboard_nav/keyboard_nav.rs
1//! Keyboard-driven directory browsing.
2//!
3//! Run with:
4//!
5//! ```sh
6//! cargo run --example keyboard_nav -- /path/to/browse
7//! ```
8//!
9//! Defaults to the current directory. Navigate with the arrow keys,
10//! `Enter` to expand/collapse folders, `Space` to re-emit the current
11//! selection, `Home`/`End` to jump to the first/last visible row.
12//!
13//! The pattern this demonstrates:
14//!
15//! 1. Subscribe to `iced::keyboard::on_key_press` in `subscription()`.
16//! 2. Pipe each key through `DirectoryTree::handle_key`.
17//! 3. If a synthetic [`DirectoryTreeEvent`] comes back, route it to
18//! `DirectoryTree::update` like any other event.
19
20use std::path::PathBuf;
21
22use iced::keyboard::{self, Modifiers};
23use iced::widget::{column, container, text};
24use iced::{Element, Length, Subscription, Task};
25use iced_swdir_tree::{DirectoryFilter, DirectoryTree, DirectoryTreeEvent};
26
27#[derive(Debug, Clone)]
28enum Message {
29 Tree(DirectoryTreeEvent),
30 Key(keyboard::Key, Modifiers),
31}
32
33struct App {
34 tree: DirectoryTree,
35 last_selected: Option<PathBuf>,
36}
37
38impl App {
39 fn new() -> (Self, Task<Message>) {
40 let root = std::env::args()
41 .nth(1)
42 .map(PathBuf::from)
43 .unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")));
44 // Proactively expand the root so the user sees something
45 // without having to click or key-press first.
46 let root_for_task = root.clone();
47 let tree = DirectoryTree::new(root).with_filter(DirectoryFilter::FilesAndFolders);
48 (
49 Self {
50 tree,
51 last_selected: None,
52 },
53 Task::done(Message::Tree(DirectoryTreeEvent::Toggled(root_for_task))),
54 )
55 }
56
57 fn update(&mut self, message: Message) -> Task<Message> {
58 match message {
59 Message::Tree(event) => {
60 if let DirectoryTreeEvent::Selected(p, _, _) = &event {
61 self.last_selected = Some(p.clone());
62 }
63 self.tree.update(event).map(Message::Tree)
64 }
65 Message::Key(key, mods) => {
66 // handle_key is `&self` — it only *produces* an
67 // event. We still have to route the returned event
68 // back through update so state transitions (cursor
69 // move, expand/collapse) actually happen.
70 if let Some(event) = self.tree.handle_key(&key, mods) {
71 if let DirectoryTreeEvent::Selected(p, _, _) = &event {
72 self.last_selected = Some(p.clone());
73 }
74 return self.tree.update(event).map(Message::Tree);
75 }
76 Task::none()
77 }
78 }
79 }
80
81 fn subscription(&self) -> Subscription<Message> {
82 // In a real app you'd gate this on "does the tree have
83 // focus?" — a checkbox in settings, a sidebar toggle,
84 // etc. Here the tree is the whole UI so it always has focus.
85 //
86 // `iced::keyboard::listen()` exposes every keyboard event as
87 // a `keyboard::Event`. We want key presses only; non-press
88 // events (KeyReleased, ModifiersChanged) are handled by the
89 // widget with `handle_key` returning `None`, so we can
90 // cheaply forward the non-KeyPressed placeholder values too
91 // — but it's tidier to map them into a no-op `Message::Tree`
92 // that gets dropped by `update`'s match.
93 keyboard::listen().map(|event| match event {
94 keyboard::Event::KeyPressed { key, modifiers, .. } => Message::Key(key, modifiers),
95 // Non-press events: use a dummy key that handle_key
96 // leaves unbound so update() returns Task::none().
97 _ => Message::Key(
98 keyboard::Key::Named(keyboard::key::Named::F35),
99 Modifiers::default(),
100 ),
101 })
102 }
103
104 fn view(&self) -> Element<'_, Message> {
105 let status = text(match &self.last_selected {
106 Some(p) => format!(
107 "Selected: {} | Try ↑ ↓ ← →, Enter, Space, Home, End.",
108 p.display()
109 ),
110 None => "Press ↓ to select the first row.".into(),
111 })
112 .size(12);
113
114 container(
115 column![self.tree.view(Message::Tree), status]
116 .spacing(8.0)
117 .padding(8.0),
118 )
119 .width(Length::Fill)
120 .height(Length::Fill)
121 .into()
122 }
123}
124
125fn main() -> iced::Result {
126 iced::application(App::new, App::update, App::view)
127 .subscription(App::subscription)
128 .title("iced-swdir-tree · keyboard navigation example")
129 .run()
130}