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 pub toggled: Option<bool>,
166 pub hidden: bool,
169 pub live_region: bool,
171 pub tooltip: Option<String>,
173 pub link_url: Option<String>,
175 pub identifier: Option<String>,
178}
179
180#[derive(Clone, Debug, PartialEq)]
181pub struct SemanticNode {
182 pub id: SemanticNodeId,
183 pub role: Role,
184 pub label: Option<String>,
185 pub value: Option<String>,
186 pub description: Option<String>,
187 pub bounds: Rect,
188 pub state: SemanticState,
189 pub actions: Vec<SemanticActionKind>,
190 pub children: Vec<SemanticNodeId>,
191}
192
193#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
194pub struct SemanticsDiagnostics {
195 pub nodes_created: u64,
196 pub nodes_removed: u64,
197 pub nodes_updated: u64,
198 pub property_updates: u64,
199 pub geometry_updates: u64,
200 pub native_updates: u64,
201 pub actions_received: u64,
202 pub revisions: u64,
206}
207
208struct RetainedNode {
210 node: SemanticNode,
211 changed_at: u64,
212}
213
214#[derive(Default)]
217pub struct SemanticsTree {
218 nodes: Arena<RetainedNode>,
219 root: Option<SemanticNodeId>,
220 diagnostics: SemanticsDiagnostics,
221}
222
223impl SemanticsTree {
224 #[must_use]
225 pub fn new() -> Self {
226 Self::default()
227 }
228 #[must_use]
229 pub const fn root(&self) -> Option<SemanticNodeId> {
230 self.root
231 }
232 pub fn set_root(&mut self, root: Option<SemanticNodeId>) {
233 if self.root != root {
234 self.root = root;
235 self.diagnostics.revisions = self.diagnostics.revisions.wrapping_add(1);
236 }
237 }
238 #[must_use]
239 pub fn node(&self, id: SemanticNodeId) -> Option<&SemanticNode> {
240 self.nodes.get(id.0).map(|retained| &retained.node)
241 }
242 #[must_use]
243 pub fn len(&self) -> usize {
244 self.nodes.len()
245 }
246 #[must_use]
247 pub fn is_empty(&self) -> bool {
248 self.nodes.is_empty()
249 }
250 #[must_use]
251 pub const fn diagnostics(&self) -> SemanticsDiagnostics {
252 self.diagnostics
253 }
254
255 #[must_use]
261 pub const fn revision(&self) -> u64 {
262 self.diagnostics.revisions
263 }
264
265 pub fn insert(&mut self, node: SemanticNode) -> SemanticNodeId {
266 self.diagnostics.revisions = self.diagnostics.revisions.wrapping_add(1);
267 let id = SemanticNodeId(self.nodes.insert(RetainedNode {
268 node,
269 changed_at: self.diagnostics.revisions,
270 }));
271 self.nodes.get_mut(id.0).expect("new semantic node").node.id = id;
272 self.diagnostics.nodes_created += 1;
273 id
274 }
275
276 pub fn update(&mut self, id: SemanticNodeId, node: SemanticNode) -> bool {
277 let Some(retained) = self.nodes.get_mut(id.0) else {
278 return false;
279 };
280 let old = &mut retained.node;
281 let properties_changed = old.role != node.role
282 || old.label != node.label
283 || old.value != node.value
284 || old.description != node.description
285 || old.state != node.state
286 || old.actions != node.actions
287 || old.children != node.children;
288 let geometry_changed = old.bounds != node.bounds;
289 if properties_changed || geometry_changed {
290 *old = node;
291 self.diagnostics.nodes_updated += 1;
292 self.diagnostics.revisions = self.diagnostics.revisions.wrapping_add(1);
293 retained.changed_at = self.diagnostics.revisions;
294 }
295 if properties_changed {
296 self.diagnostics.property_updates += 1;
297 }
298 if geometry_changed {
299 self.diagnostics.geometry_updates += 1;
300 }
301 true
302 }
303
304 pub fn update_geometry(
309 &mut self,
310 id: SemanticNodeId,
311 bounds: Rect,
312 children: &[SemanticNodeId],
313 ) -> bool {
314 let Some(retained) = self.nodes.get_mut(id.0) else {
315 return false;
316 };
317 let old = &mut retained.node;
318 let children_changed = old.children != children;
319 let geometry_changed = old.bounds != bounds;
320 if !children_changed && !geometry_changed {
321 return true;
322 }
323 old.bounds = bounds;
324 if children_changed {
325 old.children.clear();
326 old.children.extend_from_slice(children);
327 self.diagnostics.property_updates += 1;
328 }
329 if geometry_changed {
330 self.diagnostics.geometry_updates += 1;
331 }
332 self.diagnostics.nodes_updated += 1;
333 self.diagnostics.revisions = self.diagnostics.revisions.wrapping_add(1);
334 retained.changed_at = self.diagnostics.revisions;
335 true
336 }
337
338 pub fn remove(&mut self, id: SemanticNodeId) -> Option<SemanticNode> {
339 let node = self.nodes.remove(id.0)?.node;
340 self.diagnostics.nodes_removed += 1;
341 self.diagnostics.revisions = self.diagnostics.revisions.wrapping_add(1);
342 if self.root == Some(id) {
343 self.root = None;
344 }
345 Some(node)
346 }
347
348 pub fn note_action(&mut self) {
349 self.diagnostics.actions_received += 1;
350 }
351 pub fn note_native_update(&mut self) {
352 self.diagnostics.native_updates += 1;
353 }
354 pub fn iter(&self) -> impl Iterator<Item = (SemanticNodeId, &SemanticNode)> {
355 self.nodes
356 .iter()
357 .map(|(id, retained)| (SemanticNodeId(id), &retained.node))
358 }
359
360 pub fn changed_since(
366 &self,
367 revision: u64,
368 ) -> impl Iterator<Item = (SemanticNodeId, &SemanticNode)> {
369 self.nodes
370 .iter()
371 .filter(move |(_, retained)| retained.changed_at > revision)
372 .map(|(id, retained)| (SemanticNodeId(id), &retained.node))
373 }
374
375 #[must_use]
376 pub fn debug_dump(&self) -> String {
377 self.debug_dump_limited(usize::MAX, usize::MAX, usize::MAX)
378 }
379
380 #[must_use]
386 pub fn debug_dump_bounded(&self, max_nodes: usize) -> String {
387 self.debug_dump_limited(max_nodes, 256, 256)
388 }
389
390 #[must_use]
393 pub fn debug_dump_limited(
394 &self,
395 max_nodes: usize,
396 max_depth: usize,
397 max_text_chars: usize,
398 ) -> String {
399 fn truncate(value: Option<&String>, max_chars: usize) -> Option<String> {
400 let value = value?;
401 if value.chars().count() <= max_chars {
402 return Some(value.clone());
403 }
404 let mut result = value.chars().take(max_chars).collect::<String>();
405 result.push('…');
406 Some(result)
407 }
408
409 let mut output = String::new();
415 let mut parented = std::collections::HashSet::new();
416 for (_, node) in self.iter() {
417 parented.extend(node.children.iter().copied());
418 }
419 let mut extra: Vec<SemanticNodeId> = self
420 .iter()
421 .map(|(id, _)| id)
422 .filter(|id| Some(*id) != self.root && !parented.contains(id))
423 .collect();
424 extra.sort_by_key(|id| id.0);
425 let mut stack = Vec::new();
426 for id in extra.into_iter().rev() {
427 stack.push((id, 0usize));
428 }
429 if let Some(root) = self.root {
430 stack.push((root, 0usize));
431 }
432 let mut visited = 0usize;
433 while let Some((id, depth)) = stack.pop() {
434 if visited >= max_nodes {
435 output.push_str("… semantic dump truncated (node limit)\n");
436 break;
437 }
438 if depth > max_depth {
439 output.push_str(&format!(
440 "{}… semantic dump truncated (depth limit)\n",
441 " ".repeat(max_depth.min(256))
442 ));
443 continue;
444 }
445 let Some(node) = self.node(id) else { continue };
446 visited = visited.saturating_add(1);
447 output.push_str(&format!(
448 "{}{:?}#{:?} label={:?} value={:?} bounds=({:.1},{:.1},{:.1},{:.1}) focused={} actions={:?}\n",
449 " ".repeat(depth), node.role, node.id,
450 truncate(node.label.as_ref(), max_text_chars),
451 truncate(node.value.as_ref(), max_text_chars),
452 node.bounds.origin.x, node.bounds.origin.y, node.bounds.size.width,
453 node.bounds.size.height, node.state.focused, node.actions,
454 ));
455 for child in node.children.iter().rev() {
456 stack.push((*child, depth.saturating_add(1)));
457 }
458 }
459 output
460 }
461}