use std::sync::{
Arc,
Mutex,
};
use accesskit::{
Action,
Affine,
Node,
NodeBuilder,
NodeId as AccessibilityId,
Rect,
Role,
TextDirection,
Tree,
TreeUpdate,
};
use freya_common::AccessibilityDirtyNodes;
use freya_engine::prelude::{
Color,
Slant,
TextAlign,
TextDecoration,
TextDecorationStyle,
};
use freya_native_core::{
node::NodeType,
prelude::NodeImmutable,
tags::TagName,
NodeId,
};
use freya_node_state::{
AccessibilityNodeState,
Fill,
FontStyleState,
OverflowMode,
StyleState,
TransformState,
};
use rustc_hash::{
FxHashMap,
FxHashSet,
};
use torin::{
prelude::LayoutNode,
torin::Torin,
};
use super::{
AccessibilityFocusStrategy,
NodeAccessibility,
};
use crate::dom::{
DioxusDOM,
DioxusNode,
};
pub const ACCESSIBILITY_ROOT_ID: AccessibilityId = AccessibilityId(0);
pub type SharedAccessibilityTree = Arc<Mutex<AccessibilityTree>>;
pub struct AccessibilityTree {
pub map: FxHashMap<AccessibilityId, NodeId>,
pub focused_id: AccessibilityId,
}
impl AccessibilityTree {
pub fn new(focused_id: AccessibilityId) -> Self {
Self {
focused_id,
map: FxHashMap::default(),
}
}
pub fn focused_node_id(&self) -> Option<NodeId> {
self.map.get(&self.focused_id).cloned()
}
pub fn init(
&self,
rdom: &DioxusDOM,
layout: &Torin<NodeId>,
dirty_nodes: &mut AccessibilityDirtyNodes,
) -> TreeUpdate {
dirty_nodes.clear();
let mut nodes = vec![];
rdom.traverse_depth_first_advanced(|node_ref| {
if !node_ref.node_type().is_element() {
return false;
}
let accessibility_id = node_ref.get_accessibility_id();
let layout_node = layout.get(node_ref.id());
if let Some((accessibility_id, layout_node)) = accessibility_id.zip(layout_node) {
let node_accessibility_state = node_ref.get::<AccessibilityNodeState>().unwrap();
let accessibility_node =
Self::create_node(&node_ref, layout_node, &node_accessibility_state);
nodes.push((accessibility_id, accessibility_node));
}
if let Some(tag) = node_ref.node_type().tag() {
if *tag == TagName::Paragraph || *tag == TagName::Label {
return false;
}
}
true
});
#[cfg(debug_assertions)]
tracing::info!(
"Initialized the Accessibility Tree with {} nodes.",
nodes.len()
);
TreeUpdate {
nodes,
tree: Some(Tree::new(ACCESSIBILITY_ROOT_ID)),
focus: ACCESSIBILITY_ROOT_ID,
}
}
pub fn process_updates(
&mut self,
rdom: &DioxusDOM,
layout: &Torin<NodeId>,
dirty_nodes: &mut AccessibilityDirtyNodes,
) -> (TreeUpdate, NodeId) {
let requested_focus_id = dirty_nodes.requested_focus.take();
let removed_ids = dirty_nodes.removed.drain().collect::<FxHashMap<_, _>>();
let mut added_or_updated_ids = dirty_nodes
.added_or_updated
.drain()
.collect::<FxHashSet<_>>();
#[cfg(debug_assertions)]
if !removed_ids.is_empty() || !added_or_updated_ids.is_empty() {
tracing::info!(
"Updating the Accessibility Tree with {} removals and {} additions/modifications",
removed_ids.len(),
added_or_updated_ids.len()
);
}
for node_id in added_or_updated_ids.clone() {
let node_ref = rdom.get(node_id).unwrap();
let node_accessibility_state = node_ref.get::<AccessibilityNodeState>().unwrap();
added_or_updated_ids.insert(
node_accessibility_state
.closest_accessibility_node_id
.unwrap_or(rdom.root_id()),
);
self.map
.insert(node_ref.get_accessibility_id().unwrap(), node_id);
}
for (node_id, ancestor_node_id) in removed_ids {
added_or_updated_ids.insert(ancestor_node_id);
self.map.retain(|_, id| *id != node_id);
}
let mut nodes = Vec::new();
for node_id in added_or_updated_ids {
let node_ref = rdom.get(node_id).unwrap();
let node_accessibility_state = node_ref.get::<AccessibilityNodeState>();
let layout_node = layout.get(node_id);
if let Some((node_accessibility_state, layout_node)) =
node_accessibility_state.as_ref().zip(layout_node)
{
let accessibility_node =
Self::create_node(&node_ref, layout_node, node_accessibility_state);
let accessibility_id = node_ref.get_accessibility_id().unwrap();
nodes.push((accessibility_id, accessibility_node));
}
}
if let Some(node_id) = requested_focus_id {
let node_ref = rdom.get(node_id).unwrap();
let node_accessibility_state = node_ref.get::<AccessibilityNodeState>();
if let Some(focused_id) = node_accessibility_state
.as_ref()
.and_then(|state| state.a11y_id)
{
self.focused_id = focused_id;
}
}
if !self.map.contains_key(&self.focused_id) {
self.focused_id = ACCESSIBILITY_ROOT_ID;
}
let node_id = self.map.get(&self.focused_id).cloned().unwrap();
(
TreeUpdate {
nodes,
tree: Some(Tree::new(ACCESSIBILITY_ROOT_ID)),
focus: self.focused_id,
},
node_id,
)
}
pub fn set_focus_with_update(
&mut self,
new_focus_id: AccessibilityId,
) -> Option<(TreeUpdate, NodeId)> {
self.focused_id = new_focus_id;
if let Some(node_id) = self.map.get(&new_focus_id).copied() {
#[cfg(debug_assertions)]
tracing::info!("Focused {new_focus_id:?} node.");
Some((
TreeUpdate {
nodes: Vec::new(),
tree: Some(Tree::new(ACCESSIBILITY_ROOT_ID)),
focus: self.focused_id,
},
node_id,
))
} else {
None
}
}
pub fn set_focus_on_next_node(
&mut self,
stragegy: AccessibilityFocusStrategy,
rdom: &DioxusDOM,
) -> (TreeUpdate, NodeId) {
let mut nodes = Vec::new();
rdom.traverse_depth_first_advanced(|node_ref| {
if !node_ref.node_type().is_element() {
return false;
}
let accessibility_id = node_ref.get_accessibility_id();
if let Some(accessibility_id) = accessibility_id {
let accessibility_state = node_ref.get::<AccessibilityNodeState>().unwrap();
if accessibility_state.a11y_focusable.is_enabled() {
nodes.push((accessibility_id, node_ref.id()))
}
}
if let Some(tag) = node_ref.node_type().tag() {
if *tag == TagName::Paragraph || *tag == TagName::Label {
return false;
}
}
true
});
let node_index = nodes
.iter()
.enumerate()
.find(|(_, (accessibility_id, _))| *accessibility_id == self.focused_id)
.map(|(i, _)| i);
let target_node = if stragegy == AccessibilityFocusStrategy::Forward {
if let Some(node_index) = node_index {
if node_index == nodes.len() - 1 {
nodes.first()
} else {
nodes.get(node_index + 1)
}
} else {
nodes.first()
}
} else {
if let Some(node_index) = node_index {
if node_index == 0 {
nodes.last()
} else {
nodes.get(node_index - 1)
}
} else {
nodes.last()
}
};
let (accessibility_id, node_id) = target_node
.copied()
.unwrap_or((ACCESSIBILITY_ROOT_ID, rdom.root_id()));
self.focused_id = accessibility_id;
#[cfg(debug_assertions)]
tracing::info!("Focused {accessibility_id:?} node.");
(
TreeUpdate {
nodes: Vec::new(),
tree: Some(Tree::new(ACCESSIBILITY_ROOT_ID)),
focus: self.focused_id,
},
node_id,
)
}
pub fn create_node(
node_ref: &DioxusNode,
layout_node: &LayoutNode,
node_accessibility: &AccessibilityNodeState,
) -> Node {
let font_style_state = &*node_ref.get::<FontStyleState>().unwrap();
let style_state = &*node_ref.get::<StyleState>().unwrap();
let transform_state = &*node_ref.get::<TransformState>().unwrap();
let node_type = node_ref.node_type();
let mut builder = NodeBuilder::new(Role::default());
let children = node_ref.get_accessibility_children();
builder.set_children(children);
let area = layout_node.area.to_f64();
builder.set_bounds(Rect {
x0: area.min_x(),
x1: area.max_x(),
y0: area.min_y(),
y1: area.max_y(),
});
if node_accessibility.a11y_focusable.is_enabled() {
builder.add_action(Action::Focus);
}
if let Some((_, rotation)) = transform_state
.rotations
.iter()
.find(|(id, _)| id == &node_ref.id())
{
builder.set_transform(Affine::rotate(rotation.to_radians() as _));
}
if style_state.overflow == OverflowMode::Clip {
builder.set_clips_children();
}
builder.set_foreground_color(skia_color_to_rgba_u32(font_style_state.color));
if let Fill::Color(color) = style_state.background {
builder.set_background_color(skia_color_to_rgba_u32(color));
}
if !node_type.is_text() {
if let NodeType::Element(node) = &*node_type {
if node.tag != TagName::Text {
builder.set_is_line_breaking_object();
}
}
}
builder.set_font_size(font_style_state.font_size as _);
if let Some(parent_node) = node_ref.parent() {
if parent_node.get::<FontStyleState>().unwrap().font_family
!= font_style_state.font_family
{
builder.set_font_family(font_style_state.font_family.join(", "));
}
} else {
builder.set_font_family(font_style_state.font_family.join(", "));
}
if font_style_state.font_weight > 700.into() {
builder.set_bold();
}
builder.set_text_align(match font_style_state.text_align {
TextAlign::Center => accesskit::TextAlign::Center,
TextAlign::Justify => accesskit::TextAlign::Justify,
TextAlign::Left | TextAlign::Start => accesskit::TextAlign::Left,
TextAlign::Right | TextAlign::End => accesskit::TextAlign::Right,
});
builder.set_text_direction(TextDirection::LeftToRight);
match font_style_state.font_slant {
Slant::Italic | Slant::Oblique => builder.set_italic(),
_ => {}
}
if font_style_state
.decoration
.ty
.contains(TextDecoration::LINE_THROUGH)
{
builder.set_strikethrough(skia_decoration_style_to_accesskit(
font_style_state.decoration.style,
));
}
if font_style_state
.decoration
.ty
.contains(TextDecoration::UNDERLINE)
{
builder.set_underline(skia_decoration_style_to_accesskit(
font_style_state.decoration.style,
));
}
if font_style_state
.decoration
.ty
.contains(TextDecoration::OVERLINE)
{
builder.set_overline(skia_decoration_style_to_accesskit(
font_style_state.decoration.style,
));
}
if let Some(alt) = &node_accessibility.a11y_alt {
builder.set_value(alt.to_owned());
} else if let Some(value) = node_ref.get_inner_texts() {
builder.set_value(value);
builder.set_role(Role::Label);
}
if let Some(name) = &node_accessibility.a11y_name {
builder.set_name(name.to_owned());
}
if let Some(role) = node_accessibility.a11y_role {
builder.set_role(role);
}
if node_ref.id() == node_ref.real_dom().root_id() {
builder.set_role(Role::Window);
}
builder.build()
}
}
fn skia_decoration_style_to_accesskit(style: TextDecorationStyle) -> accesskit::TextDecoration {
match style {
TextDecorationStyle::Solid => accesskit::TextDecoration::Solid,
TextDecorationStyle::Dotted => accesskit::TextDecoration::Dotted,
TextDecorationStyle::Dashed => accesskit::TextDecoration::Dashed,
TextDecorationStyle::Double => accesskit::TextDecoration::Double,
TextDecorationStyle::Wavy => accesskit::TextDecoration::Wavy,
}
}
fn skia_color_to_rgba_u32(color: Color) -> u32 {
((color.a() as u32) << 24)
| ((color.b() as u32) << 16)
| (((color.g() as u32) << 8) + (color.r() as u32))
}