use accesskit::{Action, NodeId, TreeUpdate};
use collections::FxHashMap;
use crate::{Pixels, SharedString, Size};
#[derive(Default)]
pub(crate) struct FrameDebugInfo {
pub viewport_size: Size<Pixels>,
pub scale_factor: f32,
pub tab_stop_count: usize,
}
struct CapturedFrame {
rendered_at: String,
frame_number: u64,
window_title: Option<SharedString>,
node_count: usize,
tab_stop_count: usize,
viewport_size: Size<Pixels>,
scale_factor: f32,
}
#[cfg(debug_assertions)]
#[derive(Clone, Default)]
pub(crate) struct NodeDebugInfo {
pub synthetic: bool,
pub view: Option<&'static str>,
pub element_id: Option<String>,
pub source_location: Option<&'static core::panic::Location<'static>>,
}
#[cfg(debug_assertions)]
#[derive(Clone, Default)]
pub(crate) struct NodeCreator {
pub view: Option<&'static str>,
pub element_id: Option<String>,
pub source_location: Option<&'static core::panic::Location<'static>>,
}
#[derive(Default)]
pub(crate) struct A11yDebug {
last_tree_update: Option<TreeUpdate>,
last_gpui_focus: Option<NodeId>,
last_active_descendant: Option<NodeId>,
frame_number: u64,
last_frame: Option<CapturedFrame>,
#[cfg(debug_assertions)]
last_node_info: FxHashMap<NodeId, NodeDebugInfo>,
}
impl A11yDebug {
pub(crate) fn capture(
&mut self,
update: &TreeUpdate,
gpui_focus: Option<NodeId>,
active_descendant: Option<NodeId>,
window_title: Option<&SharedString>,
frame: FrameDebugInfo,
) {
self.last_tree_update = Some(update.clone());
self.last_gpui_focus = gpui_focus;
self.last_active_descendant = active_descendant;
self.frame_number += 1;
self.last_frame = Some(CapturedFrame {
rendered_at: chrono::Local::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, false),
frame_number: self.frame_number,
window_title: window_title.cloned(),
node_count: update.nodes.len(),
tab_stop_count: frame.tab_stop_count,
viewport_size: frame.viewport_size,
scale_factor: frame.scale_factor,
});
}
#[cfg(debug_assertions)]
pub(crate) fn capture_node_info(&mut self, node_info: &FxHashMap<NodeId, NodeDebugInfo>) {
self.last_node_info = node_info.clone();
}
pub(crate) fn to_json(&self) -> Option<String> {
let update = self.last_tree_update.as_ref()?;
let mut ephemeral: FxHashMap<NodeId, String> = FxHashMap::default();
for (index, (id, _)) in update.nodes.iter().enumerate() {
ephemeral.insert(*id, ephemeral_id(index));
}
let mut nodes = serde_json::Map::new();
for (id, node) in &update.nodes {
let key = ephemeral
.get(id)
.cloned()
.unwrap_or_else(|| id.0.to_string());
#[cfg(debug_assertions)]
let provenance = self
.last_node_info
.get(id)
.map(|info| NodeProvenance {
element_id: info.element_id.as_deref(),
view: info.view,
source_location: info.source_location.map(|loc| loc.to_string()),
synthetic: info.synthetic.then_some(true),
})
.unwrap_or_default();
#[cfg(not(debug_assertions))]
let provenance = NodeProvenance::default();
let value = node_to_json(*id, node, &ephemeral, &provenance);
nodes.insert(key, value);
}
let frame = self.last_frame.as_ref().map(|frame| {
serde_json::json!({
"rendered_at": frame.rendered_at,
"frame_number": frame.frame_number,
"window_title": frame.window_title.as_ref().map(|title| title.to_string()),
"node_count": frame.node_count,
"tab_stop_count": frame.tab_stop_count,
"viewport_size": {
"width": frame.viewport_size.width.0,
"height": frame.viewport_size.height.0,
},
"scale_factor": frame.scale_factor,
})
});
let root = update
.tree
.as_ref()
.map(|tree| tree.root)
.and_then(|id| ephemeral.get(&id).cloned());
let value = serde_json::json!({
"root": root,
"gpui_focus": self.last_gpui_focus.and_then(|id| ephemeral.get(&id).cloned()),
"active_descendant_focus": self.last_active_descendant.and_then(|id| ephemeral.get(&id).cloned()),
"frame": frame,
"nodes": nodes,
});
Some(serde_json::to_string_pretty(&value).unwrap_or_else(|_| "{}".to_string()))
}
}
#[derive(Default)]
struct NodeProvenance<'a> {
element_id: Option<&'a str>,
view: Option<&'a str>,
source_location: Option<String>,
synthetic: Option<bool>,
}
fn node_to_json(
id: NodeId,
node: &accesskit::Node,
ephemeral: &FxHashMap<NodeId, String>,
provenance: &NodeProvenance,
) -> serde_json::Value {
use serde_json::json;
let mut map = serde_json::Map::new();
map.insert("accesskit_id".into(), json!(id.0.to_string()));
let children: Vec<String> = node
.children()
.iter()
.map(|child| {
ephemeral
.get(child)
.cloned()
.unwrap_or_else(|| child.0.to_string())
})
.collect();
if !children.is_empty() {
map.insert("children".into(), json!(children));
}
if let Some(element_id) = provenance.element_id {
map.insert("element_id".into(), json!(element_id));
}
if let Some(view) = provenance.view {
map.insert("view".into(), json!(view));
}
if let Some(source_location) = &provenance.source_location {
map.insert("source_location".into(), json!(source_location));
}
if let Some(synthetic) = provenance.synthetic {
map.insert("synthetic".into(), json!(synthetic));
}
let mut aria = serde_json::Map::new();
aria.insert("role".into(), json!(format!("{:?}", node.role())));
let mut next_action = 0u8;
let on_action: Vec<String> = std::iter::from_fn(move || {
let action = Action::n(next_action)?;
next_action += 1;
Some(action)
})
.filter(|action| node.supports_action(*action))
.map(|action| format!("{action:?}"))
.collect();
if !on_action.is_empty() {
aria.insert("on_action".into(), json!(on_action));
}
if let Some(v) = node.label() {
aria.insert("label".into(), json!(v));
}
if let Some(v) = node.description() {
aria.insert("description".into(), json!(v));
}
if let Some(v) = node.value() {
aria.insert("value".into(), json!(v));
}
if let Some(v) = node.keyboard_shortcut() {
aria.insert("keyboard_shortcut".into(), json!(v));
}
if let Some(v) = node.access_key() {
aria.insert("access_key".into(), json!(v));
}
if let Some(v) = node.placeholder() {
aria.insert("placeholder".into(), json!(v));
}
if let Some(v) = node.tooltip() {
aria.insert("tooltip".into(), json!(v));
}
if let Some(v) = node.role_description() {
aria.insert("role_description".into(), json!(v));
}
let lengths = node.character_lengths();
if !lengths.is_empty() {
aria.insert("character_lengths".into(), json!(lengths));
}
if let Some(direction) = node.text_direction() {
aria.insert("text_direction".into(), json!(format!("{direction:?}")));
}
if let Some(previous) = node.previous_on_line() {
aria.insert(
"previous_on_line".into(),
json!(
ephemeral
.get(&previous)
.cloned()
.unwrap_or_else(|| previous.0.to_string())
),
);
}
if let Some(next) = node.next_on_line() {
aria.insert(
"next_on_line".into(),
json!(
ephemeral
.get(&next)
.cloned()
.unwrap_or_else(|| next.0.to_string())
),
);
}
if let Some(selection) = node.text_selection() {
let position = |position: accesskit::TextPosition| {
json!({
"node": ephemeral
.get(&position.node)
.cloned()
.unwrap_or_else(|| position.node.0.to_string()),
"character_index": position.character_index,
})
};
aria.insert(
"text_selection".into(),
json!({
"anchor": position(selection.anchor),
"focus": position(selection.focus),
}),
);
}
if let Some(v) = node.is_selected() {
aria.insert("selected".into(), json!(v));
}
if let Some(v) = node.is_expanded() {
aria.insert("expanded".into(), json!(v));
}
if let Some(v) = node.toggled() {
aria.insert("toggled".into(), json!(format!("{v:?}")));
}
if let Some(v) = node.orientation() {
aria.insert("orientation".into(), json!(format!("{v:?}")));
}
if node.is_disabled() {
aria.insert("disabled".into(), json!(true));
}
if node.is_read_only() {
aria.insert("read_only".into(), json!(true));
}
if node.is_modal() {
aria.insert("modal".into(), json!(true));
}
if let Some(v) = node.invalid() {
aria.insert("invalid".into(), json!(format!("{v:?}")));
}
if node.is_required() {
aria.insert("required".into(), json!(true));
}
if node.is_busy() {
aria.insert("busy".into(), json!(true));
}
if let Some(v) = node.live() {
aria.insert("live".into(), json!(format!("{v:?}")));
}
if node.is_live_atomic() {
aria.insert("live_atomic".into(), json!(true));
}
if let Some(v) = node.numeric_value() {
aria.insert("numeric_value".into(), json!(v));
}
if let Some(v) = node.min_numeric_value() {
aria.insert("min_numeric_value".into(), json!(v));
}
if let Some(v) = node.max_numeric_value() {
aria.insert("max_numeric_value".into(), json!(v));
}
if let Some(v) = node.numeric_value_step() {
aria.insert("numeric_value_step".into(), json!(v));
}
if let Some(v) = node.level() {
aria.insert("level".into(), json!(v));
}
if let Some(v) = node.position_in_set() {
aria.insert("position_in_set".into(), json!(v));
}
if let Some(v) = node.size_of_set() {
aria.insert("size_of_set".into(), json!(v));
}
if let Some(v) = node.row_index() {
aria.insert("row_index".into(), json!(v));
}
if let Some(v) = node.column_index() {
aria.insert("column_index".into(), json!(v));
}
if let Some(v) = node.row_count() {
aria.insert("row_count".into(), json!(v));
}
if let Some(v) = node.column_count() {
aria.insert("column_count".into(), json!(v));
}
map.insert("aria".into(), serde_json::Value::Object(aria));
serde_json::Value::Object(map)
}
fn ephemeral_id(mut index: usize) -> String {
let mut bytes = Vec::new();
loop {
bytes.push(b'a' + (index % 26) as u8);
if index < 26 {
break;
}
index = index / 26 - 1;
}
bytes.reverse();
String::from_utf8(bytes).unwrap_or_default()
}