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

            
6
use alloc::collections::BTreeMap;
7

            
8
use azul_core::{
9
    callbacks::{FocusTarget, FocusTargetPath},
10
    dom::{DomId, DomNodeId, NodeId},
11
    style::matches_html_element,
12
    styled_dom::NodeHierarchyItemId,
13
};
14

            
15
use 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)]
22
pub 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)]
52
pub 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

            
66
impl Default for FocusManager {
67
    fn default() -> Self {
68
        Self::new()
69
    }
70
}
71

            
72
impl FocusManager {
73
    /// Create a new focus manager
74
2577
    pub fn new() -> Self {
75
2577
        Self {
76
2577
            focused_node: None,
77
2577
            pending_focus_request: None,
78
2577
            cursor_needs_initialization: false,
79
2577
            pending_contenteditable_focus: None,
80
2577
        }
81
2577
    }
82

            
83
    /// Get the currently focused node
84
4841
    pub fn get_focused_node(&self) -> Option<&DomNodeId> {
85
4841
        self.focused_node.as_ref()
86
4841
    }
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
1260
    pub fn set_focused_node(&mut self, node: Option<DomNodeId>) {
94
1260
        self.focused_node = node;
95
1260
    }
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)]
204
pub 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)]
225
pub 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)]
247
pub 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)]
257
pub 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)]
271
enum 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

            
278
impl 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.
335
struct 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

            
344
impl<'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
430
fn 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
483
fn 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
521
fn 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
537
fn 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)
565
fn 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
601
pub 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

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