Skip to main content

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}