use accesskit::{Action, Live, Node, NodeId, Role, TextPosition, TextSelection};
use crate::widget_id::WidgetId;
pub struct AccessNodeBuilder {
inner: Node,
name: Option<String>,
value: Option<String>,
role: Role,
actions: Vec<Action>,
toggled: Option<bool>,
expanded: Option<bool>,
selected: Option<bool>,
hidden: bool,
owner: Option<WidgetId>,
pending_self_selection: Option<(usize, usize)>,
pending_explicit_selection: Option<(TextPosition, TextPosition)>,
children_collected: Vec<(NodeId, Node)>,
}
#[derive(Debug, Clone, Copy)]
#[repr(u8)]
pub enum SyntheticKind {
Paragraph = 1,
TextRun = 2,
ImageRun = 3,
Link = 4,
SceneItem = 5,
SceneGroup = 6,
SceneMagnet = 7,
ChartMark = 8,
Annotation = 9,
LaneMark = 10,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Announcement {
pub seq: u64,
pub text: String,
pub assertive: bool,
}
pub(crate) const SYNTHETIC_BIT: u64 = 1u64 << 63;
pub fn synthetic_node_id(parent: WidgetId, element_id: u64, kind: SyntheticKind) -> NodeId {
use slotmap::Key;
let parent_raw = parent.data().as_ffi();
let h = fnv_mix_u64(parent_raw, element_id, kind as u64);
NodeId((h & !SYNTHETIC_BIT) | SYNTHETIC_BIT)
}
fn to_accesskit_ordinal(one_based: usize) -> usize {
debug_assert!(
one_based >= 1,
"AccessKit ordinals are 1-based at this boundary; 0 is not a position, \
row, column or level"
);
one_based.saturating_sub(1)
}
pub fn is_synthetic(id: NodeId) -> bool {
id.0 & SYNTHETIC_BIT != 0
}
fn fnv_mix_u64(a: u64, b: u64, c: u64) -> u64 {
const FNV_OFFSET: u64 = 0xcbf29ce484222325;
const FNV_PRIME: u64 = 0x100000001b3;
let mut h = FNV_OFFSET;
for byte in a
.to_le_bytes()
.iter()
.chain(b.to_le_bytes().iter())
.chain(c.to_le_bytes().iter())
{
h ^= *byte as u64;
h = h.wrapping_mul(FNV_PRIME);
}
h
}
#[derive(Debug, Clone, Copy, Default)]
pub struct TextRunAttributes {
pub font_weight: Option<u16>,
pub bold: bool,
pub italic: bool,
pub underline: bool,
pub strikethrough: bool,
}
fn default_text_decoration() -> accesskit::TextDecoration {
accesskit::TextDecoration {
style: accesskit::TextDecorationStyle::Solid,
color: accesskit::Color {
red: 0,
green: 0,
blue: 0,
alpha: 255,
},
}
}
impl AccessNodeBuilder {
pub fn new() -> Self {
Self {
inner: Node::new(Role::Unknown),
name: None,
value: None,
role: Role::Unknown,
actions: Vec::new(),
toggled: None,
expanded: None,
selected: None,
hidden: false,
owner: None,
pending_self_selection: None,
pending_explicit_selection: None,
children_collected: Vec::new(),
}
}
pub fn for_widget(owner: WidgetId) -> Self {
let mut b = Self::new();
b.owner = Some(owner);
b
}
pub fn set_role(&mut self, role: Role) {
self.role = role;
self.inner.set_role(role);
}
pub fn set_name(&mut self, name: impl Into<String>) {
let name: String = name.into();
self.inner.set_label(name.clone());
self.name = Some(name);
}
pub fn set_disabled(&mut self) {
self.inner.set_disabled();
}
pub fn clear_disabled(&mut self) {
self.inner.clear_disabled();
}
pub fn add_action(&mut self, action: Action) {
self.inner.add_action(action);
self.actions.push(action);
}
pub fn remove_action(&mut self, action: Action) {
self.inner.remove_action(action);
self.actions.retain(|a| *a != action);
}
pub fn set_value(&mut self, value: impl Into<String>) {
let v: String = value.into();
self.inner.set_value(v.clone());
self.value = Some(v);
}
pub fn set_color_value(&mut self, color: teksilo_tokens::Color) {
let ak = accesskit::Color {
red: (color.r() * 255.0).round().clamp(0.0, 255.0) as u8,
green: (color.g() * 255.0).round().clamp(0.0, 255.0) as u8,
blue: (color.b() * 255.0).round().clamp(0.0, 255.0) as u8,
alpha: (color.a() * 255.0).round().clamp(0.0, 255.0) as u8,
};
self.inner.set_color_value(ak);
}
pub fn set_description(&mut self, description: impl Into<String>) {
self.inner.set_description(description.into());
}
pub fn set_live(&mut self, live: Live) {
self.inner.set_live(live);
}
pub fn set_described_by(&mut self, ids: impl Into<Vec<NodeId>>) {
self.inner.set_described_by(ids);
}
pub fn push_described_by(&mut self, id: NodeId) {
self.inner.push_described_by(id);
}
pub fn push_labelled_by(&mut self, id: NodeId) {
self.inner.push_labelled_by(id);
}
pub fn set_details(&mut self, ids: impl Into<Vec<NodeId>>) {
self.inner.set_details(ids);
}
pub fn push_detail(&mut self, id: NodeId) {
self.inner.push_detail(id);
}
pub fn set_author_id(&mut self, id: impl Into<String>) {
self.inner.set_author_id(id.into());
}
pub fn set_custom_actions(&mut self, actions: Vec<accesskit::CustomAction>) {
self.inner.set_custom_actions(actions);
}
pub fn set_toggled(&mut self, toggled: bool) {
self.toggled = Some(toggled);
self.inner.set_toggled(if toggled {
accesskit::Toggled::True
} else {
accesskit::Toggled::False
});
}
pub fn set_expanded(&mut self, expanded: bool) {
self.expanded = Some(expanded);
self.inner.set_expanded(expanded);
}
pub fn set_has_popup(&mut self, kind: accesskit::HasPopup) {
self.inner.set_has_popup(kind);
}
pub fn set_placeholder(&mut self, placeholder: impl Into<String>) {
self.inner.set_placeholder(placeholder.into());
}
pub fn set_url(&mut self, url: impl Into<String>) {
self.inner.set_url(url.into());
}
pub fn set_keyboard_shortcut(&mut self, shortcut: impl Into<String>) {
self.inner.set_keyboard_shortcut(shortcut.into());
}
pub fn set_auto_complete(&mut self, kind: accesskit::AutoComplete) {
self.inner.set_auto_complete(kind);
}
pub fn set_selected(&mut self, selected: bool) {
self.selected = Some(selected);
self.inner.set_selected(selected);
}
pub fn set_orientation(&mut self, orientation: accesskit::Orientation) {
self.inner.set_orientation(orientation);
}
pub fn set_position_in_set(&mut self, position: usize) {
self.inner
.set_position_in_set(to_accesskit_ordinal(position));
}
pub fn set_size_of_set(&mut self, size: usize) {
self.inner.set_size_of_set(size);
}
pub fn set_level(&mut self, level: usize) {
self.inner.set_level(to_accesskit_ordinal(level));
}
pub fn set_row_count(&mut self, count: usize) {
self.inner.set_row_count(count);
}
pub fn set_column_count(&mut self, count: usize) {
self.inner.set_column_count(count);
}
pub fn set_row_index(&mut self, index: usize) {
self.inner.set_row_index(to_accesskit_ordinal(index));
}
pub fn set_column_index(&mut self, index: usize) {
self.inner.set_column_index(to_accesskit_ordinal(index));
}
pub fn set_row_span(&mut self, span: usize) {
self.inner.set_row_span(span);
}
pub fn set_column_span(&mut self, span: usize) {
self.inner.set_column_span(span);
}
pub fn set_multiselectable(&mut self, value: bool) {
if value {
self.inner.set_multiselectable();
} else {
self.inner.clear_multiselectable();
}
}
pub fn set_active_descendant(&mut self, id: NodeId) {
self.inner.set_active_descendant(id);
}
pub fn set_modal(&mut self) {
self.inner.set_modal();
}
pub fn set_aria_current(&mut self, current: accesskit::AriaCurrent) {
self.inner.set_aria_current(current);
}
pub fn set_numeric_value_step(&mut self, step: f64) {
self.inner.set_numeric_value_step(step);
}
pub fn set_numeric_value_jump(&mut self, jump: f64) {
self.inner.set_numeric_value_jump(jump);
}
pub fn push_controlled(&mut self, id: NodeId) {
self.inner.push_controlled(id);
}
pub fn push_to_radio_group(&mut self, id: NodeId) {
self.inner.push_to_radio_group(id);
}
pub fn set_numeric_value(&mut self, value: f64) {
self.inner.set_numeric_value(value);
}
pub fn set_min_numeric_value(&mut self, value: f64) {
self.inner.set_min_numeric_value(value);
}
pub fn set_max_numeric_value(&mut self, value: f64) {
self.inner.set_max_numeric_value(value);
}
pub fn set_hidden(&mut self) {
self.hidden = true;
self.inner.set_hidden();
}
pub fn clear_hidden(&mut self) {
self.hidden = false;
self.inner.clear_hidden();
}
pub fn is_hidden(&self) -> bool {
self.hidden
}
pub fn role(&self) -> Role {
self.role
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn actions(&self) -> &[Action] {
&self.actions
}
pub fn value(&self) -> Option<&str> {
self.value.as_deref()
}
pub fn toggled(&self) -> Option<bool> {
self.toggled
}
pub fn expanded(&self) -> Option<bool> {
self.expanded
}
pub fn selected(&self) -> Option<bool> {
self.selected
}
pub fn build(mut self, id: WidgetId) -> (NodeId, Node, Vec<(NodeId, Node)>) {
let node_id = widget_id_to_node_id(id);
if let Some((anchor, focus)) = self.pending_explicit_selection.take() {
let selection = TextSelection { anchor, focus };
self.inner.set_text_selection(selection);
} else if let Some((anchor, focus)) = self.pending_self_selection.take() {
let selection = TextSelection {
anchor: TextPosition {
node: node_id,
character_index: anchor,
},
focus: TextPosition {
node: node_id,
character_index: focus,
},
};
self.inner.set_text_selection(selection);
}
if self.inner.role() == Role::Label
&& let Some(label) = self.inner.label().map(|s| s.to_string())
{
if self.inner.value().is_none() {
self.inner.set_value(label);
}
self.inner.clear_label();
}
(node_id, self.inner, self.children_collected)
}
pub fn inner_mut(&mut self) -> &mut Node {
&mut self.inner
}
pub fn owner_id(&self) -> Option<crate::widget_id::WidgetId> {
self.owner
}
pub fn with_collected_node<F: FnOnce(&mut Node)>(&mut self, node_id: NodeId, f: F) -> bool {
for (id, node) in self.children_collected.iter_mut() {
if *id == node_id {
f(node);
return true;
}
}
false
}
pub fn set_read_only(&mut self) {
self.inner.set_read_only();
}
pub fn set_text_selection(&mut self, node_id: NodeId, anchor: usize, focus: usize) {
let selection = TextSelection {
anchor: TextPosition {
node: node_id,
character_index: anchor,
},
focus: TextPosition {
node: node_id,
character_index: focus,
},
};
self.inner.set_text_selection(selection);
}
pub fn set_caret_position(&mut self, node_id: NodeId, character_index: usize) {
self.set_text_selection(node_id, character_index, character_index);
}
pub fn set_text_selection_on_self(&mut self, anchor: usize, focus: usize) {
self.pending_self_selection = Some((anchor, focus));
}
pub fn set_caret_position_on_self(&mut self, character_index: usize) {
self.set_text_selection_on_self(character_index, character_index);
}
pub fn push_paragraph_child(&mut self, element_id: u64) -> NodeId {
let Some(owner) = self.owner else {
debug_assert!(
false,
"push_paragraph_child called on a builder with no owner — \
widgets must only call this from Widget::accessibility"
);
return NodeId(0);
};
let node_id = synthetic_node_id(owner, element_id, SyntheticKind::Paragraph);
let node = Node::new(Role::Paragraph);
self.children_collected.push((node_id, node));
self.inner.push_child(node_id);
node_id
}
pub fn push_annotation_child(&mut self, group_id: u64, text: impl Into<String>) -> NodeId {
let Some(owner) = self.owner else {
debug_assert!(
false,
"push_annotation_child called on a builder with no owner — \
widgets must only call this from Widget::accessibility"
);
return NodeId(0);
};
let node_id = synthetic_node_id(owner, group_id, SyntheticKind::Annotation);
let mut node = Node::new(Role::Comment);
node.set_value(text.into());
self.children_collected.push((node_id, node));
self.inner.push_child(node_id);
node_id
}
pub fn push_detail_on_child(&mut self, child: NodeId, detail: NodeId) {
if let Some((_, node)) = self
.children_collected
.iter_mut()
.find(|(id, _)| *id == child)
{
node.push_detail(detail);
}
}
pub fn push_link_child(
&mut self,
element_id: u64,
label: impl Into<String>,
url: impl Into<String>,
) -> NodeId {
let Some(owner) = self.owner else {
debug_assert!(
false,
"push_link_child called on a builder with no owner — \
widgets must only call this from Widget::accessibility"
);
return NodeId(0);
};
let node_id = synthetic_node_id(owner, element_id, SyntheticKind::Link);
let mut node = Node::new(Role::Link);
let label: String = label.into();
if !label.is_empty() {
node.set_label(label);
}
node.set_value(url.into());
self.children_collected.push((node_id, node));
self.inner.push_child(node_id);
node_id
}
pub fn push_scene_child(
&mut self,
element_id: u64,
kind: SyntheticKind,
customize: impl FnOnce(&mut AccessNodeBuilder),
) -> NodeId {
debug_assert!(
matches!(
kind,
SyntheticKind::SceneItem
| SyntheticKind::SceneGroup
| SyntheticKind::SceneMagnet
| SyntheticKind::ChartMark
| SyntheticKind::LaneMark
),
"push_scene_child requires SyntheticKind::SceneItem, ::SceneGroup, ::SceneMagnet, ::ChartMark, or ::LaneMark"
);
let Some(owner) = self.owner else {
debug_assert!(
false,
"push_scene_child called on a builder with no owner — \
widgets must only call this from Widget::accessibility"
);
return NodeId(0);
};
let node_id = synthetic_node_id(owner, element_id, kind);
let mut child_builder = AccessNodeBuilder::for_widget(owner);
customize(&mut child_builder);
let (_unused, node, grand_children) = child_builder.build(owner);
self.children_collected.push((node_id, node));
for (gid, gnode) in grand_children {
self.children_collected.push((gid, gnode));
}
self.inner.push_child(node_id);
node_id
}
pub fn attach_scene_child_under(&mut self, parent: NodeId, child: NodeId) -> bool {
for (id, node) in self.children_collected.iter_mut() {
if *id == parent {
node.push_child(child);
return true;
}
}
false
}
pub fn push_scene_child_under(
&mut self,
parent: Option<NodeId>,
element_id: u64,
kind: SyntheticKind,
customize: impl FnOnce(&mut AccessNodeBuilder),
) -> NodeId {
debug_assert!(
matches!(
kind,
SyntheticKind::SceneItem
| SyntheticKind::SceneGroup
| SyntheticKind::SceneMagnet
| SyntheticKind::ChartMark
| SyntheticKind::LaneMark
),
"push_scene_child_under requires SyntheticKind::SceneItem, ::SceneGroup, ::SceneMagnet, ::ChartMark, or ::LaneMark"
);
let Some(owner) = self.owner else {
debug_assert!(
false,
"push_scene_child_under called on a builder with no owner — \
widgets must only call this from Widget::accessibility"
);
return NodeId(0);
};
let node_id = synthetic_node_id(owner, element_id, kind);
let mut child_builder = AccessNodeBuilder::for_widget(owner);
customize(&mut child_builder);
let (_unused, node, grand_children) = child_builder.build(owner);
self.children_collected.push((node_id, node));
for (gid, gnode) in grand_children {
self.children_collected.push((gid, gnode));
}
match parent {
Some(parent_id) => {
let attached = self.attach_scene_child_under(parent_id, node_id);
if !attached {
debug_assert!(
false,
"push_scene_child_under: parent {:?} not in children_collected — \
caller must push the parent before its children",
parent_id
);
self.inner.push_child(node_id);
}
}
None => {
self.inner.push_child(node_id);
}
}
node_id
}
pub fn set_paragraph_as_heading(&mut self, node_id: NodeId, level: u8) -> bool {
for (id, node) in self.children_collected.iter_mut() {
if *id == node_id {
node.set_role(Role::Heading);
let level: usize = to_accesskit_ordinal((level as usize).clamp(1, 6));
node.set_level(level);
return true;
}
}
false
}
pub fn set_child_position_in_set(
&mut self,
node_id: NodeId,
position: usize,
size: usize,
) -> bool {
let found = self.with_collected_node(node_id, |node| {
node.set_position_in_set(to_accesskit_ordinal(position));
});
if found {
self.inner.set_size_of_set(size);
}
found
}
pub fn link_runs_on_line(&mut self, run_ids: &[NodeId]) {
for pair in run_ids.windows(2) {
let (a, b) = (pair[0], pair[1]);
self.with_collected_node(a, |node| node.set_next_on_line(b));
self.with_collected_node(b, |node| node.set_previous_on_line(a));
}
}
#[allow(clippy::too_many_arguments)]
pub fn push_text_run_child(
&mut self,
parent_node: NodeId,
element_id: u64,
fragment_offset: usize,
value: String,
character_lengths: Vec<u8>,
word_starts: Option<Vec<u8>>,
character_positions: Option<Vec<f32>>,
character_widths: Option<Vec<f32>>,
attrs: TextRunAttributes,
) -> NodeId {
let Some(owner) = self.owner else {
debug_assert!(
false,
"push_text_run_child called on a builder with no owner — \
widgets must only call this from Widget::accessibility"
);
return NodeId(0);
};
let mixed_element = if fragment_offset == 0 {
element_id
} else {
fnv_mix_u64(element_id, fragment_offset as u64, 0)
};
let node_id = synthetic_node_id(owner, mixed_element, SyntheticKind::TextRun);
let mut node = Node::new(Role::TextRun);
node.set_value(value);
node.set_character_lengths(character_lengths);
if let Some(ws) = word_starts {
node.set_word_starts(ws);
}
if let Some(pos) = character_positions {
node.set_character_positions(pos);
}
if let Some(widths) = character_widths {
node.set_character_widths(widths);
}
if let Some(w) = attrs.font_weight {
node.set_font_weight(w as f32);
} else if attrs.bold {
node.set_font_weight(700.0);
}
if attrs.italic {
node.set_italic();
}
if attrs.underline {
node.set_underline(default_text_decoration());
}
if attrs.strikethrough {
node.set_strikethrough(default_text_decoration());
}
self.children_collected.push((node_id, node));
for (id, parent) in self.children_collected.iter_mut() {
if *id == parent_node {
parent.push_child(node_id);
return node_id;
}
}
self.inner.push_child(node_id);
node_id
}
pub fn push_text_run_child_on_self(
&mut self,
element_id: u64,
value: String,
character_lengths: Vec<u8>,
word_starts: Option<Vec<u8>>,
) -> NodeId {
let Some(owner) = self.owner else {
debug_assert!(
false,
"push_text_run_child_on_self called on a builder with no owner — \
widgets must only call this from Widget::accessibility"
);
return NodeId(0);
};
let node_id = synthetic_node_id(owner, element_id, SyntheticKind::TextRun);
let mut node = Node::new(Role::TextRun);
node.set_value(value);
node.set_character_lengths(character_lengths);
if let Some(ws) = word_starts {
node.set_word_starts(ws);
}
self.children_collected.push((node_id, node));
self.inner.push_child(node_id);
node_id
}
pub fn set_text_selection_to(&mut self, anchor: (NodeId, usize), focus: (NodeId, usize)) {
self.pending_explicit_selection = Some((
TextPosition {
node: anchor.0,
character_index: anchor.1,
},
TextPosition {
node: focus.0,
character_index: focus.1,
},
));
}
}
impl Default for AccessNodeBuilder {
fn default() -> Self {
Self::new()
}
}
pub fn widget_id_to_node_id(id: WidgetId) -> NodeId {
use slotmap::Key;
let key_data = id.data();
let raw = key_data.as_ffi();
NodeId(raw)
}
pub fn node_id_to_widget_id_maybe(node_id: NodeId) -> Option<WidgetId> {
if is_synthetic(node_id) {
return None;
}
use slotmap::KeyData;
let key_data = KeyData::from_ffi(node_id.0);
Some(key_data.into())
}
pub fn node_id_to_widget_id(node_id: NodeId) -> WidgetId {
debug_assert!(
!is_synthetic(node_id),
"node_id_to_widget_id called on synthetic NodeId — use node_id_to_widget_id_maybe"
);
use slotmap::KeyData;
let key_data = KeyData::from_ffi(node_id.0);
key_data.into()
}
pub fn root_node_id() -> NodeId {
NodeId(0)
}
#[derive(Debug)]
pub struct AccessibilityInfo {
role: Role,
name: Option<String>,
actions: Vec<Action>,
toggled: Option<bool>,
expanded: Option<bool>,
selected: Option<bool>,
disabled: bool,
hidden: bool,
}
impl AccessibilityInfo {
pub fn new(role: Role, name: Option<String>, actions: Vec<Action>) -> Self {
Self {
role,
name,
actions,
toggled: None,
expanded: None,
selected: None,
disabled: false,
hidden: false,
}
}
pub fn with_toggled(mut self, toggled: bool) -> Self {
self.toggled = Some(toggled);
self
}
pub fn with_expanded(mut self, expanded: bool) -> Self {
self.expanded = Some(expanded);
self
}
pub fn with_selected(mut self, selected: bool) -> Self {
self.selected = Some(selected);
self
}
pub fn with_disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn with_hidden(mut self, hidden: bool) -> Self {
self.hidden = hidden;
self
}
pub fn role(&self) -> Role {
self.role
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn actions(&self) -> &[Action] {
&self.actions
}
pub fn is_toggled(&self) -> bool {
self.toggled.unwrap_or(false)
}
pub fn is_expanded(&self) -> bool {
self.expanded.unwrap_or(false)
}
pub fn is_selected(&self) -> bool {
self.selected.unwrap_or(false)
}
pub fn is_disabled(&self) -> bool {
self.disabled
}
pub fn is_hidden(&self) -> bool {
self.hidden
}
}
#[cfg(test)]
mod tests {
use super::*;
fn fake_widget(id: u64) -> WidgetId {
slotmap::KeyData::from_ffi(id).into()
}
#[test]
fn widget_derived_node_id_has_bit_63_clear() {
let wid = fake_widget(1);
let nid = widget_id_to_node_id(wid);
assert_eq!(
nid.0 & SYNTHETIC_BIT,
0,
"widget NodeId must have bit 63 clear"
);
assert!(!is_synthetic(nid));
}
#[test]
fn synthetic_node_id_has_bit_63_set() {
let wid = fake_widget(42);
let nid = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
assert_eq!(nid.0 & SYNTHETIC_BIT, SYNTHETIC_BIT);
assert!(is_synthetic(nid));
}
#[test]
fn link_runs_on_line_chains_runs_both_ways() {
let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
let para = b.push_paragraph_child(1);
let run = |b: &mut AccessNodeBuilder, off: usize| {
b.push_text_run_child(
para,
10,
off,
"abc".to_string(),
vec![1, 1, 1],
None,
None,
None,
TextRunAttributes::default(),
)
};
let (r0, r1, r2) = (run(&mut b, 0), run(&mut b, 3), run(&mut b, 6));
b.link_runs_on_line(&[r0, r1, r2]);
let (_id, _n, children) = b.build(fake_widget(1));
let node = |id| {
children
.iter()
.find(|(i, _)| *i == id)
.map(|(_, n)| n)
.unwrap()
};
assert_eq!(node(r0).previous_on_line(), None);
assert_eq!(node(r0).next_on_line(), Some(r1));
assert_eq!(node(r1).previous_on_line(), Some(r0));
assert_eq!(node(r1).next_on_line(), Some(r2));
assert_eq!(node(r2).previous_on_line(), Some(r1));
assert_eq!(node(r2).next_on_line(), None);
}
#[test]
fn a_chunk_offset_does_not_alias_another_elements_run() {
let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
let para = b.push_paragraph_child(1);
let elem_a: u64 = 0xABCD_u64 << 32;
let elem_b: u64 = (0xABCD_u64 ^ 255) << 32;
let run = |b: &mut AccessNodeBuilder, elem: u64, off: usize| {
b.push_text_run_child(
para,
elem,
off,
"x".to_string(),
vec![1],
None,
None,
None,
TextRunAttributes::default(),
)
};
let a_chunk = run(&mut b, elem_a, 255); let b_whole = run(&mut b, elem_b, 0); assert_ne!(
a_chunk, b_whole,
"a chunk offset must not alias another block's run NodeId"
);
}
#[test]
fn set_child_position_in_set_numbers_a_paragraph() {
let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
let para = b.push_paragraph_child(5);
assert!(b.set_child_position_in_set(para, 42, 200));
assert!(
!b.set_child_position_in_set(NodeId(999), 1, 1),
"an unknown child is not found"
);
let (_id, own, children) = b.build(fake_widget(1));
let p = children
.iter()
.find(|(i, _)| *i == para)
.map(|(_, n)| n)
.unwrap();
assert_eq!(p.position_in_set(), Some(41));
assert_eq!(p.size_of_set(), None);
assert_eq!(own.size_of_set(), Some(200));
}
#[test]
fn every_aria_ordinal_is_converted_at_the_boundary() {
let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
b.set_position_in_set(1);
b.set_row_index(1);
b.set_column_index(1);
b.set_level(1);
let (_id, n, _children) = b.build(fake_widget(1));
assert_eq!(n.position_in_set(), Some(0), "the first item is index 0");
assert_eq!(n.row_index(), Some(0), "the header row is row 0");
assert_eq!(n.column_index(), Some(0), "the leftmost column is column 0");
assert_eq!(n.level(), Some(0), "a root item is level 0");
}
#[test]
fn a_count_is_not_an_ordinal_and_is_not_converted() {
let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
b.set_size_of_set(12);
b.set_row_count(100);
b.set_column_count(4);
b.set_row_span(2);
b.set_column_span(3);
let (_id, n, _children) = b.build(fake_widget(1));
assert_eq!(n.size_of_set(), Some(12));
assert_eq!(n.row_count(), Some(100));
assert_eq!(n.column_count(), Some(4));
assert_eq!(n.row_span(), Some(2));
assert_eq!(n.column_span(), Some(3));
}
#[test]
fn an_h1_is_accesskit_level_zero() {
for (heading, expected) in [(1u8, 0usize), (2, 1), (6, 5)] {
let mut b = AccessNodeBuilder::for_widget(fake_widget(1));
let para = b.push_paragraph_child(7);
assert!(b.set_paragraph_as_heading(para, heading));
let (_id, _n, children) = b.build(fake_widget(1));
let p = children
.iter()
.find(|(i, _)| *i == para)
.map(|(_, n)| n)
.unwrap();
assert_eq!(p.role(), Role::Heading);
assert_eq!(p.level(), Some(expected), "h{heading}");
}
}
#[test]
fn synthetic_node_id_stable_across_calls() {
let wid = fake_widget(42);
let a = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
let b = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
assert_eq!(a, b);
}
#[test]
fn synthetic_node_id_differs_by_kind() {
let wid = fake_widget(42);
let p = synthetic_node_id(wid, 17, SyntheticKind::Paragraph);
let r = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
assert_ne!(
p, r,
"paragraph and text-run kinds must produce distinct NodeIds"
);
}
#[test]
fn synthetic_node_id_differs_by_element() {
let wid = fake_widget(42);
let a = synthetic_node_id(wid, 1, SyntheticKind::TextRun);
let b = synthetic_node_id(wid, 2, SyntheticKind::TextRun);
assert_ne!(a, b);
}
#[test]
fn node_id_to_widget_id_maybe_returns_none_for_synthetic() {
let wid = fake_widget(42);
let syn = synthetic_node_id(wid, 17, SyntheticKind::TextRun);
assert!(node_id_to_widget_id_maybe(syn).is_none());
}
#[test]
fn node_id_to_widget_id_maybe_round_trips_widget_ids() {
let wid = fake_widget(99);
let nid = widget_id_to_node_id(wid);
let back = node_id_to_widget_id_maybe(nid).unwrap();
assert_eq!(wid, back);
}
#[test]
fn push_paragraph_child_and_text_run_child_emit_synthetic_nodes() {
let owner = fake_widget(7);
let mut builder = AccessNodeBuilder::for_widget(owner);
builder.set_role(Role::MultilineTextInput);
let para = builder.push_paragraph_child(100);
let run = builder.push_text_run_child(
para,
200,
0,
"hello".to_string(),
vec![1, 1, 1, 1, 1],
Some(vec![0]),
None,
None,
TextRunAttributes::default(),
);
assert!(is_synthetic(para));
assert!(is_synthetic(run));
let (_nid, _node, children) = builder.build(owner);
assert_eq!(children.len(), 2);
assert!(children.iter().any(|(id, _)| *id == para));
assert!(children.iter().any(|(id, _)| *id == run));
}
#[test]
fn text_run_attributes_reach_at_node() {
let owner = fake_widget(9);
let mut builder = AccessNodeBuilder::for_widget(owner);
builder.set_role(Role::MultilineTextInput);
let para = builder.push_paragraph_child(1);
let run = builder.push_text_run_child(
para,
2,
0,
"ab".to_string(),
vec![1, 1],
None,
None,
None,
TextRunAttributes {
bold: true,
italic: true,
underline: true,
strikethrough: true,
..Default::default()
},
);
let (_nid, _node, children) = builder.build(owner);
let (_, run_node) = children
.iter()
.find(|(id, _)| *id == run)
.expect("run node");
assert_eq!(
run_node.font_weight(),
Some(700.0),
"bold folds to font weight 700"
);
assert!(run_node.is_italic(), "italic flag set");
assert!(run_node.underline().is_some(), "underline decoration set");
assert!(
run_node.strikethrough().is_some(),
"strikethrough decoration set"
);
let owner2 = fake_widget(10);
let mut b2 = AccessNodeBuilder::for_widget(owner2);
b2.set_role(Role::MultilineTextInput);
let p2 = b2.push_paragraph_child(1);
let r2 = b2.push_text_run_child(
p2,
2,
0,
"x".to_string(),
vec![1],
None,
None,
None,
TextRunAttributes {
bold: true,
font_weight: Some(300),
..Default::default()
},
);
let (_, _, kids2) = b2.build(owner2);
let (_, r2n) = kids2.iter().find(|(id, _)| *id == r2).expect("run2 node");
assert_eq!(
r2n.font_weight(),
Some(300.0),
"explicit weight wins over bold"
);
}
#[test]
fn set_text_selection_to_wins_over_self_selection() {
let owner = fake_widget(3);
let mut builder = AccessNodeBuilder::for_widget(owner);
builder.set_role(Role::MultilineTextInput);
let para = builder.push_paragraph_child(1);
let run = builder.push_text_run_child(
para,
2,
0,
"ab".to_string(),
vec![1, 1],
None,
None,
None,
TextRunAttributes::default(),
);
builder.set_text_selection_on_self(0, 0);
builder.set_text_selection_to((run, 0), (run, 2));
let (_nid, node, _children) = builder.build(owner);
let sel = node.text_selection().expect("text selection set");
assert_eq!(sel.focus.node, run);
assert_eq!(sel.focus.character_index, 2);
}
}