cranpose_app_shell/shell_input/
key_navigation.rs1use std::fmt::Debug;
2
3use cranpose_core::{enter_event_handler_scope, run_in_mutable_snapshot};
4use cranpose_render_common::Renderer;
5use cranpose_ui::{FocusDirection, Key, KeyEvent, KeyEventType, request_render_invalidation};
6
7use super::key_activation::plain_key_down;
8use crate::SurfaceMut;
9
10#[expect(clippy::multiple_inherent_impl, reason = "one file per shell concern")]
11impl<R> SurfaceMut<'_, R>
12where
13 R: Renderer,
14 R::Error: Debug,
15{
16 pub(super) fn on_focus_key(&mut self, event: &KeyEvent) -> bool {
17 if !plain_key_down(event) || event.key() != Key::Tab {
18 return false;
19 }
20 self.on_group_navigation_key(event)
21 }
22
23 fn on_group_navigation_key(&mut self, event: &KeyEvent) -> bool {
24 let focused = cranpose_ui::active_focus_target();
25 let target = self.with_layout_tree(|tree| {
26 cranpose_ui::keyboard_focus_target(tree?, focused, event.key(), event.modifiers().shift)
27 });
28 let Some(target) = target else {
29 return false;
30 };
31 let moved =
32 run_in_mutable_snapshot(|| cranpose_ui::request_focus_from_platform(target.node_id))
33 .unwrap_or(false);
34 if moved {
35 self.mark_dirty();
36 self.note_focus_moved_by_keyboard(true);
37 if target.activate {
38 self.accessibility_activate(target.node_id, None);
39 }
40 }
41 moved
42 }
43
44 pub(super) fn on_activation_key(&mut self, event: &KeyEvent) -> bool {
49 if !matches!(
50 event.key(),
51 Key::Enter | Key::NumPadEnter | Key::Spacebar | Key::DirectionCenter
52 ) || cranpose_ui::text_field_focus::has_focused_field()
53 {
54 return false;
55 }
56 if event.type_() == KeyEventType::KeyUp {
57 return self.release_by_key(event.key());
58 }
59 if !plain_key_down(event) {
60 return false;
61 }
62 let Some(focused) = cranpose_ui::active_focus_target() else {
63 return false;
64 };
65 if event.native_key_event().repeat_count > 0 {
66 return true;
67 }
68 let pressed = self.press_by_key(focused, event.key());
69 self.note_focus_moved_by_keyboard(true);
70 pressed
71 }
72
73 pub(super) fn on_field_dpad_key(&mut self, event: &KeyEvent) -> bool {
78 if !self.surface().dpad_navigation || !cranpose_ui::text_field_focus::has_focused_field() {
79 return false;
80 }
81 match event.key() {
82 Key::DirectionCenter => {
83 if event.type_() == KeyEventType::KeyDown
84 && event.native_key_event().repeat_count == 0
85 {
86 cranpose_ui::text_field_focus::show_keyboard();
87 }
88 true
89 }
90 Key::DirectionUp | Key::DirectionDown | Key::DirectionLeft | Key::DirectionRight => {
91 self.on_navigation_key(event)
92 }
93 _ => false,
94 }
95 }
96
97 pub(super) fn on_unconsumed_key(&mut self, event: &KeyEvent) -> bool {
101 if self.on_navigation_key(event) {
102 return true;
103 }
104 if cranpose_ui::text_field_focus::has_focused_field() {
105 return false;
106 }
107 let handled = run_in_mutable_snapshot(|| cranpose_ui::dispatch_unhandled_key_event(event))
108 .unwrap_or(false);
109 if handled {
110 self.mark_dirty();
111 self.shell.app.request_layout_pass();
112 }
113 handled
114 }
115
116 fn on_navigation_key(&mut self, event: &KeyEvent) -> bool {
122 let direction = match event.key() {
123 Key::DirectionLeft => Some(FocusDirection::Left),
124 Key::DirectionRight => Some(FocusDirection::Right),
125 Key::DirectionUp => Some(FocusDirection::Up),
126 Key::DirectionDown => Some(FocusDirection::Down),
127 Key::MoveHome | Key::MoveEnd => None,
128 _ => return false,
129 };
130 if !plain_key_down(event)
131 || (cranpose_ui::text_field_focus::has_focused_field()
132 && !self.surface().dpad_navigation)
133 {
134 return false;
135 }
136 let frame = Some(self.surface().frames);
137 if event.native_key_event().repeat_count > 0 && self.surface().key_moved_focus_at == frame {
138 return true;
139 }
140 let moved = self.on_group_navigation_key(event)
141 || direction.is_some_and(|direction| {
142 self.move_focus_by_arrow(direction) || self.scroll_by_arrow(direction)
143 });
144 if moved {
145 self.surface_mut().key_moved_focus_at = frame;
146 }
147 moved
148 }
149
150 fn scroll_by_arrow(&mut self, direction: FocusDirection) -> bool {
155 let Some(focused) = cranpose_ui::active_focus_target() else {
156 return false;
157 };
158 let vertical = matches!(direction, FocusDirection::Up | FocusDirection::Down);
159 let forward = matches!(direction, FocusDirection::Down | FocusDirection::Right);
160 let container = {
161 let (app, surface) = self.parts();
162 surface.semantics_tree_for_input(app).and_then(|tree| {
163 let root = tree.root();
164 crate::focus_reveal::scroll_ancestors(root, focused)?
165 .into_iter()
166 .find_map(|ancestor| {
167 let node = crate::focus_reveal::semantics_of(root, ancestor)?;
168 let details = node.details();
169 let range = if vertical {
170 details.vertical_scroll
171 } else {
172 details.horizontal_scroll
173 }?;
174 let can_move = if forward != range.reverse {
175 range.can_scroll_forward()
176 } else {
177 range.can_scroll_backward()
178 };
179 let action = details.scroll_by.clone()?;
180 can_move.then_some((ancestor, action, range.reverse))
181 })
182 })
183 };
184 let Some((ancestor, action, reverse)) = container else {
185 return false;
186 };
187 let Some(viewport) =
188 self.with_layout_tree(|tree| crate::focus_reveal::bounds_of(tree?.root(), ancestor))
189 else {
190 return false;
191 };
192 let half = if vertical {
193 viewport.height
194 } else {
195 viewport.width
196 } * 0.5;
197 let delta = if forward != reverse { half } else { -half };
198 let (dx, dy) = if vertical { (0.0, delta) } else { (delta, 0.0) };
199 let moved = run_in_mutable_snapshot(|| action.invoke(dx, dy)).unwrap_or(false);
200 if moved {
201 self.mark_dirty();
202 self.shell.app.request_layout_pass();
203 }
204 moved
205 }
206
207 pub(crate) fn keep_focus(&mut self) {
212 if cranpose_ui::active_focus_target().is_none() && cranpose_ui::has_focus_targets() {
213 self.move_focus_by_arrow(FocusDirection::Down);
214 }
215 }
216
217 #[inline(never)]
220 fn move_focus_by_arrow(&mut self, direction: FocusDirection) -> bool {
221 let target = self.with_layout_tree(|layout_tree| {
222 cranpose_ui::arrow_focus_target(layout_tree?, direction)
223 });
224 let Some(target) = target else {
225 return false;
226 };
227 let request = if self.surface().dpad_navigation {
228 cranpose_ui::request_focus_without_keyboard
229 } else {
230 cranpose_ui::request_focus_from_platform
231 };
232 let moved = run_in_mutable_snapshot(|| request(target)).unwrap_or(false);
233 if moved {
234 self.mark_dirty();
235 self.note_focus_moved_by_keyboard(true);
236 }
237 moved
238 }
239
240 pub(super) fn note_focus_moved_by_keyboard(&mut self, keyboard: bool) {
241 if cranpose_ui::set_keyboard_focus_visible(keyboard) {
242 request_render_invalidation();
243 self.mark_dirty();
244 }
245 }
246
247 pub(super) fn on_escape_key(&mut self, event: &KeyEvent) -> bool {
248 if event.type_() != KeyEventType::KeyDown || event.key() != Key::Escape {
249 return false;
250 }
251 self.dismiss_top_modal_in_context()
252 }
253
254 pub fn dismiss_top_modal(&mut self) -> bool {
257 let _event_handler = enter_event_handler_scope();
258 let _app_context = self.shell.app.app_context.enter_scope();
259 self.dismiss_top_modal_in_context()
260 }
261
262 fn dismiss_top_modal_in_context(&mut self) -> bool {
263 let closed = run_in_mutable_snapshot(|| {
264 if cranpose_ui::modal_depth() > 0 {
265 cranpose_ui::dispatch_modal_back()
266 } else {
267 cranpose_ui::dismiss_top_popup()
268 }
269 })
270 .unwrap_or(false);
271 if closed {
272 self.mark_dirty();
273 }
274 closed
275 }
276
277 pub fn move_focus_in_context(&mut self, direction: FocusDirection) -> bool {
280 let order = self.with_layout_tree(|layout_tree| {
281 layout_tree
282 .map(cranpose_ui::collect_focus_order)
283 .unwrap_or_default()
284 });
285 self.move_focus_in_order(order, direction)
286 }
287
288 fn move_focus_in_order(
289 &mut self,
290 order: Vec<cranpose_ui::FocusEntry>,
291 direction: FocusDirection,
292 ) -> bool {
293 cranpose_ui::set_focus_order(order);
294 let moved = run_in_mutable_snapshot(|| cranpose_ui::FocusManager.move_focus(direction))
295 .unwrap_or(false);
296 if moved {
297 self.mark_dirty();
298 self.note_focus_moved_by_keyboard(true);
299 }
300 moved
301 }
302}