1use incular_core::{Arena, ArenaId, Rect};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
11pub struct SemanticNodeId(pub ArenaId);
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
14pub enum Role {
15 Button,
16 Text,
17 TextField,
18 TextArea,
19 List,
20 ListItem,
21 ScrollView,
22 GenericContainer,
23 Checkbox,
24 Radio,
25 Switch,
27 Slider,
28 Menu,
29 Dialog,
30 Image,
31 Heading,
32 Link,
33 Group,
34 Tab,
35 TabList,
36 TabPanel,
37 MenuItem,
38 ProgressBar,
39 Meter,
40 SearchField,
41}
42
43pub type SemanticRole = Role;
44
45#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
46pub enum SemanticActionKind {
47 Focus,
48 Activate,
49 SetText,
50 SetSelection,
51 Increment,
52 Decrement,
53 ScrollForward,
54 ScrollBackward,
55}
56
57#[derive(Clone, Debug, PartialEq, Eq)]
60pub enum SemanticAction {
61 Focus,
62 Activate,
63 SetText(String),
64 SetSelection { base: usize, extent: usize },
65 Increment,
66 Decrement,
67 ScrollForward,
68 ScrollBackward,
69}
70
71impl SemanticAction {
72 #[must_use]
73 pub const fn kind(&self) -> SemanticActionKind {
74 match self {
75 Self::Focus => SemanticActionKind::Focus,
76 Self::Activate => SemanticActionKind::Activate,
77 Self::SetText(_) => SemanticActionKind::SetText,
78 Self::SetSelection { .. } => SemanticActionKind::SetSelection,
79 Self::Increment => SemanticActionKind::Increment,
80 Self::Decrement => SemanticActionKind::Decrement,
81 Self::ScrollForward => SemanticActionKind::ScrollForward,
82 Self::ScrollBackward => SemanticActionKind::ScrollBackward,
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
88pub struct TextSelection {
89 pub base: usize,
90 pub extent: usize,
91}
92
93#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
96pub enum CheckedState {
97 #[default]
98 Unchecked,
99 Checked,
100 Indeterminate,
101}
102
103impl CheckedState {
104 #[must_use]
105 pub const fn is_checked(self) -> bool {
106 matches!(self, Self::Checked)
107 }
108}
109
110impl From<bool> for CheckedState {
111 fn from(checked: bool) -> Self {
112 if checked {
113 Self::Checked
114 } else {
115 Self::Unchecked
116 }
117 }
118}
119
120impl std::fmt::Display for CheckedState {
121 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
122 formatter.write_str(match self {
123 Self::Unchecked => "false",
124 Self::Checked => "true",
125 Self::Indeterminate => "mixed",
126 })
127 }
128}
129
130#[derive(Clone, Debug, Default, PartialEq)]
131pub struct SemanticState {
132 pub enabled: bool,
133 pub focused: bool,
134 pub focusable: bool,
135 pub selected: bool,
136 pub checked: Option<CheckedState>,
137 pub expanded: Option<bool>,
138 pub read_only: bool,
139 pub editable: bool,
140 pub obscured: bool,
145 pub multiline: bool,
146 pub selection: Option<TextSelection>,
147 pub item_index: Option<usize>,
148 pub set_size: Option<usize>,
149 pub busy: bool,
151 pub invalid: bool,
153 pub required: bool,
155 pub numeric_value: Option<f64>,
158 pub numeric_min: Option<f64>,
159 pub numeric_max: Option<f64>,
160 pub numeric_step: Option<f64>,
161 pub heading_level: Option<u8>,
163}
164
165#[derive(Clone, Debug, PartialEq)]
166pub struct SemanticNode {
167 pub id: SemanticNodeId,
168 pub role: Role,
169 pub label: Option<String>,
170 pub value: Option<String>,
171 pub description: Option<String>,
172 pub bounds: Rect,
173 pub state: SemanticState,
174 pub actions: Vec<SemanticActionKind>,
175 pub children: Vec<SemanticNodeId>,
176}
177
178#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
179pub struct SemanticsDiagnostics {
180 pub nodes_created: u64,
181 pub nodes_removed: u64,
182 pub nodes_updated: u64,
183 pub property_updates: u64,
184 pub geometry_updates: u64,
185 pub native_updates: u64,
186 pub actions_received: u64,
187 pub revisions: u64,
191}
192
193#[derive(Default)]
196pub struct SemanticsTree {
197 nodes: Arena<SemanticNode>,
198 root: Option<SemanticNodeId>,
199 diagnostics: SemanticsDiagnostics,
200}
201
202impl SemanticsTree {
203 #[must_use]
204 pub fn new() -> Self {
205 Self::default()
206 }
207 #[must_use]
208 pub const fn root(&self) -> Option<SemanticNodeId> {
209 self.root
210 }
211 pub fn set_root(&mut self, root: Option<SemanticNodeId>) {
212 if self.root != root {
213 self.root = root;
214 self.diagnostics.revisions = self.diagnostics.revisions.wrapping_add(1);
215 }
216 }
217 #[must_use]
218 pub fn node(&self, id: SemanticNodeId) -> Option<&SemanticNode> {
219 self.nodes.get(id.0)
220 }
221 #[must_use]
222 pub fn len(&self) -> usize {
223 self.nodes.len()
224 }
225 #[must_use]
226 pub fn is_empty(&self) -> bool {
227 self.nodes.is_empty()
228 }
229 #[must_use]
230 pub const fn diagnostics(&self) -> SemanticsDiagnostics {
231 self.diagnostics
232 }
233
234 #[must_use]
240 pub const fn revision(&self) -> u64 {
241 self.diagnostics.revisions
242 }
243
244 pub fn insert(&mut self, node: SemanticNode) -> SemanticNodeId {
245 let id = SemanticNodeId(self.nodes.insert(node));
246 self.nodes.get_mut(id.0).expect("new semantic node").id = id;
247 self.diagnostics.nodes_created += 1;
248 self.diagnostics.revisions = self.diagnostics.revisions.wrapping_add(1);
249 id
250 }
251
252 pub fn update(&mut self, id: SemanticNodeId, node: SemanticNode) -> bool {
253 let Some(old) = self.nodes.get_mut(id.0) else {
254 return false;
255 };
256 let properties_changed = old.role != node.role
257 || old.label != node.label
258 || old.value != node.value
259 || old.description != node.description
260 || old.state != node.state
261 || old.actions != node.actions
262 || old.children != node.children;
263 let geometry_changed = old.bounds != node.bounds;
264 if properties_changed || geometry_changed {
265 *old = node;
266 self.diagnostics.nodes_updated += 1;
267 self.diagnostics.revisions = self.diagnostics.revisions.wrapping_add(1);
268 }
269 if properties_changed {
270 self.diagnostics.property_updates += 1;
271 }
272 if geometry_changed {
273 self.diagnostics.geometry_updates += 1;
274 }
275 true
276 }
277
278 pub fn remove(&mut self, id: SemanticNodeId) -> Option<SemanticNode> {
279 let node = self.nodes.remove(id.0)?;
280 self.diagnostics.nodes_removed += 1;
281 self.diagnostics.revisions = self.diagnostics.revisions.wrapping_add(1);
282 if self.root == Some(id) {
283 self.root = None;
284 }
285 Some(node)
286 }
287
288 pub fn note_action(&mut self) {
289 self.diagnostics.actions_received += 1;
290 }
291 pub fn note_native_update(&mut self) {
292 self.diagnostics.native_updates += 1;
293 }
294 pub fn iter(&self) -> impl Iterator<Item = (SemanticNodeId, &SemanticNode)> {
295 self.nodes
296 .iter()
297 .map(|(id, node)| (SemanticNodeId(id), node))
298 }
299
300 #[must_use]
301 pub fn debug_dump(&self) -> String {
302 self.debug_dump_limited(usize::MAX, usize::MAX, usize::MAX)
303 }
304
305 #[must_use]
311 pub fn debug_dump_bounded(&self, max_nodes: usize) -> String {
312 self.debug_dump_limited(max_nodes, 256, 256)
313 }
314
315 #[must_use]
318 pub fn debug_dump_limited(
319 &self,
320 max_nodes: usize,
321 max_depth: usize,
322 max_text_chars: usize,
323 ) -> String {
324 fn truncate(value: Option<&String>, max_chars: usize) -> Option<String> {
325 let value = value?;
326 if value.chars().count() <= max_chars {
327 return Some(value.clone());
328 }
329 let mut result = value.chars().take(max_chars).collect::<String>();
330 result.push('…');
331 Some(result)
332 }
333
334 let mut output = String::new();
340 let mut parented = std::collections::HashSet::new();
341 for (_, node) in self.iter() {
342 parented.extend(node.children.iter().copied());
343 }
344 let mut extra: Vec<SemanticNodeId> = self
345 .iter()
346 .map(|(id, _)| id)
347 .filter(|id| Some(*id) != self.root && !parented.contains(id))
348 .collect();
349 extra.sort_by_key(|id| id.0);
350 let mut stack = Vec::new();
351 for id in extra.into_iter().rev() {
352 stack.push((id, 0usize));
353 }
354 if let Some(root) = self.root {
355 stack.push((root, 0usize));
356 }
357 let mut visited = 0usize;
358 while let Some((id, depth)) = stack.pop() {
359 if visited >= max_nodes {
360 output.push_str("… semantic dump truncated (node limit)\n");
361 break;
362 }
363 if depth > max_depth {
364 output.push_str(&format!(
365 "{}… semantic dump truncated (depth limit)\n",
366 " ".repeat(max_depth.min(256))
367 ));
368 continue;
369 }
370 let Some(node) = self.node(id) else { continue };
371 visited = visited.saturating_add(1);
372 output.push_str(&format!(
373 "{}{:?}#{:?} label={:?} value={:?} bounds=({:.1},{:.1},{:.1},{:.1}) focused={} actions={:?}\n",
374 " ".repeat(depth), node.role, node.id,
375 truncate(node.label.as_ref(), max_text_chars),
376 truncate(node.value.as_ref(), max_text_chars),
377 node.bounds.origin.x, node.bounds.origin.y, node.bounds.size.width,
378 node.bounds.size.height, node.state.focused, node.actions,
379 ));
380 for child in node.children.iter().rev() {
381 stack.push((*child, depth.saturating_add(1)));
382 }
383 }
384 output
385 }
386}