1use alloc::vec::Vec;
38use azul_core::{
39 callbacks::FocusTarget,
40 dom::{DomId, DomNodeId, NodeId},
41 events::{DefaultAction, DefaultActionResult, EventType, ScrollAmount, ScrollDirection, SyntheticEvent},
42 styled_dom::NodeHierarchyItemId,
43 window::{KeyboardState, VirtualKeyCode},
44};
45use crate::window::DomLayoutResult;
46use std::collections::BTreeMap;
47
48pub fn determine_keyboard_default_action(
64 keyboard_state: &KeyboardState,
65 focused_node: Option<DomNodeId>,
66 layout_results: &BTreeMap<DomId, DomLayoutResult>,
67 prevented: bool,
68) -> DefaultActionResult {
69 if prevented {
71 return DefaultActionResult::prevented();
72 }
73
74 let current_key = match keyboard_state.current_virtual_keycode.into_option() {
76 Some(key) => key,
77 None => return DefaultActionResult::default(),
78 };
79
80 let shift_down = keyboard_state.shift_down();
82 let ctrl_down = keyboard_state.ctrl_down();
83 let alt_down = keyboard_state.alt_down();
84
85 let action = match current_key {
87 VirtualKeyCode::Tab => {
89 if ctrl_down || alt_down {
90 DefaultAction::None
92 } else if shift_down {
93 DefaultAction::FocusPrevious
94 } else {
95 DefaultAction::FocusNext
96 }
97 }
98
99 VirtualKeyCode::Return | VirtualKeyCode::NumpadEnter => {
101 if let Some(ref focus) = focused_node {
102 if is_element_activatable(focus, layout_results) {
103 DefaultAction::ActivateFocusedElement {
104 target: focus.clone(),
105 }
106 } else {
107 DefaultAction::None
110 }
111 } else {
112 DefaultAction::None
113 }
114 }
115
116 VirtualKeyCode::Space => {
118 if let Some(ref focus) = focused_node {
119 if is_element_activatable(focus, layout_results)
122 && !is_text_input(focus, layout_results)
123 {
124 DefaultAction::ActivateFocusedElement {
125 target: focus.clone(),
126 }
127 } else {
128 DefaultAction::None
130 }
131 } else {
132 DefaultAction::None
133 }
134 }
135
136 VirtualKeyCode::Escape => {
138 if focused_node.is_some() {
139 DefaultAction::ClearFocus
140 } else {
141 DefaultAction::None
143 }
144 }
145
146 VirtualKeyCode::Up => {
148 if focused_node.is_some() && !is_text_input(&focused_node.as_ref().unwrap(), layout_results) {
149 DefaultAction::ScrollFocusedContainer {
150 direction: ScrollDirection::Up,
151 amount: ScrollAmount::Line,
152 }
153 } else {
154 DefaultAction::None
155 }
156 }
157 VirtualKeyCode::Down => {
158 if focused_node.is_some() && !is_text_input(&focused_node.as_ref().unwrap(), layout_results) {
159 DefaultAction::ScrollFocusedContainer {
160 direction: ScrollDirection::Down,
161 amount: ScrollAmount::Line,
162 }
163 } else {
164 DefaultAction::None
165 }
166 }
167 VirtualKeyCode::Left => {
168 if focused_node.is_some() && !is_text_input(&focused_node.as_ref().unwrap(), layout_results) {
169 DefaultAction::ScrollFocusedContainer {
170 direction: ScrollDirection::Left,
171 amount: ScrollAmount::Line,
172 }
173 } else {
174 DefaultAction::None
175 }
176 }
177 VirtualKeyCode::Right => {
178 if focused_node.is_some() && !is_text_input(&focused_node.as_ref().unwrap(), layout_results) {
179 DefaultAction::ScrollFocusedContainer {
180 direction: ScrollDirection::Right,
181 amount: ScrollAmount::Line,
182 }
183 } else {
184 DefaultAction::None
185 }
186 }
187
188 VirtualKeyCode::PageUp => {
190 DefaultAction::ScrollFocusedContainer {
191 direction: ScrollDirection::Up,
192 amount: ScrollAmount::Page,
193 }
194 }
195 VirtualKeyCode::PageDown => {
196 DefaultAction::ScrollFocusedContainer {
197 direction: ScrollDirection::Down,
198 amount: ScrollAmount::Page,
199 }
200 }
201
202 VirtualKeyCode::Home => {
204 if ctrl_down {
205 DefaultAction::FocusFirst
207 } else {
208 DefaultAction::ScrollFocusedContainer {
209 direction: ScrollDirection::Up,
210 amount: ScrollAmount::Document,
211 }
212 }
213 }
214 VirtualKeyCode::End => {
215 if ctrl_down {
216 DefaultAction::FocusLast
218 } else {
219 DefaultAction::ScrollFocusedContainer {
220 direction: ScrollDirection::Down,
221 amount: ScrollAmount::Document,
222 }
223 }
224 }
225
226 _ => DefaultAction::None,
228 };
229
230 DefaultActionResult::new(action)
231}
232
233fn is_element_activatable(node_id: &DomNodeId, layout_results: &BTreeMap<DomId, DomLayoutResult>) -> bool {
235 let Some(layout) = layout_results.get(&node_id.dom) else {
236 return false;
237 };
238 let Some(internal_id) = node_id.node.into_crate_internal() else {
239 return false;
240 };
241 layout.styled_dom.node_data.as_container()
242 .get(internal_id)
243 .map(|node| node.is_activatable())
244 .unwrap_or(false)
245}
246
247fn is_text_input(node_id: &DomNodeId, layout_results: &BTreeMap<DomId, DomLayoutResult>) -> bool {
249 let Some(layout) = layout_results.get(&node_id.dom) else {
250 return false;
251 };
252 let Some(internal_id) = node_id.node.into_crate_internal() else {
253 return false;
254 };
255 let node_data = layout.styled_dom.node_data.as_container();
256 let Some(node) = node_data.get(internal_id) else {
257 return false;
258 };
259
260 use azul_core::events::{EventFilter, FocusEventFilter};
263 node.get_callbacks()
264 .iter()
265 .any(|cb| matches!(cb.event, EventFilter::Focus(FocusEventFilter::TextInput)))
266}
267
268pub fn default_action_to_focus_target(action: &DefaultAction) -> Option<FocusTarget> {
273 match action {
274 DefaultAction::FocusNext => Some(FocusTarget::Next),
275 DefaultAction::FocusPrevious => Some(FocusTarget::Previous),
276 DefaultAction::FocusFirst => Some(FocusTarget::First),
277 DefaultAction::FocusLast => Some(FocusTarget::Last),
278 DefaultAction::ClearFocus => Some(FocusTarget::NoFocus),
279 _ => None,
280 }
281}
282
283pub fn create_activation_click_event(
289 target: &DomNodeId,
290 timestamp: azul_core::task::Instant,
291) -> SyntheticEvent {
292 use azul_core::events::{EventData, EventSource};
293
294 SyntheticEvent::new(
295 EventType::Click,
296 EventSource::Synthetic,
297 target.clone(),
298 timestamp,
299 EventData::None,
300 )
301}
302
303#[cfg(test)]
304mod tests {
305 use super::*;
306
307 #[test]
308 fn test_tab_focus_next() {
309 let mut keyboard_state = KeyboardState::default();
310 keyboard_state.current_virtual_keycode = Some(VirtualKeyCode::Tab).into();
311
312 let result = determine_keyboard_default_action(
313 &keyboard_state,
314 None,
315 &BTreeMap::new(),
316 false,
317 );
318
319 assert!(matches!(result.action, DefaultAction::FocusNext));
320 assert!(!result.prevented);
321 }
322
323 #[test]
324 fn test_shift_tab_focus_previous() {
325 let mut keyboard_state = KeyboardState::default();
326 keyboard_state.current_virtual_keycode = Some(VirtualKeyCode::Tab).into();
327 keyboard_state.pressed_virtual_keycodes = vec![VirtualKeyCode::LShift, VirtualKeyCode::Tab].into();
329
330 let result = determine_keyboard_default_action(
331 &keyboard_state,
332 None,
333 &BTreeMap::new(),
334 false,
335 );
336
337 assert!(matches!(result.action, DefaultAction::FocusPrevious));
338 }
339
340 #[test]
341 fn test_escape_clears_focus() {
342 let mut keyboard_state = KeyboardState::default();
343 keyboard_state.current_virtual_keycode = Some(VirtualKeyCode::Escape).into();
344
345 let focused = Some(DomNodeId {
346 dom: DomId { inner: 0 },
347 node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(1))),
348 });
349
350 let result = determine_keyboard_default_action(
351 &keyboard_state,
352 focused,
353 &BTreeMap::new(),
354 false,
355 );
356
357 assert!(matches!(result.action, DefaultAction::ClearFocus));
358 }
359
360 #[test]
361 fn test_prevented_returns_no_action() {
362 let mut keyboard_state = KeyboardState::default();
363 keyboard_state.current_virtual_keycode = Some(VirtualKeyCode::Tab).into();
364
365 let result = determine_keyboard_default_action(
366 &keyboard_state,
367 None,
368 &BTreeMap::new(),
369 true, );
371
372 assert!(result.prevented);
373 assert!(matches!(result.action, DefaultAction::None));
374 }
375}