use alloc::collections::BTreeMap;
use azul_core::{
callbacks::{FocusTarget, FocusTargetPath},
dom::{DomId, DomNodeId, NodeId},
style::matches_html_element,
styled_dom::NodeHierarchyItemId,
window::UpdateFocusWarning,
};
use crate::window::DomLayoutResult;
#[derive(Copy, Debug, Clone, PartialEq, Eq)]
pub struct PendingContentEditableFocus {
pub dom_id: DomId,
pub container_node_id: NodeId,
pub text_node_id: NodeId,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FocusManager {
pub focused_node: Option<DomNodeId>,
pub pending_focus_request: Option<FocusTarget>,
pub cursor_needs_initialization: bool,
pub pending_contenteditable_focus: Option<PendingContentEditableFocus>,
}
impl Default for FocusManager {
fn default() -> Self {
Self::new()
}
}
impl FocusManager {
#[must_use] pub const fn new() -> Self {
Self {
focused_node: None,
pending_focus_request: None,
cursor_needs_initialization: false,
pending_contenteditable_focus: None,
}
}
#[must_use] pub const fn get_focused_node(&self) -> Option<&DomNodeId> {
self.focused_node.as_ref()
}
pub const fn set_focused_node(&mut self, node: Option<DomNodeId>) {
self.focused_node = node;
}
pub fn request_focus_change(&mut self, target: FocusTarget) {
self.pending_focus_request = Some(target);
}
pub const fn take_focus_request(&mut self) -> Option<FocusTarget> {
self.pending_focus_request.take()
}
pub const fn clear_focus(&mut self) {
self.focused_node = None;
}
#[must_use] pub fn has_focus(&self, node: &DomNodeId) -> bool {
self.focused_node.as_ref() == Some(node)
}
pub const fn set_pending_contenteditable_focus(
&mut self,
dom_id: DomId,
container_node_id: NodeId,
text_node_id: NodeId,
) {
self.cursor_needs_initialization = true;
self.pending_contenteditable_focus = Some(PendingContentEditableFocus {
dom_id,
container_node_id,
text_node_id,
});
}
pub const fn clear_pending_contenteditable_focus(&mut self) {
self.cursor_needs_initialization = false;
self.pending_contenteditable_focus = None;
}
pub const fn take_pending_contenteditable_focus(&mut self) -> Option<PendingContentEditableFocus> {
if self.cursor_needs_initialization {
self.cursor_needs_initialization = false;
self.pending_contenteditable_focus.take()
} else {
None
}
}
#[must_use] pub const fn needs_cursor_initialization(&self) -> bool {
self.cursor_needs_initialization
}
}
impl crate::managers::NodeIdRemap for FocusManager {
fn remap_node_ids(&mut self, dom_id: DomId, map: &crate::managers::NodeIdMap) {
if let Some(focused) = self.focused_node {
if focused.dom == dom_id {
match focused
.node
.into_crate_internal()
.and_then(|old| map.resolve(old))
{
Some(new_id) => {
self.focused_node = Some(DomNodeId {
dom: dom_id,
node: NodeHierarchyItemId::from_crate_internal(Some(new_id)),
});
}
None => self.focused_node = None,
}
}
}
if let Some(ref mut pending) = self.pending_contenteditable_focus {
if pending.dom_id != dom_id {
return;
}
match (
map.resolve(pending.container_node_id),
map.resolve(pending.text_node_id),
) {
(Some(container), Some(text)) => {
pending.container_node_id = container;
pending.text_node_id = text;
}
_ => {
self.pending_contenteditable_focus = None;
self.cursor_needs_initialization = false;
}
}
}
}
}
fn collect_tab_order(layout_results: &BTreeMap<DomId, DomLayoutResult>) -> Vec<DomNodeId> {
use azul_core::dom::TabIndex;
let mut positive: Vec<(u32, DomNodeId)> = Vec::new();
let mut auto: Vec<DomNodeId> = Vec::new();
for (dom_id, layout) in layout_results {
let node_data = layout.styled_dom.node_data.as_container();
for index in 0..node_data.len() {
let node_id = NodeId::new(index);
let Some(nd) = node_data.get(node_id) else {
continue;
};
if !nd.is_focusable() {
continue;
}
let dom_node = FocusSearchContext::make_dom_node_id(*dom_id, node_id);
match nd.get_tab_index() {
Some(TabIndex::NoKeyboardFocus) => {}
Some(TabIndex::OverrideInParent(n)) if n > 0 => positive.push((n, dom_node)),
_ => auto.push(dom_node),
}
}
}
order_tab_entries(positive, auto)
}
fn order_tab_entries(
mut positive: Vec<(u32, DomNodeId)>,
auto: Vec<DomNodeId>,
) -> Vec<DomNodeId> {
positive.sort_by_key(|(n, _)| *n); positive.into_iter().map(|(_, id)| id).chain(auto).collect()
}
fn doc_order_key(id: &DomNodeId) -> (usize, usize) {
(
id.dom.inner,
id.node.into_crate_internal().map_or(0, |n| n.index()),
)
}
fn next_in_tab_order(
order: &[DomNodeId],
current: Option<DomNodeId>,
forward: bool,
) -> Option<DomNodeId> {
if order.is_empty() {
return None;
}
let Some(cur) = current else {
return if forward {
order.first().copied()
} else {
order.last().copied()
};
};
if let Some(pos) = order.iter().position(|x| *x == cur) {
let len = order.len();
let next = if forward {
(pos + 1) % len
} else {
(pos + len - 1) % len
};
return Some(order[next]);
}
let cur_key = doc_order_key(&cur);
let candidate = if forward {
order
.iter()
.filter(|x| doc_order_key(x) > cur_key)
.min_by_key(|x| doc_order_key(x))
} else {
order
.iter()
.filter(|x| doc_order_key(x) < cur_key)
.max_by_key(|x| doc_order_key(x))
};
candidate.copied().or_else(|| {
if forward {
order.first().copied()
} else {
order.last().copied()
}
})
}
struct FocusSearchContext<'a> {
layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
}
impl<'a> FocusSearchContext<'a> {
const fn new(layout_results: &'a BTreeMap<DomId, DomLayoutResult>) -> Self {
Self { layout_results }
}
#[allow(clippy::trivially_copy_pass_by_ref)] fn get_layout(&self, dom_id: &DomId) -> Result<&'a DomLayoutResult, UpdateFocusWarning> {
self.layout_results
.get(dom_id)
.ok_or_else(|| UpdateFocusWarning::FocusInvalidDomId(*dom_id))
}
const fn make_dom_node_id(dom_id: DomId, node_id: NodeId) -> DomNodeId {
DomNodeId {
dom: dom_id,
node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
}
}
}
#[allow(clippy::trivially_copy_pass_by_ref)] fn find_first_matching_focusable_node(
layout: &DomLayoutResult,
dom_id: &DomId,
css_path: &azul_css::css::CssPath,
) -> Option<DomNodeId> {
let styled_dom = &layout.styled_dom;
let node_hierarchy = styled_dom.node_hierarchy.as_container();
let node_data = styled_dom.node_data.as_container();
let cascade_info = styled_dom.cascade_info.as_container();
let matching_node = (0..node_data.len())
.map(NodeId::new)
.filter(|&node_id| {
matches_html_element(
css_path,
node_id,
&node_hierarchy,
&node_data,
&cascade_info,
None, )
})
.find(|&node_id| {
node_data[node_id].is_focusable()
});
matching_node.map(|node_id| DomNodeId {
dom: *dom_id,
node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
})
}
pub fn resolve_focus_target(
focus_target: &FocusTarget,
layout_results: &BTreeMap<DomId, DomLayoutResult>,
current_focus: Option<DomNodeId>,
) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
use azul_core::callbacks::FocusTarget::{Path, Id, Previous, Next, First, Last, NoFocus};
if layout_results.is_empty() {
return Ok(None);
}
let ctx = FocusSearchContext::new(layout_results);
match focus_target {
Path(FocusTargetPath { dom, css_path }) => {
let layout = ctx.get_layout(dom)?;
Ok(find_first_matching_focusable_node(layout, dom, css_path))
}
Id(dom_node_id) => {
let layout = ctx.get_layout(&dom_node_id.dom)?;
let is_valid = dom_node_id
.node
.into_crate_internal()
.is_some_and(|n| layout.styled_dom.node_data.as_container().get(n).is_some());
if is_valid {
Ok(Some(*dom_node_id))
} else {
Err(UpdateFocusWarning::FocusInvalidNodeId(
dom_node_id.node,
))
}
}
Previous => Ok(next_in_tab_order(
&collect_tab_order(layout_results),
current_focus,
false,
)),
Next => Ok(next_in_tab_order(
&collect_tab_order(layout_results),
current_focus,
true,
)),
First => Ok(collect_tab_order(layout_results).first().copied()),
Last => Ok(collect_tab_order(layout_results).last().copied()),
NoFocus => Ok(None),
}
}
impl azul_core::events::FocusManagerQuery for FocusManager {
fn get_focused_node_id(&self) -> Option<DomNodeId> {
self.focused_node
}
}
#[cfg(test)]
mod tab_order_tests {
use super::*;
fn nid(dom: usize, node: usize) -> DomNodeId {
FocusSearchContext::make_dom_node_id(DomId { inner: dom }, NodeId::new(node))
}
#[test]
fn positive_tabindex_sorts_first_ascending_then_document_order() {
let order = order_tab_entries(
vec![(2, nid(0, 3)), (1, nid(0, 7))],
vec![nid(0, 5), nid(0, 9)],
);
assert_eq!(order, vec![nid(0, 7), nid(0, 3), nid(0, 5), nid(0, 9)]);
}
#[test]
fn equal_positive_tabindex_keeps_document_order() {
let order = order_tab_entries(vec![(1, nid(0, 2)), (1, nid(0, 8))], vec![]);
assert_eq!(order, vec![nid(0, 2), nid(0, 8)]);
}
#[test]
fn next_wraps_and_previous_wraps() {
let order = vec![nid(0, 1), nid(0, 4), nid(0, 6)];
assert_eq!(next_in_tab_order(&order, Some(nid(0, 6)), true), Some(nid(0, 1)));
assert_eq!(next_in_tab_order(&order, Some(nid(0, 1)), false), Some(nid(0, 6)));
assert_eq!(next_in_tab_order(&order, Some(nid(0, 4)), true), Some(nid(0, 6)));
}
#[test]
fn no_focus_starts_at_ends() {
let order = vec![nid(0, 1), nid(0, 4)];
assert_eq!(next_in_tab_order(&order, None, true), Some(nid(0, 1)));
assert_eq!(next_in_tab_order(&order, None, false), Some(nid(0, 4)));
}
#[test]
fn non_tab_stop_focus_reenters_in_document_order() {
let order = vec![nid(0, 1), nid(0, 4), nid(0, 6)];
assert_eq!(next_in_tab_order(&order, Some(nid(0, 5)), true), Some(nid(0, 6)));
assert_eq!(next_in_tab_order(&order, Some(nid(0, 5)), false), Some(nid(0, 4)));
assert_eq!(next_in_tab_order(&order, Some(nid(0, 9)), true), Some(nid(0, 1)));
assert_eq!(next_in_tab_order(&order, Some(nid(0, 0)), false), Some(nid(0, 6)));
}
#[test]
fn empty_order_yields_none() {
assert_eq!(next_in_tab_order(&[], Some(nid(0, 1)), true), None);
assert_eq!(next_in_tab_order(&[], None, false), None);
}
}