use std::any::Any;
use accesskit::{
Action,
Affine,
Node,
Rect,
Role,
TreeId,
TreeUpdate,
};
use ragnarok::ProcessedEvents;
use rustc_hash::{
FxHashMap,
FxHashSet,
};
use torin::prelude::{
CursorPoint,
LayoutNode,
};
use crate::{
accessibility::{
focus_strategy::AccessibilityFocusStrategy,
focusable::Focusable,
id::AccessibilityId,
},
data::Overflow,
elements::{
label::LabelElement,
paragraph::ParagraphElement,
},
events::emittable::EmmitableEvent,
integration::{
EventName,
EventsChunk,
},
node_id::NodeId,
prelude::{
AccessibilityFocusMovement,
Color,
EventType,
FontSlant,
TextAlign,
TextDecoration,
WheelEventData,
WheelSource,
},
tree::Tree,
};
pub const ACCESSIBILITY_ROOT_ID: AccessibilityId = AccessibilityId(0);
pub struct AccessibilityTree {
pub map: FxHashMap<AccessibilityId, NodeId>,
pub focused_id: AccessibilityId,
}
impl Default for AccessibilityTree {
fn default() -> Self {
Self::new(ACCESSIBILITY_ROOT_ID)
}
}
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(&mut self, tree: &mut Tree) -> TreeUpdate {
tree.accessibility_diff.clear();
let mut nodes = vec![];
tree.traverse_depth(|node_id| {
let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
let layout_node = tree.layout.get(&node_id).unwrap();
let accessibility_node = Self::create_node(node_id, layout_node, tree);
nodes.push((accessibility_state.a11y_id, accessibility_node));
self.map.insert(accessibility_state.a11y_id, node_id);
});
#[cfg(debug_assertions)]
tracing::info!(
"Initialized the Accessibility Tree with {} nodes.",
nodes.len()
);
if !self.map.contains_key(&self.focused_id) {
self.focused_id = ACCESSIBILITY_ROOT_ID;
}
TreeUpdate {
tree_id: TreeId::ROOT,
nodes,
tree: Some(accesskit::Tree::new(ACCESSIBILITY_ROOT_ID)),
focus: self.focused_id,
}
}
#[cfg_attr(feature = "hotpath", hotpath::measure)]
pub fn process_updates(
&mut self,
tree: &mut Tree,
events_sender: &futures_channel::mpsc::UnboundedSender<EventsChunk>,
) -> TreeUpdate {
let requested_focus = tree.accessibility_diff.requested_focus.take();
let removed_ids = tree
.accessibility_diff
.removed
.drain()
.collect::<FxHashMap<_, _>>();
let mut added_or_updated_ids = tree
.accessibility_diff
.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 removed_ids.keys() {
added_or_updated_ids.remove(node_id);
self.map.retain(|_, id| id != node_id);
}
for parent_id in removed_ids.values() {
if !removed_ids.contains_key(parent_id) {
added_or_updated_ids.insert(*parent_id);
}
}
for node_id in added_or_updated_ids.clone() {
let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
self.map.insert(accessibility_state.a11y_id, node_id);
let node_parent_id = tree.parents.get(&node_id).unwrap_or(&NodeId::ROOT);
added_or_updated_ids.insert(*node_parent_id);
}
let mut nodes = Vec::new();
for node_id in added_or_updated_ids {
let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
let layout_node = tree.layout.get(&node_id).unwrap();
let accessibility_node = Self::create_node(node_id, layout_node, tree);
nodes.push((accessibility_state.a11y_id, accessibility_node));
}
let has_request_focus = requested_focus.is_some();
if !self.map.contains_key(&self.focused_id) {
self.focused_id = ACCESSIBILITY_ROOT_ID;
}
if let Some(requested_focus) = requested_focus {
self.focus_node_with_strategy(requested_focus, tree);
}
if let Some(node_id) = self.focused_node_id()
&& has_request_focus
{
self.scroll_to(node_id, tree, events_sender);
}
TreeUpdate {
tree_id: TreeId::ROOT,
nodes,
tree: Some(accesskit::Tree::new(ACCESSIBILITY_ROOT_ID)),
focus: self.focused_id,
}
}
pub fn focus_node_with_strategy(
&mut self,
strategy: AccessibilityFocusStrategy,
tree: &mut Tree,
) {
if let AccessibilityFocusStrategy::Node(id) = strategy {
if self.map.contains_key(&id) {
self.focused_id = id;
}
return;
}
let (navigable_nodes, focused_id) = if strategy.mode()
== Some(AccessibilityFocusMovement::InsideGroup)
{
let mut group_nodes = Vec::new();
let node_id = self.map.get(&self.focused_id).unwrap();
let accessibility_state = tree.accessibility_state.get(node_id).unwrap();
let member_accessibility_id = accessibility_state.a11y_member_of;
if let Some(member_accessibility_id) = member_accessibility_id {
group_nodes = tree
.accessibility_groups
.get(&member_accessibility_id)
.cloned()
.unwrap_or_default()
.into_iter()
.filter(|id| {
let node_id = self.map.get(id).unwrap();
let accessibility_state = tree.accessibility_state.get(node_id).unwrap();
accessibility_state.a11y_focusable == Focusable::Enabled
})
.collect();
}
(group_nodes, self.focused_id)
} else {
let mut nodes = Vec::new();
tree.traverse_depth(|node_id| {
let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
let member_accessibility_id = accessibility_state.a11y_member_of;
if let Some(member_accessibility_id) = member_accessibility_id
&& member_accessibility_id != accessibility_state.a11y_id
{
return;
}
if accessibility_state.a11y_focusable == Focusable::Enabled {
nodes.push(accessibility_state.a11y_id);
}
});
(nodes, self.focused_id)
};
let node_index = navigable_nodes
.iter()
.position(|accessibility_id| *accessibility_id == focused_id);
let target_node = match strategy {
AccessibilityFocusStrategy::Forward(_) => {
if let Some(node_index) = node_index {
if node_index == navigable_nodes.len() - 1 {
navigable_nodes.first().cloned()
} else {
navigable_nodes.get(node_index + 1).cloned()
}
} else {
navigable_nodes.first().cloned()
}
}
AccessibilityFocusStrategy::Backward(_) => {
if let Some(node_index) = node_index {
if node_index == 0 {
navigable_nodes.last().cloned()
} else {
navigable_nodes.get(node_index - 1).cloned()
}
} else {
navigable_nodes.last().cloned()
}
}
_ => unreachable!(),
};
self.focused_id = target_node.unwrap_or(focused_id);
#[cfg(debug_assertions)]
tracing::info!("Focused {:?} node.", self.focused_id);
}
fn scroll_to(
&self,
node_id: NodeId,
tree: &mut Tree,
events_sender: &futures_channel::mpsc::UnboundedSender<EventsChunk>,
) {
let Some(effect_state) = tree.effect_state.get(&node_id) else {
return;
};
let mut target_node = node_id;
let mut emmitable_events = Vec::new();
for closest_scrollable in effect_state.scrollables.iter().rev() {
let target_layout_node = tree.layout.get(&target_node).unwrap();
let target_area = target_layout_node.area;
let scrollable_layout_node = tree.layout.get(closest_scrollable).unwrap();
let scrollable_target_area = scrollable_layout_node.area;
if !effect_state.is_visible(&tree.layout, &target_area) {
let element = tree.elements.get(closest_scrollable).unwrap();
let scroll_x = element
.accessibility()
.builder
.scroll_x()
.unwrap_or_default() as f32;
let scroll_y = element
.accessibility()
.builder
.scroll_y()
.unwrap_or_default() as f32;
let diff_x = target_area.min_x() - scrollable_target_area.min_x() - scroll_x;
let diff_y = target_area.min_y() - scrollable_target_area.min_y() - scroll_y;
let delta_y = -(scroll_y + diff_y);
let delta_x = -(scroll_x + diff_x);
emmitable_events.push(EmmitableEvent {
name: EventName::Wheel,
source_event: EventName::Wheel,
node_id: *closest_scrollable,
data: EventType::Wheel(WheelEventData::new(
delta_x as f64,
delta_y as f64,
WheelSource::Custom,
CursorPoint::default(),
CursorPoint::default(),
)),
bubbles: false,
});
target_node = *closest_scrollable;
}
}
events_sender
.unbounded_send(EventsChunk::Processed(ProcessedEvents {
emmitable_events,
..Default::default()
}))
.unwrap();
}
pub fn create_node(node_id: NodeId, layout_node: &LayoutNode, tree: &Tree) -> Node {
let element = tree.elements.get(&node_id).unwrap();
let mut accessibility_data = element.accessibility().into_owned();
if node_id == NodeId::ROOT {
accessibility_data.builder.set_role(Role::Window);
}
let children = tree
.children
.get(&node_id)
.cloned()
.unwrap_or_default()
.into_iter()
.map(|child| tree.accessibility_state.get(&child).unwrap().a11y_id)
.collect::<Vec<_>>();
accessibility_data.builder.set_children(children);
let area = layout_node.area.to_f64();
accessibility_data.builder.set_bounds(Rect {
x0: area.min_x(),
x1: area.max_x(),
y0: area.min_y(),
y1: area.max_y(),
});
if let Some(children) = tree.children.get(&node_id) {
for child in children {
let child_element = tree.elements.get(child).unwrap().as_ref() as &dyn Any;
if let Some(label) = child_element.downcast_ref::<LabelElement>() {
accessibility_data.builder.set_label(label.text.as_ref());
} else if let Some(paragraph) = child_element.downcast_ref::<ParagraphElement>() {
accessibility_data.builder.set_label(
paragraph
.spans
.iter()
.map(|span| span.text.as_ref())
.collect::<String>(),
);
}
}
}
if accessibility_data.a11y_focusable.is_enabled() {
accessibility_data.builder.add_action(Action::Focus);
}
let builder = &mut accessibility_data.builder;
if let Some(effect_state) = tree.effect_state.get(&node_id) {
if let Some(rotation) = effect_state.rotation {
let rotation = (rotation as f64).to_radians();
let (sin, cos) = rotation.sin_cos();
builder.set_transform(Affine::new([cos, sin, -sin, cos, 0.0, 0.0]));
}
if effect_state.overflow == Overflow::Clip {
builder.set_clips_children();
}
}
if let Some(background) = element.style().background.as_color() {
builder.set_background_color(color_to_accesskit(background));
}
let element = element.as_ref() as &dyn Any;
let is_text_element = element.is::<LabelElement>() || element.is::<ParagraphElement>();
if !is_text_element {
builder.set_is_line_breaking_object();
}
if let Some(text_style) = tree.text_style_state.get(&node_id) {
if let Some(color) = text_style.color.as_color() {
builder.set_foreground_color(color_to_accesskit(color));
}
builder.set_font_size(f32::from(text_style.font_size));
builder.set_font_weight(f32::from(text_style.font_weight));
builder.set_font_family(text_style.font_families.join(", "));
if matches!(
text_style.font_slant,
FontSlant::Italic | FontSlant::Oblique
) {
builder.set_italic();
}
builder.set_text_align(match text_style.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(accesskit::TextDirection::LeftToRight);
let decoration = accesskit::TextDecoration {
style: accesskit::TextDecorationStyle::Solid,
color: text_style
.color
.as_color()
.map(color_to_accesskit)
.unwrap_or(color_to_accesskit(Color::BLACK)),
};
match text_style.text_decoration {
TextDecoration::Underline => builder.set_underline(decoration),
TextDecoration::Overline => builder.set_overline(decoration),
TextDecoration::LineThrough => builder.set_strikethrough(decoration),
TextDecoration::None => {}
}
}
accessibility_data.builder
}
}
fn color_to_accesskit(color: Color) -> accesskit::Color {
accesskit::Color {
red: color.r(),
green: color.g(),
blue: color.b(),
alpha: color.a(),
}
}