1use crate::runtime::{FocusId, LayoutNode, NodeContent, first_text};
20use stipple_geometry::Rect;
21
22#[derive(Clone, Copy, Debug, PartialEq, Eq)]
24pub enum Role {
25 Window,
27 Group,
29 Button,
31 TextField,
33 Text,
35}
36
37#[derive(Clone, Debug, PartialEq)]
39pub struct AccessNode {
40 pub role: Role,
41 pub name: String,
43 pub bounds: Rect,
44 pub focused: bool,
46 pub children: Vec<AccessNode>,
47}
48
49fn text_of(node: &LayoutNode) -> String {
51 match first_text(node).map(|n| &n.content) {
52 Some(NodeContent::Text { text, .. }) => text.clone(),
53 _ => String::new(),
54 }
55}
56
57fn role_of(node: &LayoutNode) -> Role {
59 if node.action.is_some() {
60 Role::Button
61 } else if node.focus.is_some() {
62 Role::TextField
63 } else if matches!(node.content, NodeContent::Text { .. }) {
64 Role::Text
65 } else {
66 Role::Group
67 }
68}
69
70fn build(node: &LayoutNode, focused: Option<FocusId>, is_root: bool) -> AccessNode {
71 let role = if is_root { Role::Window } else { role_of(node) };
72 let (name, children) = match role {
75 Role::Button | Role::TextField | Role::Text => (text_of(node), Vec::new()),
76 Role::Window | Role::Group => (
77 String::new(),
78 node.children
79 .iter()
80 .map(|c| build(c, focused, false))
81 .filter(|a| !a.is_prunable())
82 .collect(),
83 ),
84 };
85 AccessNode {
86 role,
87 name,
88 bounds: node.bounds,
89 focused: node.focus.is_some() && node.focus == focused,
90 children,
91 }
92}
93
94impl AccessNode {
95 fn is_prunable(&self) -> bool {
98 self.role == Role::Group && self.name.is_empty() && self.children.is_empty()
99 }
100
101 pub fn count(&self) -> usize {
103 1 + self.children.iter().map(AccessNode::count).sum::<usize>()
104 }
105
106 pub fn descendants(&self) -> Vec<&AccessNode> {
108 let mut out = vec![self];
109 for c in &self.children {
110 out.extend(c.descendants());
111 }
112 out
113 }
114}
115
116pub fn accessibility_tree(root: &LayoutNode, focused: Option<FocusId>) -> AccessNode {
119 build(root, focused, true)
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use crate::{Align, Axis, BoxStyle, Cx, Element, layout};
126 use stipple_render::Color;
127 use stipple_style::Theme;
128
129 fn tree(focused: Option<FocusId>) -> AccessNode {
131 let theme = Theme::light();
132 let mut cx = Cx::<()>::new(&theme);
133 let field = Element::stack(
134 Axis::Horizontal,
135 vec![Element::text("hello", 14.0, Color::BLACK)],
136 )
137 .on_key(&mut cx, |_: &mut (), _| {});
138 let button = Element::stack(
139 Axis::Horizontal,
140 vec![Element::text("OK", 14.0, Color::BLACK)],
141 )
142 .on_tap(&mut cx, |_: &mut ()| {});
143 let root = Element::stack(
144 Axis::Vertical,
145 vec![Element::text("Title", 18.0, Color::BLACK), field, button],
146 )
147 .align(Align::Start, Align::Stretch)
148 .fill(Color::WHITE)
149 .padding(stipple_geometry::Insets::uniform(4.0));
150 let laid = layout(&root, Rect::from_xywh(0.0, 0.0, 200.0, 120.0), None);
151 accessibility_tree(&laid, focused)
152 }
153
154 #[test]
155 fn derives_roles_and_names() {
156 let a = tree(None);
157 assert_eq!(a.role, Role::Window);
158 let roles: Vec<Role> = a.children.iter().map(|c| c.role).collect();
160 assert_eq!(roles, vec![Role::Text, Role::TextField, Role::Button]);
161 assert_eq!(a.children[0].name, "Title");
162 assert_eq!(a.children[1].name, "hello"); assert_eq!(a.children[2].name, "OK"); assert!(a.children[1].children.is_empty());
166 assert!(a.children[2].children.is_empty());
167 }
168
169 #[test]
170 fn marks_the_focused_field() {
171 let a = tree(Some(FocusId(0)));
173 let field = a
174 .descendants()
175 .into_iter()
176 .find(|n| n.role == Role::TextField)
177 .unwrap();
178 assert!(field.focused);
179 assert_eq!(a.descendants().iter().filter(|n| n.focused).count(), 1);
181 }
182
183 #[test]
184 fn prunes_decorative_containers() {
185 let root = Element::stack(
187 Axis::Vertical,
188 vec![
189 Element::boxed(BoxStyle::default()).width(10.0).height(10.0), Element::text("Label", 14.0, Color::BLACK),
191 ],
192 );
193 let laid = layout(&root, Rect::from_xywh(0.0, 0.0, 100.0, 100.0), None);
194 let a = accessibility_tree(&laid, None);
195 assert_eq!(a.children.len(), 1);
197 assert_eq!(a.children[0].role, Role::Text);
198 }
199}