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
174/// Direction for cursor navigation
175#[derive(Debug, Copy, Clone, PartialEq, Eq)]
176pub enum CursorNavigationDirection {
177    /// Move cursor up one line
178    Up,
179    /// Move cursor down one line
180    Down,
181    /// Move cursor left one character
182    Left,
183    /// Move cursor right one character
184    Right,
185    /// Move cursor to start of current line
186    LineStart,
187    /// Move cursor to end of current line
188    LineEnd,
189    /// Move cursor to start of document
190    DocumentStart,
191    /// Move cursor to end of document
192    DocumentEnd,
193}
194
195/// Result of a cursor movement operation
196#[derive(Debug, Clone)]
197pub enum CursorMovementResult {
198    /// Cursor moved within the same text node
199    MovedWithinNode(azul_core::selection::TextCursor),
200    /// Cursor moved to a different text node
201    MovedToNode {
202        dom_id: DomId,
203        node_id: NodeId,
204        cursor: azul_core::selection::TextCursor,
205    },
206    /// Cursor is at a boundary and cannot move further
207    AtBoundary {
208        boundary: crate::text3::cache::TextBoundary,
209        cursor: azul_core::selection::TextCursor,
210    },
211}
212
213/// Error returned when cursor navigation cannot find a valid destination.
214///
215/// This occurs when attempting to move the cursor (e.g., arrow keys in a
216/// contenteditable element) but no valid target position exists, such as
217/// when already at the start/end of text content.
218#[derive(Debug, Clone)]
219pub struct NoCursorDestination {
220    /// Human-readable explanation of why navigation failed
221    pub reason: String,
222}
223
224/// Warning/error type for focus resolution failures.
225///
226/// Returned by `resolve_focus_target` when the requested focus target
227/// cannot be resolved to a valid focusable node.
228#[derive(Debug, Clone, PartialEq)]
229pub enum UpdateFocusWarning {
230    /// The specified DOM ID does not exist in the layout results
231    FocusInvalidDomId(DomId),
232    /// The specified node ID does not exist within its DOM
233    FocusInvalidNodeId(NodeHierarchyItemId),
234    /// CSS path selector did not match any focusable node (includes the path for debugging)
235    CouldNotFindFocusNode(String),
236}
237
238/// Direction for searching focusable nodes in the DOM tree.
239///
240/// Used by `search_focusable_node` to traverse nodes either forward
241/// (towards higher indices / next DOM) or backward (towards lower indices / previous DOM).
242#[derive(Debug, Copy, Clone, PartialEq, Eq)]
243enum SearchDirection {
244    /// Search forward: increment node index, move to next DOM when at end
245    Forward,
246    /// Search backward: decrement node index, move to previous DOM when at start
247    Backward,
248}
249
250impl SearchDirection {
251    /// Compute the next node index in this direction.
252    ///
253    /// Uses saturating arithmetic to avoid overflow/underflow.
254    fn step_node(&self, index: usize) -> usize {
255        match self {
256            Self::Forward => index.saturating_add(1),
257            Self::Backward => index.saturating_sub(1),
258        }
259    }
260
261    /// Advance the DOM ID in this direction (mutates in place).
262    fn step_dom(&self, dom_id: &mut DomId) {
263        match self {
264            Self::Forward => dom_id.inner += 1,
265            Self::Backward => dom_id.inner -= 1,
266        }
267    }
268
269    /// Check if we've hit a node boundary and need to switch DOMs.
270    ///
271    /// Returns `true` if:
272    ///
273    /// - Backward: at min node and current < start (wrapped around)
274    /// - Forward: at max node and current > start (wrapped around)
275    fn is_at_boundary(&self, current: NodeId, start: NodeId, min: NodeId, max: NodeId) -> bool {
276        match self {
277            Self::Backward => current == min && current < start,
278            Self::Forward => current == max && current > start,
279        }
280    }
281
282    /// Check if we've hit a DOM boundary (first or last DOM in the layout).
283    fn is_at_dom_boundary(&self, dom_id: DomId, min: DomId, max: DomId) -> bool {
284        match self {
285            Self::Backward => dom_id == min,
286            Self::Forward => dom_id == max,
287        }
288    }
289
290    /// Get the starting node ID when entering a new DOM.
291    ///
292    /// - Forward: start at first node (index 0)
293    /// - Backward: start at last node
294    fn initial_node_for_next_dom(&self, layout: &DomLayoutResult) -> NodeId {
295        match self {
296            Self::Forward => NodeId::ZERO,
297            Self::Backward => NodeId::new(layout.styled_dom.node_data.len() - 1),
298        }
299    }
300}
301
302/// Context for focusable node search operations.
303///
304/// Holds shared state and provides helper methods for traversing
305/// the DOM tree to find focusable nodes. This avoids passing
306/// multiple parameters through the search functions.
307struct FocusSearchContext<'a> {
308    /// Reference to all DOM layouts in the window
309    layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
310    /// First DOM ID (always `ROOT_ID`)
311    min_dom_id: DomId,
312    /// Last DOM ID in the layout results
313    max_dom_id: DomId,
314}
315
316impl<'a> FocusSearchContext<'a> {
317    /// Create a new search context from layout results.
318    fn new(layout_results: &'a BTreeMap<DomId, DomLayoutResult>) -> Self {
319        Self {
320            layout_results,
321            min_dom_id: DomId::ROOT_ID,
322            max_dom_id: DomId {
323                inner: layout_results.len() - 1,
324            },
325        }
326    }
327
328    /// Get the layout for a DOM ID, or return an error if invalid.
329    fn get_layout(&self, dom_id: &DomId) -> Result<&'a DomLayoutResult, UpdateFocusWarning> {
330        self.layout_results
331            .get(dom_id)
332            .ok_or_else(|| UpdateFocusWarning::FocusInvalidDomId(dom_id.clone()))
333    }
334
335    /// Validate that a node exists in the given layout.
336    ///
337    /// Returns an error if the node ID is out of bounds or the DOM is empty.
338    fn validate_node(
339        &self,
340        layout: &DomLayoutResult,
341        node_id: NodeId,
342        dom_id: DomId,
343    ) -> Result<(), UpdateFocusWarning> {
344        let is_valid = layout
345            .styled_dom
346            .node_data
347            .as_container()
348            .get(node_id)
349            .is_some();
350        if !is_valid {
351            return Err(UpdateFocusWarning::FocusInvalidNodeId(
352                NodeHierarchyItemId::from_crate_internal(Some(node_id)),
353            ));
354        }
355        if layout.styled_dom.node_data.is_empty() {
356            return Err(UpdateFocusWarning::FocusInvalidDomId(dom_id));
357        }
358        Ok(())
359    }
360
361    /// Get the valid node ID range for a layout: `(min, max)`.
362    fn node_bounds(&self, layout: &DomLayoutResult) -> (NodeId, NodeId) {
363        (
364            NodeId::ZERO,
365            NodeId::new(layout.styled_dom.node_data.len() - 1),
366        )
367    }
368
369    /// Check if a node can receive keyboard focus.
370    fn is_focusable(&self, layout: &DomLayoutResult, node_id: NodeId) -> bool {
371        layout.styled_dom.node_data.as_container()[node_id].is_focusable()
372    }
373
374    /// Construct a `DomNodeId` from DOM and node IDs.
375    fn make_dom_node_id(&self, dom_id: DomId, node_id: NodeId) -> DomNodeId {
376        DomNodeId {
377            dom: dom_id,
378            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
379        }
380    }
381}
382
383/// Search for the next focusable node in a given direction.
384///
385/// Traverses nodes within the current DOM, then moves to adjacent DOMs
386/// if no focusable node is found. Returns `Ok(None)` if no focusable
387/// node exists in the entire layout in the given direction.
388///
389/// # Termination guarantee
390///
391/// The function is guaranteed to terminate because:
392///
393/// - The inner loop advances `node_id` by 1 each iteration (via `step_node`)
394/// - When hitting a node boundary, we either return `None` (at DOM boundary) or move to the next
395///   DOM and break to the outer loop
396/// - The outer loop only continues when we switch DOMs, which is bounded by the finite number of
397///   DOMs in `layout_results`
398/// - Each DOM is visited at most once per search direction
399///
400/// # Returns
401///
402/// * `Ok(Some(node))` - Found a focusable node
403/// * `Ok(None)` - No focusable node exists in the search direction
404/// * `Err(_)` - Invalid DOM or node ID encountered
405fn search_focusable_node(
406    ctx: &FocusSearchContext,
407    mut dom_id: DomId,
408    mut node_id: NodeId,
409    direction: SearchDirection,
410) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
411    loop {
412        let layout = ctx.get_layout(&dom_id)?;
413        ctx.validate_node(layout, node_id, dom_id)?;
414
415        let (min_node, max_node) = ctx.node_bounds(layout);
416
417        loop {
418            let next_node = NodeId::new(direction.step_node(node_id.index()))
419                .max(min_node)
420                .min(max_node);
421
422            // If we couldn't make progress (next_node == node_id due to clamping),
423            // we've hit the boundary of this DOM
424            if next_node == node_id {
425                if direction.is_at_dom_boundary(dom_id, ctx.min_dom_id, ctx.max_dom_id) {
426                    return Ok(None); // Reached end of all DOMs
427                }
428                direction.step_dom(&mut dom_id);
429                let next_layout = ctx.get_layout(&dom_id)?;
430                node_id = direction.initial_node_for_next_dom(next_layout);
431                break; // Continue outer loop with new DOM
432            }
433
434            // Check for focusable node (we made progress, so this is a different node)
435            if ctx.is_focusable(layout, next_node) {
436                return Ok(Some(ctx.make_dom_node_id(dom_id, next_node)));
437            }
438
439            // Detect if we've hit the boundary (at min/max node)
440            let at_boundary = direction.is_at_boundary(next_node, node_id, min_node, max_node);
441
442            if at_boundary {
443                if direction.is_at_dom_boundary(dom_id, ctx.min_dom_id, ctx.max_dom_id) {
444                    return Ok(None); // Reached end of all DOMs
445                }
446                direction.step_dom(&mut dom_id);
447                let next_layout = ctx.get_layout(&dom_id)?;
448                node_id = direction.initial_node_for_next_dom(next_layout);
449                break; // Continue outer loop with new DOM
450            }
451
452            node_id = next_node;
453        }
454    }
455}
456
457/// Get starting position for Previous focus search
458fn get_previous_start(
459    layout_results: &BTreeMap<DomId, DomLayoutResult>,
460    current_focus: Option<DomNodeId>,
461) -> Result<(DomId, NodeId), UpdateFocusWarning> {
462    let last_dom_id = DomId {
463        inner: layout_results.len() - 1,
464    };
465
466    let Some(focus) = current_focus else {
467        let layout = layout_results
468            .get(&last_dom_id)
469            .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
470        return Ok((
471            last_dom_id,
472            NodeId::new(layout.styled_dom.node_data.len() - 1),
473        ));
474    };
475
476    let Some(node) = focus.node.into_crate_internal() else {
477        if let Some(layout) = layout_results.get(&focus.dom) {
478            return Ok((
479                focus.dom,
480                NodeId::new(layout.styled_dom.node_data.len() - 1),
481            ));
482        }
483        let layout = layout_results
484            .get(&last_dom_id)
485            .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
486        return Ok((
487            last_dom_id,
488            NodeId::new(layout.styled_dom.node_data.len() - 1),
489        ));
490    };
491
492    Ok((focus.dom, node))
493}
494
495/// Get starting position for Next focus search
496fn get_next_start(
497    layout_results: &BTreeMap<DomId, DomLayoutResult>,
498    current_focus: Option<DomNodeId>,
499) -> (DomId, NodeId) {
500    let Some(focus) = current_focus else {
501        return (DomId::ROOT_ID, NodeId::ZERO);
502    };
503
504    match focus.node.into_crate_internal() {
505        Some(node) => (focus.dom, node),
506        None if layout_results.contains_key(&focus.dom) => (focus.dom, NodeId::ZERO),
507        None => (DomId::ROOT_ID, NodeId::ZERO),
508    }
509}
510
511/// Get starting position for Last focus search
512fn get_last_start(
513    layout_results: &BTreeMap<DomId, DomLayoutResult>,
514) -> Result<(DomId, NodeId), UpdateFocusWarning> {
515    let last_dom_id = DomId {
516        inner: layout_results.len() - 1,
517    };
518    let layout = layout_results
519        .get(&last_dom_id)
520        .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
521    Ok((
522        last_dom_id,
523        NodeId::new(layout.styled_dom.node_data.len() - 1),
524    ))
525}
526
527/// Find the first focusable node matching a CSS path selector.
528///
529/// Iterates through all nodes in the DOM in document order (index 0..n),
530/// and returns the first node that:
531///
532/// 1. Matches the CSS path selector
533/// 2. Is focusable (has `tabindex` or is naturally focusable)
534///
535/// # Returns
536///
537/// * `Ok(Some(node))` - Found a matching focusable node
538/// * `Ok(None)` - No matching focusable node exists
539/// * `Err(_)` - CSS path could not be matched (malformed selector)
540fn find_first_matching_focusable_node(
541    layout: &DomLayoutResult,
542    dom_id: &DomId,
543    css_path: &azul_css::css::CssPath,
544) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
545    let styled_dom = &layout.styled_dom;
546    let node_hierarchy = styled_dom.node_hierarchy.as_container();
547    let node_data = styled_dom.node_data.as_container();
548    let cascade_info = styled_dom.cascade_info.as_container();
549
550    // Iterate through all nodes in document order
551    let matching_node = (0..node_data.len())
552        .map(NodeId::new)
553        .filter(|&node_id| {
554            // Check if node matches the CSS path (no pseudo-selector requirement)
555            matches_html_element(
556                css_path,
557                node_id,
558                &node_hierarchy,
559                &node_data,
560                &cascade_info,
561                None, // No expected pseudo-selector ending like :hover/:focus
562            )
563        })
564        .find(|&node_id| {
565            // Among matching nodes, find first that is focusable
566            node_data[node_id].is_focusable()
567        });
568
569    Ok(matching_node.map(|node_id| DomNodeId {
570        dom: *dom_id,
571        node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
572    }))
573}
574
575/// Resolve a FocusTarget to an actual DomNodeId
576pub fn resolve_focus_target(
577    focus_target: &FocusTarget,
578    layout_results: &BTreeMap<DomId, DomLayoutResult>,
579    current_focus: Option<DomNodeId>,
580) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
581    use azul_core::callbacks::FocusTarget::*;
582
583    if layout_results.is_empty() {
584        return Ok(None);
585    }
586
587    let ctx = FocusSearchContext::new(layout_results);
588
589    match focus_target {
590        Path(FocusTargetPath { dom, css_path }) => {
591            let layout = ctx.get_layout(dom)?;
592            find_first_matching_focusable_node(layout, dom, css_path)
593        }
594
595        Id(dom_node_id) => {
596            let layout = ctx.get_layout(&dom_node_id.dom)?;
597            let is_valid = dom_node_id
598                .node
599                .into_crate_internal()
600                .map(|n| layout.styled_dom.node_data.as_container().get(n).is_some())
601                .unwrap_or(false);
602
603            if is_valid {
604                Ok(Some(dom_node_id.clone()))
605            } else {
606                Err(UpdateFocusWarning::FocusInvalidNodeId(
607                    dom_node_id.node.clone(),
608                ))
609            }
610        }
611
612        Previous => {
613            let (dom_id, node_id) = get_previous_start(layout_results, current_focus)?;
614            let result = search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Backward)?;
615            // Wrap around: if no previous focusable found, go to last focusable
616            if result.is_none() {
617                let (last_dom_id, last_node_id) = get_last_start(layout_results)?;
618                // First check if the last node itself is focusable
619                let last_layout = ctx.get_layout(&last_dom_id)?;
620                if ctx.is_focusable(last_layout, last_node_id) {
621                    Ok(Some(ctx.make_dom_node_id(last_dom_id, last_node_id)))
622                } else {
623                    // Otherwise search backward from last node
624                    search_focusable_node(&ctx, last_dom_id, last_node_id, SearchDirection::Backward)
625                }
626            } else {
627                Ok(result)
628            }
629        }
630
631        Next => {
632            let (dom_id, node_id) = get_next_start(layout_results, current_focus);
633            let result = search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Forward)?;
634            // Wrap around: if no next focusable found, go to first focusable
635            if result.is_none() {
636                // First check if the first node itself is focusable
637                let first_layout = ctx.get_layout(&DomId::ROOT_ID)?;
638                if ctx.is_focusable(first_layout, NodeId::ZERO) {
639                    Ok(Some(ctx.make_dom_node_id(DomId::ROOT_ID, NodeId::ZERO)))
640                } else {
641                    search_focusable_node(&ctx, DomId::ROOT_ID, NodeId::ZERO, SearchDirection::Forward)
642                }
643            } else {
644                Ok(result)
645            }
646        }
647
648        First => {
649            // First check if the first node itself is focusable
650            let first_layout = ctx.get_layout(&DomId::ROOT_ID)?;
651            if ctx.is_focusable(first_layout, NodeId::ZERO) {
652                Ok(Some(ctx.make_dom_node_id(DomId::ROOT_ID, NodeId::ZERO)))
653            } else {
654                search_focusable_node(&ctx, DomId::ROOT_ID, NodeId::ZERO, SearchDirection::Forward)
655            }
656        }
657
658        Last => {
659            let (dom_id, node_id) = get_last_start(layout_results)?;
660            // First check if the last node itself is focusable
661            let last_layout = ctx.get_layout(&dom_id)?;
662            if ctx.is_focusable(last_layout, node_id) {
663                Ok(Some(ctx.make_dom_node_id(dom_id, node_id)))
664            } else {
665                search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Backward)
666            }
667        }
668
669        NoFocus => Ok(None),
670    }
671}
672
673// Trait Implementations for Event Filtering
674
675impl azul_core::events::FocusManagerQuery for FocusManager {
676    fn get_focused_node_id(&self) -> Option<azul_core::dom::DomNodeId> {
677        self.focused_node
678    }
679}