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 if let (Some(container), Some(text)) = (
205 map.resolve(pending.container_node_id),
206 map.resolve(pending.text_node_id),
207 ) {
208 pending.container_node_id = container;
209 pending.text_node_id = text;
210 } else {
211 self.pending_contenteditable_focus = None;
212 self.cursor_needs_initialization = false;
213 }
214 }
215 }
216}
217
218fn collect_tab_order(layout_results: &BTreeMap<DomId, DomLayoutResult>) -> Vec<DomNodeId> {
228 use azul_core::dom::TabIndex;
229 let mut positive: Vec<(u32, DomNodeId)> = Vec::new();
230 let mut auto: Vec<DomNodeId> = Vec::new();
231 for (dom_id, layout) in layout_results {
232 let node_data = layout.styled_dom.node_data.as_container();
233 for index in 0..node_data.len() {
234 let node_id = NodeId::new(index);
235 let Some(nd) = node_data.get(node_id) else {
236 continue;
237 };
238 if !nd.is_focusable() {
239 continue;
240 }
241 let dom_node = FocusSearchContext::make_dom_node_id(*dom_id, node_id);
242 match nd.get_tab_index() {
243 Some(TabIndex::NoKeyboardFocus) => {}
244 Some(TabIndex::OverrideInParent(n)) if n > 0 => positive.push((n, dom_node)),
245 _ => auto.push(dom_node),
246 }
247 }
248 }
249 order_tab_entries(positive, auto)
250}
251
252fn order_tab_entries(
254 mut positive: Vec<(u32, DomNodeId)>,
255 auto: Vec<DomNodeId>,
256) -> Vec<DomNodeId> {
257 positive.sort_by_key(|(n, _)| *n); positive.into_iter().map(|(_, id)| id).chain(auto).collect()
259}
260
261fn doc_order_key(id: &DomNodeId) -> (usize, usize) {
264 (
265 id.dom.inner,
266 id.node.into_crate_internal().map_or(0, |n| n.index()),
267 )
268}
269
270fn next_in_tab_order(
277 order: &[DomNodeId],
278 current: Option<DomNodeId>,
279 forward: bool,
280) -> Option<DomNodeId> {
281 if order.is_empty() {
282 return None;
283 }
284 let Some(cur) = current else {
285 return if forward {
286 order.first().copied()
287 } else {
288 order.last().copied()
289 };
290 };
291 if let Some(pos) = order.iter().position(|x| *x == cur) {
292 let len = order.len();
293 let next = if forward {
294 (pos + 1) % len
295 } else {
296 (pos + len - 1) % len
297 };
298 return Some(order[next]);
299 }
300 let cur_key = doc_order_key(&cur);
301 let candidate = if forward {
302 order
303 .iter()
304 .filter(|x| doc_order_key(x) > cur_key)
305 .min_by_key(|x| doc_order_key(x))
306 } else {
307 order
308 .iter()
309 .filter(|x| doc_order_key(x) < cur_key)
310 .max_by_key(|x| doc_order_key(x))
311 };
312 candidate.copied().or_else(|| {
313 if forward {
314 order.first().copied()
315 } else {
316 order.last().copied()
317 }
318 })
319}
320
321struct FocusSearchContext<'a> {
327 layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
329}
330
331impl<'a> FocusSearchContext<'a> {
332 const fn new(layout_results: &'a BTreeMap<DomId, DomLayoutResult>) -> Self {
334 Self { layout_results }
335 }
336
337 #[allow(clippy::trivially_copy_pass_by_ref)] fn get_layout(&self, dom_id: &DomId) -> Result<&'a DomLayoutResult, UpdateFocusWarning> {
340 self.layout_results
341 .get(dom_id)
342 .ok_or_else(|| UpdateFocusWarning::FocusInvalidDomId(*dom_id))
343 }
344
345 const fn make_dom_node_id(dom_id: DomId, node_id: NodeId) -> DomNodeId {
347 DomNodeId {
348 dom: dom_id,
349 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
350 }
351 }
352}
353
354#[allow(clippy::trivially_copy_pass_by_ref)] fn find_first_matching_focusable_node(
369 layout: &DomLayoutResult,
370 dom_id: &DomId,
371 css_path: &azul_css::css::CssPath,
372) -> Option<DomNodeId> {
373 let styled_dom = &layout.styled_dom;
374 let node_hierarchy = styled_dom.node_hierarchy.as_container();
375 let node_data = styled_dom.node_data.as_container();
376 let cascade_info = styled_dom.cascade_info.as_container();
377
378 let matching_node = (0..node_data.len())
380 .map(NodeId::new)
381 .filter(|&node_id| {
382 matches_html_element(
384 css_path,
385 node_id,
386 &node_hierarchy,
387 &node_data,
388 &cascade_info,
389 None, )
391 })
392 .find(|&node_id| {
393 node_data[node_id].is_focusable()
395 });
396
397 matching_node.map(|node_id| DomNodeId {
398 dom: *dom_id,
399 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
400 })
401}
402
403pub fn resolve_focus_target(
408 focus_target: &FocusTarget,
409 layout_results: &BTreeMap<DomId, DomLayoutResult>,
410 current_focus: Option<DomNodeId>,
411) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
412 use azul_core::callbacks::FocusTarget::{Path, Id, Previous, Next, First, Last, NoFocus};
413
414 if layout_results.is_empty() {
415 return Ok(None);
416 }
417
418 let ctx = FocusSearchContext::new(layout_results);
419
420 match focus_target {
421 Path(FocusTargetPath { dom, css_path }) => {
422 let layout = ctx.get_layout(dom)?;
423 Ok(find_first_matching_focusable_node(layout, dom, css_path))
424 }
425
426 Id(dom_node_id) => {
427 let layout = ctx.get_layout(&dom_node_id.dom)?;
428 let is_valid = dom_node_id
429 .node
430 .into_crate_internal()
431 .is_some_and(|n| layout.styled_dom.node_data.as_container().get(n).is_some());
432
433 if is_valid {
434 Ok(Some(*dom_node_id))
435 } else {
436 Err(UpdateFocusWarning::FocusInvalidNodeId(
437 dom_node_id.node,
438 ))
439 }
440 }
441
442 Previous => Ok(next_in_tab_order(
446 &collect_tab_order(layout_results),
447 current_focus,
448 false,
449 )),
450
451 Next => Ok(next_in_tab_order(
452 &collect_tab_order(layout_results),
453 current_focus,
454 true,
455 )),
456
457 First => Ok(collect_tab_order(layout_results).first().copied()),
458
459 Last => Ok(collect_tab_order(layout_results).last().copied()),
460
461 NoFocus => Ok(None),
462 }
463}
464
465impl azul_core::events::FocusManagerQuery for FocusManager {
468 fn get_focused_node_id(&self) -> Option<DomNodeId> {
469 self.focused_node
470 }
471}
472
473#[cfg(test)]
474mod tab_order_tests {
475 use super::*;
476
477 fn nid(dom: usize, node: usize) -> DomNodeId {
478 FocusSearchContext::make_dom_node_id(DomId { inner: dom }, NodeId::new(node))
479 }
480
481 #[test]
482 fn positive_tabindex_sorts_first_ascending_then_document_order() {
483 let order = order_tab_entries(
485 vec![(2, nid(0, 3)), (1, nid(0, 7))],
486 vec![nid(0, 5), nid(0, 9)],
487 );
488 assert_eq!(order, vec![nid(0, 7), nid(0, 3), nid(0, 5), nid(0, 9)]);
489 }
490
491 #[test]
492 fn equal_positive_tabindex_keeps_document_order() {
493 let order = order_tab_entries(vec![(1, nid(0, 2)), (1, nid(0, 8))], vec![]);
494 assert_eq!(order, vec![nid(0, 2), nid(0, 8)]);
495 }
496
497 #[test]
498 fn next_wraps_and_previous_wraps() {
499 let order = vec![nid(0, 1), nid(0, 4), nid(0, 6)];
500 assert_eq!(next_in_tab_order(&order, Some(nid(0, 6)), true), Some(nid(0, 1)));
501 assert_eq!(next_in_tab_order(&order, Some(nid(0, 1)), false), Some(nid(0, 6)));
502 assert_eq!(next_in_tab_order(&order, Some(nid(0, 4)), true), Some(nid(0, 6)));
503 }
504
505 #[test]
506 fn no_focus_starts_at_ends() {
507 let order = vec![nid(0, 1), nid(0, 4)];
508 assert_eq!(next_in_tab_order(&order, None, true), Some(nid(0, 1)));
509 assert_eq!(next_in_tab_order(&order, None, false), Some(nid(0, 4)));
510 }
511
512 #[test]
513 fn non_tab_stop_focus_reenters_in_document_order() {
514 let order = vec![nid(0, 1), nid(0, 4), nid(0, 6)];
517 assert_eq!(next_in_tab_order(&order, Some(nid(0, 5)), true), Some(nid(0, 6)));
518 assert_eq!(next_in_tab_order(&order, Some(nid(0, 5)), false), Some(nid(0, 4)));
519 assert_eq!(next_in_tab_order(&order, Some(nid(0, 9)), true), Some(nid(0, 1)));
521 assert_eq!(next_in_tab_order(&order, Some(nid(0, 0)), false), Some(nid(0, 6)));
522 }
523
524 #[test]
525 fn empty_order_yields_none() {
526 assert_eq!(next_in_tab_order(&[], Some(nid(0, 1)), true), None);
527 assert_eq!(next_in_tab_order(&[], None, false), None);
528 }
529}
530
531#[cfg(test)]
532mod autotest_generated {
533 use std::collections::HashMap;
534
535 use azul_core::{
536 dom::{Dom, NodeType, TabIndex},
537 geom::LogicalRect,
538 styled_dom::StyledDom,
539 };
540 use azul_css::css::{CssPath, CssPathSelector};
541
542 use super::*;
543 use crate::{
544 managers::{NodeIdMap, NodeIdRemap},
545 solver3::{display_list::DisplayList, layout_tree::LayoutTree},
546 };
547
548 fn dom(inner: usize) -> DomId {
553 DomId { inner }
554 }
555
556 fn nid(dom_idx: usize, node: usize) -> DomNodeId {
557 FocusSearchContext::make_dom_node_id(dom(dom_idx), NodeId::new(node))
558 }
559
560 fn null_nid(dom_idx: usize) -> DomNodeId {
562 DomNodeId {
563 dom: dom(dom_idx),
564 node: NodeHierarchyItemId::from_crate_internal(None),
565 }
566 }
567
568 fn layout_result(styled_dom: StyledDom) -> DomLayoutResult {
571 DomLayoutResult {
572 styled_dom,
573 layout_tree: LayoutTree {
574 nodes: Vec::new(),
575 warm: Vec::new(),
576 cold: Vec::new(),
577 root: 0,
578 dom_to_layout: BTreeMap::new(),
579 children_arena: Vec::new(),
580 children_offsets: Vec::new(),
581 subtree_needs_intrinsic: Vec::new(),
582 },
583 calculated_positions: Vec::new(),
584 viewport: LogicalRect::zero(),
585 display_list: DisplayList::default(),
586 scroll_ids: HashMap::new(),
587 scroll_id_to_node_id: HashMap::new(),
588 }
589 }
590
591 fn window(entries: Vec<(DomId, StyledDom)>) -> BTreeMap<DomId, DomLayoutResult> {
592 entries
593 .into_iter()
594 .map(|(id, sd)| (id, layout_result(sd)))
595 .collect()
596 }
597
598 fn tab_fixture() -> StyledDom {
613 StyledDom::create_from_dom(
614 Dom::create_body()
615 .with_child(Dom::create_div())
616 .with_child(Dom::create_node(NodeType::Button))
617 .with_child(Dom::create_div().with_tab_index(TabIndex::OverrideInParent(2)))
618 .with_child(Dom::create_div().with_tab_index(TabIndex::NoKeyboardFocus))
619 .with_child(Dom::create_div().with_tab_index(TabIndex::OverrideInParent(1)))
620 .with_child(Dom::create_div().with_tab_index(TabIndex::OverrideInParent(0)))
621 .with_child(Dom::create_node(NodeType::TextArea)),
622 )
623 }
624
625 fn tab_order_of(fixture: StyledDom) -> Vec<DomNodeId> {
626 collect_tab_order(&window(vec![(dom(0), fixture)]))
627 }
628
629 fn class_path(class: &str) -> CssPath {
630 CssPath {
631 selectors: vec![CssPathSelector::Class(class.to_string().into())].into(),
632 }
633 }
634
635 #[test]
640 fn focus_manager_new_matches_default_and_is_fully_empty() {
641 let fm = FocusManager::new();
642 assert_eq!(fm, FocusManager::default());
643 assert_eq!(fm.get_focused_node(), None);
644 assert!(!fm.needs_cursor_initialization());
645 assert_eq!(fm.pending_focus_request, None);
646 assert_eq!(fm.pending_contenteditable_focus, None);
647 assert!(!fm.has_focus(&nid(0, 0)));
649 assert!(!fm.has_focus(&null_nid(0)));
650 }
651
652 #[test]
653 fn focus_manager_set_get_clear_focus_roundtrip() {
654 let mut fm = FocusManager::new();
655 fm.set_focused_node(Some(nid(0, 3)));
656 assert_eq!(fm.get_focused_node(), Some(&nid(0, 3)));
657 assert!(fm.has_focus(&nid(0, 3)));
658
659 fm.set_focused_node(None);
661 assert_eq!(fm.get_focused_node(), None);
662
663 fm.set_focused_node(Some(nid(0, 3)));
664 fm.clear_focus();
665 assert_eq!(fm.get_focused_node(), None);
666 assert!(!fm.has_focus(&nid(0, 3)));
667 fm.clear_focus();
669 assert_eq!(fm.get_focused_node(), None);
670 }
671
672 #[test]
673 fn focus_manager_has_focus_discriminates_both_dom_and_node() {
674 let mut fm = FocusManager::new();
675 fm.set_focused_node(Some(nid(1, 4)));
676
677 assert!(fm.has_focus(&nid(1, 4)));
678 assert!(!fm.has_focus(&nid(0, 4)));
680 assert!(!fm.has_focus(&nid(2, 4)));
681 assert!(!fm.has_focus(&nid(1, 3)));
683 assert!(!fm.has_focus(&nid(1, 5)));
684 assert!(!fm.has_focus(&null_nid(1)));
686 }
687
688 #[test]
689 fn focus_manager_has_focus_on_null_node_sentinel_is_exact() {
690 let mut fm = FocusManager::new();
694 fm.set_focused_node(Some(null_nid(0)));
695 assert!(fm.has_focus(&null_nid(0)));
696 assert!(!fm.has_focus(&nid(0, 0)));
697 assert!(!fm.has_focus(&null_nid(1)));
698 }
699
700 #[test]
701 fn focus_manager_focus_survives_extreme_node_index() {
702 let extreme = nid(usize::MAX, usize::MAX - 1);
706 let mut fm = FocusManager::new();
707 fm.set_focused_node(Some(extreme));
708 assert!(fm.has_focus(&extreme));
709 assert_eq!(
710 fm.get_focused_node()
711 .and_then(|n| n.node.into_crate_internal())
712 .map(|n| n.index()),
713 Some(usize::MAX - 1)
714 );
715 }
716
717 #[test]
722 fn focus_manager_take_focus_request_is_one_shot() {
723 let mut fm = FocusManager::new();
724 assert_eq!(fm.take_focus_request(), None);
726
727 fm.request_focus_change(FocusTarget::Next);
728 assert_eq!(fm.take_focus_request(), Some(FocusTarget::Next));
729 assert_eq!(fm.take_focus_request(), None);
731 assert_eq!(fm.take_focus_request(), None);
732 }
733
734 #[test]
735 fn focus_manager_request_focus_change_overwrites_pending_request() {
736 let mut fm = FocusManager::new();
740 fm.request_focus_change(FocusTarget::First);
741 fm.request_focus_change(FocusTarget::Last);
742 fm.request_focus_change(FocusTarget::NoFocus);
743 assert_eq!(fm.take_focus_request(), Some(FocusTarget::NoFocus));
744 assert_eq!(fm.take_focus_request(), None);
745 }
746
747 #[test]
748 fn focus_manager_request_focus_change_accepts_every_variant() {
749 let path = FocusTarget::Path(FocusTargetPath {
750 dom: dom(usize::MAX),
751 css_path: class_path("nonexistent"),
752 });
753 let targets = vec![
754 FocusTarget::Id(nid(0, 0)),
755 FocusTarget::Id(null_nid(usize::MAX)),
756 path,
757 FocusTarget::Previous,
758 FocusTarget::Next,
759 FocusTarget::First,
760 FocusTarget::Last,
761 FocusTarget::NoFocus,
762 ];
763 for t in targets {
764 let mut fm = FocusManager::new();
765 fm.request_focus_change(t.clone());
766 assert_eq!(fm.take_focus_request(), Some(t));
767 }
768 }
769
770 #[test]
775 fn focus_manager_pending_contenteditable_set_then_take_is_one_shot() {
776 let mut fm = FocusManager::new();
777 assert!(!fm.needs_cursor_initialization());
778 assert_eq!(fm.take_pending_contenteditable_focus(), None);
779
780 fm.set_pending_contenteditable_focus(dom(2), NodeId::new(7), NodeId::new(9));
781 assert!(fm.needs_cursor_initialization());
782
783 assert_eq!(
784 fm.take_pending_contenteditable_focus(),
785 Some(PendingContentEditableFocus {
786 dom_id: dom(2),
787 container_node_id: NodeId::new(7),
788 text_node_id: NodeId::new(9),
789 })
790 );
791 assert!(!fm.needs_cursor_initialization());
793 assert_eq!(fm.take_pending_contenteditable_focus(), None);
794 }
795
796 #[test]
797 fn focus_manager_set_pending_contenteditable_overwrites_and_accepts_extremes() {
798 let mut fm = FocusManager::new();
799 fm.set_pending_contenteditable_focus(dom(0), NodeId::new(1), NodeId::new(2));
800 fm.set_pending_contenteditable_focus(
803 dom(usize::MAX),
804 NodeId::new(usize::MAX),
805 NodeId::new(usize::MAX),
806 );
807 assert_eq!(
808 fm.take_pending_contenteditable_focus(),
809 Some(PendingContentEditableFocus {
810 dom_id: dom(usize::MAX),
811 container_node_id: NodeId::new(usize::MAX),
812 text_node_id: NodeId::new(usize::MAX),
813 })
814 );
815 }
816
817 #[test]
818 fn focus_manager_clear_pending_contenteditable_clears_flag_and_value() {
819 let mut fm = FocusManager::new();
820 fm.set_pending_contenteditable_focus(dom(0), NodeId::new(1), NodeId::new(2));
821 fm.clear_pending_contenteditable_focus();
822
823 assert!(!fm.needs_cursor_initialization());
824 assert_eq!(fm.pending_contenteditable_focus, None);
825 assert_eq!(fm.take_pending_contenteditable_focus(), None);
826 fm.clear_pending_contenteditable_focus();
828 assert!(!fm.needs_cursor_initialization());
829 }
830
831 #[test]
832 fn focus_manager_take_pending_without_flag_strands_the_value() {
833 let mut fm = FocusManager::new();
839 fm.pending_contenteditable_focus = Some(PendingContentEditableFocus {
840 dom_id: dom(0),
841 container_node_id: NodeId::new(1),
842 text_node_id: NodeId::new(2),
843 });
844
845 assert!(!fm.needs_cursor_initialization());
846 assert_eq!(fm.take_pending_contenteditable_focus(), None);
847 assert!(fm.pending_contenteditable_focus.is_some());
848 }
849
850 #[test]
851 fn focus_manager_flag_without_value_take_returns_none_and_clears_flag() {
852 let mut fm = FocusManager::new();
856 fm.cursor_needs_initialization = true;
857
858 assert!(fm.needs_cursor_initialization());
859 assert_eq!(fm.take_pending_contenteditable_focus(), None);
860 assert!(!fm.needs_cursor_initialization());
861 }
862
863 #[test]
864 fn focus_manager_clear_focus_does_not_clear_pending_cursor_state() {
865 let mut fm = FocusManager::new();
871 fm.set_focused_node(Some(nid(0, 4)));
872 fm.set_pending_contenteditable_focus(dom(0), NodeId::new(4), NodeId::new(5));
873
874 fm.clear_focus();
875
876 assert_eq!(fm.get_focused_node(), None);
877 assert!(fm.needs_cursor_initialization());
878 assert!(fm.pending_contenteditable_focus.is_some());
879 }
880
881 #[test]
886 fn order_tab_entries_empty_inputs_yield_empty() {
887 assert_eq!(order_tab_entries(Vec::new(), Vec::new()), Vec::new());
888 }
889
890 #[test]
891 fn order_tab_entries_u32_max_sorts_after_smaller_positives() {
892 let order = order_tab_entries(
895 vec![
896 (u32::MAX, nid(0, 1)),
897 (1, nid(0, 2)),
898 (u32::MAX - 1, nid(0, 3)),
899 ],
900 vec![nid(0, 4)],
901 );
902 assert_eq!(
903 order,
904 vec![nid(0, 2), nid(0, 3), nid(0, 1), nid(0, 4)],
905 "u32::MAX must sort last among positives, and all positives before auto"
906 );
907 }
908
909 #[test]
910 fn order_tab_entries_positive_always_precedes_auto() {
911 let order = order_tab_entries(vec![(u32::MAX, nid(9, 99))], vec![nid(0, 0), nid(0, 1)]);
913 assert_eq!(order[0], nid(9, 99));
914 assert_eq!(order.len(), 3);
915 }
916
917 #[test]
918 fn order_tab_entries_does_not_deduplicate() {
919 let order = order_tab_entries(
922 vec![(1, nid(0, 1)), (1, nid(0, 1))],
923 vec![nid(0, 2), nid(0, 2)],
924 );
925 assert_eq!(order, vec![nid(0, 1), nid(0, 1), nid(0, 2), nid(0, 2)]);
926 }
927
928 #[test]
929 fn doc_order_key_null_node_collides_with_node_index_zero() {
930 assert_eq!(doc_order_key(&null_nid(0)), (0, 0));
936 assert_eq!(doc_order_key(&nid(0, 0)), (0, 0));
937 assert_eq!(doc_order_key(&null_nid(0)), doc_order_key(&nid(0, 0)));
938 }
939
940 #[test]
941 fn doc_order_key_is_dom_major_then_arena_index() {
942 assert_eq!(doc_order_key(&nid(3, 7)), (3, 7));
943 assert!(doc_order_key(&nid(0, usize::MAX - 1)) < doc_order_key(&nid(1, 0)));
946 assert_eq!(doc_order_key(&nid(usize::MAX, usize::MAX - 1)), (usize::MAX, usize::MAX - 1));
947 }
948
949 #[test]
954 fn next_in_tab_order_single_entry_wraps_onto_itself() {
955 let order = vec![nid(0, 1)];
957 assert_eq!(next_in_tab_order(&order, Some(nid(0, 1)), true), Some(nid(0, 1)));
958 assert_eq!(next_in_tab_order(&order, Some(nid(0, 1)), false), Some(nid(0, 1)));
959 }
960
961 #[test]
962 fn next_in_tab_order_duplicate_entries_resolve_to_first_position() {
963 let order = vec![nid(0, 1), nid(0, 2), nid(0, 1)];
967 assert_eq!(next_in_tab_order(&order, Some(nid(0, 1)), true), Some(nid(0, 2)));
968 assert_eq!(next_in_tab_order(&order, Some(nid(0, 1)), false), Some(nid(0, 1)));
970 }
971
972 #[test]
973 fn next_in_tab_order_unknown_current_uses_cross_dom_document_order() {
974 let order = vec![nid(0, 5), nid(2, 1)];
976 assert_eq!(next_in_tab_order(&order, Some(nid(1, 0)), true), Some(nid(2, 1)));
977 assert_eq!(next_in_tab_order(&order, Some(nid(1, 9)), false), Some(nid(0, 5)));
978 }
979
980 #[test]
981 fn next_in_tab_order_unknown_current_past_both_ends_wraps() {
982 let order = vec![nid(1, 2), nid(1, 4)];
983 assert_eq!(next_in_tab_order(&order, Some(nid(9, 9)), true), Some(nid(1, 2)));
985 assert_eq!(next_in_tab_order(&order, Some(nid(0, 0)), false), Some(nid(1, 4)));
987 }
988
989 #[test]
990 fn next_in_tab_order_null_current_node_is_deterministic() {
991 let order = vec![nid(0, 1), nid(0, 3)];
994 assert_eq!(next_in_tab_order(&order, Some(null_nid(0)), true), Some(nid(0, 1)));
995 assert_eq!(next_in_tab_order(&order, Some(null_nid(0)), false), Some(nid(0, 3)));
996 }
997
998 #[test]
999 fn next_in_tab_order_empty_order_is_none_for_every_input() {
1000 assert_eq!(next_in_tab_order(&[], None, true), None);
1001 assert_eq!(next_in_tab_order(&[], None, false), None);
1002 assert_eq!(next_in_tab_order(&[], Some(null_nid(0)), true), None);
1003 assert_eq!(
1004 next_in_tab_order(&[], Some(nid(usize::MAX, usize::MAX - 1)), false),
1005 None
1006 );
1007 }
1008
1009 #[test]
1014 fn collect_tab_order_empty_window_is_empty() {
1015 assert_eq!(collect_tab_order(&BTreeMap::new()), Vec::new());
1016 }
1017
1018 #[test]
1019 fn collect_tab_order_dom_without_focusables_is_empty() {
1020 let sd = StyledDom::create_from_dom(
1021 Dom::create_body()
1022 .with_child(Dom::create_div())
1023 .with_child(Dom::create_div()),
1024 );
1025 assert_eq!(tab_order_of(sd), Vec::new());
1026 }
1027
1028 #[test]
1029 fn collect_tab_order_positives_first_then_document_order_minus_one_excluded() {
1030 assert_eq!(
1032 tab_order_of(tab_fixture()),
1033 vec![nid(0, 5), nid(0, 3), nid(0, 2), nid(0, 6), nid(0, 7)],
1034 "tabindex=1 then tabindex=2, then auto nodes in document order"
1035 );
1036 }
1037
1038 #[test]
1039 fn collect_tab_order_excludes_tabindex_minus_one_though_it_is_focusable() {
1040 let order = tab_order_of(tab_fixture());
1043 assert!(!order.contains(&nid(0, 4)), "tabindex=-1 must not be a tab stop");
1044 assert!(!order.contains(&nid(0, 1)));
1046 assert!(!order.contains(&nid(0, 0)));
1048 }
1049
1050 #[test]
1051 fn collect_tab_order_huge_tabindex_truncates_at_28_bits() {
1052 let sd = StyledDom::create_from_dom(
1060 Dom::create_body()
1061 .with_child(Dom::create_div().with_tab_index(TabIndex::OverrideInParent(u32::MAX)))
1062 .with_child(Dom::create_div().with_tab_index(TabIndex::OverrideInParent(1 << 28)))
1063 .with_child(Dom::create_div().with_tab_index(TabIndex::OverrideInParent(1)))
1064 .with_child(Dom::create_node(NodeType::Button)),
1065 );
1066
1067 assert_eq!(
1068 tab_order_of(sd),
1069 vec![nid(0, 3), nid(0, 1), nid(0, 2), nid(0, 4)],
1070 "u32::MAX stays positive (sorts after tabindex=1); 1<<28 truncates to 0 and \
1071 falls back into the auto bucket behind every positive"
1072 );
1073 }
1074
1075 #[test]
1076 fn collect_tab_order_tab_order_is_global_across_doms() {
1077 let order = collect_tab_order(&window(vec![
1080 (dom(0), tab_fixture()),
1081 (dom(1), tab_fixture()),
1082 ]));
1083
1084 assert_eq!(
1085 order,
1086 vec![
1087 nid(0, 5),
1089 nid(1, 5),
1090 nid(0, 3),
1091 nid(1, 3),
1092 nid(0, 2),
1094 nid(0, 6),
1095 nid(0, 7),
1096 nid(1, 2),
1097 nid(1, 6),
1098 nid(1, 7),
1099 ]
1100 );
1101 }
1102
1103 #[test]
1108 fn focus_search_context_get_layout_hit_and_miss() {
1109 let results = window(vec![(dom(0), tab_fixture())]);
1110 let ctx = FocusSearchContext::new(&results);
1111
1112 assert!(ctx.get_layout(&dom(0)).is_ok());
1114 assert_eq!(
1115 ctx.get_layout(&dom(1)).err(),
1116 Some(UpdateFocusWarning::FocusInvalidDomId(dom(1)))
1117 );
1118 assert_eq!(
1119 ctx.get_layout(&dom(usize::MAX)).err(),
1120 Some(UpdateFocusWarning::FocusInvalidDomId(dom(usize::MAX)))
1121 );
1122 }
1123
1124 #[test]
1125 fn focus_search_context_new_on_empty_map_never_resolves() {
1126 let empty = BTreeMap::new();
1127 let ctx = FocusSearchContext::new(&empty);
1128 assert_eq!(
1129 ctx.get_layout(&dom(0)).err(),
1130 Some(UpdateFocusWarning::FocusInvalidDomId(dom(0)))
1131 );
1132 }
1133
1134 #[test]
1135 fn make_dom_node_id_round_trips_including_boundary_index() {
1136 for idx in [0usize, 1, 42, usize::MAX - 1] {
1139 let d = FocusSearchContext::make_dom_node_id(dom(7), NodeId::new(idx));
1140 assert_eq!(d.dom, dom(7));
1141 assert_eq!(d.node.into_crate_internal(), Some(NodeId::new(idx)));
1142 assert_eq!(doc_order_key(&d), (7, idx));
1143 }
1144 }
1145
1146 #[test]
1151 fn find_first_matching_skips_matching_but_unfocusable_nodes() {
1152 let sd = StyledDom::create_from_dom(
1155 Dom::create_body()
1156 .with_child(Dom::create_div().with_class("target".to_string().into()))
1157 .with_child(
1158 Dom::create_div()
1159 .with_class("target".to_string().into())
1160 .with_tab_index(TabIndex::Auto),
1161 ),
1162 );
1163 let results = window(vec![(dom(0), sd)]);
1164 let layout = results.get(&dom(0)).unwrap();
1165
1166 assert_eq!(
1167 find_first_matching_focusable_node(layout, &dom(0), &class_path("target")),
1168 Some(nid(0, 2))
1169 );
1170 }
1171
1172 #[test]
1173 fn find_first_matching_empty_css_path_matches_nothing() {
1174 let results = window(vec![(dom(0), tab_fixture())]);
1176 let layout = results.get(&dom(0)).unwrap();
1177 let empty = CssPath {
1178 selectors: Vec::<CssPathSelector>::new().into(),
1179 };
1180
1181 assert_eq!(
1182 find_first_matching_focusable_node(layout, &dom(0), &empty),
1183 None
1184 );
1185 }
1186
1187 #[test]
1188 fn find_first_matching_unicode_class_matches_and_misses_cleanly() {
1189 let class = "クラス-día-🎯";
1192 let sd = StyledDom::create_from_dom(
1193 Dom::create_body().with_child(
1194 Dom::create_div()
1195 .with_class(class.to_string().into())
1196 .with_tab_index(TabIndex::Auto),
1197 ),
1198 );
1199 let results = window(vec![(dom(0), sd)]);
1200 let layout = results.get(&dom(0)).unwrap();
1201
1202 assert_eq!(
1203 find_first_matching_focusable_node(layout, &dom(0), &class_path(class)),
1204 Some(nid(0, 1))
1205 );
1206 assert_eq!(
1208 find_first_matching_focusable_node(layout, &dom(0), &class_path("クラス-día-🎲")),
1209 None
1210 );
1211 let huge = "x".repeat(10_000);
1213 assert_eq!(
1214 find_first_matching_focusable_node(layout, &dom(0), &class_path(&huge)),
1215 None
1216 );
1217 }
1218
1219 #[test]
1224 fn resolve_focus_target_empty_window_short_circuits_every_variant() {
1225 let empty = BTreeMap::new();
1229 let targets = vec![
1230 FocusTarget::Id(nid(usize::MAX, 0)),
1231 FocusTarget::Id(null_nid(0)),
1232 FocusTarget::Path(FocusTargetPath {
1233 dom: dom(usize::MAX),
1234 css_path: class_path("nope"),
1235 }),
1236 FocusTarget::Previous,
1237 FocusTarget::Next,
1238 FocusTarget::First,
1239 FocusTarget::Last,
1240 FocusTarget::NoFocus,
1241 ];
1242 for t in targets {
1243 assert_eq!(
1244 resolve_focus_target(&t, &empty, Some(nid(0, 1))),
1245 Ok(None),
1246 "empty window must short-circuit {t:?}"
1247 );
1248 }
1249 }
1250
1251 #[test]
1252 fn resolve_focus_target_id_rejects_unknown_dom() {
1253 let results = window(vec![(dom(0), tab_fixture())]);
1254 assert_eq!(
1255 resolve_focus_target(&FocusTarget::Id(nid(1, 2)), &results, None),
1256 Err(UpdateFocusWarning::FocusInvalidDomId(dom(1)))
1257 );
1258 }
1259
1260 #[test]
1261 fn resolve_focus_target_id_rejects_out_of_range_node() {
1262 let results = window(vec![(dom(0), tab_fixture())]);
1265 for idx in [8usize, 9, 1_000_000, usize::MAX - 1] {
1266 let target = nid(0, idx);
1267 assert_eq!(
1268 resolve_focus_target(&FocusTarget::Id(target), &results, None),
1269 Err(UpdateFocusWarning::FocusInvalidNodeId(target.node)),
1270 "node {idx} is out of range and must be rejected"
1271 );
1272 }
1273 }
1274
1275 #[test]
1276 fn resolve_focus_target_id_rejects_null_node_sentinel() {
1277 let results = window(vec![(dom(0), tab_fixture())]);
1278 let target = null_nid(0);
1279 assert_eq!(
1280 resolve_focus_target(&FocusTarget::Id(target), &results, None),
1281 Err(UpdateFocusWarning::FocusInvalidNodeId(target.node))
1282 );
1283 }
1284
1285 #[test]
1286 fn resolve_focus_target_id_accepts_valid_but_unfocusable_node() {
1287 let results = window(vec![(dom(0), tab_fixture())]);
1291 assert_eq!(
1292 resolve_focus_target(&FocusTarget::Id(nid(0, 0)), &results, None),
1293 Ok(Some(nid(0, 0)))
1294 );
1295 assert_eq!(
1296 resolve_focus_target(&FocusTarget::Id(nid(0, 4)), &results, None),
1297 Ok(Some(nid(0, 4)))
1298 );
1299 }
1300
1301 #[test]
1302 fn resolve_focus_target_path_rejects_unknown_dom() {
1303 let results = window(vec![(dom(0), tab_fixture())]);
1304 let target = FocusTarget::Path(FocusTargetPath {
1305 dom: dom(3),
1306 css_path: class_path("target"),
1307 });
1308 assert_eq!(
1309 resolve_focus_target(&target, &results, None),
1310 Err(UpdateFocusWarning::FocusInvalidDomId(dom(3)))
1311 );
1312 }
1313
1314 #[test]
1315 fn resolve_focus_target_path_with_no_match_is_ok_none_not_err() {
1316 let results = window(vec![(dom(0), tab_fixture())]);
1321 let target = FocusTarget::Path(FocusTargetPath {
1322 dom: dom(0),
1323 css_path: class_path("no-such-class"),
1324 });
1325 assert_eq!(resolve_focus_target(&target, &results, None), Ok(None));
1326 }
1327
1328 #[test]
1329 fn resolve_focus_target_no_focus_is_always_none() {
1330 let results = window(vec![(dom(0), tab_fixture())]);
1331 assert_eq!(
1332 resolve_focus_target(&FocusTarget::NoFocus, &results, Some(nid(0, 5))),
1333 Ok(None)
1334 );
1335 }
1336
1337 #[test]
1338 fn resolve_focus_target_first_and_last_are_the_tab_order_ends() {
1339 let results = window(vec![(dom(0), tab_fixture())]);
1340 assert_eq!(
1342 resolve_focus_target(&FocusTarget::First, &results, None),
1343 Ok(Some(nid(0, 5))),
1344 "First must be the lowest positive tabindex, not document node 0"
1345 );
1346 assert_eq!(
1347 resolve_focus_target(&FocusTarget::Last, &results, None),
1348 Ok(Some(nid(0, 7)))
1349 );
1350 assert_eq!(
1352 resolve_focus_target(&FocusTarget::First, &results, Some(nid(0, 7))),
1353 Ok(Some(nid(0, 5)))
1354 );
1355 }
1356
1357 #[test]
1358 fn resolve_focus_target_first_and_last_on_unfocusable_dom_are_none() {
1359 let sd = StyledDom::create_from_dom(Dom::create_body().with_child(Dom::create_div()));
1360 let results = window(vec![(dom(0), sd)]);
1361 assert_eq!(resolve_focus_target(&FocusTarget::First, &results, None), Ok(None));
1362 assert_eq!(resolve_focus_target(&FocusTarget::Last, &results, None), Ok(None));
1363 assert_eq!(resolve_focus_target(&FocusTarget::Next, &results, None), Ok(None));
1364 assert_eq!(
1365 resolve_focus_target(&FocusTarget::Previous, &results, Some(nid(0, 0))),
1366 Ok(None)
1367 );
1368 }
1369
1370 #[test]
1371 fn resolve_focus_target_next_and_previous_wrap_around_the_tab_order() {
1372 let results = window(vec![(dom(0), tab_fixture())]);
1373 assert_eq!(
1375 resolve_focus_target(&FocusTarget::Next, &results, Some(nid(0, 7))),
1376 Ok(Some(nid(0, 5)))
1377 );
1378 assert_eq!(
1379 resolve_focus_target(&FocusTarget::Previous, &results, Some(nid(0, 5))),
1380 Ok(Some(nid(0, 7)))
1381 );
1382 assert_eq!(
1384 resolve_focus_target(&FocusTarget::Next, &results, Some(nid(0, 3))),
1385 Ok(Some(nid(0, 2)))
1386 );
1387 assert_eq!(
1388 resolve_focus_target(&FocusTarget::Previous, &results, Some(nid(0, 2))),
1389 Ok(Some(nid(0, 3)))
1390 );
1391 }
1392
1393 #[test]
1394 fn resolve_focus_target_next_from_a_non_tab_stop_reenters_in_document_order() {
1395 let results = window(vec![(dom(0), tab_fixture())]);
1396 assert_eq!(
1401 resolve_focus_target(&FocusTarget::Previous, &results, Some(nid(0, 4))),
1402 Ok(Some(nid(0, 3)))
1403 );
1404 assert_eq!(
1408 resolve_focus_target(&FocusTarget::Next, &results, Some(nid(0, 1))),
1409 Ok(Some(nid(0, 2)))
1410 );
1411 }
1412
1413 #[test]
1414 fn resolve_focus_target_next_from_a_stale_removed_node_never_panics() {
1415 let results = window(vec![(dom(0), tab_fixture())]);
1418 let stale = nid(0, 9_999);
1419 assert_eq!(
1420 resolve_focus_target(&FocusTarget::Next, &results, Some(stale)),
1421 Ok(Some(nid(0, 5))),
1422 "no tab stop past node 9999 -> wrap to the first"
1423 );
1424 let alien = nid(5, 1);
1426 assert_eq!(
1427 resolve_focus_target(&FocusTarget::Next, &results, Some(alien)),
1428 Ok(Some(nid(0, 5)))
1429 );
1430 assert_eq!(
1431 resolve_focus_target(&FocusTarget::Previous, &results, Some(alien)),
1432 Ok(Some(nid(0, 7)))
1433 );
1434 }
1435
1436 #[test]
1441 fn remap_rewrites_the_focused_node() {
1442 let mut fm = FocusManager::new();
1443 fm.set_focused_node(Some(nid(0, 5)));
1444 fm.remap_node_ids(
1445 dom(0),
1446 &NodeIdMap::from_pairs([(NodeId::new(5), NodeId::new(2))]),
1447 );
1448 assert_eq!(fm.get_focused_node(), Some(&nid(0, 2)));
1449 }
1450
1451 #[test]
1452 fn remap_clears_focus_on_an_unmounted_node() {
1453 let mut fm = FocusManager::new();
1456 fm.set_focused_node(Some(nid(0, 5)));
1457 fm.remap_node_ids(
1458 dom(0),
1459 &NodeIdMap::from_pairs([(NodeId::new(1), NodeId::new(1))]),
1460 );
1461 assert_eq!(fm.get_focused_node(), None);
1462 }
1463
1464 #[test]
1465 fn remap_leaves_other_doms_untouched() {
1466 let mut fm = FocusManager::new();
1467 fm.set_focused_node(Some(nid(1, 5)));
1468 fm.remap_node_ids(
1470 dom(0),
1471 &NodeIdMap::from_pairs([(NodeId::new(5), NodeId::new(2))]),
1472 );
1473 assert_eq!(fm.get_focused_node(), Some(&nid(1, 5)));
1474 }
1475
1476 #[test]
1477 fn remap_rewrites_pending_contenteditable_focus() {
1478 let mut fm = FocusManager::new();
1479 fm.set_pending_contenteditable_focus(dom(0), NodeId::new(3), NodeId::new(4));
1480 fm.remap_node_ids(
1481 dom(0),
1482 &NodeIdMap::from_pairs([
1483 (NodeId::new(3), NodeId::new(10)),
1484 (NodeId::new(4), NodeId::new(11)),
1485 ]),
1486 );
1487 assert!(fm.needs_cursor_initialization());
1488 assert_eq!(
1489 fm.take_pending_contenteditable_focus(),
1490 Some(PendingContentEditableFocus {
1491 dom_id: dom(0),
1492 container_node_id: NodeId::new(10),
1493 text_node_id: NodeId::new(11),
1494 })
1495 );
1496 }
1497
1498 #[test]
1499 fn remap_partially_resolvable_pending_focus_drops_it_entirely() {
1500 for pairs in [
1504 vec![(NodeId::new(3), NodeId::new(10))], vec![(NodeId::new(4), NodeId::new(11))], vec![], ] {
1508 let mut fm = FocusManager::new();
1509 fm.set_pending_contenteditable_focus(dom(0), NodeId::new(3), NodeId::new(4));
1510 fm.remap_node_ids(dom(0), &NodeIdMap::from_pairs(pairs));
1511
1512 assert!(!fm.needs_cursor_initialization());
1513 assert_eq!(fm.pending_contenteditable_focus, None);
1514 }
1515 }
1516}