Skip to repository content331 lines · 11.5 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:44:37.767Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
debug.rs
1//! Developer tooling for inspecting the accessibility tree.
2//!
3//! [`A11yDebug`] retains the last [`TreeUpdate`] sent to the platform adapter so
4//! it can be serialized on demand (see
5//! [`crate::Window::debug_a11y_tree_json`]). In `cfg(debug_assertions)` builds,
6//! we capture extra info.
7
8use accesskit::{Action, NodeId, TreeUpdate};
9use collections::FxHashMap;
10
11use crate::{Pixels, SharedString, Size};
12
13#[derive(Default)]
14pub(crate) struct FrameDebugInfo {
15 pub viewport_size: Size<Pixels>,
16 pub scale_factor: f32,
17 pub tab_stop_count: usize,
18}
19
20struct CapturedFrame {
21 rendered_at: String,
22 frame_number: u64,
23 window_title: Option<SharedString>,
24 node_count: usize,
25 tab_stop_count: usize,
26 viewport_size: Size<Pixels>,
27 scale_factor: f32,
28}
29
30#[cfg(debug_assertions)]
31#[derive(Clone, Default)]
32pub(crate) struct NodeDebugInfo {
33 /// Whether the node was synthesized via
34 /// [`crate::Element::a11y_synthetic_children`] rather than created from a
35 /// real element with a role and ID.
36 pub synthetic: bool,
37 /// The type name of the `Render` view that was rendering when the node was
38 /// created.
39 pub view: Option<&'static str>,
40 /// The [`ElementId`](crate::ElementId) of the creating element (the leaf of
41 /// its `GlobalElementId`, not the full path). For a synthetic node, this is
42 /// the real element whose `a11y_synthetic_children` produced it.
43 pub element_id: Option<String>,
44 /// Source location where the creating element was constructed.
45 pub source_location: Option<&'static core::panic::Location<'static>>,
46}
47
48#[cfg(debug_assertions)]
49#[derive(Clone, Default)]
50pub(crate) struct NodeCreator {
51 pub view: Option<&'static str>,
52 pub element_id: Option<String>,
53 pub source_location: Option<&'static core::panic::Location<'static>>,
54}
55
56#[derive(Default)]
57pub(crate) struct A11yDebug {
58 last_tree_update: Option<TreeUpdate>,
59 last_gpui_focus: Option<NodeId>,
60 last_active_descendant: Option<NodeId>,
61 /// Monotonic counter incremented on each captured frame, so a re-dump makes
62 /// it obvious whether the tree actually refreshed.
63 frame_number: u64,
64 /// Metadata about the most recently captured frame.
65 last_frame: Option<CapturedFrame>,
66 #[cfg(debug_assertions)]
67 last_node_info: FxHashMap<NodeId, NodeDebugInfo>,
68}
69
70impl A11yDebug {
71 pub(crate) fn capture(
72 &mut self,
73 update: &TreeUpdate,
74 gpui_focus: Option<NodeId>,
75 active_descendant: Option<NodeId>,
76 window_title: Option<&SharedString>,
77 frame: FrameDebugInfo,
78 ) {
79 self.last_tree_update = Some(update.clone());
80 self.last_gpui_focus = gpui_focus;
81 self.last_active_descendant = active_descendant;
82 self.frame_number += 1;
83 self.last_frame = Some(CapturedFrame {
84 rendered_at: chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, false),
85 frame_number: self.frame_number,
86 window_title: window_title.cloned(),
87 node_count: update.nodes.len(),
88 tab_stop_count: frame.tab_stop_count,
89 viewport_size: frame.viewport_size,
90 scale_factor: frame.scale_factor,
91 });
92 }
93
94 #[cfg(debug_assertions)]
95 pub(crate) fn capture_node_info(&mut self, node_info: &FxHashMap<NodeId, NodeDebugInfo>) {
96 self.last_node_info = node_info.clone();
97 }
98
99 /// Serialize the last tree update to a readable JSON string. Node ids are
100 /// replaced with short ephemeral ids (`a`, `b`, ..., `z`, `aa`, ...).
101 pub(crate) fn to_json(&self) -> Option<String> {
102 let update = self.last_tree_update.as_ref()?;
103
104 let mut ephemeral: FxHashMap<NodeId, String> = FxHashMap::default();
105 for (index, (id, _)) in update.nodes.iter().enumerate() {
106 ephemeral.insert(*id, ephemeral_id(index));
107 }
108
109 let mut nodes = serde_json::Map::new();
110 for (id, node) in &update.nodes {
111 let key = ephemeral
112 .get(id)
113 .cloned()
114 .unwrap_or_else(|| id.0.to_string());
115 #[cfg(debug_assertions)]
116 let provenance = self
117 .last_node_info
118 .get(id)
119 .map(|info| NodeProvenance {
120 element_id: info.element_id.as_deref(),
121 view: info.view,
122 source_location: info.source_location.map(|loc| loc.to_string()),
123 // Only surface synthetic nodes; `false` is the default and
124 // would just be noise on every real node.
125 synthetic: info.synthetic.then_some(true),
126 })
127 .unwrap_or_default();
128 #[cfg(not(debug_assertions))]
129 let provenance = NodeProvenance::default();
130 let value = node_to_json(*id, node, &ephemeral, &provenance);
131 nodes.insert(key, value);
132 }
133
134 let frame = self.last_frame.as_ref().map(|frame| {
135 serde_json::json!({
136 "rendered_at": frame.rendered_at,
137 "frame_number": frame.frame_number,
138 "window_title": frame.window_title.as_ref().map(|title| title.to_string()),
139 "node_count": frame.node_count,
140 "tab_stop_count": frame.tab_stop_count,
141 "viewport_size": {
142 "width": frame.viewport_size.width.0,
143 "height": frame.viewport_size.height.0,
144 },
145 "scale_factor": frame.scale_factor,
146 })
147 });
148
149 let root = update
150 .tree
151 .as_ref()
152 .map(|tree| tree.root)
153 .and_then(|id| ephemeral.get(&id).cloned());
154
155 let value = serde_json::json!({
156 "root": root,
157 "gpui_focus": self.last_gpui_focus.and_then(|id| ephemeral.get(&id).cloned()),
158 "active_descendant_focus": self.last_active_descendant.and_then(|id| ephemeral.get(&id).cloned()),
159 "frame": frame,
160 "nodes": nodes,
161 });
162 Some(serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string()))
163 }
164}
165
166#[derive(Default)]
167struct NodeProvenance<'a> {
168 element_id: Option<&'a str>,
169 view: Option<&'a str>,
170 source_location: Option<String>,
171 synthetic: Option<bool>,
172}
173
174fn node_to_json(
175 id: NodeId,
176 node: &accesskit::Node,
177 ephemeral: &FxHashMap<NodeId, String>,
178 provenance: &NodeProvenance,
179) -> serde_json::Value {
180 use serde_json::json;
181
182 let mut map = serde_json::Map::new();
183 map.insert("accesskit_id".into(), json!(id.0.to_string()));
184
185 let children: Vec<String> = node
186 .children()
187 .iter()
188 .map(|child| {
189 ephemeral
190 .get(child)
191 .cloned()
192 .unwrap_or_else(|| child.0.to_string())
193 })
194 .collect();
195 if !children.is_empty() {
196 map.insert("children".into(), json!(children));
197 }
198
199 // Provenance (debug builds only), ordered before the accessibility section.
200 if let Some(element_id) = provenance.element_id {
201 map.insert("element_id".into(), json!(element_id));
202 }
203 if let Some(view) = provenance.view {
204 map.insert("view".into(), json!(view));
205 }
206 if let Some(source_location) = &provenance.source_location {
207 map.insert("source_location".into(), json!(source_location));
208 }
209 if let Some(synthetic) = provenance.synthetic {
210 map.insert("synthetic".into(), json!(synthetic));
211 }
212
213 // Accessibility semantics for this node, grouped together.
214 let mut aria = serde_json::Map::new();
215 aria.insert("role".into(), json!(format!("{:?}", node.role())));
216
217 // Which action types the node supports. AccessKit keeps these in a private
218 // bitset with no getter or iterator, so we probe each variant. `Action::n`
219 // (from AccessKit's `enumn` feature) maps a discriminant to its variant,
220 // returning `None` past the last one - so this can't drift out of sync with
221 // AccessKit's `Action` enum the way a hand-maintained list would.
222 let mut next_action = 0u8;
223 let on_action: Vec<String> = std::iter::from_fn(move || {
224 let action = Action::n(next_action)?;
225 next_action += 1;
226 Some(action)
227 })
228 .filter(|action| node.supports_action(*action))
229 .map(|action| format!("{action:?}"))
230 .collect();
231 if !on_action.is_empty() {
232 aria.insert("on_action".into(), json!(on_action));
233 }
234
235 // String properties.
236 if let Some(v) = node.label() {
237 aria.insert("label".into(), json!(v));
238 }
239 if let Some(v) = node.description() {
240 aria.insert("description".into(), json!(v));
241 }
242 if let Some(v) = node.value() {
243 aria.insert("value".into(), json!(v));
244 }
245 if let Some(v) = node.keyboard_shortcut() {
246 aria.insert("keyboard_shortcut".into(), json!(v));
247 }
248 if let Some(v) = node.access_key() {
249 aria.insert("access_key".into(), json!(v));
250 }
251 if let Some(v) = node.placeholder() {
252 aria.insert("placeholder".into(), json!(v));
253 }
254 if let Some(v) = node.tooltip() {
255 aria.insert("tooltip".into(), json!(v));
256 }
257 if let Some(v) = node.role_description() {
258 aria.insert("role_description".into(), json!(v));
259 }
260
261 // Boolean / enum states.
262 if let Some(v) = node.is_selected() {
263 aria.insert("selected".into(), json!(v));
264 }
265 if let Some(v) = node.is_expanded() {
266 aria.insert("expanded".into(), json!(v));
267 }
268 if let Some(v) = node.toggled() {
269 aria.insert("toggled".into(), json!(format!("{v:?}")));
270 }
271 if let Some(v) = node.orientation() {
272 aria.insert("orientation".into(), json!(format!("{v:?}")));
273 }
274
275 // Numeric properties.
276 if let Some(v) = node.numeric_value() {
277 aria.insert("numeric_value".into(), json!(v));
278 }
279 if let Some(v) = node.min_numeric_value() {
280 aria.insert("min_numeric_value".into(), json!(v));
281 }
282 if let Some(v) = node.max_numeric_value() {
283 aria.insert("max_numeric_value".into(), json!(v));
284 }
285 if let Some(v) = node.numeric_value_step() {
286 aria.insert("numeric_value_step".into(), json!(v));
287 }
288
289 // Set / table properties.
290 if let Some(v) = node.level() {
291 aria.insert("level".into(), json!(v));
292 }
293 if let Some(v) = node.position_in_set() {
294 aria.insert("position_in_set".into(), json!(v));
295 }
296 if let Some(v) = node.size_of_set() {
297 aria.insert("size_of_set".into(), json!(v));
298 }
299 if let Some(v) = node.row_index() {
300 aria.insert("row_index".into(), json!(v));
301 }
302 if let Some(v) = node.column_index() {
303 aria.insert("column_index".into(), json!(v));
304 }
305 if let Some(v) = node.row_count() {
306 aria.insert("row_count".into(), json!(v));
307 }
308 if let Some(v) = node.column_count() {
309 aria.insert("column_count".into(), json!(v));
310 }
311
312 map.insert("aria".into(), serde_json::Value::Object(aria));
313
314 serde_json::Value::Object(map)
315}
316
317/// Maps a 0-based index to a short id in the sequence `a, b, ..., z, aa, ab,
318/// ...` (bijective base-26).
319fn ephemeral_id(mut index: usize) -> String {
320 let mut bytes = Vec::new();
321 loop {
322 bytes.push(b'a' + (index % 26) as u8);
323 if index < 26 {
324 break;
325 }
326 index = index / 26 - 1;
327 }
328 bytes.reverse();
329 String::from_utf8(bytes).unwrap_or_default()
330}
331