1use alloc::collections::BTreeMap;
7
8use azul_core::{
9 callbacks::{FocusTarget, FocusTargetPath},
10 dom::{DomId, DomNodeId, NodeId},
11 style::matches_html_element,
12 styled_dom::NodeHierarchyItemId,
13 window::UpdateFocusWarning,
14};
15
16use crate::window::DomLayoutResult;
17
18#[derive(Copy, Debug, Clone, PartialEq, Eq)]
23pub struct PendingContentEditableFocus {
24 pub dom_id: DomId,
26 pub container_node_id: NodeId,
28 pub text_node_id: NodeId,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct FocusManager {
54 pub focused_node: Option<DomNodeId>,
56 pub pending_focus_request: Option<FocusTarget>,
58
59 pub cursor_needs_initialization: bool,
63 pub pending_contenteditable_focus: Option<PendingContentEditableFocus>,
65}
66
67impl Default for FocusManager {
68 fn default() -> Self {
69 Self::new()
70 }
71}
72
73impl FocusManager {
74 #[must_use] pub const fn new() -> Self {
76 Self {
77 focused_node: None,
78 pending_focus_request: None,
79 cursor_needs_initialization: false,
80 pending_contenteditable_focus: None,
81 }
82 }
83
84 #[must_use] pub const fn get_focused_node(&self) -> Option<&DomNodeId> {
86 self.focused_node.as_ref()
87 }
88
89 pub const fn set_focused_node(&mut self, node: Option<DomNodeId>) {
95 self.focused_node = node;
96 }
97
98 pub fn request_focus_change(&mut self, target: FocusTarget) {
100 self.pending_focus_request = Some(target);
101 }
102
103 pub const fn take_focus_request(&mut self) -> Option<FocusTarget> {
105 self.pending_focus_request.take()
106 }
107
108 pub const fn clear_focus(&mut self) {
110 self.focused_node = None;
111 }
112
113 #[must_use] pub fn has_focus(&self, node: &DomNodeId) -> bool {
115 self.focused_node.as_ref() == Some(node)
116 }
117
118 pub const fn set_pending_contenteditable_focus(
134 &mut self,
135 dom_id: DomId,
136 container_node_id: NodeId,
137 text_node_id: NodeId,
138 ) {
139 self.cursor_needs_initialization = true;
140 self.pending_contenteditable_focus = Some(PendingContentEditableFocus {
141 dom_id,
142 container_node_id,
143 text_node_id,
144 });
145 }
146
147 pub const fn clear_pending_contenteditable_focus(&mut self) {
149 self.cursor_needs_initialization = false;
150 self.pending_contenteditable_focus = None;
151 }
152
153 pub const fn take_pending_contenteditable_focus(&mut self) -> Option<PendingContentEditableFocus> {
158 if self.cursor_needs_initialization {
159 self.cursor_needs_initialization = false;
160 self.pending_contenteditable_focus.take()
161 } else {
162 None
163 }
164 }
165
166 #[must_use] pub const fn needs_cursor_initialization(&self) -> bool {
168 self.cursor_needs_initialization
169 }
170
171}
172
173impl crate::managers::NodeIdRemap for FocusManager {
174 fn remap_node_ids(&mut self, dom_id: DomId, map: &crate::managers::NodeIdMap) {
179 if let Some(focused) = self.focused_node {
181 if focused.dom == dom_id {
182 match focused
183 .node
184 .into_crate_internal()
185 .and_then(|old| map.resolve(old))
186 {
187 Some(new_id) => {
188 self.focused_node = Some(DomNodeId {
189 dom: dom_id,
190 node: NodeHierarchyItemId::from_crate_internal(Some(new_id)),
191 });
192 }
193 None => self.focused_node = None,
194 }
195 }
196 }
197
198 if let Some(ref mut pending) = self.pending_contenteditable_focus {
201 if pending.dom_id != dom_id {
202 return;
203 }
204 match (
205 map.resolve(pending.container_node_id),
206 map.resolve(pending.text_node_id),
207 ) {
208 (Some(container), Some(text)) => {
209 pending.container_node_id = container;
210 pending.text_node_id = text;
211 }
212 _ => {
213 self.pending_contenteditable_focus = None;
214 self.cursor_needs_initialization = false;
215 }
216 }
217 }
218 }
219}
220
221fn collect_tab_order(layout_results: &BTreeMap<DomId, DomLayoutResult>) -> Vec<DomNodeId> {
231 use azul_core::dom::TabIndex;
232 let mut positive: Vec<(u32, DomNodeId)> = Vec::new();
233 let mut auto: Vec<DomNodeId> = Vec::new();
234 for (dom_id, layout) in layout_results {
235 let node_data = layout.styled_dom.node_data.as_container();
236 for index in 0..node_data.len() {
237 let node_id = NodeId::new(index);
238 let Some(nd) = node_data.get(node_id) else {
239 continue;
240 };
241 if !nd.is_focusable() {
242 continue;
243 }
244 let dom_node = FocusSearchContext::make_dom_node_id(*dom_id, node_id);
245 match nd.get_tab_index() {
246 Some(TabIndex::NoKeyboardFocus) => {}
247 Some(TabIndex::OverrideInParent(n)) if n > 0 => positive.push((n, dom_node)),
248 _ => auto.push(dom_node),
249 }
250 }
251 }
252 order_tab_entries(positive, auto)
253}
254
255fn order_tab_entries(
257 mut positive: Vec<(u32, DomNodeId)>,
258 auto: Vec<DomNodeId>,
259) -> Vec<DomNodeId> {
260 positive.sort_by_key(|(n, _)| *n); positive.into_iter().map(|(_, id)| id).chain(auto).collect()
262}
263
264fn doc_order_key(id: &DomNodeId) -> (usize, usize) {
267 (
268 id.dom.inner,
269 id.node.into_crate_internal().map_or(0, |n| n.index()),
270 )
271}
272
273fn next_in_tab_order(
280 order: &[DomNodeId],
281 current: Option<DomNodeId>,
282 forward: bool,
283) -> Option<DomNodeId> {
284 if order.is_empty() {
285 return None;
286 }
287 let Some(cur) = current else {
288 return if forward {
289 order.first().copied()
290 } else {
291 order.last().copied()
292 };
293 };
294 if let Some(pos) = order.iter().position(|x| *x == cur) {
295 let len = order.len();
296 let next = if forward {
297 (pos + 1) % len
298 } else {
299 (pos + len - 1) % len
300 };
301 return Some(order[next]);
302 }
303 let cur_key = doc_order_key(&cur);
304 let candidate = if forward {
305 order
306 .iter()
307 .filter(|x| doc_order_key(x) > cur_key)
308 .min_by_key(|x| doc_order_key(x))
309 } else {
310 order
311 .iter()
312 .filter(|x| doc_order_key(x) < cur_key)
313 .max_by_key(|x| doc_order_key(x))
314 };
315 candidate.copied().or_else(|| {
316 if forward {
317 order.first().copied()
318 } else {
319 order.last().copied()
320 }
321 })
322}
323
324struct FocusSearchContext<'a> {
330 layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
332}
333
334impl<'a> FocusSearchContext<'a> {
335 const fn new(layout_results: &'a BTreeMap<DomId, DomLayoutResult>) -> Self {
337 Self { layout_results }
338 }
339
340 #[allow(clippy::trivially_copy_pass_by_ref)] fn get_layout(&self, dom_id: &DomId) -> Result<&'a DomLayoutResult, UpdateFocusWarning> {
343 self.layout_results
344 .get(dom_id)
345 .ok_or_else(|| UpdateFocusWarning::FocusInvalidDomId(*dom_id))
346 }
347
348 const fn make_dom_node_id(dom_id: DomId, node_id: NodeId) -> DomNodeId {
350 DomNodeId {
351 dom: dom_id,
352 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
353 }
354 }
355}
356
357#[allow(clippy::trivially_copy_pass_by_ref)] fn find_first_matching_focusable_node(
372 layout: &DomLayoutResult,
373 dom_id: &DomId,
374 css_path: &azul_css::css::CssPath,
375) -> Option<DomNodeId> {
376 let styled_dom = &layout.styled_dom;
377 let node_hierarchy = styled_dom.node_hierarchy.as_container();
378 let node_data = styled_dom.node_data.as_container();
379 let cascade_info = styled_dom.cascade_info.as_container();
380
381 let matching_node = (0..node_data.len())
383 .map(NodeId::new)
384 .filter(|&node_id| {
385 matches_html_element(
387 css_path,
388 node_id,
389 &node_hierarchy,
390 &node_data,
391 &cascade_info,
392 None, )
394 })
395 .find(|&node_id| {
396 node_data[node_id].is_focusable()
398 });
399
400 matching_node.map(|node_id| DomNodeId {
401 dom: *dom_id,
402 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
403 })
404}
405
406pub fn resolve_focus_target(
411 focus_target: &FocusTarget,
412 layout_results: &BTreeMap<DomId, DomLayoutResult>,
413 current_focus: Option<DomNodeId>,
414) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
415 use azul_core::callbacks::FocusTarget::{Path, Id, Previous, Next, First, Last, NoFocus};
416
417 if layout_results.is_empty() {
418 return Ok(None);
419 }
420
421 let ctx = FocusSearchContext::new(layout_results);
422
423 match focus_target {
424 Path(FocusTargetPath { dom, css_path }) => {
425 let layout = ctx.get_layout(dom)?;
426 Ok(find_first_matching_focusable_node(layout, dom, css_path))
427 }
428
429 Id(dom_node_id) => {
430 let layout = ctx.get_layout(&dom_node_id.dom)?;
431 let is_valid = dom_node_id
432 .node
433 .into_crate_internal()
434 .is_some_and(|n| layout.styled_dom.node_data.as_container().get(n).is_some());
435
436 if is_valid {
437 Ok(Some(*dom_node_id))
438 } else {
439 Err(UpdateFocusWarning::FocusInvalidNodeId(
440 dom_node_id.node,
441 ))
442 }
443 }
444
445 Previous => Ok(next_in_tab_order(
449 &collect_tab_order(layout_results),
450 current_focus,
451 false,
452 )),
453
454 Next => Ok(next_in_tab_order(
455 &collect_tab_order(layout_results),
456 current_focus,
457 true,
458 )),
459
460 First => Ok(collect_tab_order(layout_results).first().copied()),
461
462 Last => Ok(collect_tab_order(layout_results).last().copied()),
463
464 NoFocus => Ok(None),
465 }
466}
467
468impl azul_core::events::FocusManagerQuery for FocusManager {
471 fn get_focused_node_id(&self) -> Option<DomNodeId> {
472 self.focused_node
473 }
474}
475
476#[cfg(test)]
477mod tab_order_tests {
478 use super::*;
479
480 fn nid(dom: usize, node: usize) -> DomNodeId {
481 FocusSearchContext::make_dom_node_id(DomId { inner: dom }, NodeId::new(node))
482 }
483
484 #[test]
485 fn positive_tabindex_sorts_first_ascending_then_document_order() {
486 let order = order_tab_entries(
488 vec![(2, nid(0, 3)), (1, nid(0, 7))],
489 vec![nid(0, 5), nid(0, 9)],
490 );
491 assert_eq!(order, vec![nid(0, 7), nid(0, 3), nid(0, 5), nid(0, 9)]);
492 }
493
494 #[test]
495 fn equal_positive_tabindex_keeps_document_order() {
496 let order = order_tab_entries(vec![(1, nid(0, 2)), (1, nid(0, 8))], vec![]);
497 assert_eq!(order, vec![nid(0, 2), nid(0, 8)]);
498 }
499
500 #[test]
501 fn next_wraps_and_previous_wraps() {
502 let order = vec![nid(0, 1), nid(0, 4), nid(0, 6)];
503 assert_eq!(next_in_tab_order(&order, Some(nid(0, 6)), true), Some(nid(0, 1)));
504 assert_eq!(next_in_tab_order(&order, Some(nid(0, 1)), false), Some(nid(0, 6)));
505 assert_eq!(next_in_tab_order(&order, Some(nid(0, 4)), true), Some(nid(0, 6)));
506 }
507
508 #[test]
509 fn no_focus_starts_at_ends() {
510 let order = vec![nid(0, 1), nid(0, 4)];
511 assert_eq!(next_in_tab_order(&order, None, true), Some(nid(0, 1)));
512 assert_eq!(next_in_tab_order(&order, None, false), Some(nid(0, 4)));
513 }
514
515 #[test]
516 fn non_tab_stop_focus_reenters_in_document_order() {
517 let order = vec![nid(0, 1), nid(0, 4), nid(0, 6)];
520 assert_eq!(next_in_tab_order(&order, Some(nid(0, 5)), true), Some(nid(0, 6)));
521 assert_eq!(next_in_tab_order(&order, Some(nid(0, 5)), false), Some(nid(0, 4)));
522 assert_eq!(next_in_tab_order(&order, Some(nid(0, 9)), true), Some(nid(0, 1)));
524 assert_eq!(next_in_tab_order(&order, Some(nid(0, 0)), false), Some(nid(0, 6)));
525 }
526
527 #[test]
528 fn empty_order_yields_none() {
529 assert_eq!(next_in_tab_order(&[], Some(nid(0, 1)), true), None);
530 assert_eq!(next_in_tab_order(&[], None, false), None);
531 }
532}