1use serde::{Deserialize, Serialize};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
11pub enum Role {
12 Window,
13 Button,
14 TextInput,
15 Label,
16 Checkbox,
17 RadioButton,
18 Slider,
19 ProgressBar,
20 List,
21 ListItem,
22 Table,
23 TableRow,
24 TableCell,
25 Menu,
26 MenuItem,
27 MenuBar,
28 Tab,
29 TabPanel,
30 Tree,
31 TreeItem,
32 Dialog,
33 Tooltip,
34 ScrollBar,
35 Toolbar,
36 Group,
37 Image,
38 Link,
39 Separator,
40 Custom,
41}
42
43impl std::fmt::Display for Role {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 write!(f, "{self:?}")
46 }
47}
48
49#[derive(Debug, Clone, Default, Serialize, Deserialize)]
51pub struct AccessibleState {
52 pub checked: Option<bool>,
53 pub selected: bool,
54 pub expanded: Option<bool>,
55 pub disabled: bool,
56 pub focused: bool,
57 pub read_only: bool,
58 pub required: bool,
59 pub value_now: Option<f64>,
60 pub value_min: Option<f64>,
61 pub value_max: Option<f64>,
62 pub value_text: Option<String>,
63}
64
65#[derive(Debug, Clone)]
67pub struct AccessibilityNode {
68 pub id: String,
69 pub role: Role,
70 pub label: Option<String>,
71 pub description: Option<String>,
72 pub state: AccessibleState,
73 pub children: Vec<AccessibilityNode>,
74}
75
76impl AccessibilityNode {
77 pub fn new(id: impl Into<String>, role: Role) -> Self {
78 Self {
79 id: id.into(),
80 role,
81 label: None,
82 description: None,
83 state: AccessibleState::default(),
84 children: Vec::new(),
85 }
86 }
87
88 pub fn label(mut self, label: impl Into<String>) -> Self {
89 self.label = Some(label.into());
90 self
91 }
92
93 pub fn description(mut self, desc: impl Into<String>) -> Self {
94 self.description = Some(desc.into());
95 self
96 }
97
98 pub fn state(mut self, state: AccessibleState) -> Self {
99 self.state = state;
100 self
101 }
102
103 pub fn child(mut self, child: AccessibilityNode) -> Self {
104 self.children.push(child);
105 self
106 }
107
108 pub fn find(&self, id: &str) -> Option<&AccessibilityNode> {
110 if self.id == id {
111 return Some(self);
112 }
113 for child in &self.children {
114 if let Some(found) = child.find(id) {
115 return Some(found);
116 }
117 }
118 None
119 }
120
121 pub fn find_by_role(&self, role: Role) -> Vec<&AccessibilityNode> {
123 let mut results = Vec::new();
124 if self.role == role {
125 results.push(self);
126 }
127 for child in &self.children {
128 results.extend(child.find_by_role(role));
129 }
130 results
131 }
132
133 pub fn count(&self) -> usize {
135 1 + self.children.iter().map(|c| c.count()).sum::<usize>()
136 }
137}
138
139pub struct AccessibilityTree {
141 root: Option<AccessibilityNode>,
142}
143
144impl AccessibilityTree {
145 pub fn new() -> Self {
146 Self { root: None }
147 }
148
149 pub fn set_root(&mut self, root: AccessibilityNode) {
150 self.root = Some(root);
151 }
152
153 pub fn root(&self) -> Option<&AccessibilityNode> {
154 self.root.as_ref()
155 }
156
157 pub fn find(&self, id: &str) -> Option<&AccessibilityNode> {
158 self.root.as_ref()?.find(id)
159 }
160
161 pub fn find_by_role(&self, role: Role) -> Vec<&AccessibilityNode> {
162 match &self.root {
163 Some(root) => root.find_by_role(role),
164 None => Vec::new(),
165 }
166 }
167
168 pub fn node_count(&self) -> usize {
169 self.root.as_ref().map_or(0, |r| r.count())
170 }
171}
172
173impl Default for AccessibilityTree {
174 fn default() -> Self {
175 Self::new()
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn role_display() {
185 assert_eq!(format!("{}", Role::Button), "Button");
186 }
187
188 #[test]
189 fn node_builder() {
190 let node = AccessibilityNode::new("btn1", Role::Button)
191 .label("Submit")
192 .description("Submit the form");
193 assert_eq!(node.label.as_deref(), Some("Submit"));
194 assert_eq!(node.role, Role::Button);
195 }
196
197 #[test]
198 fn node_children() {
199 let tree = AccessibilityNode::new("root", Role::Window)
200 .child(AccessibilityNode::new("btn1", Role::Button).label("OK"))
201 .child(AccessibilityNode::new("btn2", Role::Button).label("Cancel"));
202 assert_eq!(tree.children.len(), 2);
203 }
204
205 #[test]
206 fn node_find() {
207 let tree = AccessibilityNode::new("root", Role::Window).child(
208 AccessibilityNode::new("toolbar", Role::Toolbar)
209 .child(AccessibilityNode::new("btn1", Role::Button)),
210 );
211 assert!(tree.find("btn1").is_some());
212 assert!(tree.find("nope").is_none());
213 }
214
215 #[test]
216 fn node_find_by_role() {
217 let tree = AccessibilityNode::new("root", Role::Window)
218 .child(AccessibilityNode::new("b1", Role::Button))
219 .child(AccessibilityNode::new("t1", Role::TextInput))
220 .child(AccessibilityNode::new("b2", Role::Button));
221 let buttons = tree.find_by_role(Role::Button);
222 assert_eq!(buttons.len(), 2);
223 }
224
225 #[test]
226 fn node_count() {
227 let tree = AccessibilityNode::new("root", Role::Window)
228 .child(AccessibilityNode::new("a", Role::Button))
229 .child(
230 AccessibilityNode::new("b", Role::Group)
231 .child(AccessibilityNode::new("c", Role::Label)),
232 );
233 assert_eq!(tree.count(), 4);
234 }
235
236 #[test]
237 fn accessible_state_defaults() {
238 let state = AccessibleState::default();
239 assert!(!state.selected);
240 assert!(!state.disabled);
241 assert!(state.checked.is_none());
242 }
243
244 #[test]
245 fn tree_empty() {
246 let tree = AccessibilityTree::new();
247 assert_eq!(tree.node_count(), 0);
248 assert!(tree.root().is_none());
249 }
250
251 #[test]
252 fn tree_with_root() {
253 let mut tree = AccessibilityTree::new();
254 tree.set_root(
255 AccessibilityNode::new("app", Role::Window)
256 .child(AccessibilityNode::new("btn", Role::Button)),
257 );
258 assert_eq!(tree.node_count(), 2);
259 assert!(tree.find("btn").is_some());
260 }
261
262 #[test]
263 fn tree_find_by_role() {
264 let mut tree = AccessibilityTree::new();
265 tree.set_root(
266 AccessibilityNode::new("app", Role::Window)
267 .child(AccessibilityNode::new("b1", Role::Button))
268 .child(AccessibilityNode::new("b2", Role::Button)),
269 );
270 assert_eq!(tree.find_by_role(Role::Button).len(), 2);
271 }
272}