azul-layout 0.0.7

Layout solver + font and image loader the Azul GUI framework
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
//! Focus and tab navigation management.
//!
//! Manages keyboard focus, tab navigation, and programmatic focus changes
//! with a recursive event system for focus/blur callbacks (max depth: 5).

use alloc::{collections::BTreeMap, vec::Vec};

use azul_core::{
    callbacks::{FocusTarget, FocusTargetPath},
    dom::{DomId, DomNodeId, NodeId},
    style::matches_html_element,
    styled_dom::NodeHierarchyItemId,
};

use crate::window::DomLayoutResult;

/// CSS path for selecting elements (placeholder - needs proper implementation)
pub type CssPathString = alloc::string::String;

/// Information about a pending contenteditable focus that needs cursor initialization
/// after layout is complete (W3C "flag and defer" pattern).
///
/// This is set during focus event handling and consumed after layout pass.
#[derive(Debug, Clone, PartialEq)]
pub struct PendingContentEditableFocus {
    /// The DOM where the contenteditable element is
    pub dom_id: DomId,
    /// The contenteditable container node that received focus
    pub container_node_id: NodeId,
    /// The text node where the cursor should be placed (often a child of the container)
    pub text_node_id: NodeId,
}

/// Manager for keyboard focus and tab navigation
///
/// Note: Text cursor management is now handled by the separate `CursorManager`.
///
/// The `FocusManager` only tracks which node has focus, while `CursorManager`
/// tracks the cursor position within that node (if it's contenteditable).
///
/// ## W3C Focus/Selection Model
///
/// The W3C model maintains a strict separation between **keyboard focus** and **selection**:
///
/// 1. **Focus** lands on the contenteditable container (`document.activeElement`)
/// 2. **Selection/Cursor** is placed in a descendant text node (`Selection.focusNode`)
///
/// This separation requires a "flag and defer" pattern:
/// - During focus event: Set `cursor_needs_initialization = true`
/// - After layout pass: Call `finalize_pending_focus_changes()` to actually initialize the cursor
///
/// This is necessary because cursor positioning requires text layout information,
/// which isn't available during the focus event handling phase.
#[derive(Debug, Clone, PartialEq)]
pub struct FocusManager {
    /// Currently focused node (if any)
    pub focused_node: Option<DomNodeId>,
    /// Pending focus request from callback
    pub pending_focus_request: Option<FocusTarget>,
    
    // --- W3C "flag and defer" pattern fields ---
    
    /// Flag indicating that cursor initialization is pending (set during focus, consumed after layout)
    pub cursor_needs_initialization: bool,
    /// Information about the pending contenteditable focus
    pub pending_contenteditable_focus: Option<PendingContentEditableFocus>,
}

impl Default for FocusManager {
    fn default() -> Self {
        Self::new()
    }
}

impl FocusManager {
    /// Create a new focus manager
    pub fn new() -> Self {
        Self {
            focused_node: None,
            pending_focus_request: None,
            cursor_needs_initialization: false,
            pending_contenteditable_focus: None,
        }
    }

    /// Get the currently focused node
    pub fn get_focused_node(&self) -> Option<&DomNodeId> {
        self.focused_node.as_ref()
    }

    /// Set the focused node directly (used by event system)
    ///
    /// Note: Cursor initialization/clearing is now handled by `CursorManager`.
    /// The event system should check if the newly focused node is contenteditable
    /// and call `CursorManager::initialize_cursor_at_end()` if needed.
    pub fn set_focused_node(&mut self, node: Option<DomNodeId>) {
        self.focused_node = node;
    }

    /// Request a focus change (to be processed by event system)
    pub fn request_focus_change(&mut self, target: FocusTarget) {
        self.pending_focus_request = Some(target);
    }

    /// Take the pending focus request (one-shot)
    pub fn take_focus_request(&mut self) -> Option<FocusTarget> {
        self.pending_focus_request.take()
    }

    /// Clear focus
    pub fn clear_focus(&mut self) {
        self.focused_node = None;
    }

    /// Check if a specific node has focus
    pub fn has_focus(&self, node: &DomNodeId) -> bool {
        self.focused_node.as_ref() == Some(node)
    }
    
    // --- W3C "flag and defer" pattern methods ---
    
    /// Mark that cursor initialization is needed for a contenteditable element.
    ///
    /// This is called during focus event handling. The actual cursor initialization
    /// happens later in `finalize_pending_focus_changes()` after layout is complete.
    ///
    /// # W3C Conformance
    ///
    /// In the W3C model, when focus lands on a contenteditable element:
    /// 1. The focus event fires on the container element
    /// 2. The browser's editing engine modifies the Selection to place a caret
    /// 3. The Selection's anchorNode/focusNode point to the child text node
    ///
    /// Since we need layout information to position the cursor, we defer step 2+3.
    pub fn set_pending_contenteditable_focus(
        &mut self,
        dom_id: DomId,
        container_node_id: NodeId,
        text_node_id: NodeId,
    ) {
        self.cursor_needs_initialization = true;
        self.pending_contenteditable_focus = Some(PendingContentEditableFocus {
            dom_id,
            container_node_id,
            text_node_id,
        });
    }
    
    /// Clear the pending contenteditable focus (when focus moves away or is cleared).
    pub fn clear_pending_contenteditable_focus(&mut self) {
        self.cursor_needs_initialization = false;
        self.pending_contenteditable_focus = None;
    }
    
    /// Take the pending contenteditable focus (consumes the flag).
    ///
    /// Returns `Some(info)` if cursor initialization is pending, `None` otherwise.
    /// After calling this, `cursor_needs_initialization` is set to `false`.
    pub fn take_pending_contenteditable_focus(&mut self) -> Option<PendingContentEditableFocus> {
        if self.cursor_needs_initialization {
            self.cursor_needs_initialization = false;
            self.pending_contenteditable_focus.take()
        } else {
            None
        }
    }
    
    /// Check if cursor initialization is pending.
    pub fn needs_cursor_initialization(&self) -> bool {
        self.cursor_needs_initialization
    }

    /// Remap NodeIds in pending contenteditable focus after DOM reconciliation.
    ///
    /// This handles the edge case where a DOM rebuild happens between setting
    /// pending focus and consuming it after layout.
    pub fn remap_pending_focus_node_ids(
        &mut self,
        dom_id: DomId,
        node_id_map: &std::collections::BTreeMap<NodeId, NodeId>,
    ) {
        if let Some(ref mut pending) = self.pending_contenteditable_focus {
            if pending.dom_id != dom_id {
                return;
            }
            match node_id_map.get(&pending.container_node_id) {
                Some(&new_id) => pending.container_node_id = new_id,
                None => {
                    self.pending_contenteditable_focus = None;
                    self.cursor_needs_initialization = false;
                    return;
                }
            }
            match node_id_map.get(&pending.text_node_id) {
                Some(&new_id) => pending.text_node_id = new_id,
                None => {
                    self.pending_contenteditable_focus = None;
                    self.cursor_needs_initialization = false;
                }
            }
        }
    }
}

/// Direction for cursor navigation
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum CursorNavigationDirection {
    /// Move cursor up one line
    Up,
    /// Move cursor down one line
    Down,
    /// Move cursor left one character
    Left,
    /// Move cursor right one character
    Right,
    /// Move cursor to start of current line
    LineStart,
    /// Move cursor to end of current line
    LineEnd,
    /// Move cursor to start of document
    DocumentStart,
    /// Move cursor to end of document
    DocumentEnd,
}

/// Result of a cursor movement operation
#[derive(Debug, Clone)]
pub enum CursorMovementResult {
    /// Cursor moved within the same text node
    MovedWithinNode(azul_core::selection::TextCursor),
    /// Cursor moved to a different text node
    MovedToNode {
        dom_id: DomId,
        node_id: NodeId,
        cursor: azul_core::selection::TextCursor,
    },
    /// Cursor is at a boundary and cannot move further
    AtBoundary {
        boundary: crate::text3::cache::TextBoundary,
        cursor: azul_core::selection::TextCursor,
    },
}

/// Error returned when cursor navigation cannot find a valid destination.
///
/// This occurs when attempting to move the cursor (e.g., arrow keys in a
/// contenteditable element) but no valid target position exists, such as
/// when already at the start/end of text content.
#[derive(Debug, Clone)]
pub struct NoCursorDestination {
    /// Human-readable explanation of why navigation failed
    pub reason: String,
}

/// Warning/error type for focus resolution failures.
///
/// Returned by `resolve_focus_target` when the requested focus target
/// cannot be resolved to a valid focusable node.
#[derive(Debug, Clone, PartialEq)]
pub enum UpdateFocusWarning {
    /// The specified DOM ID does not exist in the layout results
    FocusInvalidDomId(DomId),
    /// The specified node ID does not exist within its DOM
    FocusInvalidNodeId(NodeHierarchyItemId),
    /// CSS path selector did not match any focusable node (includes the path for debugging)
    CouldNotFindFocusNode(String),
}

/// Direction for searching focusable nodes in the DOM tree.
///
/// Used by `search_focusable_node` to traverse nodes either forward
/// (towards higher indices / next DOM) or backward (towards lower indices / previous DOM).
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum SearchDirection {
    /// Search forward: increment node index, move to next DOM when at end
    Forward,
    /// Search backward: decrement node index, move to previous DOM when at start
    Backward,
}

impl SearchDirection {
    /// Compute the next node index in this direction.
    ///
    /// Uses saturating arithmetic to avoid overflow/underflow.
    fn step_node(&self, index: usize) -> usize {
        match self {
            Self::Forward => index.saturating_add(1),
            Self::Backward => index.saturating_sub(1),
        }
    }

    /// Advance the DOM ID in this direction (mutates in place).
    fn step_dom(&self, dom_id: &mut DomId) {
        match self {
            Self::Forward => dom_id.inner += 1,
            Self::Backward => dom_id.inner -= 1,
        }
    }

    /// Check if we've hit a node boundary and need to switch DOMs.
    ///
    /// Returns `true` if:
    ///
    /// - Backward: at min node and current < start (wrapped around)
    /// - Forward: at max node and current > start (wrapped around)
    fn is_at_boundary(&self, current: NodeId, start: NodeId, min: NodeId, max: NodeId) -> bool {
        match self {
            Self::Backward => current == min && current < start,
            Self::Forward => current == max && current > start,
        }
    }

    /// Check if we've hit a DOM boundary (first or last DOM in the layout).
    fn is_at_dom_boundary(&self, dom_id: DomId, min: DomId, max: DomId) -> bool {
        match self {
            Self::Backward => dom_id == min,
            Self::Forward => dom_id == max,
        }
    }

    /// Get the starting node ID when entering a new DOM.
    ///
    /// - Forward: start at first node (index 0)
    /// - Backward: start at last node
    fn initial_node_for_next_dom(&self, layout: &DomLayoutResult) -> NodeId {
        match self {
            Self::Forward => NodeId::ZERO,
            Self::Backward => NodeId::new(layout.styled_dom.node_data.len() - 1),
        }
    }
}

/// Context for focusable node search operations.
///
/// Holds shared state and provides helper methods for traversing
/// the DOM tree to find focusable nodes. This avoids passing
/// multiple parameters through the search functions.
struct FocusSearchContext<'a> {
    /// Reference to all DOM layouts in the window
    layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
    /// First DOM ID (always `ROOT_ID`)
    min_dom_id: DomId,
    /// Last DOM ID in the layout results
    max_dom_id: DomId,
}

impl<'a> FocusSearchContext<'a> {
    /// Create a new search context from layout results.
    fn new(layout_results: &'a BTreeMap<DomId, DomLayoutResult>) -> Self {
        Self {
            layout_results,
            min_dom_id: DomId::ROOT_ID,
            max_dom_id: DomId {
                inner: layout_results.len() - 1,
            },
        }
    }

    /// Get the layout for a DOM ID, or return an error if invalid.
    fn get_layout(&self, dom_id: &DomId) -> Result<&'a DomLayoutResult, UpdateFocusWarning> {
        self.layout_results
            .get(dom_id)
            .ok_or_else(|| UpdateFocusWarning::FocusInvalidDomId(dom_id.clone()))
    }

    /// Validate that a node exists in the given layout.
    ///
    /// Returns an error if the node ID is out of bounds or the DOM is empty.
    fn validate_node(
        &self,
        layout: &DomLayoutResult,
        node_id: NodeId,
        dom_id: DomId,
    ) -> Result<(), UpdateFocusWarning> {
        let is_valid = layout
            .styled_dom
            .node_data
            .as_container()
            .get(node_id)
            .is_some();
        if !is_valid {
            return Err(UpdateFocusWarning::FocusInvalidNodeId(
                NodeHierarchyItemId::from_crate_internal(Some(node_id)),
            ));
        }
        if layout.styled_dom.node_data.is_empty() {
            return Err(UpdateFocusWarning::FocusInvalidDomId(dom_id));
        }
        Ok(())
    }

    /// Get the valid node ID range for a layout: `(min, max)`.
    fn node_bounds(&self, layout: &DomLayoutResult) -> (NodeId, NodeId) {
        (
            NodeId::ZERO,
            NodeId::new(layout.styled_dom.node_data.len() - 1),
        )
    }

    /// Check if a node can receive keyboard focus.
    fn is_focusable(&self, layout: &DomLayoutResult, node_id: NodeId) -> bool {
        layout.styled_dom.node_data.as_container()[node_id].is_focusable()
    }

    /// Construct a `DomNodeId` from DOM and node IDs.
    fn make_dom_node_id(&self, dom_id: DomId, node_id: NodeId) -> DomNodeId {
        DomNodeId {
            dom: dom_id,
            node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
        }
    }
}

/// Search for the next focusable node in a given direction.
///
/// Traverses nodes within the current DOM, then moves to adjacent DOMs
/// if no focusable node is found. Returns `Ok(None)` if no focusable
/// node exists in the entire layout in the given direction.
///
/// # Termination guarantee
///
/// The function is guaranteed to terminate because:
///
/// - The inner loop advances `node_id` by 1 each iteration (via `step_node`)
/// - When hitting a node boundary, we either return `None` (at DOM boundary) or move to the next
///   DOM and break to the outer loop
/// - The outer loop only continues when we switch DOMs, which is bounded by the finite number of
///   DOMs in `layout_results`
/// - Each DOM is visited at most once per search direction
///
/// # Returns
///
/// * `Ok(Some(node))` - Found a focusable node
/// * `Ok(None)` - No focusable node exists in the search direction
/// * `Err(_)` - Invalid DOM or node ID encountered
fn search_focusable_node(
    ctx: &FocusSearchContext,
    mut dom_id: DomId,
    mut node_id: NodeId,
    direction: SearchDirection,
) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
    loop {
        let layout = ctx.get_layout(&dom_id)?;
        ctx.validate_node(layout, node_id, dom_id)?;

        let (min_node, max_node) = ctx.node_bounds(layout);

        loop {
            let next_node = NodeId::new(direction.step_node(node_id.index()))
                .max(min_node)
                .min(max_node);

            // If we couldn't make progress (next_node == node_id due to clamping),
            // we've hit the boundary of this DOM
            if next_node == node_id {
                if direction.is_at_dom_boundary(dom_id, ctx.min_dom_id, ctx.max_dom_id) {
                    return Ok(None); // Reached end of all DOMs
                }
                direction.step_dom(&mut dom_id);
                let next_layout = ctx.get_layout(&dom_id)?;
                node_id = direction.initial_node_for_next_dom(next_layout);
                break; // Continue outer loop with new DOM
            }

            // Check for focusable node (we made progress, so this is a different node)
            if ctx.is_focusable(layout, next_node) {
                return Ok(Some(ctx.make_dom_node_id(dom_id, next_node)));
            }

            // Detect if we've hit the boundary (at min/max node)
            let at_boundary = direction.is_at_boundary(next_node, node_id, min_node, max_node);

            if at_boundary {
                if direction.is_at_dom_boundary(dom_id, ctx.min_dom_id, ctx.max_dom_id) {
                    return Ok(None); // Reached end of all DOMs
                }
                direction.step_dom(&mut dom_id);
                let next_layout = ctx.get_layout(&dom_id)?;
                node_id = direction.initial_node_for_next_dom(next_layout);
                break; // Continue outer loop with new DOM
            }

            node_id = next_node;
        }
    }
}

/// Get starting position for Previous focus search
fn get_previous_start(
    layout_results: &BTreeMap<DomId, DomLayoutResult>,
    current_focus: Option<DomNodeId>,
) -> Result<(DomId, NodeId), UpdateFocusWarning> {
    let last_dom_id = DomId {
        inner: layout_results.len() - 1,
    };

    let Some(focus) = current_focus else {
        let layout = layout_results
            .get(&last_dom_id)
            .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
        return Ok((
            last_dom_id,
            NodeId::new(layout.styled_dom.node_data.len() - 1),
        ));
    };

    let Some(node) = focus.node.into_crate_internal() else {
        if let Some(layout) = layout_results.get(&focus.dom) {
            return Ok((
                focus.dom,
                NodeId::new(layout.styled_dom.node_data.len() - 1),
            ));
        }
        let layout = layout_results
            .get(&last_dom_id)
            .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
        return Ok((
            last_dom_id,
            NodeId::new(layout.styled_dom.node_data.len() - 1),
        ));
    };

    Ok((focus.dom, node))
}

/// Get starting position for Next focus search
fn get_next_start(
    layout_results: &BTreeMap<DomId, DomLayoutResult>,
    current_focus: Option<DomNodeId>,
) -> (DomId, NodeId) {
    let Some(focus) = current_focus else {
        return (DomId::ROOT_ID, NodeId::ZERO);
    };

    match focus.node.into_crate_internal() {
        Some(node) => (focus.dom, node),
        None if layout_results.contains_key(&focus.dom) => (focus.dom, NodeId::ZERO),
        None => (DomId::ROOT_ID, NodeId::ZERO),
    }
}

/// Get starting position for Last focus search
fn get_last_start(
    layout_results: &BTreeMap<DomId, DomLayoutResult>,
) -> Result<(DomId, NodeId), UpdateFocusWarning> {
    let last_dom_id = DomId {
        inner: layout_results.len() - 1,
    };
    let layout = layout_results
        .get(&last_dom_id)
        .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
    Ok((
        last_dom_id,
        NodeId::new(layout.styled_dom.node_data.len() - 1),
    ))
}

/// Find the first focusable node matching a CSS path selector.
///
/// Iterates through all nodes in the DOM in document order (index 0..n),
/// and returns the first node that:
///
/// 1. Matches the CSS path selector
/// 2. Is focusable (has `tabindex` or is naturally focusable)
///
/// # Returns
///
/// * `Ok(Some(node))` - Found a matching focusable node
/// * `Ok(None)` - No matching focusable node exists
/// * `Err(_)` - CSS path could not be matched (malformed selector)
fn find_first_matching_focusable_node(
    layout: &DomLayoutResult,
    dom_id: &DomId,
    css_path: &azul_css::css::CssPath,
) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
    let styled_dom = &layout.styled_dom;
    let node_hierarchy = styled_dom.node_hierarchy.as_container();
    let node_data = styled_dom.node_data.as_container();
    let cascade_info = styled_dom.cascade_info.as_container();

    // Iterate through all nodes in document order
    let matching_node = (0..node_data.len())
        .map(NodeId::new)
        .filter(|&node_id| {
            // Check if node matches the CSS path (no pseudo-selector requirement)
            matches_html_element(
                css_path,
                node_id,
                &node_hierarchy,
                &node_data,
                &cascade_info,
                None, // No expected pseudo-selector ending like :hover/:focus
            )
        })
        .find(|&node_id| {
            // Among matching nodes, find first that is focusable
            node_data[node_id].is_focusable()
        });

    Ok(matching_node.map(|node_id| DomNodeId {
        dom: *dom_id,
        node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
    }))
}

/// Resolve a FocusTarget to an actual DomNodeId
pub fn resolve_focus_target(
    focus_target: &FocusTarget,
    layout_results: &BTreeMap<DomId, DomLayoutResult>,
    current_focus: Option<DomNodeId>,
) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
    use azul_core::callbacks::FocusTarget::*;

    if layout_results.is_empty() {
        return Ok(None);
    }

    let ctx = FocusSearchContext::new(layout_results);

    match focus_target {
        Path(FocusTargetPath { dom, css_path }) => {
            let layout = ctx.get_layout(dom)?;
            find_first_matching_focusable_node(layout, dom, css_path)
        }

        Id(dom_node_id) => {
            let layout = ctx.get_layout(&dom_node_id.dom)?;
            let is_valid = dom_node_id
                .node
                .into_crate_internal()
                .map(|n| layout.styled_dom.node_data.as_container().get(n).is_some())
                .unwrap_or(false);

            if is_valid {
                Ok(Some(dom_node_id.clone()))
            } else {
                Err(UpdateFocusWarning::FocusInvalidNodeId(
                    dom_node_id.node.clone(),
                ))
            }
        }

        Previous => {
            let (dom_id, node_id) = get_previous_start(layout_results, current_focus)?;
            let result = search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Backward)?;
            // Wrap around: if no previous focusable found, go to last focusable
            if result.is_none() {
                let (last_dom_id, last_node_id) = get_last_start(layout_results)?;
                // First check if the last node itself is focusable
                let last_layout = ctx.get_layout(&last_dom_id)?;
                if ctx.is_focusable(last_layout, last_node_id) {
                    Ok(Some(ctx.make_dom_node_id(last_dom_id, last_node_id)))
                } else {
                    // Otherwise search backward from last node
                    search_focusable_node(&ctx, last_dom_id, last_node_id, SearchDirection::Backward)
                }
            } else {
                Ok(result)
            }
        }

        Next => {
            let (dom_id, node_id) = get_next_start(layout_results, current_focus);
            let result = search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Forward)?;
            // Wrap around: if no next focusable found, go to first focusable
            if result.is_none() {
                // First check if the first node itself is focusable
                let first_layout = ctx.get_layout(&DomId::ROOT_ID)?;
                if ctx.is_focusable(first_layout, NodeId::ZERO) {
                    Ok(Some(ctx.make_dom_node_id(DomId::ROOT_ID, NodeId::ZERO)))
                } else {
                    search_focusable_node(&ctx, DomId::ROOT_ID, NodeId::ZERO, SearchDirection::Forward)
                }
            } else {
                Ok(result)
            }
        }

        First => {
            // First check if the first node itself is focusable
            let first_layout = ctx.get_layout(&DomId::ROOT_ID)?;
            if ctx.is_focusable(first_layout, NodeId::ZERO) {
                Ok(Some(ctx.make_dom_node_id(DomId::ROOT_ID, NodeId::ZERO)))
            } else {
                search_focusable_node(&ctx, DomId::ROOT_ID, NodeId::ZERO, SearchDirection::Forward)
            }
        }

        Last => {
            let (dom_id, node_id) = get_last_start(layout_results)?;
            // First check if the last node itself is focusable
            let last_layout = ctx.get_layout(&dom_id)?;
            if ctx.is_focusable(last_layout, node_id) {
                Ok(Some(ctx.make_dom_node_id(dom_id, node_id)))
            } else {
                search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Backward)
            }
        }

        NoFocus => Ok(None),
    }
}

// Trait Implementations for Event Filtering

impl azul_core::events::FocusManagerQuery for FocusManager {
    fn get_focused_node_id(&self) -> Option<azul_core::dom::DomNodeId> {
        self.focused_node
    }
}