Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T07:50:22.323Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

debug.rs

331 lines · 11.5 KB · rust
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
Served at tenant.openagents/omega Member data and write actions are omitted.