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