1use accesskit::{
9 Action, ActionData, ActionRequest, Invalid, Live, Node, NodeId, Rect as AccessKitRect,
10 Role as AccessKitRole, TextPosition, TextSelection as AccessKitSelection, Toggled, Tree,
11 TreeId, TreeUpdate,
12};
13use incular_core::Rect;
14use std::collections::HashMap;
15
16pub use incular_semantics::*;
17
18const HOST_NODE_ID: NodeId = NodeId(0);
19
20#[derive(Clone, Debug, Default, PartialEq)]
22pub struct SemanticsSnapshot {
23 pub root: Option<SemanticNodeId>,
24 pub nodes: Vec<SemanticNode>,
25}
26
27impl From<&SemanticsTree> for SemanticsSnapshot {
28 fn from(tree: &SemanticsTree) -> Self {
29 Self {
30 root: tree.root(),
31 nodes: tree.iter().map(|(_, node)| node.clone()).collect(),
32 }
33 }
34}
35
36#[derive(Clone, Debug, PartialEq, Eq)]
38pub struct SemanticActionRequest {
39 pub node: SemanticNodeId,
40 pub action: SemanticAction,
41}
42
43pub trait SemanticsAdapter {
45 fn publish(&mut self, snapshot: SemanticsSnapshot);
46 fn poll_action(&mut self) -> Option<SemanticActionRequest>;
47}
48
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
52pub enum MobileAccessibilityEventKind {
53 ContentChanged,
54 BoundsChanged,
55 FocusChanged,
56 ValueChanged,
57 StateChanged,
58}
59
60#[derive(Clone, Debug, PartialEq, Eq)]
61pub struct MobileAccessibilityEvent {
62 pub native_id: u64,
63 pub kind: MobileAccessibilityEventKind,
64}
65
66#[derive(Clone, Debug, PartialEq)]
69pub struct MobileAccessibilityNode {
70 pub native_id: u64,
71 pub semantic: SemanticNode,
72 pub children: Vec<u64>,
73}
74
75#[derive(Clone, Debug, Default, PartialEq)]
76pub struct MobileAccessibilityUpdate {
77 pub revision: u64,
78 pub root: Option<u64>,
79 pub nodes: Vec<MobileAccessibilityNode>,
80 pub removed: Vec<u64>,
81 pub events: Vec<MobileAccessibilityEvent>,
82}
83
84pub struct MobileAccessibilityProjection {
88 semantic_to_native: HashMap<SemanticNodeId, u64>,
89 native_to_semantic: HashMap<u64, SemanticNodeId>,
90 previous: HashMap<SemanticNodeId, SemanticNode>,
91 previous_root: Option<SemanticNodeId>,
92 synced_revision: Option<u64>,
93 synced_removals: u64,
95 next_native_id: u64,
96 active: bool,
97 force_full_update: bool,
98 diagnostics: AccessibilityDiagnostics,
99}
100
101impl Default for MobileAccessibilityProjection {
102 fn default() -> Self {
103 Self::new()
104 }
105}
106
107impl MobileAccessibilityProjection {
108 #[must_use]
109 pub fn new() -> Self {
110 Self {
111 semantic_to_native: HashMap::new(),
112 native_to_semantic: HashMap::new(),
113 previous: HashMap::new(),
114 previous_root: None,
115 synced_revision: None,
116 synced_removals: 0,
117 next_native_id: 1,
118 active: true,
119 force_full_update: true,
120 diagnostics: AccessibilityDiagnostics::default(),
121 }
122 }
123
124 #[must_use]
125 pub fn diagnostics(&self) -> AccessibilityDiagnostics {
126 self.diagnostics
127 }
128
129 pub fn activate(&mut self) {
130 self.active = true;
131 self.force_full_update = true;
132 }
133
134 pub fn deactivate(&mut self) {
135 self.active = false;
136 self.force_full_update = true;
137 }
138
139 #[must_use]
140 pub fn native_node_id(&self, node: SemanticNodeId) -> Option<u64> {
141 self.semantic_to_native.get(&node).copied()
142 }
143
144 #[must_use]
145 pub fn semantic_node_id(&self, native_id: u64) -> Option<SemanticNodeId> {
146 self.native_to_semantic.get(&native_id).copied()
147 }
148
149 pub fn sync(&mut self, tree: &SemanticsTree) -> Option<MobileAccessibilityUpdate> {
151 if !self.active {
152 return None;
153 }
154 let revision = tree.revision();
155 if !self.force_full_update && self.synced_revision == Some(revision) {
156 return None;
157 }
158 let full = self.force_full_update;
159 let regressed = self.synced_revision.is_some_and(|synced| revision < synced);
163 let since = if full || regressed {
164 0
165 } else {
166 self.synced_revision.unwrap_or(0)
167 };
168 let mut changed = Vec::new();
169 for (id, node) in tree.changed_since(since) {
170 if !self.ensure_native_id(id) {
171 self.active = false;
172 self.force_full_update = true;
173 return None;
174 }
175 if full || self.previous.get(&id) != Some(node) {
176 changed.push(id);
177 }
178 }
179 changed.sort_by_key(|id| self.semantic_to_native[id]);
180 let removed = if !regressed && self.synced_removals == tree.diagnostics().nodes_removed {
181 Vec::new()
182 } else {
183 self.previous
184 .keys()
185 .filter(|id| tree.node(**id).is_none())
186 .copied()
187 .collect::<Vec<_>>()
188 };
189 let root = tree
190 .root()
191 .and_then(|id| self.semantic_to_native.get(&id).copied());
192 if !full && changed.is_empty() && removed.is_empty() && self.previous_root == tree.root() {
193 self.synced_revision = Some(revision);
194 self.synced_removals = tree.diagnostics().nodes_removed;
195 return None;
196 }
197
198 let mut nodes = Vec::with_capacity(changed.len());
199 let mut events = Vec::new();
200 for &id in &changed {
201 let Some(semantic) = tree.node(id) else {
202 continue;
203 };
204 let native_id = self.semantic_to_native[&id];
205 let children = semantic
206 .children
207 .iter()
208 .filter_map(|child| self.semantic_to_native.get(child).copied())
209 .collect();
210 nodes.push(MobileAccessibilityNode {
211 native_id,
212 semantic: mobile_semantic_node(semantic),
213 children,
214 });
215 if full || !self.previous.contains_key(&id) {
216 events.push(MobileAccessibilityEvent {
217 native_id,
218 kind: MobileAccessibilityEventKind::ContentChanged,
219 });
220 } else if let Some(old) = self.previous.get(&id) {
221 if old.state.focused != semantic.state.focused {
222 events.push(MobileAccessibilityEvent {
223 native_id,
224 kind: MobileAccessibilityEventKind::FocusChanged,
225 });
226 }
227 if old.value != semantic.value {
228 events.push(MobileAccessibilityEvent {
229 native_id,
230 kind: MobileAccessibilityEventKind::ValueChanged,
231 });
232 }
233 if old.state != semantic.state {
234 events.push(MobileAccessibilityEvent {
235 native_id,
236 kind: MobileAccessibilityEventKind::StateChanged,
237 });
238 }
239 if old.bounds != semantic.bounds {
240 events.push(MobileAccessibilityEvent {
241 native_id,
242 kind: MobileAccessibilityEventKind::BoundsChanged,
243 });
244 }
245 if old.role != semantic.role
246 || old.label != semantic.label
247 || old.description != semantic.description
248 || old.actions != semantic.actions
249 || old.children != semantic.children
250 {
251 events.push(MobileAccessibilityEvent {
252 native_id,
253 kind: MobileAccessibilityEventKind::ContentChanged,
254 });
255 }
256 }
257 }
258 let mut removed_native = removed
259 .iter()
260 .filter_map(|id| self.semantic_to_native.get(id).copied())
261 .collect::<Vec<_>>();
262 removed_native.sort_unstable();
263 for id in removed {
264 self.previous.remove(&id);
265 if let Some(native_id) = self.semantic_to_native.remove(&id) {
266 self.native_to_semantic.remove(&native_id);
267 }
268 }
269 if full {
270 self.previous.clear();
271 }
272 for id in changed {
273 if let Some(semantic) = tree.node(id) {
274 self.previous.insert(id, semantic.clone());
275 }
276 }
277 self.previous_root = tree.root();
278 self.synced_revision = Some(revision);
279 self.synced_removals = tree.diagnostics().nodes_removed;
280 self.force_full_update = false;
281 Some(MobileAccessibilityUpdate {
282 revision,
283 root,
284 nodes,
285 removed: removed_native,
286 events,
287 })
288 }
289
290 pub fn translate_action(
293 &mut self,
294 native_id: u64,
295 action: SemanticAction,
296 ) -> Option<SemanticActionRequest> {
297 self.diagnostics.actions_received = self.diagnostics.actions_received.wrapping_add(1);
298 if !self.active {
299 self.diagnostics.unsupported_actions =
300 self.diagnostics.unsupported_actions.wrapping_add(1);
301 return None;
302 }
303 let Some(node) = self.native_to_semantic.get(&native_id).copied() else {
304 self.diagnostics.stale_actions_rejected =
305 self.diagnostics.stale_actions_rejected.wrapping_add(1);
306 return None;
307 };
308 let Some(semantic) = self.previous.get(&node) else {
309 self.diagnostics.stale_actions_rejected =
310 self.diagnostics.stale_actions_rejected.wrapping_add(1);
311 return None;
312 };
313 if !semantic.actions.contains(&action.kind()) {
314 self.diagnostics.unsupported_actions =
315 self.diagnostics.unsupported_actions.wrapping_add(1);
316 return None;
317 }
318 self.diagnostics.actions_dispatched = self.diagnostics.actions_dispatched.wrapping_add(1);
319 Some(SemanticActionRequest { node, action })
320 }
321
322 fn ensure_native_id(&mut self, node: SemanticNodeId) -> bool {
323 if self.semantic_to_native.contains_key(&node) {
324 return true;
325 }
326 let native_id = self.next_native_id;
327 let Some(next_native_id) = self.next_native_id.checked_add(1) else {
328 return false;
329 };
330 self.next_native_id = next_native_id;
331 self.semantic_to_native.insert(node, native_id);
332 self.native_to_semantic.insert(native_id, node);
333 true
334 }
335}
336
337fn mobile_semantic_node(node: &SemanticNode) -> SemanticNode {
340 let mut node = node.clone();
341 if node.state.obscured {
342 node.value = None;
343 }
344 node
345}
346
347#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
350pub struct AccessibilityDiagnostics {
351 pub adapters_created: u64,
352 pub adapters_destroyed: u64,
353 pub adapter_activations: u64,
354 pub adapter_deactivations: u64,
355 pub full_tree_updates: u64,
356 pub incremental_tree_updates: u64,
357 pub nodes_published: u64,
358 pub nodes_updated: u64,
359 pub nodes_removed: u64,
360 pub actions_received: u64,
361 pub actions_dispatched: u64,
362 pub stale_actions_rejected: u64,
363 pub unsupported_actions: u64,
364 pub focus_updates: u64,
365 pub bounds_updates: u64,
366 pub semantic_updates_skipped_unchanged: u64,
367 pub accesskit_conversion_failures: u64,
368}
369
370#[derive(Clone, Copy, Debug, PartialEq, Eq)]
371pub enum AccessKitUpdateKind {
372 Full,
373 Incremental,
374}
375
376#[derive(Debug)]
378pub struct NativeAccessibilityUpdate {
379 kind: AccessKitUpdateKind,
380 update: TreeUpdate,
381}
382
383impl NativeAccessibilityUpdate {
384 #[must_use]
385 pub const fn kind(&self) -> AccessKitUpdateKind {
386 self.kind
387 }
388
389 #[doc(hidden)]
392 #[must_use]
393 pub fn into_accesskit(self) -> TreeUpdate {
394 self.update
395 }
396}
397
398#[derive(Clone, Debug, PartialEq)]
399struct ProjectedNode {
400 semantic: SemanticNode,
401 scale_factor: f64,
402}
403
404#[derive(Clone, Copy, Debug, PartialEq)]
405struct HostProjection {
406 root: Option<SemanticNodeId>,
407 bounds: Option<Rect>,
408 scale_factor: f64,
409}
410
411pub struct AccessKitProjection {
414 semantic_to_native: HashMap<SemanticNodeId, NodeId>,
415 native_to_semantic: HashMap<NodeId, SemanticNodeId>,
416 previous: HashMap<SemanticNodeId, ProjectedNode>,
417 host: Option<HostProjection>,
418 focus: NodeId,
420 synced_revision: Option<u64>,
421 synced_removals: u64,
424 next_native_id: u64,
425 force_full_update: bool,
426 diagnostics: AccessibilityDiagnostics,
427}
428
429impl Default for AccessKitProjection {
430 fn default() -> Self {
431 Self::new()
432 }
433}
434
435impl AccessKitProjection {
436 #[must_use]
437 pub fn new() -> Self {
438 Self {
439 semantic_to_native: HashMap::new(),
440 native_to_semantic: HashMap::new(),
441 previous: HashMap::new(),
442 host: None,
443 focus: HOST_NODE_ID,
444 synced_revision: None,
445 synced_removals: 0,
446 next_native_id: 1,
447 force_full_update: true,
448 diagnostics: AccessibilityDiagnostics::default(),
449 }
450 }
451
452 #[must_use]
453 pub const fn diagnostics(&self) -> AccessibilityDiagnostics {
454 self.diagnostics
455 }
456
457 #[doc(hidden)]
459 #[must_use]
460 pub fn native_node_id(&self, node: SemanticNodeId) -> Option<u64> {
461 self.semantic_to_native.get(&node).map(|id| id.0)
462 }
463
464 pub fn note_adapter_created(&mut self) {
465 self.diagnostics.adapters_created = self.diagnostics.adapters_created.wrapping_add(1);
466 }
467
468 pub fn note_adapter_destroyed(&mut self) {
469 self.diagnostics.adapters_destroyed = self.diagnostics.adapters_destroyed.wrapping_add(1);
470 }
471
472 pub fn activate(&mut self) {
474 self.diagnostics.adapter_activations = self.diagnostics.adapter_activations.wrapping_add(1);
475 self.force_full_update = true;
476 }
477
478 pub fn deactivate(&mut self) {
482 self.diagnostics.adapter_deactivations =
483 self.diagnostics.adapter_deactivations.wrapping_add(1);
484 self.force_full_update = true;
485 }
486
487 pub fn note_runtime_stale_action(&mut self) {
491 self.diagnostics.stale_actions_rejected =
492 self.diagnostics.stale_actions_rejected.wrapping_add(1);
493 }
494
495 pub fn sync(
498 &mut self,
499 tree: &SemanticsTree,
500 scale_factor: f64,
501 ) -> Option<NativeAccessibilityUpdate> {
502 let scale_factor = if scale_factor.is_finite() && scale_factor > 0.0 {
503 scale_factor
504 } else {
505 1.0
506 };
507 let revision = tree.revision();
508 if !self.force_full_update && self.synced_revision == Some(revision) {
509 self.diagnostics.semantic_updates_skipped_unchanged = self
510 .diagnostics
511 .semantic_updates_skipped_unchanged
512 .wrapping_add(1);
513 return None;
514 }
515
516 let full = self.force_full_update || self.synced_revision.is_none();
517 let rescaled = self
522 .host
523 .is_some_and(|host| host.scale_factor != scale_factor);
524 let regressed = self.synced_revision.is_some_and(|synced| revision < synced);
525 let since = if full || rescaled || regressed {
526 0
527 } else {
528 self.synced_revision.unwrap_or(0)
529 };
530 let mut changed = Vec::new();
531 for (id, semantic) in tree.changed_since(since) {
532 if !self.ensure_native_id(id) {
533 self.force_full_update = true;
534 return None;
535 }
536 if full
537 || self
538 .previous
539 .get(&id)
540 .is_none_or(|old| old.scale_factor != scale_factor || old.semantic != *semantic)
541 {
542 changed.push(id);
543 }
544 }
545 let next_host = HostProjection {
546 root: tree.root(),
547 bounds: tree
548 .root()
549 .and_then(|root| tree.node(root))
550 .map(|node| node.bounds),
551 scale_factor,
552 };
553 let removed: Vec<_> =
554 if !regressed && self.synced_removals == tree.diagnostics().nodes_removed {
555 Vec::new()
556 } else {
557 self.previous
558 .keys()
559 .copied()
560 .filter(|id| tree.node(*id).is_none())
561 .collect()
562 };
563 let host_changed = full || self.host != Some(next_host);
564 if !full && changed.is_empty() && removed.is_empty() && !host_changed {
565 self.synced_revision = Some(revision);
566 self.synced_removals = tree.diagnostics().nodes_removed;
567 self.diagnostics.semantic_updates_skipped_unchanged = self
568 .diagnostics
569 .semantic_updates_skipped_unchanged
570 .wrapping_add(1);
571 return None;
572 }
573
574 let old_focus = self.focus;
575 let mut nodes = Vec::with_capacity(changed.len() + usize::from(host_changed));
576 if host_changed {
577 nodes.push((HOST_NODE_ID, self.host_node(next_host)));
578 }
579 changed.sort_by_key(|id| self.semantic_to_native[id].0);
580 for &id in &changed {
581 let semantic = tree.node(id).expect("changed node is current");
582 nodes.push((
583 self.semantic_to_native[&id],
584 self.accesskit_node(semantic, scale_factor),
585 ));
586 }
587 let focus = tree
588 .iter()
589 .find_map(|(id, node)| node.state.focused.then_some(id))
590 .and_then(|id| self.semantic_to_native.get(&id).copied())
591 .unwrap_or(HOST_NODE_ID);
592
593 self.diagnostics.focus_updates = self
594 .diagnostics
595 .focus_updates
596 .wrapping_add(u64::from(focus != old_focus));
597 self.diagnostics.bounds_updates = self.diagnostics.bounds_updates.wrapping_add(
598 changed
599 .iter()
600 .filter(|id| {
601 self.previous.get(id).is_some_and(|old| {
602 tree.node(**id).is_some_and(|semantic| {
603 old.semantic.bounds != semantic.bounds
604 || old.scale_factor != scale_factor
605 })
606 })
607 })
608 .count() as u64,
609 );
610 if full {
611 self.diagnostics.full_tree_updates = self.diagnostics.full_tree_updates.wrapping_add(1);
612 self.diagnostics.nodes_published = self
613 .diagnostics
614 .nodes_published
615 .wrapping_add(tree.len() as u64 + 1);
616 } else {
617 self.diagnostics.incremental_tree_updates =
618 self.diagnostics.incremental_tree_updates.wrapping_add(1);
619 self.diagnostics.nodes_updated = self
620 .diagnostics
621 .nodes_updated
622 .wrapping_add(nodes.len().saturating_sub(usize::from(host_changed)) as u64);
623 self.diagnostics.nodes_removed = self
624 .diagnostics
625 .nodes_removed
626 .wrapping_add(removed.len() as u64);
627 }
628 for id in removed {
629 self.previous.remove(&id);
630 if let Some(native) = self.semantic_to_native.remove(&id) {
631 self.native_to_semantic.remove(&native);
632 }
633 }
634 if full {
635 self.previous.clear();
636 }
637 for id in changed {
638 let semantic = tree.node(id).expect("changed node is current").clone();
639 self.previous.insert(
640 id,
641 ProjectedNode {
642 semantic,
643 scale_factor,
644 },
645 );
646 }
647 self.host = Some(next_host);
648 self.focus = focus;
649 self.synced_revision = Some(revision);
650 self.synced_removals = tree.diagnostics().nodes_removed;
651 self.force_full_update = false;
652
653 Some(NativeAccessibilityUpdate {
654 kind: if full {
655 AccessKitUpdateKind::Full
656 } else {
657 AccessKitUpdateKind::Incremental
658 },
659 update: TreeUpdate {
660 nodes,
661 tree: full.then(|| {
662 let mut tree = Tree::new(HOST_NODE_ID);
663 tree.toolkit_name = Some("Incular".into());
664 tree.toolkit_version = Some(env!("CARGO_PKG_VERSION").into());
665 tree
666 }),
667 tree_id: TreeId::ROOT,
668 focus,
669 },
670 })
671 }
672
673 pub fn translate_action(&mut self, request: &ActionRequest) -> Option<SemanticActionRequest> {
676 self.diagnostics.actions_received = self.diagnostics.actions_received.wrapping_add(1);
677 if request.target_tree != TreeId::ROOT {
678 self.diagnostics.unsupported_actions =
679 self.diagnostics.unsupported_actions.wrapping_add(1);
680 return None;
681 }
682 let Some(node) = self.native_to_semantic.get(&request.target_node).copied() else {
683 self.diagnostics.stale_actions_rejected =
684 self.diagnostics.stale_actions_rejected.wrapping_add(1);
685 return None;
686 };
687 let Some(projected) = self.previous.get(&node) else {
688 self.diagnostics.stale_actions_rejected =
689 self.diagnostics.stale_actions_rejected.wrapping_add(1);
690 return None;
691 };
692 let action = match request.action {
693 Action::Click if supports(&projected.semantic, SemanticActionKind::Activate) => {
694 Some(SemanticAction::Activate)
695 }
696 Action::Focus if supports(&projected.semantic, SemanticActionKind::Focus) => {
697 Some(SemanticAction::Focus)
698 }
699 Action::ScrollDown | Action::ScrollRight
700 if supports(&projected.semantic, SemanticActionKind::ScrollForward) =>
701 {
702 Some(SemanticAction::ScrollForward)
703 }
704 Action::ScrollUp | Action::ScrollLeft
705 if supports(&projected.semantic, SemanticActionKind::ScrollBackward) =>
706 {
707 Some(SemanticAction::ScrollBackward)
708 }
709 Action::SetValue | Action::ReplaceSelectedText
710 if supports(&projected.semantic, SemanticActionKind::SetText) =>
711 {
712 match request.data.as_ref() {
713 Some(ActionData::Value(value)) => {
714 Some(SemanticAction::SetText(value.to_string()))
715 }
716 _ => None,
717 }
718 }
719 Action::SetTextSelection
720 if supports(&projected.semantic, SemanticActionKind::SetSelection) =>
721 {
722 match request.data.as_ref() {
723 Some(ActionData::SetTextSelection(selection))
724 if selection.anchor.node == request.target_node
725 && selection.focus.node == request.target_node =>
726 {
727 let text = projected.semantic.value.as_deref().unwrap_or_default();
728 Some(SemanticAction::SetSelection {
729 base: utf8_offset(text, selection.anchor.character_index),
730 extent: utf8_offset(text, selection.focus.character_index),
731 })
732 }
733 _ => None,
734 }
735 }
736 Action::Increment if supports(&projected.semantic, SemanticActionKind::Increment) => {
737 Some(SemanticAction::Increment)
738 }
739 Action::Decrement if supports(&projected.semantic, SemanticActionKind::Decrement) => {
740 Some(SemanticAction::Decrement)
741 }
742 _ => None,
743 };
744 let Some(action) = action else {
745 self.diagnostics.unsupported_actions =
746 self.diagnostics.unsupported_actions.wrapping_add(1);
747 return None;
748 };
749 self.diagnostics.actions_dispatched = self.diagnostics.actions_dispatched.wrapping_add(1);
750 Some(SemanticActionRequest { node, action })
751 }
752
753 fn ensure_native_id(&mut self, node: SemanticNodeId) -> bool {
754 if self.semantic_to_native.contains_key(&node) {
755 return true;
756 }
757 let native = NodeId(self.next_native_id);
758 let Some(next_native_id) = self.next_native_id.checked_add(1) else {
759 return false;
760 };
761 self.next_native_id = next_native_id;
762 self.semantic_to_native.insert(node, native);
763 self.native_to_semantic.insert(native, node);
764 true
765 }
766
767 fn host_node(&self, host: HostProjection) -> Node {
768 let mut node = Node::new(AccessKitRole::Window);
769 node.set_label("Incular");
770 node.set_children(
771 host.root
772 .and_then(|id| self.semantic_to_native.get(&id).copied())
773 .into_iter()
774 .collect::<Vec<_>>(),
775 );
776 if let Some(bounds) = host.bounds {
777 node.set_bounds(to_accesskit_rect(bounds, host.scale_factor));
778 }
779 node
780 }
781
782 fn accesskit_node(&self, semantic: &SemanticNode, scale_factor: f64) -> Node {
783 let native_id = self.semantic_to_native[&semantic.id];
784 let mut node = Node::new(map_role(semantic.role, semantic.state.obscured));
785 node.set_bounds(to_accesskit_rect(semantic.bounds, scale_factor));
786 if semantic.role == Role::Text && !semantic.state.obscured {
787 if let Some(text) = semantic.value.as_ref().or(semantic.label.as_ref()) {
788 node.set_value(text.as_str());
789 }
790 } else if !semantic.state.obscured {
791 if let Some(label) = &semantic.label {
792 node.set_label(label.as_str());
793 }
794 if let Some(value) = &semantic.value {
795 node.set_value(value.as_str());
796 }
797 } else if let Some(label) = &semantic.label {
798 node.set_label(label.as_str());
801 }
802 if let Some(description) = &semantic.description {
803 node.set_description(description.as_str());
804 }
805 if is_interactive_role(semantic.role) && !semantic.state.enabled {
806 node.set_disabled();
807 }
808 if semantic.state.selected {
809 node.set_selected(true);
810 }
811 if let Some(checked) = semantic.state.checked {
812 node.set_toggled(match checked {
813 incular_semantics::CheckedState::Checked => Toggled::True,
814 incular_semantics::CheckedState::Unchecked => Toggled::False,
815 incular_semantics::CheckedState::Indeterminate => Toggled::Mixed,
816 });
817 }
818 if semantic.state.checked.is_none()
819 && let Some(toggled) = semantic.state.toggled
820 {
821 node.set_toggled(if toggled {
822 Toggled::True
823 } else {
824 Toggled::False
825 });
826 }
827 if let Some(expanded) = semantic.state.expanded {
828 node.set_expanded(expanded);
829 }
830 if semantic.state.hidden {
831 node.set_hidden();
832 }
833 if semantic.state.live_region {
834 node.set_live(Live::Polite);
835 }
836 if let Some(tooltip) = &semantic.state.tooltip {
837 node.set_tooltip(tooltip.as_str());
838 }
839 if let Some(url) = &semantic.state.link_url {
840 node.set_url(url.as_str());
841 }
842 if let Some(identifier) = &semantic.state.identifier {
843 node.set_author_id(identifier.as_str());
844 }
845 if semantic.state.busy {
846 node.set_busy();
847 }
848 if semantic.state.invalid {
849 node.set_invalid(Invalid::True);
850 }
851 if semantic.state.required {
852 node.set_required();
853 }
854 if let Some(level) = semantic.state.heading_level {
855 node.set_level(usize::from(level));
856 }
857 if let Some(value) = semantic
858 .state
859 .numeric_value
860 .filter(|value| value.is_finite())
861 {
862 node.set_numeric_value(value);
863 }
864 if let Some(value) = semantic.state.numeric_min.filter(|value| value.is_finite()) {
865 node.set_min_numeric_value(value);
866 }
867 if let Some(value) = semantic.state.numeric_max.filter(|value| value.is_finite()) {
868 node.set_max_numeric_value(value);
869 }
870 if let Some(value) = semantic
871 .state
872 .numeric_step
873 .filter(|value| value.is_finite())
874 {
875 node.set_numeric_value_step(value);
876 }
877 if semantic.state.read_only {
878 node.set_read_only();
879 }
880 if let Some(size) = semantic.state.set_size {
881 node.set_size_of_set(size);
882 }
883 if let Some(index) = semantic.state.item_index {
884 node.set_position_in_set(index.saturating_add(1));
885 }
886 if matches!(semantic.role, Role::ScrollView | Role::List) {
887 let (offset, maximum) = scroll_value(semantic.value.as_deref());
888 node.set_scroll_y(offset);
889 node.set_scroll_y_min(0.0);
890 node.set_scroll_y_max(maximum);
891 }
892 if semantic.state.editable
893 && !semantic.state.obscured
894 && let Some(value) = semantic.value.as_deref()
895 {
896 node.set_character_lengths(
897 value
898 .chars()
899 .map(|character| character.len_utf8() as u8)
900 .collect::<Vec<_>>(),
901 );
902 if let Some(selection) = semantic.state.selection {
903 node.set_text_selection(AccessKitSelection {
904 anchor: TextPosition {
905 node: native_id,
906 character_index: character_offset(value, selection.base),
907 },
908 focus: TextPosition {
909 node: native_id,
910 character_index: character_offset(value, selection.extent),
911 },
912 });
913 }
914 }
915 for action in &semantic.actions {
916 if let Some(action) = map_action(*action) {
917 node.add_action(action);
918 }
919 }
920 node.set_children(
921 semantic
922 .children
923 .iter()
924 .filter_map(|child| self.semantic_to_native.get(child).copied())
925 .collect::<Vec<_>>(),
926 );
927 node
928 }
929}
930
931fn supports(node: &SemanticNode, action: SemanticActionKind) -> bool {
932 node.actions.contains(&action)
933}
934
935fn map_role(role: Role, obscured: bool) -> AccessKitRole {
936 match role {
937 Role::Button => AccessKitRole::Button,
938 Role::Text => AccessKitRole::Label,
939 Role::TextField => {
940 if obscured {
941 AccessKitRole::PasswordInput
942 } else {
943 AccessKitRole::TextInput
944 }
945 }
946 Role::TextArea => AccessKitRole::MultilineTextInput,
947 Role::List => AccessKitRole::List,
948 Role::ListItem => AccessKitRole::ListItem,
949 Role::ScrollView => AccessKitRole::ScrollView,
950 Role::GenericContainer => AccessKitRole::GenericContainer,
951 Role::Checkbox => AccessKitRole::CheckBox,
952 Role::Radio => AccessKitRole::RadioButton,
953 Role::Switch => AccessKitRole::Switch,
954 Role::Slider => AccessKitRole::Slider,
955 Role::Menu => AccessKitRole::Menu,
956 Role::Dialog => AccessKitRole::Dialog,
957 Role::Image => AccessKitRole::Image,
958 Role::Heading => AccessKitRole::Heading,
959 Role::Link => AccessKitRole::Link,
960 Role::Group => AccessKitRole::Group,
961 Role::Tab => AccessKitRole::Tab,
962 Role::TabList => AccessKitRole::TabList,
963 Role::TabPanel => AccessKitRole::TabPanel,
964 Role::MenuItem => AccessKitRole::MenuItem,
965 Role::ProgressBar => AccessKitRole::ProgressIndicator,
966 Role::Meter => AccessKitRole::Meter,
967 Role::SearchField => AccessKitRole::SearchInput,
968 }
969}
970
971fn is_interactive_role(role: Role) -> bool {
972 matches!(
973 role,
974 Role::Button
975 | Role::TextField
976 | Role::TextArea
977 | Role::Checkbox
978 | Role::Radio
979 | Role::Switch
980 | Role::Slider
981 | Role::Link
982 | Role::Tab
983 | Role::MenuItem
984 | Role::SearchField
985 )
986}
987
988fn map_action(action: SemanticActionKind) -> Option<Action> {
989 match action {
990 SemanticActionKind::Focus => Some(Action::Focus),
991 SemanticActionKind::Activate => Some(Action::Click),
992 SemanticActionKind::SetText => Some(Action::SetValue),
993 SemanticActionKind::SetSelection => Some(Action::SetTextSelection),
994 SemanticActionKind::Increment => Some(Action::Increment),
995 SemanticActionKind::Decrement => Some(Action::Decrement),
996 SemanticActionKind::ScrollForward => Some(Action::ScrollDown),
997 SemanticActionKind::ScrollBackward => Some(Action::ScrollUp),
998 }
999}
1000
1001fn to_accesskit_rect(rect: Rect, scale_factor: f64) -> AccessKitRect {
1002 AccessKitRect {
1003 x0: f64::from(rect.origin.x) * scale_factor,
1004 y0: f64::from(rect.origin.y) * scale_factor,
1005 x1: f64::from(rect.origin.x + rect.size.width) * scale_factor,
1006 y1: f64::from(rect.origin.y + rect.size.height) * scale_factor,
1007 }
1008}
1009
1010fn scroll_value(value: Option<&str>) -> (f64, f64) {
1011 let Some((offset, maximum)) = value.and_then(|value| value.split_once('/')) else {
1012 return (0.0, 0.0);
1013 };
1014 let parse = |value: &str| {
1015 value
1016 .parse::<f64>()
1017 .ok()
1018 .filter(|value| value.is_finite())
1019 .unwrap_or(0.0)
1020 .max(0.0)
1021 };
1022 (parse(offset), parse(maximum))
1023}
1024
1025fn character_offset(text: &str, byte_offset: usize) -> usize {
1026 let mut boundary = byte_offset.min(text.len());
1027 while boundary > 0 && !text.is_char_boundary(boundary) {
1028 boundary -= 1;
1029 }
1030 text[..boundary].chars().count()
1031}
1032
1033fn utf8_offset(text: &str, character_offset: usize) -> usize {
1034 text.char_indices()
1035 .nth(character_offset)
1036 .map_or(text.len(), |(offset, _)| offset)
1037}