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;
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 current_key = match keyboard_state.current_virtual_keycode.into_option() {
Some(key) => key,
None => 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 => {
if let Some(ref focus) = focused_node {
if is_element_activatable(focus, layout_results) {
DefaultAction::ActivateFocusedElement {
target: focus.clone(),
}
} else {
DefaultAction::None
}
} else {
DefaultAction::None
}
}
VirtualKeyCode::Space => {
if let Some(ref focus) = focused_node {
if is_element_activatable(focus, layout_results)
&& !is_text_input(focus, layout_results)
{
DefaultAction::ActivateFocusedElement {
target: focus.clone(),
}
} else {
DefaultAction::None
}
} else {
DefaultAction::None
}
}
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,
};
if let Some(ref focus) = focused_node {
if !is_text_input(focus, layout_results) {
DefaultAction::ScrollFocusedContainer {
direction,
amount: ScrollAmount::Line,
}
} else {
DefaultAction::None
}
} else {
DefaultAction::None
}
}
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)
.map(|node| node.is_activatable())
.unwrap_or(false)
}
fn is_text_input(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;
};
let node_data = layout.styled_dom.node_data.as_container();
let Some(node) = node_data.get(internal_id) else {
return false;
};
use azul_core::events::{EventFilter, FocusEventFilter};
node.get_callbacks()
.iter()
.any(|cb| matches!(cb.event, EventFilter::Focus(FocusEventFilter::TextInput)))
}
pub 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]
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]
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]
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]
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));
}
}