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;
pub type CssPathString = alloc::string::String;
#[derive(Debug, Clone, PartialEq)]
pub struct PendingContentEditableFocus {
pub dom_id: DomId,
pub container_node_id: NodeId,
pub text_node_id: NodeId,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FocusManager {
pub focused_node: Option<DomNodeId>,
pub pending_focus_request: Option<FocusTarget>,
pub cursor_needs_initialization: bool,
pub pending_contenteditable_focus: Option<PendingContentEditableFocus>,
}
impl Default for FocusManager {
fn default() -> Self {
Self::new()
}
}
impl FocusManager {
pub fn new() -> Self {
Self {
focused_node: None,
pending_focus_request: None,
cursor_needs_initialization: false,
pending_contenteditable_focus: None,
}
}
pub fn get_focused_node(&self) -> Option<&DomNodeId> {
self.focused_node.as_ref()
}
pub fn set_focused_node(&mut self, node: Option<DomNodeId>) {
self.focused_node = node;
}
pub fn request_focus_change(&mut self, target: FocusTarget) {
self.pending_focus_request = Some(target);
}
pub fn take_focus_request(&mut self) -> Option<FocusTarget> {
self.pending_focus_request.take()
}
pub fn clear_focus(&mut self) {
self.focused_node = None;
}
pub fn has_focus(&self, node: &DomNodeId) -> bool {
self.focused_node.as_ref() == Some(node)
}
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,
});
}
pub fn clear_pending_contenteditable_focus(&mut self) {
self.cursor_needs_initialization = false;
self.pending_contenteditable_focus = None;
}
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
}
}
pub fn needs_cursor_initialization(&self) -> bool {
self.cursor_needs_initialization
}
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;
}
}
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum CursorNavigationDirection {
Up,
Down,
Left,
Right,
LineStart,
LineEnd,
DocumentStart,
DocumentEnd,
}
#[derive(Debug, Clone)]
pub enum CursorMovementResult {
MovedWithinNode(azul_core::selection::TextCursor),
MovedToNode {
dom_id: DomId,
node_id: NodeId,
cursor: azul_core::selection::TextCursor,
},
AtBoundary {
boundary: crate::text3::cache::TextBoundary,
cursor: azul_core::selection::TextCursor,
},
}
#[derive(Debug, Clone)]
pub struct NoCursorDestination {
pub reason: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum UpdateFocusWarning {
FocusInvalidDomId(DomId),
FocusInvalidNodeId(NodeHierarchyItemId),
CouldNotFindFocusNode(String),
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
enum SearchDirection {
Forward,
Backward,
}
impl SearchDirection {
fn step_node(&self, index: usize) -> usize {
match self {
Self::Forward => index.saturating_add(1),
Self::Backward => index.saturating_sub(1),
}
}
fn step_dom(&self, dom_id: &mut DomId) {
match self {
Self::Forward => dom_id.inner += 1,
Self::Backward => dom_id.inner -= 1,
}
}
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,
}
}
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,
}
}
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),
}
}
}
struct FocusSearchContext<'a> {
layout_results: &'a BTreeMap<DomId, DomLayoutResult>,
min_dom_id: DomId,
max_dom_id: DomId,
}
impl<'a> FocusSearchContext<'a> {
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,
},
}
}
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()))
}
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(())
}
fn node_bounds(&self, layout: &DomLayoutResult) -> (NodeId, NodeId) {
(
NodeId::ZERO,
NodeId::new(layout.styled_dom.node_data.len() - 1),
)
}
fn is_focusable(&self, layout: &DomLayoutResult, node_id: NodeId) -> bool {
layout.styled_dom.node_data.as_container()[node_id].is_focusable()
}
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)),
}
}
}
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 next_node == node_id {
if direction.is_at_dom_boundary(dom_id, ctx.min_dom_id, ctx.max_dom_id) {
return Ok(None); }
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; }
if ctx.is_focusable(layout, next_node) {
return Ok(Some(ctx.make_dom_node_id(dom_id, next_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); }
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; }
node_id = next_node;
}
}
}
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))
}
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),
}
}
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),
))
}
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();
let matching_node = (0..node_data.len())
.map(NodeId::new)
.filter(|&node_id| {
matches_html_element(
css_path,
node_id,
&node_hierarchy,
&node_data,
&cascade_info,
None, )
})
.find(|&node_id| {
node_data[node_id].is_focusable()
});
Ok(matching_node.map(|node_id| DomNodeId {
dom: *dom_id,
node: NodeHierarchyItemId::from_crate_internal(Some(node_id)),
}))
}
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)?;
if result.is_none() {
let (last_dom_id, last_node_id) = get_last_start(layout_results)?;
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 {
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)?;
if result.is_none() {
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 => {
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)?;
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),
}
}
impl azul_core::events::FocusManagerQuery for FocusManager {
fn get_focused_node_id(&self) -> Option<azul_core::dom::DomNodeId> {
self.focused_node
}
}