Skip to main content

iced_runtime/
keyboard.rs

1//! Track keyboard events and handle keyboard navigation.
2pub use iced_core::keyboard::*;
3
4use crate::UserInterface;
5use crate::core;
6use crate::core::widget::operation::{self, Operation as _};
7
8/// Runs a chained operation to completion.
9fn run_operation<Message, Theme, Renderer>(
10    ui: &mut UserInterface<'_, Message, Theme, Renderer>,
11    renderer: &Renderer,
12    mut op: Box<dyn operation::Operation>,
13) where
14    Renderer: core::Renderer,
15{
16    loop {
17        ui.operate(renderer, op.as_mut());
18
19        match op.finish() {
20            operation::Outcome::Chain(next) => {
21                op = next;
22            }
23            _ => break,
24        }
25    }
26}
27
28/// Moves focus and scrolls the newly focused widget into view.
29fn focus_and_scroll<Message, Theme, Renderer>(
30    shift: bool,
31    ui: &mut UserInterface<'_, Message, Theme, Renderer>,
32    renderer: &Renderer,
33) where
34    Renderer: core::Renderer,
35{
36    let op: Box<dyn operation::Operation> = if shift {
37        Box::new(operation::focusable::focus_previous::<()>())
38    } else {
39        Box::new(operation::focusable::focus_next::<()>())
40    };
41
42    run_operation(ui, renderer, op);
43
44    run_operation(
45        ui,
46        renderer,
47        Box::new(operation::focusable::scroll_focused_into_view::<()>()),
48    );
49}
50
51/// Moves focus within a scope and scrolls the newly focused widget into view.
52fn focus_and_scroll_within<Message, Theme, Renderer>(
53    shift: bool,
54    ui: &mut UserInterface<'_, Message, Theme, Renderer>,
55    renderer: &Renderer,
56    scope: core::widget::Id,
57) where
58    Renderer: core::Renderer,
59{
60    let op: Box<dyn operation::Operation> = if shift {
61        Box::new(operation::focusable::focus_previous_within::<()>(scope))
62    } else {
63        Box::new(operation::focusable::focus_next_within::<()>(scope))
64    };
65
66    run_operation(ui, renderer, op);
67
68    run_operation(
69        ui,
70        renderer,
71        Box::new(operation::focusable::scroll_focused_into_view::<()>()),
72    );
73}
74
75/// Handle Ctrl+Tab / Ctrl+Shift+Tab for unconditional focus navigation.
76///
77/// This always moves focus regardless of whether any widget captured
78/// the event. It is the emergency exit from any focus trap.
79///
80/// Returns `true` if the event was consumed.
81pub fn handle_ctrl_tab<Message, Theme, Renderer>(
82    event: &core::Event,
83    ui: &mut UserInterface<'_, Message, Theme, Renderer>,
84    renderer: &Renderer,
85) -> bool
86where
87    Renderer: core::Renderer,
88{
89    if let core::Event::Keyboard(core::keyboard::Event::KeyPressed {
90        key: core::keyboard::Key::Named(core::keyboard::key::Named::Tab),
91        modifiers,
92        ..
93    }) = event
94        && modifiers.control()
95    {
96        focus_and_scroll(modifiers.shift(), ui, renderer);
97        return true;
98    }
99
100    false
101}
102
103/// Handle uncaptured Tab / Shift+Tab for focus navigation.
104///
105/// Moves focus to the next (or previous) focusable widget and scrolls
106/// it into view. Only runs when the event was not captured by any widget.
107///
108/// Returns `true` if the event was consumed.
109pub fn handle_tab<Message, Theme, Renderer>(
110    event: &core::Event,
111    status: core::event::Status,
112    ui: &mut UserInterface<'_, Message, Theme, Renderer>,
113    renderer: &Renderer,
114) -> bool
115where
116    Renderer: core::Renderer,
117{
118    if status != core::event::Status::Ignored {
119        return false;
120    }
121
122    if let core::Event::Keyboard(core::keyboard::Event::KeyPressed {
123        key: core::keyboard::Key::Named(core::keyboard::key::Named::Tab),
124        modifiers,
125        ..
126    }) = event
127    {
128        focus_and_scroll(modifiers.shift(), ui, renderer);
129        return true;
130    }
131
132    false
133}
134
135/// Handle Ctrl+Tab / Ctrl+Shift+Tab for focus navigation within a scope.
136///
137/// Behaves like [`handle_ctrl_tab`] but restricts focus cycling to widgets
138/// that are descendants of the container with the given `scope` [`Id`].
139///
140/// Returns `true` if the event was consumed.
141pub fn handle_ctrl_tab_within<Message, Theme, Renderer>(
142    event: &core::Event,
143    ui: &mut UserInterface<'_, Message, Theme, Renderer>,
144    renderer: &Renderer,
145    scope: core::widget::Id,
146) -> bool
147where
148    Renderer: core::Renderer,
149{
150    if let core::Event::Keyboard(core::keyboard::Event::KeyPressed {
151        key: core::keyboard::Key::Named(core::keyboard::key::Named::Tab),
152        modifiers,
153        ..
154    }) = event
155        && modifiers.control()
156    {
157        focus_and_scroll_within(modifiers.shift(), ui, renderer, scope);
158        return true;
159    }
160
161    false
162}
163
164/// Handle uncaptured Tab / Shift+Tab for focus navigation within a scope.
165///
166/// Behaves like [`handle_tab`] but restricts focus cycling to widgets
167/// that are descendants of the container with the given `scope` [`Id`].
168///
169/// Returns `true` if the event was consumed.
170pub fn handle_tab_within<Message, Theme, Renderer>(
171    event: &core::Event,
172    status: core::event::Status,
173    ui: &mut UserInterface<'_, Message, Theme, Renderer>,
174    renderer: &Renderer,
175    scope: core::widget::Id,
176) -> bool
177where
178    Renderer: core::Renderer,
179{
180    if status != core::event::Status::Ignored {
181        return false;
182    }
183
184    if let core::Event::Keyboard(core::keyboard::Event::KeyPressed {
185        key: core::keyboard::Key::Named(core::keyboard::key::Named::Tab),
186        modifiers,
187        ..
188    }) = event
189    {
190        focus_and_scroll_within(modifiers.shift(), ui, renderer, scope);
191        return true;
192    }
193
194    false
195}
196
197/// Handle Alt+letter mnemonics for widget activation.
198///
199/// When the user presses Alt plus a letter key (without Ctrl or Super),
200/// searches the widget tree for a widget whose [`mnemonic`] matches the
201/// pressed character. If found and the widget has an [`Id`], it is
202/// focused. Returns `Some(bounds)` with the matched widget's bounding
203/// rectangle so the caller can generate a synthetic click, or `None` if
204/// no match was found.
205///
206/// [`mnemonic`]: core::widget::operation::accessible::Accessible::mnemonic
207/// [`Id`]: core::widget::Id
208pub fn handle_mnemonic<Message, Theme, Renderer>(
209    event: &core::Event,
210    status: core::event::Status,
211    ui: &mut UserInterface<'_, Message, Theme, Renderer>,
212    renderer: &Renderer,
213) -> Option<core::Rectangle>
214where
215    Renderer: core::Renderer,
216{
217    if status != core::event::Status::Ignored {
218        return None;
219    }
220
221    if let core::Event::Keyboard(core::keyboard::Event::KeyPressed {
222        key: core::keyboard::Key::Character(smol),
223        modifiers,
224        ..
225    }) = event
226    {
227        if !modifiers.alt() || modifiers.control() || modifiers.logo() {
228            return None;
229        }
230
231        let ch = smol.chars().next()?;
232
233        let mut op = operation::focusable::find_mnemonic(ch);
234        ui.operate(renderer, &mut operation::black_box::<_, ()>(&mut op));
235
236        if let operation::Outcome::Some(target) = op.finish() {
237            if let Some(id) = target.id {
238                run_operation(
239                    ui,
240                    renderer,
241                    Box::new(operation::focusable::focus::<()>(id)),
242                );
243            }
244
245            return Some(target.bounds);
246        }
247    }
248
249    None
250}
251
252/// Handle uncaptured scroll keys for focused ancestor scrolling.
253///
254/// Maps keyboard scroll keys (Page Up/Down, arrows, Home/End, and
255/// their Shift variants for horizontal scrolling) to scroll actions
256/// on the focused widget's nearest scrollable ancestor.
257///
258/// Returns `true` if the event was consumed.
259pub fn handle_scroll_keys<Message, Theme, Renderer>(
260    event: &core::Event,
261    status: core::event::Status,
262    ui: &mut UserInterface<'_, Message, Theme, Renderer>,
263    renderer: &Renderer,
264) -> bool
265where
266    Renderer: core::Renderer,
267{
268    if status != core::event::Status::Ignored {
269        return false;
270    }
271
272    if let core::Event::Keyboard(core::keyboard::Event::KeyPressed {
273        key: core::keyboard::Key::Named(named),
274        modifiers,
275        ..
276    }) = event
277    {
278        use operation::focusable::ScrollAction;
279
280        let action = match named {
281            core::keyboard::key::Named::PageDown if modifiers.shift() => {
282                Some(ScrollAction::PageRight)
283            }
284            core::keyboard::key::Named::PageDown => Some(ScrollAction::PageDown),
285            core::keyboard::key::Named::PageUp if modifiers.shift() => Some(ScrollAction::PageLeft),
286            core::keyboard::key::Named::PageUp => Some(ScrollAction::PageUp),
287            core::keyboard::key::Named::ArrowDown if modifiers.shift() => {
288                Some(ScrollAction::LineRight)
289            }
290            core::keyboard::key::Named::ArrowDown => Some(ScrollAction::LineDown),
291            core::keyboard::key::Named::ArrowUp if modifiers.shift() => {
292                Some(ScrollAction::LineLeft)
293            }
294            core::keyboard::key::Named::ArrowUp => Some(ScrollAction::LineUp),
295            core::keyboard::key::Named::ArrowRight => Some(ScrollAction::LineRight),
296            core::keyboard::key::Named::ArrowLeft => Some(ScrollAction::LineLeft),
297            core::keyboard::key::Named::Home if modifiers.shift() => Some(ScrollAction::ShiftHome),
298            core::keyboard::key::Named::Home => Some(ScrollAction::Home),
299            core::keyboard::key::Named::End if modifiers.shift() => Some(ScrollAction::ShiftEnd),
300            core::keyboard::key::Named::End => Some(ScrollAction::End),
301            _ => None,
302        };
303
304        if let Some(action) = action {
305            run_operation(
306                ui,
307                renderer,
308                Box::new(operation::focusable::scroll_focused_ancestor::<()>(action)),
309            );
310            return true;
311        }
312    }
313
314    false
315}