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 pub fn remap_pending_focus_node_ids(
178 &mut self,
179 dom_id: DomId,
180 node_id_map: &std::collections::BTreeMap<NodeId, NodeId>,
181 ) {
182 if let Some(ref mut pending) = self.pending_contenteditable_focus {
183 if pending.dom_id != dom_id {
184 return;
185 }
186 match node_id_map.get(&pending.container_node_id) {
187 Some(&new_id) => pending.container_node_id = new_id,
188 None => {
189 self.pending_contenteditable_focus = None;
190 self.cursor_needs_initialization = false;
191 return;
192 }
193 }
194 match node_id_map.get(&pending.text_node_id) {
195 Some(&new_id) => pending.text_node_id = new_id,
196 None => {
197 self.pending_contenteditable_focus = None;
198 self.cursor_needs_initialization = false;
199 }
200 }
201 }
202 }
203}
204
205#[derive(Debug, Copy, Clone, PartialEq, Eq)]
207pub enum CursorNavigationDirection {
208 Up,
210 Down,
212 Left,
214 Right,
216 LineStart,
218 LineEnd,
220 DocumentStart,
222 DocumentEnd,
224}
225
226#[derive(Debug, Clone)]
228pub enum CursorMovementResult {
229 MovedWithinNode(azul_core::selection::TextCursor),
231 MovedToNode {
233 dom_id: DomId,
234 node_id: NodeId,
235 cursor: azul_core::selection::TextCursor,
236 },
237 AtBoundary {
239 boundary: crate::text3::cache::TextBoundary,
240 cursor: azul_core::selection::TextCursor,
241 },
242}
243
244#[derive(Debug, Clone)]
250pub struct NoCursorDestination {
251 pub reason: String,
253}
254
255#[derive(Debug, Clone, PartialEq)]
260pub enum UpdateFocusWarning {
261 FocusInvalidDomId(DomId),
263 FocusInvalidNodeId(NodeHierarchyItemId),
265 CouldNotFindFocusNode(String),
267}
268
269#[derive(Debug, Copy, Clone, PartialEq, Eq)]
274enum SearchDirection {
275 Forward,
277 Backward,
279}
280
281impl SearchDirection {
282 fn step_node(&self, index: usize) -> usize {
286 match self {
287 Self::Forward => index.saturating_add(1),
288 Self::Backward => index.saturating_sub(1),
289 }
290 }
291
292 fn step_dom(&self, dom_id: &mut DomId) {
294 match self {
295 Self::Forward => dom_id.inner += 1,
296 Self::Backward => dom_id.inner -= 1,
297 }
298 }
299
300 fn is_at_boundary(&self, current: NodeId, start: NodeId, min: NodeId, max: NodeId) -> bool {
307 match self {
308 Self::Backward => current == min && current < start,
309 Self::Forward => current == max && current > start,
310 }
311 }
312
313 fn is_at_dom_boundary(&self, dom_id: DomId, min: DomId, max: DomId) -> bool {
315 match self {
316 Self::Backward => dom_id == min,
317 Self::Forward => dom_id == max,
318 }
319 }
320
321 fn initial_node_for_next_dom(&self, layout: &DomLayoutResult) -> NodeId {
326 match self {
327 Self::Forward => NodeId::ZERO,
328 Self::Backward => NodeId::new(layout.styled_dom.node_data.len() - 1),
329 }
330 }
331}
332
333struct FocusSearchContext<'a> {
339 layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
341 min_dom_id: DomId,
343 max_dom_id: DomId,
345}
346
347impl<'a> FocusSearchContext<'a> {
348 fn new(layout_results: &'a BTreeMap<DomId, DomLayoutResult>) -> Self {
350 Self {
351 layout_results,
352 min_dom_id: DomId::ROOT_ID,
353 max_dom_id: DomId {
354 inner: layout_results.len() - 1,
355 },
356 }
357 }
358
359 fn get_layout(&self, dom_id: &DomId) -> Result<&'a DomLayoutResult, UpdateFocusWarning> {
361 self.layout_results
362 .get(dom_id)
363 .ok_or_else(|| UpdateFocusWarning::FocusInvalidDomId(dom_id.clone()))
364 }
365
366 fn validate_node(
370 &self,
371 layout: &DomLayoutResult,
372 node_id: NodeId,
373 dom_id: DomId,
374 ) -> Result<(), UpdateFocusWarning> {
375 let is_valid = layout
376 .styled_dom
377 .node_data
378 .as_container()
379 .get(node_id)
380 .is_some();
381 if !is_valid {
382 return Err(UpdateFocusWarning::FocusInvalidNodeId(
383 NodeHierarchyItemId::from_crate_internal(Some(node_id)),
384 ));
385 }
386 if layout.styled_dom.node_data.is_empty() {
387 return Err(UpdateFocusWarning::FocusInvalidDomId(dom_id));
388 }
389 Ok(())
390 }
391
392 fn node_bounds(&self, layout: &DomLayoutResult) -> (NodeId, NodeId) {
394 (
395 NodeId::ZERO,
396 NodeId::new(layout.styled_dom.node_data.len() - 1),
397 )
398 }
399
400 fn is_focusable(&self, layout: &DomLayoutResult, node_id: NodeId) -> bool {
402 layout.styled_dom.node_data.as_container()[node_id].is_focusable()
403 }
404
405 fn make_dom_node_id(&self, dom_id: DomId, node_id: NodeId) -> DomNodeId {
407 DomNodeId {
408 dom: dom_id,
409 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
410 }
411 }
412}
413
414fn search_focusable_node(
437 ctx: &FocusSearchContext,
438 mut dom_id: DomId,
439 mut node_id: NodeId,
440 direction: SearchDirection,
441) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
442 loop {
443 let layout = ctx.get_layout(&dom_id)?;
444 ctx.validate_node(layout, node_id, dom_id)?;
445
446 let (min_node, max_node) = ctx.node_bounds(layout);
447
448 loop {
449 let next_node = NodeId::new(direction.step_node(node_id.index()))
450 .max(min_node)
451 .min(max_node);
452
453 if next_node == node_id {
456 if direction.is_at_dom_boundary(dom_id, ctx.min_dom_id, ctx.max_dom_id) {
457 return Ok(None); }
459 direction.step_dom(&mut dom_id);
460 let next_layout = ctx.get_layout(&dom_id)?;
461 node_id = direction.initial_node_for_next_dom(next_layout);
462 break; }
464
465 if ctx.is_focusable(layout, next_node) {
467 return Ok(Some(ctx.make_dom_node_id(dom_id, next_node)));
468 }
469
470 let at_boundary = direction.is_at_boundary(next_node, node_id, min_node, max_node);
472
473 if at_boundary {
474 if direction.is_at_dom_boundary(dom_id, ctx.min_dom_id, ctx.max_dom_id) {
475 return Ok(None); }
477 direction.step_dom(&mut dom_id);
478 let next_layout = ctx.get_layout(&dom_id)?;
479 node_id = direction.initial_node_for_next_dom(next_layout);
480 break; }
482
483 node_id = next_node;
484 }
485 }
486}
487
488fn get_previous_start(
490 layout_results: &BTreeMap<DomId, DomLayoutResult>,
491 current_focus: Option<DomNodeId>,
492) -> Result<(DomId, NodeId), UpdateFocusWarning> {
493 let last_dom_id = DomId {
494 inner: layout_results.len() - 1,
495 };
496
497 let Some(focus) = current_focus else {
498 let layout = layout_results
499 .get(&last_dom_id)
500 .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
501 return Ok((
502 last_dom_id,
503 NodeId::new(layout.styled_dom.node_data.len() - 1),
504 ));
505 };
506
507 let Some(node) = focus.node.into_crate_internal() else {
508 if let Some(layout) = layout_results.get(&focus.dom) {
509 return Ok((
510 focus.dom,
511 NodeId::new(layout.styled_dom.node_data.len() - 1),
512 ));
513 }
514 let layout = layout_results
515 .get(&last_dom_id)
516 .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
517 return Ok((
518 last_dom_id,
519 NodeId::new(layout.styled_dom.node_data.len() - 1),
520 ));
521 };
522
523 Ok((focus.dom, node))
524}
525
526fn get_next_start(
528 layout_results: &BTreeMap<DomId, DomLayoutResult>,
529 current_focus: Option<DomNodeId>,
530) -> (DomId, NodeId) {
531 let Some(focus) = current_focus else {
532 return (DomId::ROOT_ID, NodeId::ZERO);
533 };
534
535 match focus.node.into_crate_internal() {
536 Some(node) => (focus.dom, node),
537 None if layout_results.contains_key(&focus.dom) => (focus.dom, NodeId::ZERO),
538 None => (DomId::ROOT_ID, NodeId::ZERO),
539 }
540}
541
542fn get_last_start(
544 layout_results: &BTreeMap<DomId, DomLayoutResult>,
545) -> Result<(DomId, NodeId), UpdateFocusWarning> {
546 let last_dom_id = DomId {
547 inner: layout_results.len() - 1,
548 };
549 let layout = layout_results
550 .get(&last_dom_id)
551 .ok_or(UpdateFocusWarning::FocusInvalidDomId(last_dom_id))?;
552 Ok((
553 last_dom_id,
554 NodeId::new(layout.styled_dom.node_data.len() - 1),
555 ))
556}
557
558fn find_first_matching_focusable_node(
572 layout: &DomLayoutResult,
573 dom_id: &DomId,
574 css_path: &azul_css::css::CssPath,
575) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
576 let styled_dom = &layout.styled_dom;
577 let node_hierarchy = styled_dom.node_hierarchy.as_container();
578 let node_data = styled_dom.node_data.as_container();
579 let cascade_info = styled_dom.cascade_info.as_container();
580
581 let matching_node = (0..node_data.len())
583 .map(NodeId::new)
584 .filter(|&node_id| {
585 matches_html_element(
587 css_path,
588 node_id,
589 &node_hierarchy,
590 &node_data,
591 &cascade_info,
592 None, )
594 })
595 .find(|&node_id| {
596 node_data[node_id].is_focusable()
598 });
599
600 Ok(matching_node.map(|node_id| DomNodeId {
601 dom: *dom_id,
602 node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
603 }))
604}
605
606pub fn resolve_focus_target(
608 focus_target: &FocusTarget,
609 layout_results: &BTreeMap<DomId, DomLayoutResult>,
610 current_focus: Option<DomNodeId>,
611) -> Result<Option<DomNodeId>, UpdateFocusWarning> {
612 use azul_core::callbacks::FocusTarget::*;
613
614 if layout_results.is_empty() {
615 return Ok(None);
616 }
617
618 let ctx = FocusSearchContext::new(layout_results);
619
620 match focus_target {
621 Path(FocusTargetPath { dom, css_path }) => {
622 let layout = ctx.get_layout(dom)?;
623 find_first_matching_focusable_node(layout, dom, css_path)
624 }
625
626 Id(dom_node_id) => {
627 let layout = ctx.get_layout(&dom_node_id.dom)?;
628 let is_valid = dom_node_id
629 .node
630 .into_crate_internal()
631 .map(|n| layout.styled_dom.node_data.as_container().get(n).is_some())
632 .unwrap_or(false);
633
634 if is_valid {
635 Ok(Some(dom_node_id.clone()))
636 } else {
637 Err(UpdateFocusWarning::FocusInvalidNodeId(
638 dom_node_id.node.clone(),
639 ))
640 }
641 }
642
643 Previous => {
644 let (dom_id, node_id) = get_previous_start(layout_results, current_focus)?;
645 let result = search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Backward)?;
646 if result.is_none() {
648 let (last_dom_id, last_node_id) = get_last_start(layout_results)?;
649 let last_layout = ctx.get_layout(&last_dom_id)?;
651 if ctx.is_focusable(last_layout, last_node_id) {
652 Ok(Some(ctx.make_dom_node_id(last_dom_id, last_node_id)))
653 } else {
654 search_focusable_node(&ctx, last_dom_id, last_node_id, SearchDirection::Backward)
656 }
657 } else {
658 Ok(result)
659 }
660 }
661
662 Next => {
663 let (dom_id, node_id) = get_next_start(layout_results, current_focus);
664 let result = search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Forward)?;
665 if result.is_none() {
667 let first_layout = ctx.get_layout(&DomId::ROOT_ID)?;
669 if ctx.is_focusable(first_layout, NodeId::ZERO) {
670 Ok(Some(ctx.make_dom_node_id(DomId::ROOT_ID, NodeId::ZERO)))
671 } else {
672 search_focusable_node(&ctx, DomId::ROOT_ID, NodeId::ZERO, SearchDirection::Forward)
673 }
674 } else {
675 Ok(result)
676 }
677 }
678
679 First => {
680 let first_layout = ctx.get_layout(&DomId::ROOT_ID)?;
682 if ctx.is_focusable(first_layout, NodeId::ZERO) {
683 Ok(Some(ctx.make_dom_node_id(DomId::ROOT_ID, NodeId::ZERO)))
684 } else {
685 search_focusable_node(&ctx, DomId::ROOT_ID, NodeId::ZERO, SearchDirection::Forward)
686 }
687 }
688
689 Last => {
690 let (dom_id, node_id) = get_last_start(layout_results)?;
691 let last_layout = ctx.get_layout(&dom_id)?;
693 if ctx.is_focusable(last_layout, node_id) {
694 Ok(Some(ctx.make_dom_node_id(dom_id, node_id)))
695 } else {
696 search_focusable_node(&ctx, dom_id, node_id, SearchDirection::Backward)
697 }
698 }
699
700 NoFocus => Ok(None),
701 }
702}
703
704impl azul_core::events::FocusManagerQuery for FocusManager {
707 fn get_focused_node_id(&self) -> Option<azul_core::dom::DomNodeId> {
708 self.focused_node
709 }
710}