1use std::ops::Range;
2
3use super::{InteractionRole, UiId, UiNode, UiNodeKind, UiRect};
4
5#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
6pub enum SemanticRole {
7 #[default]
8 Generic,
9 Window,
10 Group,
11 Label,
12 Text,
13 Image,
14 Button,
15 Link,
16 Navigation,
17 List,
18 ListItem,
19 Table,
20 Row,
21 Cell,
22 CheckBox,
23 Switch,
24 RadioButton,
25 TextInput,
26 PasswordInput,
27 SearchInput,
28 ComboBox,
29 ListBoxOption,
30 Slider,
31 ProgressIndicator,
32 Dialog,
33 Alert,
34 Tab,
35 TabList,
36 TabPanel,
37 Menu,
38 MenuItem,
39 ScrollView,
40 Custom,
41}
42
43#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
44pub enum SemanticAction {
45 Click,
46 Focus,
47 Blur,
48 SetValue,
49 Increment,
50 Decrement,
51 ScrollIntoView,
52 ScrollUp,
53 ScrollDown,
54 ScrollLeft,
55 ScrollRight,
56 SetTextSelection,
57}
58
59#[derive(Clone, Debug, Default, PartialEq)]
60pub struct SemanticState {
61 pub disabled: bool,
62 pub selected: bool,
63 pub checked: Option<bool>,
64 pub expanded: Option<bool>,
65 pub read_only: bool,
66 pub required: bool,
67 pub busy: bool,
68 pub invalid: bool,
69 pub hidden: bool,
70 pub multiline: bool,
71 pub password: bool,
72}
73
74#[derive(Clone, Debug, Default, PartialEq, Eq)]
75pub struct SemanticRelationships {
76 pub labelled_by: Vec<UiId>,
77 pub described_by: Vec<UiId>,
78 pub controls: Vec<UiId>,
79 pub owns: Vec<UiId>,
80}
81
82#[derive(Clone, Debug, Default, PartialEq, Eq)]
83pub struct SemanticText {
84 pub selection: Option<Range<usize>>,
85 pub character_count: Option<usize>,
86}
87
88#[derive(Clone, Debug, PartialEq)]
89pub struct Semantics {
90 pub role: SemanticRole,
91 pub name: Option<String>,
92 pub description: Option<String>,
93 pub value: Option<String>,
94 pub numeric_value: Option<f64>,
95 pub numeric_min: Option<f64>,
96 pub numeric_max: Option<f64>,
97 pub numeric_step: Option<f64>,
98 pub state: SemanticState,
99 pub actions: Vec<SemanticAction>,
100 pub relationships: SemanticRelationships,
101 pub text: SemanticText,
102}
103
104impl Semantics {
105 pub fn new(role: SemanticRole) -> Self {
106 Self {
107 role,
108 name: None,
109 description: None,
110 value: None,
111 numeric_value: None,
112 numeric_min: None,
113 numeric_max: None,
114 numeric_step: None,
115 state: SemanticState::default(),
116 actions: Vec::new(),
117 relationships: SemanticRelationships::default(),
118 text: SemanticText::default(),
119 }
120 }
121
122 pub fn name(mut self, value: impl Into<String>) -> Self {
123 self.name = Some(value.into());
124 self
125 }
126
127 pub fn description(mut self, value: impl Into<String>) -> Self {
128 self.description = Some(value.into());
129 self
130 }
131
132 pub fn value(mut self, value: impl Into<String>) -> Self {
133 self.value = Some(value.into());
134 self
135 }
136
137 pub fn action(mut self, action: SemanticAction) -> Self {
138 if !self.actions.contains(&action) {
139 self.actions.push(action);
140 }
141 self
142 }
143}
144
145#[derive(Clone, Debug, PartialEq)]
146pub struct SemanticNode {
147 pub id: UiId,
148 pub parent: Option<UiId>,
149 pub children: Vec<UiId>,
150 pub bounds: UiRect,
151 pub semantics: Semantics,
152}
153
154impl SemanticNode {
155 pub(crate) fn from_ui_node(node: &UiNode) -> Self {
156 let mut semantics = node
157 .semantics
158 .clone()
159 .unwrap_or_else(|| inferred_semantics(node));
160 if node.event_policy.focus && !semantics.actions.contains(&SemanticAction::Focus) {
161 semantics.actions.push(SemanticAction::Focus);
162 }
163 if (node.click_action.is_some() || node.click_handler.is_some())
164 && !semantics.actions.contains(&SemanticAction::Click)
165 {
166 semantics.actions.push(SemanticAction::Click);
167 }
168 Self {
169 id: node.id.clone(),
170 parent: node.parent.clone(),
171 children: node.children.clone(),
172 bounds: node.hit_rect,
173 semantics,
174 }
175 }
176}
177
178#[derive(Clone, Debug, Default, PartialEq)]
179pub struct SemanticUpdate {
180 pub nodes: Vec<SemanticNode>,
181 pub removed: Vec<UiId>,
182 pub focus: Option<UiId>,
183 pub full: bool,
184}
185
186impl SemanticUpdate {
187 #[cfg(feature = "accessibility")]
188 pub(crate) fn full_from_tree(tree: &super::HostTree, focus: Option<UiId>) -> Self {
189 Self {
190 nodes: tree
191 .nodes()
192 .iter()
193 .map(|node| SemanticNode::from_ui_node(node))
194 .collect(),
195 removed: Vec::new(),
196 focus,
197 full: true,
198 }
199 }
200}
201
202fn inferred_semantics(node: &UiNode) -> Semantics {
203 let role = match (node.kind, node.interaction) {
204 (UiNodeKind::Root, _) => SemanticRole::Window,
205 (UiNodeKind::Text, _) => SemanticRole::Text,
206 (UiNodeKind::Image | UiNodeKind::Icon, _) => SemanticRole::Image,
207 (UiNodeKind::Button, _) | (_, InteractionRole::Button) => SemanticRole::Button,
208 (UiNodeKind::Table, _) => SemanticRole::Table,
209 (UiNodeKind::TableRow, _) | (_, InteractionRole::Row) => SemanticRole::Row,
210 (_, InteractionRole::Navigation) => SemanticRole::Navigation,
211 (_, InteractionRole::DragHandle) => SemanticRole::Slider,
212 (UiNodeKind::Group | UiNodeKind::Panel, _) => SemanticRole::Group,
213 (_, InteractionRole::Custom(_)) => SemanticRole::Custom,
214 _ => SemanticRole::Generic,
215 };
216 let mut semantics = Semantics::new(role);
217 if let Some(text) = node.text.as_ref() {
218 if matches!(role, SemanticRole::Text | SemanticRole::Label) {
219 semantics.value = Some(text.to_string());
220 semantics.text.character_count = Some(text.chars().count());
221 } else {
222 semantics.name = Some(text.to_string());
223 }
224 }
225 semantics
226}