use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use accesskit::{Node, NodeId, Role, Tree, TreeId, TreeUpdate};
use crate::props::{A11yNodeProps, Toggled3};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WidgetRole {
Window,
Group,
Button,
Label,
TextInput,
TableRow,
TableCell,
ScrollView,
Image,
Unknown,
Checkbox,
Slider,
ProgressBar,
Tab,
TabPanel,
Menu,
MenuItem,
Dialog,
Alert,
Tooltip,
Tree,
TreeItem,
ListItem,
Link,
ColumnHeader,
Banner,
Navigation,
Main,
Complementary,
ContentInfo,
}
impl From<WidgetRole> for Role {
fn from(r: WidgetRole) -> Role {
match r {
WidgetRole::Window => Role::Window,
WidgetRole::Group => Role::Group,
WidgetRole::Button => Role::Button,
WidgetRole::Label => Role::Label,
WidgetRole::TextInput => Role::TextInput,
WidgetRole::TableRow => Role::Row,
WidgetRole::TableCell => Role::Cell,
WidgetRole::ScrollView => Role::ScrollView,
WidgetRole::Image => Role::Image,
WidgetRole::Unknown => Role::Unknown,
WidgetRole::Checkbox => Role::CheckBox,
WidgetRole::Slider => Role::Slider,
WidgetRole::ProgressBar => Role::ProgressIndicator,
WidgetRole::Tab => Role::Tab,
WidgetRole::TabPanel => Role::TabPanel,
WidgetRole::Menu => Role::Menu,
WidgetRole::MenuItem => Role::MenuItem,
WidgetRole::Dialog => Role::Dialog,
WidgetRole::Alert => Role::Alert,
WidgetRole::Tooltip => Role::Tooltip,
WidgetRole::Tree => Role::Tree,
WidgetRole::TreeItem => Role::TreeItem,
WidgetRole::ListItem => Role::ListItem,
WidgetRole::Link => Role::Link,
WidgetRole::ColumnHeader => Role::ColumnHeader,
WidgetRole::Banner => Role::Banner,
WidgetRole::Navigation => Role::Navigation,
WidgetRole::Main => Role::Main,
WidgetRole::Complementary => Role::Complementary,
WidgetRole::ContentInfo => Role::ContentInfo,
}
}
}
impl std::fmt::Display for WidgetRole {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let name = match self {
WidgetRole::Window => "window",
WidgetRole::Group => "group",
WidgetRole::Button => "button",
WidgetRole::Label => "label",
WidgetRole::TextInput => "text-input",
WidgetRole::TableRow => "table-row",
WidgetRole::TableCell => "table-cell",
WidgetRole::ScrollView => "scroll-view",
WidgetRole::Image => "image",
WidgetRole::Unknown => "unknown",
WidgetRole::Checkbox => "checkbox",
WidgetRole::Slider => "slider",
WidgetRole::ProgressBar => "progress-bar",
WidgetRole::Tab => "tab",
WidgetRole::TabPanel => "tab-panel",
WidgetRole::Menu => "menu",
WidgetRole::MenuItem => "menu-item",
WidgetRole::Dialog => "dialog",
WidgetRole::Alert => "alert",
WidgetRole::Tooltip => "tooltip",
WidgetRole::Tree => "tree",
WidgetRole::TreeItem => "tree-item",
WidgetRole::ListItem => "list-item",
WidgetRole::Link => "link",
WidgetRole::ColumnHeader => "column-header",
WidgetRole::Banner => "banner",
WidgetRole::Navigation => "navigation",
WidgetRole::Main => "main",
WidgetRole::Complementary => "complementary",
WidgetRole::ContentInfo => "content-info",
};
f.write_str(name)
}
}
pub struct A11yNode {
pub id: NodeId,
pub role: WidgetRole,
pub label: Option<String>,
pub children: Vec<A11yNode>,
pub props: A11yNodeProps,
pub text_content: Option<String>,
}
impl std::fmt::Debug for A11yNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("A11yNode")
.field("id", &self.id)
.field("role", &self.role)
.field("label", &self.label)
.field("children", &self.children)
.field("props", &self.props)
.field("text_content", &self.text_content)
.finish()
}
}
impl A11yNode {
pub fn simple(id: NodeId, role: WidgetRole, label: Option<String>) -> Self {
Self {
id,
role,
label,
children: Vec::new(),
props: A11yNodeProps::default(),
text_content: None,
}
}
pub fn content_hash(&self) -> u64 {
let mut h = DefaultHasher::new();
self.label.hash(&mut h);
format!("{:?}", self.role).hash(&mut h);
self.text_content.hash(&mut h);
format!("{:?}", self.props).hash(&mut h);
let child_ids: Vec<u64> = self.children.iter().map(|c| c.id.0).collect();
child_ids.hash(&mut h);
h.finish()
}
}
#[derive(Default)]
pub struct A11yTree {
pub(crate) root_id: Option<NodeId>,
pub(crate) snapshot: Vec<(NodeId, Node)>,
pub(crate) hashes: std::collections::HashMap<NodeId, u64>,
pub(crate) focus: Option<NodeId>,
}
impl A11yTree {
pub fn build(root: &A11yNode) -> TreeUpdate {
let mut nodes: Vec<(NodeId, Node)> = Vec::new();
collect_nodes(root, &mut nodes);
let root_id = root.id;
TreeUpdate {
nodes,
tree: Some(Tree::new(root_id)),
tree_id: TreeId::ROOT,
focus: root_id,
}
}
pub fn build_and_store(&mut self, root: &A11yNode) -> TreeUpdate {
let mut nodes: Vec<(NodeId, Node)> = Vec::new();
let mut hashes: std::collections::HashMap<NodeId, u64> = std::collections::HashMap::new();
collect_nodes(root, &mut nodes);
collect_hashes(root, &mut hashes);
let root_id = root.id;
self.root_id = Some(root_id);
self.snapshot = nodes.clone();
self.hashes = hashes;
let focus = self.focus.unwrap_or(root_id);
TreeUpdate {
nodes,
tree: Some(Tree::new(root_id)),
tree_id: TreeId::ROOT,
focus,
}
}
pub fn set_focus(&mut self, id: Option<NodeId>) {
self.focus = id;
}
pub fn focus(&self) -> Option<NodeId> {
self.focus
}
pub fn focus_update(&self) -> TreeUpdate {
TreeUpdate {
nodes: Vec::new(),
tree: None,
tree_id: TreeId::ROOT,
focus: self.focus.unwrap_or(NodeId(0)),
}
}
pub fn announce(&mut self, text: &str, urgency: crate::props::LiveSetting) -> NodeId {
use accesskit::Live;
let new_raw: u64 = self
.snapshot
.iter()
.map(|(id, _)| id.0)
.max()
.unwrap_or(0)
.saturating_add(1)
.max(0x8000_0000);
let id = NodeId(new_raw);
let mut node = Node::new(Role::Status);
node.set_value(text);
node.set_live(Live::from(urgency));
node.set_live_atomic();
self.snapshot.push((id, node));
id
}
pub fn diff(old: &A11yTree, new_tree: &A11yTree) -> TreeUpdate {
let new_node_map: std::collections::HashMap<NodeId, &Node> = new_tree
.snapshot
.iter()
.map(|(id, node)| (*id, node))
.collect();
let mut changed: Vec<(NodeId, Node)> = Vec::new();
for (id, new_node) in &new_tree.snapshot {
let should_include = match old.hashes.get(id) {
None => true,
Some(&old_hash) => {
let new_hash = new_tree.hashes.get(id).copied().unwrap_or(0);
old_hash != new_hash
}
};
if should_include {
changed.push((*id, new_node.clone()));
}
}
let _ = new_node_map;
let focus = new_tree.focus.unwrap_or(NodeId(0));
let tree = if old.root_id != new_tree.root_id {
new_tree.root_id.map(Tree::new)
} else {
None
};
TreeUpdate {
nodes: changed,
tree,
tree_id: TreeId::ROOT,
focus,
}
}
}
pub fn table_row_node(id: NodeId, row_index: usize) -> A11yNode {
let mut node = A11yNode::simple(id, WidgetRole::TableRow, None);
node.props.description = Some(format!("Row {}", row_index + 1));
node
}
pub fn table_cell_node(id: NodeId, row: usize, col: usize, text: &str) -> A11yNode {
let mut node = A11yNode::simple(id, WidgetRole::TableCell, None);
node.text_content = Some(text.to_string());
node.props.description = Some(format!("Row {} Column {}", row + 1, col + 1));
node
}
pub fn column_header_node(id: NodeId, col: usize, label: &str) -> A11yNode {
let mut node = A11yNode::simple(id, WidgetRole::ColumnHeader, None);
node.label = Some(label.to_string());
node.props.description = Some(format!("Column {} header", col + 1));
node
}
pub fn build_table_a11y(row_count: usize, col_count: usize, col_headers: &[&str]) -> A11yNode {
let mut next_id: u64 = 0;
let mut root = A11yNode::simple(NodeId(next_id), WidgetRole::Group, None);
next_id += 1;
for col_idx in 0..col_count {
let header_label = col_headers.get(col_idx).copied().unwrap_or("");
let header = column_header_node(NodeId(next_id), col_idx, header_label);
next_id += 1;
root.children.push(header);
}
for row_idx in 0..row_count {
let mut row = table_row_node(NodeId(next_id), row_idx);
next_id += 1;
for col_idx in 0..col_count {
let cell = table_cell_node(NodeId(next_id), row_idx, col_idx, "");
next_id += 1;
row.children.push(cell);
}
root.children.push(row);
}
root
}
pub fn synthesize_text_run_children(
text: &str,
selection: Option<&crate::props::TextSelection>,
) -> Vec<crate::props::TextRunChild> {
use crate::props::{byte_offset_to_char_index, TextRunChild};
if text.is_empty() {
return Vec::new();
}
let sel = match selection {
None => {
return vec![TextRunChild {
text: text.to_string(),
char_offset: 0,
byte_offset: 0,
is_selected: false,
}];
}
Some(s) => s,
};
let lo_byte = sel.anchor.min(sel.focus).min(text.len());
let hi_byte = sel.anchor.max(sel.focus).min(text.len());
let lo_byte = snap_to_char_boundary(text, lo_byte);
let hi_byte = snap_to_char_boundary(text, hi_byte);
let mut segments: Vec<TextRunChild> = Vec::with_capacity(3);
if lo_byte > 0 {
let before = &text[..lo_byte];
segments.push(TextRunChild {
text: before.to_string(),
char_offset: 0,
byte_offset: 0,
is_selected: false,
});
}
if lo_byte < hi_byte {
let selected = &text[lo_byte..hi_byte];
let char_off = byte_offset_to_char_index(text, lo_byte);
segments.push(TextRunChild {
text: selected.to_string(),
char_offset: char_off,
byte_offset: lo_byte,
is_selected: true,
});
} else {
let char_off = byte_offset_to_char_index(text, lo_byte);
segments.push(TextRunChild {
text: String::new(),
char_offset: char_off,
byte_offset: lo_byte,
is_selected: true,
});
}
if hi_byte < text.len() {
let after = &text[hi_byte..];
let char_off = byte_offset_to_char_index(text, hi_byte);
segments.push(TextRunChild {
text: after.to_string(),
char_offset: char_off,
byte_offset: hi_byte,
is_selected: false,
});
}
segments
}
fn snap_to_char_boundary(text: &str, offset: usize) -> usize {
if offset >= text.len() {
return text.len();
}
let mut pos = offset;
while pos < text.len() && !text.is_char_boundary(pos) {
pos += 1;
}
pos
}
fn collect_hashes(node: &A11yNode, out: &mut std::collections::HashMap<NodeId, u64>) {
out.insert(node.id, node.content_hash());
for child in &node.children {
collect_hashes(child, out);
}
}
fn collect_nodes(node: &A11yNode, out: &mut Vec<(NodeId, Node)>) {
let child_ids: Vec<NodeId> = node.children.iter().map(|c| c.id).collect();
let mut ak_node = Node::new(Role::from(node.role));
if let Some(label) = &node.label {
ak_node.set_label(label.as_str());
}
for &child_id in &child_ids {
ak_node.push_child(child_id);
}
apply_props(&mut ak_node, &node.props);
if let Some(text) = &node.text_content {
ak_node.set_value(text.as_str());
}
out.push((node.id, ak_node));
for child in &node.children {
collect_nodes(child, out);
}
}
fn apply_props(ak: &mut Node, props: &A11yNodeProps) {
if let Some(ref desc) = props.description {
ak.set_description(desc.as_str());
}
if let Some(ref ph) = props.placeholder {
ak.set_placeholder(ph.as_str());
}
if let Some(ref ks) = props.key_shortcut {
ak.set_keyboard_shortcut(ks.as_str());
}
if props.disabled {
ak.set_disabled();
}
if let Some(expanded) = props.expanded {
ak.set_expanded(expanded);
}
if let Some(selected) = props.selected {
ak.set_selected(selected);
}
if let Some(ref checked) = props.checked {
use accesskit::Toggled;
ak.set_toggled(Toggled::from(Toggled3::from(checked)));
}
if let Some(value) = props.value_now {
ak.set_numeric_value(value);
}
if let Some(min) = props.value_min {
ak.set_min_numeric_value(min);
}
if let Some(max) = props.value_max {
ak.set_max_numeric_value(max);
}
if let Some(step) = props.value_step {
ak.set_numeric_value_step(step);
}
if !props.labelled_by.is_empty() {
ak.set_labelled_by(props.labelled_by.clone());
}
if !props.described_by.is_empty() {
ak.set_described_by(props.described_by.clone());
}
if !props.controlled_by.is_empty() {
ak.set_controls(props.controlled_by.clone());
}
if !props.owns.is_empty() {
ak.set_owns(props.owns.clone());
}
}