ravenrs_widgets/ui/
access.rs1use std::collections::HashMap;
5
6use ravenrs_core::{AccessAction, AccessNode, AccessTree, Key, Modifiers, Point, Rect, Role};
7
8use super::Ui;
9use crate::context::{NodeId, WidgetId};
10
11const ROOT_ID: u64 = 1;
13
14#[derive(Debug, Default)]
16pub(crate) struct AccessMap {
17 nodes: HashMap<u64, NodeId>,
18}
19
20#[cfg_attr(feature = "live", ravenrs_live::expose)]
21impl Ui {
22 pub fn access_tree(&mut self) -> AccessTree {
27 let mut builder = Builder {
28 ui: self,
29 nodes: Vec::new(),
30 seen: HashMap::new(),
31 map: HashMap::new(),
32 focus: None,
33 active: None,
34 };
35 builder.nodes.push(AccessNode::new(
36 ROOT_ID,
37 Role::Window,
38 Rect::from_origin_size(Point::ZERO, self.viewport),
39 ));
40 let mut roots = Vec::new();
41 match self.modal_root() {
42 Some(modal) => {
43 let from = self.overlays.iter().position(|o| o.root == modal).unwrap_or(0);
44 roots.extend(self.overlays[from..].iter().map(|o| o.root));
45 }
46 None => {
47 roots.extend(self.root_node);
48 roots.extend(self.overlays.iter().map(|o| o.root));
49 }
50 }
51 let mut children = Vec::new();
52 for root in roots {
53 builder.visit(root, &mut children, None);
54 }
55 let Builder { mut nodes, map, focus, active, .. } = builder;
56 nodes[0].children = children;
57 if let (Some(focus), Some(active)) = (focus, active)
58 && let Some(node) = nodes.iter_mut().find(|n| n.id == focus)
59 {
60 node.active_descendant = Some(active);
61 }
62 self.access_map = AccessMap { nodes: map };
63 AccessTree { root: ROOT_ID, nodes, focus }
64 }
65
66 pub fn shortcuts(&self) -> Vec<crate::Hotkey> {
69 self.shortcuts
70 .iter()
71 .map(|s| {
72 let key = match &s.key {
73 Key::Character(c) => Key::Character(c.to_lowercase()),
74 other => other.clone(),
75 };
76 crate::Hotkey::of(key, s.command, s.shift)
77 })
78 .collect()
79 }
80
81 pub fn perform_action(&mut self, id: u64, action: AccessAction) -> bool {
85 let Some(node) = self.access_map.nodes.get(&id).copied() else { return false };
86 let Some(element) = self.elements.get(node.0 as usize) else { return false };
87 let widget = element.widget;
88 match action {
89 AccessAction::Click => {
90 if let Some(handler) = element.on_click.clone() {
91 handler();
92 } else if element.focus_radius.is_some() {
93 self.focus_widget(widget);
94 }
95 }
96 AccessAction::Focus => {
97 if element.focus_radius.is_some() {
98 self.focus_widget(widget);
99 }
100 }
101 AccessAction::Increment | AccessAction::Decrement => {
102 let key = if action == AccessAction::Increment { Key::ArrowUp } else { Key::ArrowDown };
103 if let Some(handler) = element.on_key.clone() {
104 handler(&key, Modifiers::default());
105 }
106 }
107 _ => return false,
108 }
109 self.needs_redraw()
110 }
111
112 fn focus_widget(&mut self, widget: WidgetId) {
113 if self.interaction.focused != Some(widget) {
114 self.interaction.focused = Some(widget);
115 self.interaction.focus_visible = true;
116 self.needs_build = true;
117 }
118 }
119}
120
121struct Builder<'a> {
122 ui: &'a Ui,
123 nodes: Vec<AccessNode>,
124 seen: HashMap<WidgetId, u64>,
126 map: HashMap<u64, NodeId>,
127 focus: Option<u64>,
128 active: Option<u64>,
130}
131
132#[cfg_attr(feature = "live", ravenrs_live::expose)]
133impl Builder<'_> {
134 fn id_for(&mut self, widget: WidgetId) -> u64 {
135 let n = self.seen.entry(widget).or_insert(0);
136 *n += 1;
137 let mut h = widget.raw() ^ n.wrapping_mul(0x9e37_79b9_7f4a_7c15);
138 h = (h ^ (h >> 31)).wrapping_mul(0xbf58_476d_1ce4_e5b9);
139 (h ^ (h >> 29)).max(ROOT_ID + 1)
141 }
142
143 fn visit(&mut self, node: NodeId, out: &mut Vec<u64>, mut name: Option<&mut String>) {
146 let ui = self.ui;
147 let i = node.0 as usize;
148 let e = &ui.elements[i];
149 let semantics = e.semantics.as_deref();
150 if semantics.is_some_and(|s| s.hidden) {
151 return;
152 }
153 if semantics.is_none()
154 && let Some(text) = &e.text
155 {
156 let text = readable(text);
157 if text.is_empty() {
158 return;
159 }
160 match name {
161 Some(name) => {
162 if !name.is_empty() {
163 name.push(' ');
164 }
165 name.push_str(&text);
166 }
167 None => {
168 let id = self.id_for(e.widget);
169 let mut label = AccessNode::new(id, Role::Label, ui.frames[i]);
170 label.name = Some(text);
171 self.map.insert(id, node);
172 self.nodes.push(label);
173 out.push(id);
174 }
175 }
176 return;
177 }
178 let focusable = e.focus_radius.is_some();
179 if semantics.is_none() && !focusable {
180 for child in &e.children {
181 self.visit(*child, out, name.as_deref_mut());
182 }
183 return;
184 }
185 let role = semantics.map_or(Role::Button, |s| s.role);
186 let id = self.id_for(e.widget);
187 let index = self.nodes.len();
188 let mut a = AccessNode::new(id, role, ui.frames[i]);
189 a.focusable = focusable;
190 if let Some(s) = semantics {
191 a.name = s.name.clone();
192 a.description = s.description.clone();
193 a.value = s.value.clone();
194 a.numeric = s.numeric;
195 a.toggled = s.toggled;
196 a.selected = s.selected;
197 a.expanded = s.expanded;
198 a.disabled = s.disabled;
199 a.read_only = s.read_only;
200 a.modal = s.modal;
201 a.level = s.level;
202 if s.active {
203 self.active = Some(id);
204 }
205 }
206 if e.on_click.is_some() {
207 a.actions.push(AccessAction::Click);
208 }
209 if focusable {
210 a.actions.push(AccessAction::Focus);
211 if ui.interaction.focused == Some(e.widget) {
212 self.focus = Some(id);
213 }
214 }
215 if e.on_key.is_some() && matches!(role, Role::Slider | Role::SpinButton) {
216 a.actions.extend([AccessAction::Increment, AccessAction::Decrement]);
217 }
218 self.map.insert(id, node);
219 self.nodes.push(a);
220 let mut content = e.text.as_deref().map(readable).unwrap_or_default();
221 let from_content = role.takes_name_from_content();
224 let mut children = Vec::new();
225 for child in &e.children {
226 self.visit(*child, &mut children, from_content.then_some(&mut content));
227 }
228 let node = &mut self.nodes[index];
229 node.children = children;
230 if from_content && node.name.is_none() && !content.is_empty() {
231 node.name = Some(content.clone());
232 }
233 if let Some(name) = name
236 && let Some(own) = &node.name
237 {
238 if !name.is_empty() {
239 name.push(' ');
240 }
241 name.push_str(own);
242 }
243 out.push(id);
244 }
245}
246
247fn readable(text: &str) -> String {
250 let kept: String =
251 text.chars().filter(|c| !matches!(*c as u32, 0xE000..=0xF8FF | 0xF0000..=0x10FFFF)).collect();
252 kept.split_whitespace().collect::<Vec<_>>().join(" ")
253}
254
255#[cfg(test)]
256mod tests {
257 use super::readable;
258
259 #[test]
260 fn readable_text_drops_icons_and_collapses_spaces() {
261 assert_eq!(readable("\u{e001} Save\n file "), "Save file");
262 assert_eq!(readable("\u{e001}"), "");
263 }
264}