Skip to main content

azul_layout/managers/
focus_cursor.rs

1//! Focus and tab navigation management.
2//!
3//! Manages keyboard focus, tab navigation, and programmatic focus changes
4//! with a recursive event system for focus/blur callbacks (max depth: 5).
5
6use alloc::{collections::BTreeMap, vec::Vec};
7
8use azul_core::{
9    callbacks::{FocusTarget, FocusTargetPath},
10    dom::{DomId, DomNodeId, NodeId},
11    style::matches_html_element,
12    styled_dom::NodeHierarchyItemId,
13};
14
15use crate::window::DomLayoutResult;
16
17/// CSS path for selecting elements (placeholder - needs proper implementation)
18pub type CssPathString = alloc::string::String;
19
20/// Information about a pending contenteditable focus that needs cursor initialization
21/// after layout is complete (W3C "flag and defer" pattern).
22///
23/// This is set during focus event handling and consumed after layout pass.
24#[derive(Debug, Clone, PartialEq)]
25pub struct PendingContentEditableFocus {
26    /// The DOM where the contenteditable element is
27    pub dom_id: DomId,
28    /// The contenteditable container node that received focus
29    pub container_node_id: NodeId,
30    /// The text node where the cursor should be placed (often a child of the container)
31    pub text_node_id: NodeId,
32}
33
34/// Manager for keyboard focus and tab navigation
35///
36/// Note: Text cursor management is now handled by the separate `CursorManager`.
37///
38/// The `FocusManager` only tracks which node has focus, while `CursorManager`
39/// tracks the cursor position within that node (if it's contenteditable).
40///
41/// ## W3C Focus/Selection Model
42///
43/// The W3C model maintains a strict separation between **keyboard focus** and **selection**:
44///
45/// 1. **Focus** lands on the contenteditable container (`document.activeElement`)
46/// 2. **Selection/Cursor** is placed in a descendant text node (`Selection.focusNode`)
47///
48/// This separation requires a "flag and defer" pattern:
49/// - During focus event: Set `cursor_needs_initialization = true`
50/// - After layout pass: Call `finalize_pending_focus_changes()` to actually initialize the cursor
51///
52/// This is necessary because cursor positioning requires text layout information,
53/// which isn't available during the focus event handling phase.
54#[derive(Debug, Clone, PartialEq)]
55pub struct FocusManager {
56    /// Currently focused node (if any)
57    pub focused_node: Option<DomNodeId>,
58    /// Pending focus request from callback
59    pub pending_focus_request: Option<FocusTarget>,
60    
61    // --- W3C "flag and defer" pattern fields ---
62    
63    /// Flag indicating that cursor initialization is pending (set during focus, consumed after layout)
64    pub cursor_needs_initialization: bool,
65    /// Information about the pending contenteditable focus
66    pub pending_contenteditable_focus: Option<PendingContentEditableFocus>,
67}
68
69impl Default for FocusManager {
70    fn default() -> Self {
71        Self::new()
72    }
73}
74
75impl FocusManager {
76    /// Create a new focus manager
77    pub fn new() -> Self {
78        Self {
79            focused_node: None,
80            pending_focus_request: None,
81            cursor_needs_initialization: false,
82            pending_contenteditable_focus: None,
83        }
84    }
85
86    /// Get the currently focused node
87    pub fn get_focused_node(&self) -> Option<&DomNodeId> {
88        self.focused_node.as_ref()
89    }
90
91    /// Set the focused node directly (used by event system)
92    ///
93    /// Note: Cursor initialization/clearing is now handled by `CursorManager`.
94    /// The event system should check if the newly focused node is contenteditable
95    /// and call `CursorManager::initialize_cursor_at_end()` if needed.
96    pub fn set_focused_node(&mut self, node: Option<DomNodeId>) {
97        self.focused_node = node;
98    }
99
100    /// Request a focus change (to be processed by event system)
101    pub fn request_focus_change(&mut self, target: FocusTarget) {
102        self.pending_focus_request = Some(target);
103    }
104
105    /// Take the pending focus request (one-shot)
106    pub fn take_focus_request(&mut self) -> Option<FocusTarget> {
107        self.pending_focus_request.take()
108    }
109
110    /// Clear focus
111    pub fn clear_focus(&mut self) {
112        self.focused_node = None;
113    }
114
115    /// Check if a specific node has focus
116    pub fn has_focus(&self, node: &DomNodeId) -> bool {
117        self.focused_node.as_ref() == Some(node)
118    }
119    
120    // --- W3C "flag and defer" pattern methods ---
121    
122    /// Mark that cursor initialization is needed for a contenteditable element.
123    ///
124    /// This is called during focus event handling. The actual cursor initialization
125    /// happens later in `finalize_pending_focus_changes()` after layout is complete.
126    ///
127    /// # W3C Conformance
128    ///
129    /// In the W3C model, when focus lands on a contenteditable element:
130    /// 1. The focus event fires on the container element
131    /// 2. The browser's editing engine modifies the Selection to place a caret
132    /// 3. The Selection's anchorNode/focusNode point to the child text node
133    ///
134    /// Since we need layout information to position the cursor, we defer step 2+3.
135    pub fn set_pending_contenteditable_focus(
136        &mut self,
137        dom_id: DomId,
138        container_node_id: NodeId,
139        text_node_id: NodeId,
140    ) {
141        self.cursor_needs_initialization = true;
142        self.pending_contenteditable_focus = Some(PendingContentEditableFocus {
143            dom_id,
144            container_node_id,
145            text_node_id,
146        });
147    }
148    
149    /// Clear the pending contenteditable focus (when focus moves away or is cleared).
150    pub fn clear_pending_contenteditable_focus(&mut self) {
151        self.cursor_needs_initialization = false;
152        self.pending_contenteditable_focus = None;
153    }
154    
155    /// Take the pending contenteditable focus (consumes the flag).
156    ///
157    /// Returns `Some(info)` if cursor initialization is pending, `None` otherwise.
158    /// After calling this, `cursor_needs_initialization` is set to `false`.
159    pub fn take_pending_contenteditable_focus(&mut self) -> Option<PendingContentEditableFocus> {
160        if self.cursor_needs_initialization {
161            self.cursor_needs_initialization = false;
162            self.pending_contenteditable_focus.take()
163        } else {
164            None
165        }
166    }
167    
168    /// Check if cursor initialization is pending.
169    pub fn needs_cursor_initialization(&self) -> bool {
170        self.cursor_needs_initialization
171    }
172
173    /// Remap NodeIds in pending contenteditable focus after DOM reconciliation.
174    ///
175    /// This handles the edge case where a DOM rebuild happens between setting
176    /// pending focus and consuming it after layout.
177    pub fn remap_pending_focus_node_ids(
178        &mut self,
179        dom_id: DomId,
180        node_id_map: &std::collections::BTreeMap<NodeId, NodeId>,
181    ) {
182        if let Some(ref mut pending) = self.pending_contenteditable_focus {
183            if pending.dom_id != dom_id {
184                return;
185            }
186            match node_id_map.get(&pending.container_node_id) {
187                Some(&new_id) => pending.container_node_id = new_id,
188                None => {
189                    self.pending_contenteditable_focus = None;
190                    self.cursor_needs_initialization = false;
191                    return;
192                }
193            }
194            match node_id_map.get(&pending.text_node_id) {
195                Some(&new_id) => pending.text_node_id = new_id,
196                None => {
197                    self.pending_contenteditable_focus = None;
198                    self.cursor_needs_initialization = false;
199                }
200            }
201        }
202    }
203}
204
205/// Direction for cursor navigation
206#[derive(Debug, Copy, Clone, PartialEq, Eq)]
207pub enum CursorNavigationDirection {
208    /// Move cursor up one line
209    Up,
210    /// Move cursor down one line
211    Down,
212    /// Move cursor left one character
213    Left,
214    /// Move cursor right one character
215    Right,
216    /// Move cursor to start of current line
217    LineStart,
218    /// Move cursor to end of current line
219    LineEnd,
220    /// Move cursor to start of document
221    DocumentStart,
222    /// Move cursor to end of document
223    DocumentEnd,
224}
225
226/// Result of a cursor movement operation
227#[derive(Debug, Clone)]
228pub enum CursorMovementResult {
229    /// Cursor moved within the same text node
230    MovedWithinNode(azul_core::selection::TextCursor),
231    /// Cursor moved to a different text node
232    MovedToNode {
233        dom_id: DomId,
234        node_id: NodeId,
235        cursor: azul_core::selection::TextCursor,
236    },
237    /// Cursor is at a boundary and cannot move further
238    AtBoundary {
239        boundary: crate::text3::cache::TextBoundary,
240        cursor: azul_core::selection::TextCursor,
241    },
242}
243
244/// Error returned when cursor navigation cannot find a valid destination.
245///
246/// This occurs when attempting to move the cursor (e.g., arrow keys in a
247/// contenteditable element) but no valid target position exists, such as
248/// when already at the start/end of text content.
249#[derive(Debug, Clone)]
250pub struct NoCursorDestination {
251    /// Human-readable explanation of why navigation failed
252    pub reason: String,
253}
254
255/// Warning/error type for focus resolution failures.
256///
257/// Returned by `resolve_focus_target` when the requested focus target
258/// cannot be resolved to a valid focusable node.
259#[derive(Debug, Clone, PartialEq)]
260pub enum UpdateFocusWarning {
261    /// The specified DOM ID does not exist in the layout results
262    FocusInvalidDomId(DomId),
263    /// The specified node ID does not exist within its DOM
264    FocusInvalidNodeId(NodeHierarchyItemId),
265    /// CSS path selector did not match any focusable node (includes the path for debugging)
266    CouldNotFindFocusNode(String),
267}
268
269/// Direction for searching focusable nodes in the DOM tree.
270///
271/// Used by `search_focusable_node` to traverse nodes either forward
272/// (towards higher indices / next DOM) or backward (towards lower indices / previous DOM).
273#[derive(Debug, Copy, Clone, PartialEq, Eq)]
274enum SearchDirection {
275    /// Search forward: increment node index, move to next DOM when at end
276    Forward,
277    /// Search backward: decrement node index, move to previous DOM when at start
278    Backward,
279}
280
281impl SearchDirection {
282    /// Compute the next node index in this direction.
283    ///
284    /// Uses saturating arithmetic to avoid overflow/underflow.
285    fn step_node(&self, index: usize) -> usize {
286        match self {
287            Self::Forward => index.saturating_add(1),
288            Self::Backward => index.saturating_sub(1),
289        }
290    }
291
292    /// Advance the DOM ID in this direction (mutates in place).
293    fn step_dom(&self, dom_id: &mut DomId) {
294        match self {
295            Self::Forward => dom_id.inner += 1,
296            Self::Backward => dom_id.inner -= 1,
297        }
298    }
299
300    /// Check if we've hit a node boundary and need to switch DOMs.
301    ///
302    /// Returns `true` if:
303    ///
304    /// - Backward: at min node and current < start (wrapped around)
305    /// - Forward: at max node and current > start (wrapped around)
306    fn is_at_boundary(&self, current: NodeId, start: NodeId, min: NodeId, max: NodeId) -> bool {
307        match self {
308            Self::Backward => current == min && current < start,
309            Self::Forward => current == max && current > start,
310        }
311    }
312
313    /// Check if we've hit a DOM boundary (first or last DOM in the layout).
314    fn is_at_dom_boundary(&self, dom_id: DomId, min: DomId, max: DomId) -> bool {
315        match self {
316            Self::Backward => dom_id == min,
317            Self::Forward => dom_id == max,
318        }
319    }
320
321    /// Get the starting node ID when entering a new DOM.
322    ///
323    /// - Forward: start at first node (index 0)
324    /// - Backward: start at last node
325    fn initial_node_for_next_dom(&self, layout: &DomLayoutResult) -> NodeId {
326        match self {
327            Self::Forward => NodeId::ZERO,
328            Self::Backward => NodeId::new(layout.styled_dom.node_data.len() - 1),
329        }
330    }
331}
332
333/// Context for focusable node search operations.
334///
335/// Holds shared state and provides helper methods for traversing
336/// the DOM tree to find focusable nodes. This avoids passing
337/// multiple parameters through the search functions.
338struct FocusSearchContext<'a> {
339    /// Reference to all DOM layouts in the window
340    layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
341    /// First DOM ID (always `ROOT_ID`)
342    min_dom_id: DomId,
343    /// Last DOM ID in the layout results
344    max_dom_id: DomId,
345}
346
347impl<'a> FocusSearchContext<'a> {
348    /// Create a new search context from layout results.
349    fn new(layout_results: &'a BTreeMap<DomId, DomLayoutResult>) -> Self {
350        Self {
351            layout_results,
352            min_dom_id: DomId::ROOT_ID,
353            max_dom_id: DomId {
354                inner: layout_results.len() - 1,
355            },
356        }
357    }
358
359    /// Get the layout for a DOM ID, or return an error if invalid.
360    fn get_layout(&self, dom_id: &DomId) -> Result<&'a DomLayoutResult, UpdateFocusWarning> {
361        self.layout_results
362            .get(dom_id)
363            .ok_or_else(|| UpdateFocusWarning::FocusInvalidDomId(dom_id.clone()))
364    }
365
366    /// Validate that a node exists in the given layout.
367    ///
368    /// Returns an error if the node ID is out of bounds or the DOM is empty.
369    fn validate_node(
370        &self,
371        layout: &DomLayoutResult,
372        node_id: NodeId,
373        dom_id: DomId,
374    ) -> Result<(), UpdateFocusWarning> {
375        let is_valid = layout
376            .styled_dom
377            .node_data
378            .as_container()
379            .get(node_id)
380            .is_some();
381        if !is_valid {
382            return Err(UpdateFocusWarning::FocusInvalidNodeId(
383                NodeHierarchyItemId::from_crate_internal(Some(node_id)),
384            ));
385        }
386        if layout.styled_dom.node_data.is_empty() {
387            return Err(UpdateFocusWarning::FocusInvalidDomId(dom_id));
388        }
389        Ok(())
390    }
391
392    /// Get the valid node ID range for a layout: `(min, max)`.
393    fn node_bounds(&self, layout: &DomLayoutResult) -> (NodeId, NodeId) {
394        (
395            NodeId::ZERO,
396            NodeId::new(layout.styled_dom.node_data.len() - 1),
397        )
398    }
399
400    /// Check if a node can receive keyboard focus.
401    fn is_focusable(&self, layout: &DomLayoutResult, node_id: NodeId) -> bool {
402        layout.styled_dom.node_data.as_container()[node_id].is_focusable()
403    }
404
405    /// Construct a `DomNodeId` from DOM and node IDs.
406    fn make_dom_node_id(&self, dom_id: DomId, node_id: NodeId) -> DomNodeId {
407        DomNodeId {
408            dom: dom_id,
409            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
410        }
411    }
412}
413
414/// Search for the next focusable node in a given direction.
415///
416/// Traverses nodes within the current DOM, then moves to adjacent DOMs
417/// if no focusable node is found. Returns `Ok(None)` if no focusable
418/// node exists in the entire layout in the given direction.
419///
420/// # Termination guarantee
421///
422/// The function is guaranteed to terminate because:
423///
424/// - The inner loop advances `node_id` by 1 each iteration (via `step_node`)
425/// - When hitting a node boundary, we either return `None` (at DOM boundary) or move to the next
426///   DOM and break to the outer loop
427/// - The outer loop only continues when we switch DOMs, which is bounded by the finite number of
428///   DOMs in `layout_results`
429/// - Each DOM is visited at most once per search direction
430///
431/// # Returns
432///
433/// * `Ok(Some(node))` - Found a focusable node
434/// * `Ok(None)` - No focusable node exists in the search direction
435/// * `Err(_)` - Invalid DOM or node ID encountered
436fn search_focusable_node(
437    ctx: &FocusSearchContext,
438    mut dom_id: DomId,
439    mut node_id: NodeId,
440    direction: SearchDirection,
441) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
442    loop {
443        let layout = ctx.get_layout(&dom_id)?;
444        ctx.validate_node(layout, node_id, dom_id)?;
445
446        let (min_node, max_node) = ctx.node_bounds(layout);
447
448        loop {
449            let next_node = NodeId::new(direction.step_node(node_id.index()))
450                .max(min_node)
451                .min(max_node);
452
453            // If we couldn't make progress (next_node == node_id due to clamping),
454            // we've hit the boundary of this DOM
455            if next_node == node_id {
456                if direction.is_at_dom_boundary(dom_id, ctx.min_dom_id, ctx.max_dom_id) {
457                    return Ok(None); // Reached end of all DOMs
458                }
459                direction.step_dom(&mut dom_id);
460                let next_layout = ctx.get_layout(&dom_id)?;
461                node_id = direction.initial_node_for_next_dom(next_layout);
462                break; // Continue outer loop with new DOM
463            }
464
465            // Check for focusable node (we made progress, so this is a different node)
466            if ctx.is_focusable(layout, next_node) {
467                return Ok(Some(ctx.make_dom_node_id(dom_id, next_node)));
468            }
469
470            // Detect if we've hit the boundary (at min/max node)
471            let at_boundary = direction.is_at_boundary(next_node, node_id, min_node, max_node);
472
473            if at_boundary {
474                if direction.is_at_dom_boundary(dom_id, ctx.min_dom_id, ctx.max_dom_id) {
475                    return Ok(None); // Reached end of all DOMs
476                }
477                direction.step_dom(&mut dom_id);
478                let next_layout = ctx.get_layout(&dom_id)?;
479                node_id = direction.initial_node_for_next_dom(next_layout);
480                break; // Continue outer loop with new DOM
481            }
482
483            node_id = next_node;
484        }
485    }
486}
487
488/// Get starting position for Previous focus search
489fn get_previous_start(
490    layout_results: &BTreeMap<DomId, DomLayoutResult>,
491    current_focus: Option<DomNodeId>,
492) -> Result<(DomId, NodeId), UpdateFocusWarning> {
493    let last_dom_id = DomId {
494        inner: layout_results.len() - 1,
495    };
496
497    let Some(focus) = current_focus else {
498        let layout = layout_results
499            .get(&last_dom_id)
500            .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
501        return Ok((
502            last_dom_id,
503            NodeId::new(layout.styled_dom.node_data.len() - 1),
504        ));
505    };
506
507    let Some(node) = focus.node.into_crate_internal() else {
508        if let Some(layout) = layout_results.get(&focus.dom) {
509            return Ok((
510                focus.dom,
511                NodeId::new(layout.styled_dom.node_data.len() - 1),
512            ));
513        }
514        let layout = layout_results
515            .get(&last_dom_id)
516            .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
517        return Ok((
518            last_dom_id,
519            NodeId::new(layout.styled_dom.node_data.len() - 1),
520        ));
521    };
522
523    Ok((focus.dom, node))
524}
525
526/// Get starting position for Next focus search
527fn get_next_start(
528    layout_results: &BTreeMap<DomId, DomLayoutResult>,
529    current_focus: Option<DomNodeId>,
530) -> (DomId, NodeId) {
531    let Some(focus) = current_focus else {
532        return (DomId::ROOT_ID, NodeId::ZERO);
533    };
534
535    match focus.node.into_crate_internal() {
536        Some(node) => (focus.dom, node),
537        None if layout_results.contains_key(&focus.dom) => (focus.dom, NodeId::ZERO),
538        None => (DomId::ROOT_ID, NodeId::ZERO),
539    }
540}
541
542/// Get starting position for Last focus search
543fn get_last_start(
544    layout_results: &BTreeMap<DomId, DomLayoutResult>,
545) -> Result<(DomId, NodeId), UpdateFocusWarning> {
546    let last_dom_id = DomId {
547        inner: layout_results.len() - 1,
548    };
549    let layout = layout_results
550        .get(&last_dom_id)
551        .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
552    Ok((
553        last_dom_id,
554        NodeId::new(layout.styled_dom.node_data.len() - 1),
555    ))
556}
557
558/// Find the first focusable node matching a CSS path selector.
559///
560/// Iterates through all nodes in the DOM in document order (index 0..n),
561/// and returns the first node that:
562///
563/// 1. Matches the CSS path selector
564/// 2. Is focusable (has `tabindex` or is naturally focusable)
565///
566/// # Returns
567///
568/// * `Ok(Some(node))` - Found a matching focusable node
569/// * `Ok(None)` - No matching focusable node exists
570/// * `Err(_)` - CSS path could not be matched (malformed selector)
571fn find_first_matching_focusable_node(
572    layout: &DomLayoutResult,
573    dom_id: &DomId,
574    css_path: &azul_css::css::CssPath,
575) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
576    let styled_dom = &layout.styled_dom;
577    let node_hierarchy = styled_dom.node_hierarchy.as_container();
578    let node_data = styled_dom.node_data.as_container();
579    let cascade_info = styled_dom.cascade_info.as_container();
580
581    // Iterate through all nodes in document order
582    let matching_node = (0..node_data.len())
583        .map(NodeId::new)
584        .filter(|&node_id| {
585            // Check if node matches the CSS path (no pseudo-selector requirement)
586            matches_html_element(
587                css_path,
588                node_id,
589                &node_hierarchy,
590                &node_data,
591                &cascade_info,
592                None, // No expected pseudo-selector ending like :hover/:focus
593            )
594        })
595        .find(|&node_id| {
596            // Among matching nodes, find first that is focusable
597            node_data[node_id].is_focusable()
598        });
599
600    Ok(matching_node.map(|node_id| DomNodeId {
601        dom: *dom_id,
602        node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
603    }))
604}
605
606/// Resolve a FocusTarget to an actual DomNodeId
607pub fn resolve_focus_target(
608    focus_target: &FocusTarget,
609    layout_results: &BTreeMap<DomId, DomLayoutResult>,
610    current_focus: Option<DomNodeId>,
611) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
612    use azul_core::callbacks::FocusTarget::*;
613
614    if layout_results.is_empty() {
615        return Ok(None);
616    }
617
618    let ctx = FocusSearchContext::new(layout_results);
619
620    match focus_target {
621        Path(FocusTargetPath { dom, css_path }) => {
622            let layout = ctx.get_layout(dom)?;
623            find_first_matching_focusable_node(layout, dom, css_path)
624        }
625
626        Id(dom_node_id) => {
627            let layout = ctx.get_layout(&dom_node_id.dom)?;
628            let is_valid = dom_node_id
629                .node
630                .into_crate_internal()
631                .map(|n| layout.styled_dom.node_data.as_container().get(n).is_some())
632                .unwrap_or(false);
633
634            if is_valid {
635                Ok(Some(dom_node_id.clone()))
636            } else {
637                Err(UpdateFocusWarning::FocusInvalidNodeId(
638                    dom_node_id.node.clone(),
639                ))
640            }
641        }
642
643        Previous => {
644            let (dom_id, node_id) = get_previous_start(layout_results, current_focus)?;
645            let result = search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Backward)?;
646            // Wrap around: if no previous focusable found, go to last focusable
647            if result.is_none() {
648                let (last_dom_id, last_node_id) = get_last_start(layout_results)?;
649                // First check if the last node itself is focusable
650                let last_layout = ctx.get_layout(&last_dom_id)?;
651                if ctx.is_focusable(last_layout, last_node_id) {
652                    Ok(Some(ctx.make_dom_node_id(last_dom_id, last_node_id)))
653                } else {
654                    // Otherwise search backward from last node
655                    search_focusable_node(&ctx, last_dom_id, last_node_id, SearchDirection::Backward)
656                }
657            } else {
658                Ok(result)
659            }
660        }
661
662        Next => {
663            let (dom_id, node_id) = get_next_start(layout_results, current_focus);
664            let result = search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Forward)?;
665            // Wrap around: if no next focusable found, go to first focusable
666            if result.is_none() {
667                // First check if the first node itself is focusable
668                let first_layout = ctx.get_layout(&DomId::ROOT_ID)?;
669                if ctx.is_focusable(first_layout, NodeId::ZERO) {
670                    Ok(Some(ctx.make_dom_node_id(DomId::ROOT_ID, NodeId::ZERO)))
671                } else {
672                    search_focusable_node(&ctx, DomId::ROOT_ID, NodeId::ZERO, SearchDirection::Forward)
673                }
674            } else {
675                Ok(result)
676            }
677        }
678
679        First => {
680            // First check if the first node itself is focusable
681            let first_layout = ctx.get_layout(&DomId::ROOT_ID)?;
682            if ctx.is_focusable(first_layout, NodeId::ZERO) {
683                Ok(Some(ctx.make_dom_node_id(DomId::ROOT_ID, NodeId::ZERO)))
684            } else {
685                search_focusable_node(&ctx, DomId::ROOT_ID, NodeId::ZERO, SearchDirection::Forward)
686            }
687        }
688
689        Last => {
690            let (dom_id, node_id) = get_last_start(layout_results)?;
691            // First check if the last node itself is focusable
692            let last_layout = ctx.get_layout(&dom_id)?;
693            if ctx.is_focusable(last_layout, node_id) {
694                Ok(Some(ctx.make_dom_node_id(dom_id, node_id)))
695            } else {
696                search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Backward)
697            }
698        }
699
700        NoFocus => Ok(None),
701    }
702}
703
704// Trait Implementations for Event Filtering
705
706impl azul_core::events::FocusManagerQuery for FocusManager {
707    fn get_focused_node_id(&self) -> Option<azul_core::dom::DomNodeId> {
708        self.focused_node
709    }
710}