1use crate::tree::NodeId;
16use crate::platform::NativeHandle;
17use std::collections::HashMap;
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum AccessibilityRole {
22 None,
24 Button,
26 Text,
28 Heading,
30 TextField,
32 Image,
34 Switch,
36 Adjustable,
38 Link,
40 SearchField,
42 TabBar,
44 Tab,
46 List,
48 ListItem,
50 Alert,
52 ProgressBar,
54 Menu,
56 MenuItem,
58}
59
60#[derive(Debug, Clone, Default)]
62pub struct AccessibilityState {
63 pub disabled: bool,
64 pub selected: bool,
65 pub checked: Option<bool>, pub expanded: Option<bool>, pub busy: bool,
68}
69
70#[derive(Debug, Clone, Default)]
72pub struct AccessibilityValue {
73 pub min: Option<f64>,
74 pub max: Option<f64>,
75 pub now: Option<f64>,
76 pub text: Option<String>, }
78
79#[derive(Debug, Clone)]
81pub struct AccessibilityInfo {
82 pub role: AccessibilityRole,
83 pub label: Option<String>, pub hint: Option<String>, pub state: AccessibilityState,
86 pub value: AccessibilityValue,
87 pub heading_level: Option<u8>, pub live_region: LiveRegion, pub actions: Vec<AccessibilityAction>,
90 pub is_modal: bool, pub hides_descendants: bool, }
93
94impl Default for AccessibilityInfo {
95 fn default() -> Self {
96 Self {
97 role: AccessibilityRole::None,
98 label: None,
99 hint: None,
100 state: AccessibilityState::default(),
101 value: AccessibilityValue::default(),
102 heading_level: None,
103 live_region: LiveRegion::Off,
104 actions: Vec::new(),
105 is_modal: false,
106 hides_descendants: false,
107 }
108 }
109}
110
111#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
113pub enum LiveRegion {
114 #[default]
116 Off,
117 Polite,
119 Assertive,
121}
122
123#[derive(Debug, Clone)]
125pub struct AccessibilityAction {
126 pub name: String,
127 pub label: String, }
129
130pub struct FocusManager {
136 focused: Option<NodeId>,
138
139 focus_order: Vec<NodeId>,
141
142 focus_traps: Vec<NodeId>,
144
145 focusable: HashMap<NodeId, FocusConfig>,
147}
148
149#[derive(Debug, Clone)]
150pub struct FocusConfig {
151 pub tab_index: i32,
153 pub auto_focus: bool,
155}
156
157impl FocusManager {
158 pub fn new() -> Self {
159 Self {
160 focused: None,
161 focus_order: Vec::new(),
162 focus_traps: Vec::new(),
163 focusable: HashMap::new(),
164 }
165 }
166
167 pub fn register(&mut self, node_id: NodeId, config: FocusConfig) {
169 if config.tab_index >= 0 {
170 if config.tab_index > 0 {
172 let pos = self.focus_order.iter()
174 .position(|&id| {
175 self.focusable.get(&id)
176 .map(|c| c.tab_index > config.tab_index)
177 .unwrap_or(true)
178 })
179 .unwrap_or(self.focus_order.len());
180 self.focus_order.insert(pos, node_id);
181 } else {
182 self.focus_order.push(node_id);
184 }
185 }
186 self.focusable.insert(node_id, config);
187 }
188
189 pub fn unregister(&mut self, node_id: NodeId) {
191 self.focusable.remove(&node_id);
192 self.focus_order.retain(|&id| id != node_id);
193 if self.focused == Some(node_id) {
194 self.focused = None;
195 }
196 }
197
198 pub fn focus(&mut self, node_id: NodeId) -> Option<FocusChange> {
200 let previous = self.focused;
201 self.focused = Some(node_id);
202
203 Some(FocusChange {
204 previous,
205 current: node_id,
206 })
207 }
208
209 pub fn focus_next(&mut self) -> Option<FocusChange> {
211 let candidates = self.get_candidates();
212 if candidates.is_empty() { return None; }
213
214 let current_index = self.focused
215 .and_then(|id| candidates.iter().position(|&c| c == id))
216 .unwrap_or(candidates.len().wrapping_sub(1));
217
218 let next_index = (current_index + 1) % candidates.len();
219 self.focus(candidates[next_index])
220 }
221
222 pub fn focus_previous(&mut self) -> Option<FocusChange> {
224 let candidates = self.get_candidates();
225 if candidates.is_empty() { return None; }
226
227 let current_index = self.focused
228 .and_then(|id| candidates.iter().position(|&c| c == id))
229 .unwrap_or(0);
230
231 let prev_index = if current_index == 0 {
232 candidates.len() - 1
233 } else {
234 current_index - 1
235 };
236
237 self.focus(candidates[prev_index])
238 }
239
240 pub fn push_trap(&mut self, root_node: NodeId) {
242 self.focus_traps.push(root_node);
243 }
244
245 pub fn pop_trap(&mut self) -> Option<NodeId> {
247 self.focus_traps.pop()
248 }
249
250 pub fn focused(&self) -> Option<NodeId> {
252 self.focused
253 }
254
255 fn get_candidates(&self) -> Vec<NodeId> {
257 if let Some(&_trap_root) = self.focus_traps.last() {
258 self.focus_order.clone()
262 } else {
263 self.focus_order.clone()
264 }
265 }
266}
267
268#[derive(Debug, Clone)]
269pub struct FocusChange {
270 pub previous: Option<NodeId>,
271 pub current: NodeId,
272}
273
274pub trait AccessibilityBridge {
280 fn update_accessibility(
282 &self,
283 handle: NativeHandle,
284 info: &AccessibilityInfo,
285 );
286
287 fn announce(&self, message: &str, priority: LiveRegion);
289
290 fn set_accessibility_focus(&self, handle: NativeHandle);
292}
293
294impl AccessibilityRole {
297 pub fn ios_traits(&self) -> &'static str {
299 match self {
300 Self::None => "none",
301 Self::Button => "button",
302 Self::Text => "staticText",
303 Self::Heading => "header",
304 Self::TextField => "none", Self::Image => "image",
306 Self::Switch => "button", Self::Adjustable => "adjustable",
308 Self::Link => "link",
309 Self::SearchField => "searchField",
310 Self::TabBar => "tabBar",
311 Self::Tab => "button", Self::List => "none", Self::ListItem => "none",
314 Self::Alert => "none", Self::ProgressBar => "none", Self::Menu => "none",
317 Self::MenuItem => "button",
318 }
319 }
320
321 pub fn aria_role(&self) -> &'static str {
323 match self {
324 Self::None => "presentation",
325 Self::Button => "button",
326 Self::Text => "", Self::Heading => "heading",
328 Self::TextField => "textbox",
329 Self::Image => "img",
330 Self::Switch => "switch",
331 Self::Adjustable => "slider",
332 Self::Link => "link",
333 Self::SearchField => "searchbox",
334 Self::TabBar => "tablist",
335 Self::Tab => "tab",
336 Self::List => "list",
337 Self::ListItem => "listitem",
338 Self::Alert => "alert",
339 Self::ProgressBar => "progressbar",
340 Self::Menu => "menu",
341 Self::MenuItem => "menuitem",
342 }
343 }
344
345 pub fn android_class(&self) -> &'static str {
347 match self {
348 Self::None => "android.view.View",
349 Self::Button => "android.widget.Button",
350 Self::Text => "android.widget.TextView",
351 Self::Heading => "android.widget.TextView", Self::TextField => "android.widget.EditText",
353 Self::Image => "android.widget.ImageView",
354 Self::Switch => "android.widget.Switch",
355 Self::Adjustable => "android.widget.SeekBar",
356 Self::Link => "android.widget.TextView", Self::SearchField => "android.widget.EditText",
358 Self::TabBar => "android.widget.TabWidget",
359 Self::Tab => "android.widget.TabWidget",
360 Self::List => "android.widget.ListView",
361 Self::ListItem => "android.widget.ListView",
362 Self::Alert => "android.app.AlertDialog",
363 Self::ProgressBar => "android.widget.ProgressBar",
364 Self::Menu => "android.widget.PopupMenu",
365 Self::MenuItem => "android.widget.PopupMenu",
366 }
367 }
368}
369
370#[cfg(test)]
371mod tests {
372 use super::*;
373
374 #[test]
375 fn test_focus_cycle() {
376 let mut fm = FocusManager::new();
377 fm.register(NodeId(1), FocusConfig { tab_index: 0, auto_focus: false });
378 fm.register(NodeId(2), FocusConfig { tab_index: 0, auto_focus: false });
379 fm.register(NodeId(3), FocusConfig { tab_index: 0, auto_focus: false });
380
381 let c = fm.focus_next().unwrap();
383 assert_eq!(c.current, NodeId(1));
384
385 let c = fm.focus_next().unwrap();
386 assert_eq!(c.current, NodeId(2));
387
388 let c = fm.focus_next().unwrap();
389 assert_eq!(c.current, NodeId(3));
390
391 let c = fm.focus_next().unwrap();
393 assert_eq!(c.current, NodeId(1));
394 }
395
396 #[test]
397 fn test_focus_reverse() {
398 let mut fm = FocusManager::new();
399 fm.register(NodeId(1), FocusConfig { tab_index: 0, auto_focus: false });
400 fm.register(NodeId(2), FocusConfig { tab_index: 0, auto_focus: false });
401
402 fm.focus(NodeId(2));
403 let c = fm.focus_previous().unwrap();
404 assert_eq!(c.current, NodeId(1));
405 }
406
407 #[test]
408 fn test_role_mappings() {
409 assert_eq!(AccessibilityRole::Button.aria_role(), "button");
410 assert_eq!(AccessibilityRole::Switch.ios_traits(), "button");
411 assert_eq!(AccessibilityRole::TextField.android_class(), "android.widget.EditText");
412 }
413}