1use 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
17pub type CssPathString = alloc::string::String;
19
20#[derive(Debug, Clone, PartialEq)]
25pub struct PendingContentEditableFocus {
26 pub dom_id: DomId,
28 pub container_node_id: NodeId,
30 pub text_node_id: NodeId,
32}
33
34#[derive(Debug, Clone, PartialEq)]
55pub struct FocusManager {
56 pub focused_node: Option<DomNodeId>,
58 pub pending_focus_request: Option<FocusTarget>,
60
61 pub cursor_needs_initialization: bool,
65 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 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 pub fn get_focused_node(&self) -> Option<&DomNodeId> {
88 self.focused_node.as_ref()
89 }
90
91 pub fn set_focused_node(&mut self, node: Option<DomNodeId>) {
97 self.focused_node = node;
98 }
99
100 pub fn request_focus_change(&mut self, target: FocusTarget) {
102 self.pending_focus_request = Some(target);
103 }
104
105 pub fn take_focus_request(&mut self) -> Option<FocusTarget> {
107 self.pending_focus_request.take()
108 }
109
110 pub fn clear_focus(&mut self) {
112 self.focused_node = None;
113 }
114
115 pub fn has_focus(&self, node: &DomNodeId) -> bool {
117 self.focused_node.as_ref() == Some(node)
118 }
119
120 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 pub fn clear_pending_contenteditable_focus(&mut self) {
151 self.cursor_needs_initialization = false;
152 self.pending_contenteditable_focus = None;
153 }
154
155 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 pub fn needs_cursor_initialization(&self) -> bool {
170 self.cursor_needs_initialization
171 }
172}
173
174#[derive(Debug, Copy, Clone, PartialEq, Eq)]
176pub enum CursorNavigationDirection {
177 Up,
179 Down,
181 Left,
183 Right,
185 LineStart,
187 LineEnd,
189 DocumentStart,
191 DocumentEnd,
193}
194
195#[derive(Debug, Clone)]
197pub enum CursorMovementResult {
198 MovedWithinNode(azul_core::selection::TextCursor),
200 MovedToNode {
202 dom_id: DomId,
203 node_id: NodeId,
204 cursor: azul_core::selection::TextCursor,
205 },
206 AtBoundary {
208 boundary: crate::text3::cache::TextBoundary,
209 cursor: azul_core::selection::TextCursor,
210 },
211}
212
213#[derive(Debug, Clone)]
219pub struct NoCursorDestination {
220 pub reason: String,
222}
223
224#[derive(Debug, Clone, PartialEq)]
229pub enum UpdateFocusWarning {
230 FocusInvalidDomId(DomId),
232 FocusInvalidNodeId(NodeHierarchyItemId),
234 CouldNotFindFocusNode(String),
236}
237
238#[derive(Debug, Copy, Clone, PartialEq, Eq)]
243enum SearchDirection {
244 Forward,
246 Backward,
248}
249
250impl SearchDirection {
251 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 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 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 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 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
302struct FocusSearchContext<'a> {
308 layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
310 min_dom_id: DomId,
312 max_dom_id: DomId,
314}
315
316impl<'a> FocusSearchContext<'a> {
317 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 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 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 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 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 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
383fn 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 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); }
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; }
433
434 if ctx.is_focusable(layout, next_node) {
436 return Ok(Some(ctx.make_dom_node_id(dom_id, next_node)));
437 }
438
439 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); }
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; }
451
452 node_id = next_node;
453 }
454 }
455}
456
457fn 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
495fn 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
511fn 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
527fn 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 let matching_node = (0..node_data.len())
552 .map(NodeId::new)
553 .filter(|&node_id| {
554 matches_html_element(
556 css_path,
557 node_id,
558 &node_hierarchy,
559 &node_data,
560 &cascade_info,
561 None, )
563 })
564 .find(|&node_id| {
565 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
575pub 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 if result.is_none() {
617 let (last_dom_id, last_node_id) = get_last_start(layout_results)?;
618 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 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 if result.is_none() {
636 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 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 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
673impl azul_core::events::FocusManagerQuery for FocusManager {
676 fn get_focused_node_id(&self) -> Option<azul_core::dom::DomNodeId> {
677 self.focused_node
678 }
679}