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