Skip to main content

repose_platform/
a11y.rs

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