Skip to main content

repose_platform/
a11y.rs

1use accesskit::{
2    Action, ActionHandler, ActionRequest, Node, NodeId, Rect, Role, Tree, TreeId, TreeUpdate,
3};
4use repose_core::runtime::SemNode;
5use repose_core::semantics::Role as CoreRole;
6use rustc_hash::FxHashMap;
7use std::hash::{Hash, Hasher};
8use std::sync::{Arc, Mutex};
9
10pub const WINDOW_ID: NodeId = NodeId(1);
11
12pub struct ReposeActionHandler {
13    pub pending_actions: Arc<Mutex<Vec<ActionRequest>>>,
14}
15
16impl ActionHandler for ReposeActionHandler {
17    fn do_action(&mut self, request: ActionRequest) {
18        let mut q = self.pending_actions.lock().unwrap();
19        q.push(request);
20    }
21}
22
23#[derive(Default)]
24pub struct A11yTree {
25    prev_hash: FxHashMap<u64, u64>,
26    prev_root_hash: u64,
27    prev_focus: Option<u64>,
28    initialized: bool,
29}
30
31impl A11yTree {
32    pub fn initial_tree() -> TreeUpdate {
33        let root = Node::new(Role::Window);
34        TreeUpdate {
35            nodes: vec![(WINDOW_ID, root)],
36            tree: Some(Tree::new(WINDOW_ID)),
37            focus: WINDOW_ID,
38            tree_id: TreeId::ROOT,
39        }
40    }
41
42    pub fn update(
43        &mut self,
44        sems: &[SemNode],
45        scale: f64,
46        focused_id: Option<u64>,
47    ) -> Option<TreeUpdate> {
48        let (root_children, children_map) = build_children(sems);
49
50        let mut changed_nodes: Vec<(NodeId, Node)> = Vec::new();
51
52        let focus = focused_id.map(NodeId).unwrap_or(WINDOW_ID);
53        let focus_changed = self.prev_focus != focused_id;
54        self.prev_focus = focused_id;
55
56        // root hash includes ordered root children + scale (bounds math depends on scale)
57        let root_hash = {
58            let mut h = rustc_hash::FxHasher::default();
59            (scale.to_bits()).hash(&mut h);
60            root_children.len().hash(&mut h);
61            for &id in &root_children {
62                id.hash(&mut h);
63            }
64            h.finish()
65        };
66
67        let root_changed = !self.initialized || self.prev_root_hash != root_hash;
68        self.prev_root_hash = root_hash;
69
70        if root_changed {
71            let mut root = Node::new(Role::Window);
72            root.set_children(
73                root_children
74                    .iter()
75                    .copied()
76                    .map(NodeId)
77                    .collect::<Vec<_>>(),
78            );
79            changed_nodes.push((WINDOW_ID, root));
80        }
81
82        let mut new_hashes: FxHashMap<u64, u64> = FxHashMap::default();
83
84        for sem in sems {
85            let kids = children_map
86                .get(&sem.id)
87                .map(|v| v.as_slice())
88                .unwrap_or(&[]);
89            let node_hash = hash_sem_node(sem, kids, scale);
90            new_hashes.insert(sem.id, node_hash);
91
92            let prev = self.prev_hash.get(&sem.id).copied();
93            let needs_update = !self.initialized || prev != Some(node_hash);
94
95            if needs_update {
96                let node = build_accesskit_node(sem, kids, scale);
97                changed_nodes.push((NodeId(sem.id), node));
98            }
99        }
100
101        // removals: drop old hashes for nodes not present anymore
102        // (AccessKit removal is implied by parent children updates; root/parent updates handle this)
103        self.prev_hash = new_hashes;
104        self.initialized = true;
105
106        if changed_nodes.is_empty() && !focus_changed {
107            return None;
108        }
109
110        Some(TreeUpdate {
111            nodes: changed_nodes,
112            tree: None,
113            focus,
114            tree_id: TreeId::ROOT,
115        })
116    }
117}
118
119fn build_children(sems: &[SemNode]) -> (Vec<u64>, FxHashMap<u64, Vec<u64>>) {
120    let mut roots: Vec<u64> = Vec::new();
121    let mut map: FxHashMap<u64, Vec<u64>> = FxHashMap::default();
122
123    for s in sems {
124        if let Some(p) = s.parent {
125            map.entry(p).or_default().push(s.id);
126        } else {
127            roots.push(s.id);
128        }
129    }
130
131    (roots, map)
132}
133
134fn build_accesskit_node(sem: &SemNode, children: &[u64], scale: f64) -> Node {
135    let mut node = Node::new(map_role(sem.role));
136
137    let r = sem.rect;
138    node.set_bounds(Rect {
139        x0: r.x as f64 / scale,
140        y0: r.y as f64 / scale,
141        x1: (r.x + r.w) as f64 / scale,
142        y1: (r.y + r.h) as f64 / scale,
143    });
144
145    if let Some(label) = &sem.label {
146        if !label.is_empty() {
147            node.set_label(label.clone());
148        }
149    }
150
151    if !children.is_empty() {
152        node.set_children(children.iter().copied().map(NodeId).collect::<Vec<_>>());
153    }
154
155    if sem.enabled {
156        match sem.role {
157            CoreRole::Button | CoreRole::Checkbox | CoreRole::Switch | CoreRole::RadioButton => {
158                node.add_action(Action::Click);
159            }
160            CoreRole::TextField | CoreRole::Slider => {
161                node.add_action(Action::Focus);
162            }
163            _ => {}
164        }
165    }
166
167    node
168}
169
170fn map_role(role: CoreRole) -> Role {
171    match role {
172        CoreRole::Text => Role::Label,
173        CoreRole::Button => Role::Button,
174        CoreRole::TextField => Role::TextInput,
175        CoreRole::Container => Role::GenericContainer,
176        CoreRole::Checkbox => Role::CheckBox,
177        CoreRole::RadioButton => Role::RadioButton,
178        CoreRole::Switch => Role::Switch,
179        CoreRole::Slider => Role::Slider,
180        CoreRole::ProgressBar => Role::ProgressIndicator,
181    }
182}
183
184fn hash_sem_node(sem: &SemNode, children: &[u64], scale: f64) -> u64 {
185    let mut h = rustc_hash::FxHasher::default();
186
187    (scale.to_bits()).hash(&mut h);
188
189    sem.id.hash(&mut h);
190    sem.parent.hash(&mut h);
191    std::mem::discriminant(&sem.role).hash(&mut h);
192
193    // quantize rect to reduce churn from tiny float noise
194    let q = |v: f32| (v * 8.0) as i32;
195    q(sem.rect.x).hash(&mut h);
196    q(sem.rect.y).hash(&mut h);
197    q(sem.rect.w).hash(&mut h);
198    q(sem.rect.h).hash(&mut h);
199
200    sem.focused.hash(&mut h);
201    sem.enabled.hash(&mut h);
202
203    if let Some(lbl) = &sem.label {
204        lbl.hash(&mut h);
205    }
206
207    children.len().hash(&mut h);
208    for &c in children {
209        c.hash(&mut h);
210    }
211
212    h.finish()
213}