use alloc::vec::Vec;
use azul_core::{
callbacks::FocusTarget,
dom::{DomId, DomNodeId, NodeId},
events::{DefaultAction, DefaultActionResult, ScrollAmount, ScrollDirection},
window::{KeyboardState, VirtualKeyCode},
};
use crate::window::DomLayoutResult;
use std::collections::BTreeMap;
#[allow(clippy::too_many_lines)] #[must_use] pub fn determine_keyboard_default_action(
keyboard_state: &KeyboardState,
focused_node: Option<DomNodeId>,
layout_results: &BTreeMap<DomId, DomLayoutResult>,
prevented: bool,
) -> DefaultActionResult {
if prevented {
return DefaultActionResult::prevented();
}
let Some(current_key) = keyboard_state.current_virtual_keycode.into_option() else {
return DefaultActionResult::default();
};
let shift_down = keyboard_state.shift_down();
let ctrl_down = keyboard_state.ctrl_down();
let alt_down = keyboard_state.alt_down();
let action = match current_key {
VirtualKeyCode::Tab => {
if ctrl_down || alt_down {
DefaultAction::None
} else if shift_down {
DefaultAction::FocusPrevious
} else {
DefaultAction::FocusNext
}
}
VirtualKeyCode::Return | VirtualKeyCode::NumpadEnter => {
focused_node.as_ref().map_or(DefaultAction::None, |focus| if is_element_activatable(focus, layout_results) {
DefaultAction::ActivateFocusedElement {
target: *focus,
}
} else {
DefaultAction::None
})
}
VirtualKeyCode::Space => {
match focused_node.as_ref() {
Some(focus)
if is_element_activatable(focus, layout_results)
&& !is_text_input(focus, layout_results) =>
{
DefaultAction::ActivateFocusedElement { target: *focus }
}
Some(focus) if is_text_input(focus, layout_results) => DefaultAction::None,
_ => DefaultAction::ScrollFocusedContainer {
direction: if shift_down {
ScrollDirection::Up
} else {
ScrollDirection::Down
},
amount: ScrollAmount::Page,
},
}
}
VirtualKeyCode::Escape => {
if focused_node.is_some() {
DefaultAction::ClearFocus
} else {
DefaultAction::None
}
}
VirtualKeyCode::Up | VirtualKeyCode::Down | VirtualKeyCode::Left | VirtualKeyCode::Right => {
let direction = match current_key {
VirtualKeyCode::Up => ScrollDirection::Up,
VirtualKeyCode::Down => ScrollDirection::Down,
VirtualKeyCode::Left => ScrollDirection::Left,
_ => ScrollDirection::Right,
};
focused_node.as_ref().map_or(
DefaultAction::ScrollFocusedContainer {
direction,
amount: ScrollAmount::Line,
},
|focus| {
if is_text_input(focus, layout_results) {
DefaultAction::None
} else {
DefaultAction::ScrollFocusedContainer {
direction,
amount: ScrollAmount::Line,
}
}
},
)
}
VirtualKeyCode::PageUp => {
DefaultAction::ScrollFocusedContainer {
direction: ScrollDirection::Up,
amount: ScrollAmount::Page,
}
}
VirtualKeyCode::PageDown => {
DefaultAction::ScrollFocusedContainer {
direction: ScrollDirection::Down,
amount: ScrollAmount::Page,
}
}
VirtualKeyCode::Home => {
if ctrl_down {
DefaultAction::FocusFirst
} else {
DefaultAction::ScrollFocusedContainer {
direction: ScrollDirection::Up,
amount: ScrollAmount::Document,
}
}
}
VirtualKeyCode::End => {
if ctrl_down {
DefaultAction::FocusLast
} else {
DefaultAction::ScrollFocusedContainer {
direction: ScrollDirection::Down,
amount: ScrollAmount::Document,
}
}
}
_ => DefaultAction::None,
};
DefaultActionResult::new(action)
}
fn is_element_activatable(node_id: &DomNodeId, layout_results: &BTreeMap<DomId, DomLayoutResult>) -> bool {
let Some(layout) = layout_results.get(&node_id.dom) else {
return false;
};
let Some(internal_id) = node_id.node.into_crate_internal() else {
return false;
};
layout.styled_dom.node_data.as_container()
.get(internal_id)
.is_some_and(azul_core::dom::NodeData::is_activatable)
}
fn is_text_input(node_id: &DomNodeId, layout_results: &BTreeMap<DomId, DomLayoutResult>) -> bool {
use azul_core::events::{EventFilter, FocusEventFilter};
let Some(layout) = layout_results.get(&node_id.dom) else {
return false;
};
let Some(internal_id) = node_id.node.into_crate_internal() else {
return false;
};
let node_data = layout.styled_dom.node_data.as_container();
let Some(node) = node_data.get(internal_id) else {
return false;
};
node.get_callbacks()
.iter()
.any(|cb| matches!(cb.event, EventFilter::Focus(FocusEventFilter::TextInput)))
}
#[must_use] pub const fn default_action_to_focus_target(action: &DefaultAction) -> Option<FocusTarget> {
match action {
DefaultAction::FocusNext => Some(FocusTarget::Next),
DefaultAction::FocusPrevious => Some(FocusTarget::Previous),
DefaultAction::FocusFirst => Some(FocusTarget::First),
DefaultAction::FocusLast => Some(FocusTarget::Last),
DefaultAction::ClearFocus => Some(FocusTarget::NoFocus),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use azul_core::styled_dom::NodeHierarchyItemId;
#[test]
#[allow(clippy::field_reassign_with_default)] fn test_tab_focus_next() {
let mut keyboard_state = KeyboardState::default();
keyboard_state.current_virtual_keycode = Some(VirtualKeyCode::Tab).into();
let result = determine_keyboard_default_action(
&keyboard_state,
None,
&BTreeMap::new(),
false,
);
assert!(matches!(result.action, DefaultAction::FocusNext));
assert!(!result.prevented);
}
#[test]
#[allow(clippy::field_reassign_with_default)] fn test_shift_tab_focus_previous() {
let mut keyboard_state = KeyboardState::default();
keyboard_state.current_virtual_keycode = Some(VirtualKeyCode::Tab).into();
keyboard_state.pressed_virtual_keycodes = vec![VirtualKeyCode::LShift, VirtualKeyCode::Tab].into();
let result = determine_keyboard_default_action(
&keyboard_state,
None,
&BTreeMap::new(),
false,
);
assert!(matches!(result.action, DefaultAction::FocusPrevious));
}
#[test]
#[allow(clippy::field_reassign_with_default)] fn test_escape_clears_focus() {
let mut keyboard_state = KeyboardState::default();
keyboard_state.current_virtual_keycode = Some(VirtualKeyCode::Escape).into();
let focused = Some(DomNodeId {
dom: DomId { inner: 0 },
node: NodeHierarchyItemId::from_crate_internal(Some(NodeId::new(1))),
});
let result = determine_keyboard_default_action(
&keyboard_state,
focused,
&BTreeMap::new(),
false,
);
assert!(matches!(result.action, DefaultAction::ClearFocus));
}
#[test]
#[allow(clippy::field_reassign_with_default)] fn test_prevented_returns_no_action() {
let mut keyboard_state = KeyboardState::default();
keyboard_state.current_virtual_keycode = Some(VirtualKeyCode::Tab).into();
let result = determine_keyboard_default_action(
&keyboard_state,
None,
&BTreeMap::new(),
true, );
assert!(result.prevented);
assert!(matches!(result.action, DefaultAction::None));
}
}