1use 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#[derive(Debug, Clone, PartialEq)]
22pub struct PendingContentEditableFocus {
23 pub dom_id: DomId,
25 pub container_node_id: NodeId,
27 pub text_node_id: NodeId,
29}
30
31#[derive(Debug, Clone, PartialEq)]
52pub struct FocusManager {
53 pub focused_node: Option<DomNodeId>,
55 pub pending_focus_request: Option<FocusTarget>,
57
58 pub cursor_needs_initialization: bool,
62 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 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 pub fn get_focused_node(&self) -> Option<&DomNodeId> {
85 self.focused_node.as_ref()
86 }
87
88 pub fn set_focused_node(&mut self, node: Option<DomNodeId>) {
94 self.focused_node = node;
95 }
96
97 pub fn request_focus_change(&mut self, target: FocusTarget) {
99 self.pending_focus_request = Some(target);
100 }
101
102 pub fn take_focus_request(&mut self) -> Option<FocusTarget> {
104 self.pending_focus_request.take()
105 }
106
107 pub fn clear_focus(&mut self) {
109 self.focused_node = None;
110 }
111
112 pub fn has_focus(&self, node: &DomNodeId) -> bool {
114 self.focused_node.as_ref() == Some(node)
115 }
116
117 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 pub fn clear_pending_contenteditable_focus(&mut self) {
148 self.cursor_needs_initialization = false;
149 self.pending_contenteditable_focus = None;
150 }
151
152 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 pub fn needs_cursor_initialization(&self) -> bool {
167 self.cursor_needs_initialization
168 }
169
170 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#[derive(Debug, Copy, Clone, PartialEq, Eq)]
204pub enum CursorNavigationDirection {
205 Up,
207 Down,
209 Left,
211 Right,
213 LineStart,
215 LineEnd,
217 DocumentStart,
219 DocumentEnd,
221}
222
223#[derive(Debug, Clone)]
225pub enum CursorMovementResult {
226 MovedWithinNode(azul_core::selection::TextCursor),
228 MovedToNode {
230 dom_id: DomId,
231 node_id: NodeId,
232 cursor: azul_core::selection::TextCursor,
233 },
234 AtBoundary {
236 boundary: crate::text3::cache::TextBoundary,
237 cursor: azul_core::selection::TextCursor,
238 },
239}
240
241#[derive(Debug, Clone)]
247pub struct NoCursorDestination {
248 pub reason: String,
250}
251
252#[derive(Debug, Clone, PartialEq)]
257pub enum UpdateFocusWarning {
258 FocusInvalidDomId(DomId),
260 FocusInvalidNodeId(NodeHierarchyItemId),
262 CouldNotFindFocusNode(String),
264}
265
266#[derive(Debug, Copy, Clone, PartialEq, Eq)]
271enum SearchDirection {
272 Forward,
274 Backward,
276}
277
278impl SearchDirection {
279 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 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 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 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 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
330struct FocusSearchContext<'a> {
336 layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
338 min_dom_id: DomId,
340 max_dom_id: DomId,
342}
343
344impl<'a> FocusSearchContext<'a> {
345 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 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 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 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 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 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
408fn 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 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); }
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; }
458
459 if ctx.is_focusable(layout, next_node) {
461 return Ok(Some(ctx.make_dom_node_id(dom_id, next_node)));
462 }
463
464 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); }
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; }
476
477 node_id = next_node;
478 }
479 }
480}
481
482fn 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
520fn 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
536fn 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
552fn 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 let matching_node = (0..node_data.len())
577 .map(NodeId::new)
578 .filter(|&node_id| {
579 matches_html_element(
581 css_path,
582 node_id,
583 &node_hierarchy,
584 &node_data,
585 &cascade_info,
586 None, )
588 })
589 .find(|&node_id| {
590 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
600pub 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 if result.is_none() {
642 let (last_dom_id, last_node_id) = get_last_start(layout_results)?;
643 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 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 if result.is_none() {
661 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 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 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
698impl azul_core::events::FocusManagerQuery for FocusManager {
701 fn get_focused_node_id(&self) -> Option<azul_core::dom::DomNodeId> {
702 self.focused_node
703 }
704}