From f92ca521d6c586d9395c9bfe9873a4f4c2927927 Mon Sep 17 00:00:00 2001 From: munenick Date: Thu, 23 Jul 2026 13:58:13 +0900 Subject: [PATCH 1/2] gpui: Qualify accessibility node routing by tree --- crates/gpui/src/element.rs | 2 +- crates/gpui/src/platform.rs | 12 +- crates/gpui/src/tab_stop.rs | 29 +++ crates/gpui/src/window.rs | 210 ++++++++++++++++-- crates/gpui/src/window/a11y.rs | 169 +++++++++++++- crates/gpui_linux/src/linux/wayland/window.rs | 6 +- crates/gpui_linux/src/linux/x11/window.rs | 6 +- crates/gpui_macos/src/window.rs | 6 +- crates/gpui_windows/src/window.rs | 6 +- 9 files changed, 415 insertions(+), 31 deletions(-) diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index c817492b949a31..3c3a5fa6c37889 100644 --- a/crates/gpui/src/element.rs +++ b/crates/gpui/src/element.rs @@ -376,7 +376,7 @@ impl Drawable { y1: ((bounds.origin.y.0 + bounds.size.height.0) * scale) as f64, }); self.element.write_a11y_info(&mut node); - window.a11y.node_bounds.insert(node_id, bounds); + window.a11y.set_node_bounds(node_id, bounds); pushed_a11y_node = window.a11y.nodes.push(node_id, node); #[cfg(debug_assertions)] if pushed_a11y_node { diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index 3d23df0e944cdf..028257ce05dc00 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -635,6 +635,14 @@ pub struct A11yCallbacks { pub deactivation: Box, } +/// An accessibility tree update and its matching sequential focus order. +pub struct PlatformAccessibilityUpdate { + /// The complete AccessKit tree update. + pub tree_update: accesskit::TreeUpdate, + /// Root-tree node IDs in GPUI tab order. + pub sequential_focus_order: Vec, +} + #[derive(Debug, Copy, Clone, Eq, PartialEq, Default)] #[expect(missing_docs)] pub struct RequestFrameOptions { @@ -853,8 +861,8 @@ pub trait PlatformWindow: HasWindowHandle + HasDisplayHandle { /// Initialize the accessibility adapter with callbacks. fn a11y_init(&self, _callbacks: A11yCallbacks) {} - /// Provide a TreeUpdate to the accessibility adapter. - fn a11y_tree_update(&self, _tree_update: accesskit::TreeUpdate) {} + /// Provide an accessibility update to the platform adapter. + fn a11y_tree_update(&self, _update: PlatformAccessibilityUpdate) {} /// Inform the adapter of updated window bounds. fn a11y_update_window_bounds(&self) {} diff --git a/crates/gpui/src/tab_stop.rs b/crates/gpui/src/tab_stop.rs index bde651ae5c4572..1d865a840316ee 100644 --- a/crates/gpui/src/tab_stop.rs +++ b/crates/gpui/src/tab_stop.rs @@ -76,6 +76,9 @@ impl Default for TabStopMap { impl TabStopMap { pub fn insert(&mut self, focus_handle: &FocusHandle) { + if let Some(previous) = self.by_id.remove(&focus_handle.id) { + self.order.remove(&previous, ()); + } self.insertion_history .push(TabStopOperation::Insert(focus_handle.clone())); let mut path = self.current_path.clone(); @@ -200,6 +203,19 @@ impl TabStopMap { self.by_id.values().filter(|node| node.tab_stop).count() } + pub(crate) fn ordered_tab_stop_focus_ids(&self) -> Vec { + self.order + .iter() + .filter(|node| node.tab_stop) + .filter_map(|node| { + self.insertion_history + .get(node.node_insertion_index) + .and_then(TabStopOperation::focus_handle) + .map(|handle| handle.id) + }) + .collect() + } + fn focus_handle_for_order(&self, order: &TabStopNode) -> Option { let handle = self.insertion_history[order.node_insertion_index].focus_handle(); debug_assert!( @@ -408,6 +424,19 @@ mod tests { ); } + #[test] + fn replacing_a_focus_handle_updates_its_tab_order_entry() { + let focus_map = Arc::new(FocusMap::default()); + let mut tab_stop_map = TabStopMap::default(); + let handle = FocusHandle::new(&focus_map).tab_stop(true).tab_index(2); + tab_stop_map.insert(&handle); + let replacement = handle.clone().tab_index(0); + tab_stop_map.insert(&replacement); + + assert_eq!(tab_stop_map.ordered_tab_stop_focus_ids(), vec![handle.id]); + assert_eq!(tab_stop_map.tab_stop_count(), 1); + } + #[test] fn test_tab_non_stop_filtering() { let focus_map = Arc::new(FocusMap::default()); diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 15193b3c78bd6b..97597954d11c86 100644 --- a/crates/gpui/src/window.rs +++ b/crates/gpui/src/window.rs @@ -64,9 +64,9 @@ mod prompts; pub use a11y::A11ySubtreeBuilder; -use self::a11y::A11y; #[cfg(not(target_family = "wasm"))] use self::a11y::ROOT_NODE_ID; +use self::a11y::{A11y, QualifiedA11yNodeId}; use crate::util::{ atomic_incr_if_not_zero, ceil_to_device_pixel, floor_to_device_pixel, round_half_toward_zero, round_half_toward_zero_f64, round_stroke_to_device_pixel, round_to_device_pixel, @@ -2937,6 +2937,8 @@ impl Window { tab_stop_count: self.next_frame.tab_stops.tab_stop_count(), }; // clear the builder state regardless + let ordered_focus_ids = self.next_frame.tab_stops.ordered_tab_stop_focus_ids(); + let sequential_focus_order = self.a11y.sequential_focus_order(&ordered_focus_ids); let tree_update = self.a11y.end_frame(frame_info); if should_send_a11y_update { @@ -2944,7 +2946,11 @@ impl Window { "Sending a11y tree update: {} nodes", tree_update.nodes.len() ); - self.platform_window.a11y_tree_update(tree_update); + self.platform_window + .a11y_tree_update(crate::PlatformAccessibilityUpdate { + tree_update, + sequential_focus_order, + }); } } } @@ -5610,19 +5616,30 @@ impl Window { node_id: accesskit::NodeId, action: accesskit::Action, listener: impl FnMut(Option<&accesskit::ActionData>, &mut Window, &mut App) + 'static, + ) { + self.on_a11y_action_in_tree(accesskit::TreeId::ROOT, node_id, action, listener); + } + + /// Register a listener for an accessibility action on a node in a specific tree. + pub fn on_a11y_action_in_tree( + &mut self, + tree_id: accesskit::TreeId, + node_id: accesskit::NodeId, + action: accesskit::Action, + listener: impl FnMut(Option<&accesskit::ActionData>, &mut Window, &mut App) + 'static, ) { self.a11y .action_listeners - .entry(node_id) - .or_default() + .get_or_insert_qualified_default(QualifiedA11yNodeId::new(tree_id, node_id)) .push((action, Box::new(listener))); } #[cfg(not(target_family = "wasm"))] pub(crate) fn handle_a11y_action(&mut self, request: accesskit::ActionRequest, cx: &mut App) { + let target = QualifiedA11yNodeId::new(request.target_tree, request.target_node); // Take listeners out temporarily so the closures can borrow Window // mutably, then restore them afterward. - if let Some(mut listeners) = self.a11y.action_listeners.remove(&request.target_node) { + if let Some(mut listeners) = self.a11y.action_listeners.remove_qualified(target) { let extra_data = request.data.as_ref(); let mut matched = false; for (action, listener) in &mut listeners { @@ -5633,7 +5650,7 @@ impl Window { } self.a11y .action_listeners - .insert(request.target_node, listeners); + .insert_qualified(target, listeners); if matched { return; } @@ -5642,7 +5659,7 @@ impl Window { // Fall back to built-in action handling. match request.action { accesskit::Action::Click => { - if let Some(bounds) = self.a11y.node_bounds.get(&request.target_node).copied() { + if let Some(bounds) = self.a11y.node_bounds.get_qualified(target).copied() { let center = bounds.center(); let mouse_down = PlatformInput::MouseDown(crate::MouseDownEvent { button: MouseButton::Left, @@ -5662,19 +5679,32 @@ impl Window { } } accesskit::Action::Focus => { - if let Some(focus_id) = self.a11y.focus_ids.get(&request.target_node).copied() + if let Some(focus_id) = self.a11y.focus_ids.get_qualified(target).copied() && let Some(handle) = FocusHandle::for_id(focus_id, &cx.focus_handles) { self.focus(&handle, cx); } } accesskit::Action::Blur => { - self.blur(); + if request.target_tree == accesskit::TreeId::ROOT { + self.blur(); + } else if let Some(focus_id) = self.a11y.focus_ids.get_qualified(target).copied() + && self.focus == Some(focus_id) + { + self.blur(); + } else { + log::debug!( + "Unhandled a11y blur on tree {:?}, node {:?}", + request.target_tree, + request.target_node + ); + } } _ => { log::debug!( - "Unhandled a11y action: {:?} on {:?}", + "Unhandled a11y action: {:?} on tree {:?}, node {:?}", request.action, + request.target_tree, request.target_node ); } @@ -6392,12 +6422,17 @@ pub fn outline( #[cfg(test)] mod tests { + use super::QualifiedA11yNodeId; use crate::{ - AppContext as _, Bounds, Context, FocusHandle, InteractiveElement as _, IntoElement, - ParentElement as _, Pixels, Render, Styled as _, TestAppContext, Window, canvas, div, px, - size, + AppContext as _, Bounds, Context, DispatchPhase, FocusHandle, InteractiveElement as _, + IntoElement, MouseDownEvent, ParentElement as _, Pixels, Render, Styled as _, + TestAppContext, Window, canvas, div, point, px, size, + }; + use accesskit::{Action, ActionRequest, NodeId, TreeId, Uuid}; + use std::{ + cell::{Cell, RefCell}, + rc::Rc, }; - use std::{cell::Cell, rc::Rc}; struct RootView { explicit_size: bool, @@ -6464,11 +6499,158 @@ mod tests { assert_eq!(child_bounds.get().size, size(px(300.), px(200.))); } + #[gpui::test] + fn a11y_actions_route_by_tree_and_node(cx: &mut TestAppContext) { + let root_action_count = Rc::new(Cell::new(0)); + let subtree_action_count = Rc::new(Cell::new(0)); + let window = cx.add_window(|_, _| RootView { + explicit_size: false, + child_bounds: Rc::new(Cell::new(Bounds::default())), + }); + let node_id = NodeId(1); + let subtree_id = TreeId(Uuid::from_u128(1)); + + let update_result = window.update(cx, |_, window, cx| { + window.on_a11y_action(node_id, Action::Click, { + let root_action_count = root_action_count.clone(); + move |_, _, _| root_action_count.set(root_action_count.get() + 1) + }); + window.on_a11y_action_in_tree(subtree_id, node_id, Action::Click, { + let subtree_action_count = subtree_action_count.clone(); + move |_, _, _| subtree_action_count.set(subtree_action_count.get() + 1) + }); + + for _ in 0..2 { + window.handle_a11y_action( + ActionRequest { + action: Action::Click, + target_tree: subtree_id, + target_node: node_id, + data: None, + }, + cx, + ); + } + window.handle_a11y_action( + ActionRequest { + action: Action::Click, + target_tree: TreeId(Uuid::from_u128(2)), + target_node: node_id, + data: None, + }, + cx, + ); + window.handle_a11y_action( + ActionRequest { + action: Action::Click, + target_tree: TreeId::ROOT, + target_node: node_id, + data: None, + }, + cx, + ); + }); + assert!(update_result.is_ok(), "test window should remain available"); + + assert_eq!(root_action_count.get(), 1); + assert_eq!(subtree_action_count.get(), 2); + } + struct FocusForwarder { a: FocusHandle, b: FocusHandle, } + #[gpui::test] + fn built_in_a11y_focus_and_blur_use_qualified_targets(cx: &mut TestAppContext) { + let window = cx.add_window(|_, cx| FocusForwarder { + a: cx.focus_handle(), + b: cx.focus_handle(), + }); + let node_id = NodeId(1); + let subtree_id = TreeId(Uuid::from_u128(1)); + + let update_result = window.update(cx, |this, window, cx| { + window.a11y.set_focusable(node_id, this.a.id); + window + .a11y + .focus_ids + .insert_qualified(QualifiedA11yNodeId::new(subtree_id, node_id), this.b.id); + + let request = |action, target_tree| ActionRequest { + action, + target_tree, + target_node: node_id, + data: None, + }; + + window.handle_a11y_action(request(Action::Focus, subtree_id), cx); + assert!(this.b.is_focused(window)); + window.handle_a11y_action(request(Action::Focus, TreeId::ROOT), cx); + assert!(this.a.is_focused(window)); + window.handle_a11y_action(request(Action::Blur, subtree_id), cx); + assert!(this.a.is_focused(window)); + window.handle_a11y_action(request(Action::Focus, subtree_id), cx); + window.handle_a11y_action(request(Action::Blur, subtree_id), cx); + assert!(window.focus.is_none()); + window.handle_a11y_action(request(Action::Focus, TreeId::ROOT), cx); + window.handle_a11y_action(request(Action::Blur, TreeId::ROOT), cx); + assert!(window.focus.is_none()); + }); + assert!(update_result.is_ok(), "test window should remain available"); + } + + #[gpui::test] + fn built_in_a11y_click_uses_qualified_bounds(cx: &mut TestAppContext) { + let window = cx.add_window(|_, cx| FocusForwarder { + a: cx.focus_handle(), + b: cx.focus_handle(), + }); + let node_id = NodeId(1); + let subtree_id = TreeId(Uuid::from_u128(1)); + let unrelated_tree_id = TreeId(Uuid::from_u128(2)); + let root_bounds = Bounds::new(point(px(10.), px(20.)), size(px(8.), px(10.))); + let subtree_bounds = Bounds::new(point(px(30.), px(40.)), size(px(12.), px(14.))); + let mouse_down_positions = Rc::new(RefCell::new(Vec::new())); + + let update_result = window.update(cx, |_, window, cx| { + window + .a11y + .node_bounds + .insert_qualified(QualifiedA11yNodeId::root(node_id), root_bounds); + window.a11y.node_bounds.insert_qualified( + QualifiedA11yNodeId::new(subtree_id, node_id), + subtree_bounds, + ); + window.rendered_frame.mouse_listeners.push(Some(Box::new({ + let mouse_down_positions = mouse_down_positions.clone(); + move |event, phase, _, _| { + if matches!(phase, DispatchPhase::Bubble) + && let Some(event) = event.downcast_ref::() + { + mouse_down_positions.borrow_mut().push(event.position); + } + } + }))); + + let request = |target_tree| ActionRequest { + action: Action::Click, + target_tree, + target_node: node_id, + data: None, + }; + + window.handle_a11y_action(request(TreeId::ROOT), cx); + window.handle_a11y_action(request(subtree_id), cx); + window.handle_a11y_action(request(unrelated_tree_id), cx); + }); + assert!(update_result.is_ok(), "test window should remain available"); + assert_eq!( + *mouse_down_positions.borrow(), + vec![root_bounds.center(), subtree_bounds.center()] + ); + } + impl Render for FocusForwarder { fn render(&mut self, _: &mut Window, _: &mut Context) -> impl IntoElement { div() diff --git a/crates/gpui/src/window/a11y.rs b/crates/gpui/src/window/a11y.rs index 234e7f0acd9fb8..d565b5f4a64794 100644 --- a/crates/gpui/src/window/a11y.rs +++ b/crates/gpui/src/window/a11y.rs @@ -103,7 +103,7 @@ use crate::*; pub(crate) mod debug; use crate::{App, Bounds, FocusId, Pixels, SharedString, Window}; -use accesskit::{Action, NodeId, TreeUpdate}; +use accesskit::{Action, NodeId, TreeId, TreeUpdate}; use collections::{FxHashMap, FxHashSet}; use smallvec::SmallVec; use std::hash::{Hash, Hasher}; @@ -115,6 +115,63 @@ use std::sync::{ /// The fixed AccessKit node ID used for the root of every window's a11y tree. pub(crate) const ROOT_NODE_ID: NodeId = NodeId(0); +#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] +pub(crate) struct QualifiedA11yNodeId { + pub(crate) tree_id: TreeId, + pub(crate) node_id: NodeId, +} + +impl QualifiedA11yNodeId { + pub(crate) fn new(tree_id: TreeId, node_id: NodeId) -> Self { + Self { tree_id, node_id } + } + + pub(crate) fn root(node_id: NodeId) -> Self { + Self::new(TreeId::ROOT, node_id) + } +} + +#[derive(Default)] +pub(crate) struct QualifiedA11yNodeMap(FxHashMap); + +impl QualifiedA11yNodeMap { + pub(crate) fn insert(&mut self, node_id: NodeId, value: V) -> Option { + self.insert_qualified(QualifiedA11yNodeId::root(node_id), value) + } + + pub(crate) fn insert_qualified(&mut self, node_id: QualifiedA11yNodeId, value: V) -> Option { + self.0.insert(node_id, value) + } + + pub(crate) fn get(&self, node_id: &NodeId) -> Option<&V> { + self.get_qualified(QualifiedA11yNodeId::root(*node_id)) + } + + pub(crate) fn get_qualified(&self, node_id: QualifiedA11yNodeId) -> Option<&V> { + self.0.get(&node_id) + } + + pub(crate) fn contains_key(&self, node_id: &NodeId) -> bool { + self.get(node_id).is_some() + } + + #[cfg(not(target_family = "wasm"))] + pub(crate) fn remove_qualified(&mut self, node_id: QualifiedA11yNodeId) -> Option { + self.0.remove(&node_id) + } + + pub(crate) fn clear(&mut self) { + self.0.clear(); + } + + pub(crate) fn get_or_insert_qualified_default(&mut self, node_id: QualifiedA11yNodeId) -> &mut V + where + V: Default, + { + self.0.entry(node_id).or_default() + } +} + /// A listener for an accessibility action on a specific node. pub(crate) type A11yActionListener = Box, &mut Window, &mut App) + 'static>; @@ -146,9 +203,10 @@ pub(crate) struct A11y { /// determine whether we should actually send the finished [`TreeUpdate`]. active_this_frame: bool, pub(crate) nodes: A11yNodeBuilder, - pub(crate) focus_ids: FxHashMap, - pub(crate) node_bounds: FxHashMap>, - pub(crate) action_listeners: FxHashMap>, + pub(crate) focus_ids: QualifiedA11yNodeMap, + root_focus_nodes: FxHashMap, + pub(crate) node_bounds: QualifiedA11yNodeMap>, + pub(crate) action_listeners: QualifiedA11yNodeMap>, /// The window's title, used to label the root node so assistive /// technology can tell windows apart. window_title: Option, @@ -174,9 +232,10 @@ impl A11y { active_flag, active_this_frame: false, nodes: A11yNodeBuilder::new(), - focus_ids: FxHashMap::default(), - node_bounds: FxHashMap::default(), - action_listeners: FxHashMap::default(), + focus_ids: QualifiedA11yNodeMap::default(), + root_focus_nodes: FxHashMap::default(), + node_bounds: QualifiedA11yNodeMap::default(), + action_listeners: QualifiedA11yNodeMap::default(), window_title, last_focus_without_node: None, debug: debug::A11yDebug::default(), @@ -219,6 +278,21 @@ impl A11y { pub(crate) fn set_focusable(&mut self, node_id: NodeId, focus_id: FocusId) { self.focus_ids.insert(node_id, focus_id); + self.root_focus_nodes.insert(focus_id, node_id); + } + + pub(crate) fn set_node_bounds(&mut self, node_id: NodeId, bounds: Bounds) { + self.node_bounds.insert(node_id, bounds); + } + + pub(crate) fn sequential_focus_order(&self, focus_ids: &[FocusId]) -> Vec { + let mut seen = FxHashSet::default(); + focus_ids + .iter() + .filter_map(|focus_id| self.root_focus_nodes.get(focus_id).copied()) + .filter(|node_id| self.nodes.has_node(*node_id)) + .filter(|node_id| seen.insert(*node_id)) + .collect() } /// Report `node_id` as the currently-focused node, if it is present in the @@ -270,6 +344,7 @@ impl A11y { /// Clear per-frame state and push the root node to start a new frame. pub(crate) fn begin_frame(&mut self) { self.focus_ids.clear(); + self.root_focus_nodes.clear(); self.node_bounds.clear(); self.action_listeners.clear(); self.nodes.begin_frame(self.window_title.as_ref()); @@ -633,9 +708,9 @@ impl A11yNodeBuilder { mod tests { // Import specific items rather than glob-importing `super`, which would pull // in gpui's own `test` attribute macro and shadow the standard one. - use super::{A11y, A11yNodeBuilder, ROOT_NODE_ID}; - use crate::FocusId; - use accesskit::{NodeId, Role}; + use super::{A11y, A11yNodeBuilder, QualifiedA11yNodeId, ROOT_NODE_ID}; + use crate::{Bounds, FocusHandle, FocusId, FocusMap, point, px, size}; + use accesskit::{NodeId, Role, TreeId, Uuid}; use std::sync::{Arc, atomic::AtomicBool}; fn test_node() -> accesskit::Node { @@ -654,6 +729,80 @@ mod tests { a11y } + #[test] + fn qualified_storage_distinguishes_equal_node_ids_in_different_trees() { + let mut a11y = new_a11y(); + let node_id = NodeId(1); + let root_node = QualifiedA11yNodeId::root(node_id); + let subtree_node = QualifiedA11yNodeId::new(TreeId(Uuid::from_u128(1)), node_id); + let root_bounds = Bounds::new(point(px(1.), px(2.)), size(px(3.), px(4.))); + let subtree_bounds = Bounds::new(point(px(5.), px(6.)), size(px(7.), px(8.))); + + a11y.focus_ids + .insert_qualified(root_node, FocusId::default()); + a11y.focus_ids + .insert_qualified(subtree_node, FocusId::default()); + a11y.node_bounds.insert_qualified(root_node, root_bounds); + a11y.node_bounds + .insert_qualified(subtree_node, subtree_bounds); + a11y.action_listeners + .insert_qualified(root_node, Vec::new()); + a11y.action_listeners + .insert_qualified(subtree_node, Vec::new()); + + assert!(a11y.focus_ids.get_qualified(root_node).is_some()); + assert!(a11y.focus_ids.get_qualified(subtree_node).is_some()); + assert_eq!( + a11y.node_bounds.get_qualified(root_node), + Some(&root_bounds) + ); + assert_eq!( + a11y.node_bounds.get_qualified(subtree_node), + Some(&subtree_bounds) + ); + assert!(a11y.action_listeners.get_qualified(root_node).is_some()); + assert!(a11y.action_listeners.get_qualified(subtree_node).is_some()); + } + + #[test] + fn sequential_focus_order_uses_last_root_node_and_deduplicates() { + let mut a11y = new_a11y(); + let focus_map = Arc::new(FocusMap::default()); + let first = FocusHandle::new(&focus_map); + let second = FocusHandle::new(&focus_map); + + a11y.set_focusable(NodeId(1), first.id); + a11y.set_focusable(NodeId(2), first.id); + a11y.set_focusable(NodeId(3), second.id); + assert!(a11y.nodes.push(NodeId(2), test_node())); + a11y.nodes.pop(); + assert!(a11y.nodes.push(NodeId(3), test_node())); + a11y.nodes.pop(); + + assert_eq!( + a11y.sequential_focus_order(&[first.id, first.id, second.id]), + vec![NodeId(2), NodeId(3)] + ); + } + + #[test] + fn sequential_focus_order_omits_focus_handles_without_accessibility_nodes() { + let mut a11y = new_a11y(); + let focus_map = Arc::new(FocusMap::default()); + let missing = FocusHandle::new(&focus_map); + let exposed = FocusHandle::new(&focus_map); + + a11y.set_focusable(NodeId(1), missing.id); + a11y.set_focusable(NodeId(2), exposed.id); + assert!(a11y.nodes.push(NodeId(2), test_node())); + a11y.nodes.pop(); + + assert_eq!( + a11y.sequential_focus_order(&[missing.id, exposed.id]), + vec![NodeId(2)] + ); + } + #[test] fn active_descendant_honored_when_container_focused() { let mut builder = new_builder(); diff --git a/crates/gpui_linux/src/linux/wayland/window.rs b/crates/gpui_linux/src/linux/wayland/window.rs index 6d2753e131dfc5..2badccb21b4bd2 100644 --- a/crates/gpui_linux/src/linux/wayland/window.rs +++ b/crates/gpui_linux/src/linux/wayland/window.rs @@ -1849,7 +1849,11 @@ impl PlatformWindow for WaylandWindow { self.borrow_mut().accesskit_adapter = Some(adapter); } - fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { + fn a11y_tree_update(&self, update: gpui::PlatformAccessibilityUpdate) { + let gpui::PlatformAccessibilityUpdate { + tree_update, + sequential_focus_order: _, + } = update; let mut state = self.borrow_mut(); if let Some(adapter) = state.accesskit_adapter.as_mut() { adapter.update_if_active(|| tree_update); diff --git a/crates/gpui_linux/src/linux/x11/window.rs b/crates/gpui_linux/src/linux/x11/window.rs index 4fde66f06ca858..c1770fe1815bd3 100644 --- a/crates/gpui_linux/src/linux/x11/window.rs +++ b/crates/gpui_linux/src/linux/x11/window.rs @@ -1944,7 +1944,11 @@ impl PlatformWindow for X11Window { self.0.state.borrow_mut().accesskit_adapter = Some(adapter); } - fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { + fn a11y_tree_update(&self, update: gpui::PlatformAccessibilityUpdate) { + let gpui::PlatformAccessibilityUpdate { + tree_update, + sequential_focus_order: _, + } = update; let mut state = self.0.state.borrow_mut(); if let Some(adapter) = state.accesskit_adapter.as_mut() { adapter.update_if_active(|| tree_update); diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index 4314d07cc825f3..7ebaf329817d62 100644 --- a/crates/gpui_macos/src/window.rs +++ b/crates/gpui_macos/src/window.rs @@ -1878,7 +1878,11 @@ impl PlatformWindow for MacWindow { lock.accesskit_adapter = Some(adapter); } - fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { + fn a11y_tree_update(&self, update: gpui::PlatformAccessibilityUpdate) { + let gpui::PlatformAccessibilityUpdate { + tree_update, + sequential_focus_order: _, + } = update; let events = { let mut lock = self.0.lock(); lock.accesskit_adapter diff --git a/crates/gpui_windows/src/window.rs b/crates/gpui_windows/src/window.rs index 49a935bafe42ed..64df84a723aa97 100644 --- a/crates/gpui_windows/src/window.rs +++ b/crates/gpui_windows/src/window.rs @@ -1030,7 +1030,11 @@ impl PlatformWindow for WindowsWindow { }); } - fn a11y_tree_update(&self, tree_update: accesskit::TreeUpdate) { + fn a11y_tree_update(&self, update: gpui::PlatformAccessibilityUpdate) { + let gpui::PlatformAccessibilityUpdate { + tree_update, + sequential_focus_order: _, + } = update; let events = { let mut a11y = self.state.a11y.borrow_mut(); a11y.as_mut() From 400740b303092182cd914b60bc8a10e8280b6a8c Mon Sep 17 00:00:00 2001 From: munenick Date: Thu, 23 Jul 2026 14:13:52 +0900 Subject: [PATCH 2/2] Validate Recovery Phase A on native runners --- .github/workflows/ra-native-validation.yml | 85 ++++++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 .github/workflows/ra-native-validation.yml diff --git a/.github/workflows/ra-native-validation.yml b/.github/workflows/ra-native-validation.yml new file mode 100644 index 00000000000000..6917f820b1aa30 --- /dev/null +++ b/.github/workflows/ra-native-validation.yml @@ -0,0 +1,85 @@ +name: Recovery Phase A native accessibility validation + +on: + pull_request: + paths: + - .github/workflows/ra-native-validation.yml + +permissions: + contents: read + +env: + TARGET_SHA: f92ca521d6c586d9395c9bfe9873a4f4c2927927 + CARGO_TERM_COLOR: always + RUST_BACKTRACE: "1" + +jobs: + windows: + runs-on: windows-2022 + env: + CARGO_HOME: C:\c + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + with: + ref: ${{ env.TARGET_SHA }} + - name: Verify checkout and toolchain + shell: pwsh + run: | + if ((git rev-parse HEAD) -ne $env:TARGET_SHA) { throw "checkout SHA mismatch" } + git config --global core.longpaths true + rustup toolchain install 1.95.0 --profile minimal --component clippy + rustup default 1.95.0 + Write-Output "ImageOS=$env:ImageOS ImageVersion=$env:ImageVersion RUNNER_OS=$env:RUNNER_OS RUNNER_ARCH=$env:RUNNER_ARCH" + rustup show active-toolchain + rustc -Vv + cargo -V + - name: Validate gpui_windows + shell: pwsh + run: | + ./script/clippy.ps1 -p gpui_windows + if ($LASTEXITCODE) { throw "clippy exit $LASTEXITCODE" } + $env:RUSTFLAGS='-Dwarnings' + cargo test -p gpui_windows --all-features --no-run + if ($LASTEXITCODE) { throw "test build exit $LASTEXITCODE" } + + macos: + runs-on: macos-14 + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + with: + ref: ${{ env.TARGET_SHA }} + - name: Verify checkout and toolchain + run: | + test "$(git rev-parse HEAD)" = "$TARGET_SHA" + rustup toolchain install 1.95.0 --profile minimal --component clippy + rustup default 1.95.0 + echo "ImageOS=$ImageOS ImageVersion=$ImageVersion RUNNER_OS=$RUNNER_OS RUNNER_ARCH=$RUNNER_ARCH" + rustup show active-toolchain + rustc -Vv + cargo -V + - name: Validate gpui_macos + run: | + ./script/clippy -p gpui_macos + RUSTFLAGS='-Dwarnings' cargo test -p gpui_macos --all-features --no-run + + linux: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd + with: + ref: ${{ env.TARGET_SHA }} + - name: Verify checkout and toolchain + run: | + test "$(git rev-parse HEAD)" = "$TARGET_SHA" + rustup toolchain install 1.95.0 --profile minimal --component clippy + rustup default 1.95.0 + echo "ImageOS=$ImageOS ImageVersion=$ImageVersion RUNNER_OS=$RUNNER_OS RUNNER_ARCH=$RUNNER_ARCH" + rustup show active-toolchain + rustc -Vv + cargo -V + - name: Set up Linux dependencies + run: ./script/linux + - name: Validate gpui_linux + run: | + ./script/clippy -p gpui_linux + RUSTFLAGS='-Dwarnings' cargo test -p gpui_linux --all-features --no-run