[wip] macos implementation (broken due to strange NSView subclassing

chris and cameron created

issues)

Co-authored-by: cameron <cameron.studdstreet@gmail.com>

Change summary

Cargo.lock                      | 257 +++++++++++++++++++++++++++++++---
Cargo.toml                      |   3 
crates/gpui/src/element.rs      |   2 
crates/gpui/src/window.rs       |   2 
crates/gpui_macos/Cargo.toml    |   2 
crates/gpui_macos/src/window.rs |  78 ++++++++++
crates/workspace/src/welcome.rs |   3 
7 files changed, 319 insertions(+), 28 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -18,12 +18,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d5469156abf83b372574df59b660375739725f7b5a3c78dc47b80f9ec0450c43"
 dependencies = [
  "accesskit",
- "accesskit_consumer",
+ "accesskit_consumer 0.34.0",
  "atspi-common",
  "serde",
  "zvariant",
 ]
 
+[[package]]
+name = "accesskit_atspi_common"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "842fd8203e6dfcf531d24f5bac792088edfba7d6b35844fead191603fb32a260"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer 0.35.0",
+ "atspi-common",
+ "phf 0.13.1",
+ "serde",
+ "zvariant",
+]
+
 [[package]]
 name = "accesskit_consumer"
 version = "0.34.0"
@@ -34,6 +48,30 @@ dependencies = [
  "hashbrown 0.16.1",
 ]
 
+[[package]]
+name = "accesskit_consumer"
+version = "0.35.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "53cf47daed85312e763fbf85ceca136e0d7abc68e0a7e12abe11f48172bc3b10"
+dependencies = [
+ "accesskit",
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "accesskit_macos"
+version = "0.26.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "534bc3fdc89a64a1db3c46b33c198fde2b7c3c7d094e5809c8c8bf2970c18243"
+dependencies = [
+ "accesskit",
+ "accesskit_consumer 0.35.0",
+ "hashbrown 0.16.1",
+ "objc2 0.5.2",
+ "objc2-app-kit",
+ "objc2-foundation 0.2.2",
+]
+
 [[package]]
 name = "accesskit_unix"
 version = "0.20.0"
@@ -41,7 +79,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "14984a017441d2efc3b0fb6a08ef9b83c17719479ba72f1f0c6e08bea9dd92ec"
 dependencies = [
  "accesskit",
- "accesskit_atspi_common",
+ "accesskit_atspi_common 0.17.0",
+ "async-channel 2.5.0",
+ "async-executor",
+ "async-task",
+ "atspi",
+ "futures-lite 2.6.1",
+ "futures-util",
+ "serde",
+ "zbus",
+]
+
+[[package]]
+name = "accesskit_unix"
+version = "0.21.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90e549dd7c6562b6a2ea807b44726e6241707db054a817dc4c7e2b8d3b39bfac"
+dependencies = [
+ "accesskit",
+ "accesskit_atspi_common 0.18.0",
  "async-channel 2.5.0",
  "async-executor",
  "async-task",
@@ -2232,13 +2288,22 @@ dependencies = [
  "generic-array",
 ]
 
+[[package]]
+name = "block2"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2c132eebf10f5cad5289222520a4a058514204aed6d791f1cf4fe8088b82d15f"
+dependencies = [
+ "objc2 0.5.2",
+]
+
 [[package]]
 name = "block2"
 version = "0.6.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
 dependencies = [
- "objc2",
+ "objc2 0.6.3",
 ]
 
 [[package]]
@@ -2280,7 +2345,7 @@ version = "3.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "89ec27229c38ed0eb3c0feee3d2c1d6a4379ae44f418a29a658890e062d8f365"
 dependencies = [
- "darling 0.20.11",
+ "darling 0.21.3",
  "ident_case",
  "prettyplease",
  "proc-macro2",
@@ -3089,7 +3154,7 @@ dependencies = [
  "http_client_tls",
  "httparse",
  "log",
- "objc2-foundation",
+ "objc2-foundation 0.3.1",
  "parking_lot",
  "paths",
  "postage",
@@ -3996,13 +4061,13 @@ dependencies = [
  "ndk-context",
  "num-derive",
  "num-traits",
- "objc2",
+ "objc2 0.6.3",
  "objc2-audio-toolbox",
  "objc2-avf-audio",
  "objc2-core-audio",
  "objc2-core-audio-types",
  "objc2-core-foundation",
- "objc2-foundation",
+ "objc2-foundation 0.3.1",
  "wasm-bindgen",
  "wasm-bindgen-futures",
  "web-sys",
@@ -5113,9 +5178,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
 dependencies = [
  "bitflags 2.10.0",
- "block2",
+ "block2 0.6.2",
  "libc",
- "objc2",
+ "objc2 0.6.3",
 ]
 
 [[package]]
@@ -7738,8 +7803,8 @@ dependencies = [
  "naga 28.0.0",
  "num_cpus",
  "objc",
- "objc2",
- "objc2-metal",
+ "objc2 0.6.3",
+ "objc2-metal 0.3.1",
  "parking",
  "parking_lot",
  "pathfinder_geometry",
@@ -7784,7 +7849,7 @@ name = "gpui_linux"
 version = "0.1.0"
 dependencies = [
  "accesskit",
- "accesskit_unix",
+ "accesskit_unix 0.21.0",
  "anyhow",
  "as-raw-xcb-connection",
  "ashpd",
@@ -7832,6 +7897,8 @@ dependencies = [
 name = "gpui_macos"
 version = "0.1.0"
 dependencies = [
+ "accesskit",
+ "accesskit_macos",
  "anyhow",
  "async-task",
  "block",
@@ -11390,6 +11457,22 @@ dependencies = [
  "objc_id",
 ]
 
+[[package]]
+name = "objc-sys"
+version = "0.3.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb91bdd390c7ce1a8607f35f3ca7151b65afc0ff5ff3b34fa350f7d7c7e4310"
+
+[[package]]
+name = "objc2"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46a785d4eeff09c14c487497c162e92766fbb3e4059a71840cecc03d9a50b804"
+dependencies = [
+ "objc-sys",
+ "objc2-encode",
+]
+
 [[package]]
 name = "objc2"
 version = "0.6.3"
@@ -11399,6 +11482,22 @@ dependencies = [
  "objc2-encode",
 ]
 
+[[package]]
+name = "objc2-app-kit"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
+dependencies = [
+ "bitflags 2.10.0",
+ "block2 0.5.1",
+ "libc",
+ "objc2 0.5.2",
+ "objc2-core-data",
+ "objc2-core-image",
+ "objc2-foundation 0.2.2",
+ "objc2-quartz-core",
+]
+
 [[package]]
 name = "objc2-audio-toolbox"
 version = "0.3.1"
@@ -11407,11 +11506,11 @@ checksum = "10cbe18d879e20a4aea544f8befe38bcf52255eb63d3f23eca2842f3319e4c07"
 dependencies = [
  "bitflags 2.10.0",
  "libc",
- "objc2",
+ "objc2 0.6.3",
  "objc2-core-audio",
  "objc2-core-audio-types",
  "objc2-core-foundation",
- "objc2-foundation",
+ "objc2-foundation 0.3.1",
 ]
 
 [[package]]
@@ -11420,8 +11519,8 @@ version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bfc1d11521c211a7ebe17739fc806719da41f56c6b3f949d9861b459188ce910"
 dependencies = [
- "objc2",
- "objc2-foundation",
+ "objc2 0.6.3",
+ "objc2-foundation 0.3.1",
 ]
 
 [[package]]
@@ -11431,10 +11530,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ca44961e888e19313b808f23497073e3f6b3c22bb485056674c8b49f3b025c82"
 dependencies = [
  "dispatch2",
- "objc2",
+ "objc2 0.6.3",
  "objc2-core-audio-types",
  "objc2-core-foundation",
- "objc2-foundation",
+ "objc2-foundation 0.3.1",
 ]
 
 [[package]]
@@ -11444,7 +11543,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
 dependencies = [
  "bitflags 2.10.0",
- "objc2",
+ "objc2 0.6.3",
+]
+
+[[package]]
+name = "objc2-core-data"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
+dependencies = [
+ "bitflags 2.10.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
 ]
 
 [[package]]
@@ -11454,10 +11565,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
 dependencies = [
  "bitflags 2.10.0",
- "block2",
+ "block2 0.6.2",
  "dispatch2",
  "libc",
- "objc2",
+ "objc2 0.6.3",
+]
+
+[[package]]
+name = "objc2-core-image"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
+dependencies = [
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal 0.2.2",
 ]
 
 [[package]]
@@ -11466,6 +11589,18 @@ version = "4.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33"
 
+[[package]]
+name = "objc2-foundation"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
+dependencies = [
+ "bitflags 2.10.0",
+ "block2 0.5.1",
+ "libc",
+ "objc2 0.5.2",
+]
+
 [[package]]
 name = "objc2-foundation"
 version = "0.3.1"
@@ -11473,9 +11608,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
 dependencies = [
  "bitflags 2.10.0",
- "block2",
+ "block2 0.6.2",
  "libc",
- "objc2",
+ "objc2 0.6.3",
  "objc2-core-foundation",
 ]
 
@@ -11489,6 +11624,18 @@ dependencies = [
  "objc2-core-foundation",
 ]
 
+[[package]]
+name = "objc2-metal"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
+dependencies = [
+ "bitflags 2.10.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+]
+
 [[package]]
 name = "objc2-metal"
 version = "0.3.1"
@@ -11496,11 +11643,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874"
 dependencies = [
  "bitflags 2.10.0",
- "block2",
+ "block2 0.6.2",
  "dispatch2",
- "objc2",
+ "objc2 0.6.3",
  "objc2-core-foundation",
- "objc2-foundation",
+ "objc2-foundation 0.3.1",
+]
+
+[[package]]
+name = "objc2-quartz-core"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
+dependencies = [
+ "bitflags 2.10.0",
+ "block2 0.5.1",
+ "objc2 0.5.2",
+ "objc2-foundation 0.2.2",
+ "objc2-metal 0.2.2",
 ]
 
 [[package]]
@@ -12633,6 +12793,17 @@ dependencies = [
  "phf_shared 0.12.1",
 ]
 
+[[package]]
+name = "phf"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf"
+dependencies = [
+ "phf_macros 0.13.1",
+ "phf_shared 0.13.1",
+ "serde",
+]
+
 [[package]]
 name = "phf_codegen"
 version = "0.11.3"
@@ -12663,6 +12834,16 @@ dependencies = [
  "phf_shared 0.12.1",
 ]
 
+[[package]]
+name = "phf_generator"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737"
+dependencies = [
+ "fastrand 2.3.0",
+ "phf_shared 0.13.1",
+]
+
 [[package]]
 name = "phf_macros"
 version = "0.11.3"
@@ -12689,6 +12870,19 @@ dependencies = [
  "syn 2.0.117",
 ]
 
+[[package]]
+name = "phf_macros"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef"
+dependencies = [
+ "phf_generator 0.13.1",
+ "phf_shared 0.13.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.117",
+]
+
 [[package]]
 name = "phf_shared"
 version = "0.11.3"
@@ -12707,6 +12901,15 @@ dependencies = [
  "siphasher 1.0.1",
 ]
 
+[[package]]
+name = "phf_shared"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266"
+dependencies = [
+ "siphasher 1.0.1",
+]
+
 [[package]]
 name = "picker"
 version = "0.1.0"
@@ -21927,7 +22130,7 @@ dependencies = [
 name = "zed"
 version = "0.229.0"
 dependencies = [
- "accesskit_unix",
+ "accesskit_unix 0.20.0",
  "acp_thread",
  "acp_tools",
  "action_log",

Cargo.toml 🔗

@@ -474,7 +474,8 @@ ztracing_macro = { path = "crates/ztracing_macro" }
 #
 
 accesskit = "0.24.0"
-accesskit_unix = "0.20.0"  # todo! feature flag
+accesskit_macos = "0.26.0"
+accesskit_unix = "0.21.0"  # todo! feature flag
 agent-client-protocol = { version = "=0.10.2", features = ["unstable"] }
 aho-corasick = "1.1"
 alacritty_terminal = { git = "https://github.com/zed-industries/alacritty", rev = "9d9640d4" }

crates/gpui/src/element.rs 🔗

@@ -46,6 +46,8 @@ use std::{
     sync::Arc,
 };
 
+// div().id("hello").role(Role::Button)
+
 /// Implemented by types that participate in laying out and painting the contents of a window.
 /// Elements form a tree and are laid out according to web-based layout rules, as implemented by Taffy.
 /// You can create custom elements by implementing this trait, see the module-level documentation

crates/gpui/src/window.rs 🔗

@@ -2543,9 +2543,11 @@ impl Window {
 
         #[cfg(any(feature = "inspector", debug_assertions))]
         self.paint_inspector_hitbox(cx);
+        
 
         if self.is_a11y_active() {
             let tree_update = self.a11y_nodes.finalize();
+            // dbg!(&tree_update);
             self.platform_window.a11y_tree_update(tree_update);
         }
     }

crates/gpui_macos/Cargo.toml 🔗

@@ -24,6 +24,8 @@ gpui.workspace = true
 [target.'cfg(target_os = "macos")'.dependencies]
 anyhow.workspace = true
 async-task = "4.7"
+accesskit.workspace = true
+accesskit_macos.workspace = true
 block = "0.1"
 cocoa.workspace = true
 collections.workspace = true

crates/gpui_macos/src/window.rs 🔗

@@ -2,6 +2,7 @@ use crate::{
     BoolExt, DisplayLink, MacDisplay, NSRange, NSStringExt, events::platform_input_from_native,
     ns_string, renderer,
 };
+use accesskit_macos::SubclassingAdapter;
 #[cfg(any(test, feature = "test-support"))]
 use anyhow::Result;
 use block::ConcreteBlock;
@@ -113,11 +114,38 @@ unsafe extern "C" {
     ) -> i32;
 }
 
+// todo! Apache license from accesskit_winit until end
+pub struct AdapterWrapper {
+    adapter: SubclassingAdapter,
+}
+
+impl AdapterWrapper {
+    pub unsafe fn new(
+        window: *mut c_void,
+        activation_handler: impl 'static + accesskit::ActivationHandler,
+        action_handler: impl 'static + accesskit::ActionHandler,
+    ) -> Self {
+        let adapter = unsafe { SubclassingAdapter::for_window(window, activation_handler, action_handler) };
+        Self { adapter }
+    }
+
+    pub fn update_if_active(&mut self, updater: impl FnOnce() -> accesskit::TreeUpdate) {
+        if let Some(events) = self.adapter.update_if_active(updater) {
+            events.raise();
+        }
+    }
+}
+// todo! end
+
 #[ctor]
 unsafe fn build_classes() {
     unsafe {
         WINDOW_CLASS = build_window_class("GPUIWindow", class!(NSWindow));
         PANEL_CLASS = build_window_class("GPUIPanel", class!(NSPanel));
+
+        accesskit_macos::add_focus_forwarder_to_window_class("GPUIWindow");
+        accesskit_macos::add_focus_forwarder_to_window_class("GPUIPanel");
+
         VIEW_CLASS = {
             let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
             decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
@@ -442,6 +470,7 @@ struct MacWindowState {
     activated_least_once: bool,
     // The parent window if this window is a sheet (Dialog kind)
     sheet_parent: Option<id>,
+    accesskit: Option<AdapterWrapper>,
 }
 
 impl MacWindowState {
@@ -765,6 +794,7 @@ impl MacWindow {
                 toggle_tab_bar_callback: None,
                 activated_least_once: false,
                 sheet_parent: None,
+                accesskit: None,
             })));
 
             (*native_window).set_ivar(
@@ -1636,6 +1666,54 @@ impl PlatformWindow for MacWindow {
         let mut this = self.0.lock();
         this.renderer.render_to_image(scene)
     }
+
+    fn a11y_init(&self, callbacks: gpui::A11yCallbacks) {
+        let mut state = self.0.lock();
+
+        if state.accesskit.is_some() {
+            panic!("cannot initialize accesskit twice");
+        }
+
+        // dbg!(state.native_window);
+        let window = state.native_window as *mut c_void;
+        // let native_view = state.native_view.as_ptr();
+        // unsafe {
+        //     eprintln!(
+        //         "[a11y] native_view class: {}",
+        //         (*native_view).class().name()
+        //     )
+        // };
+
+        let adapter = unsafe { AdapterWrapper::new(window, callbacks.activation, callbacks.action) };
+
+        unsafe {
+            let content_view: id = msg_send![state.native_window, contentView];
+            let class = (*content_view).class();
+            eprintln!(
+                "[a11y debug] Content view class after adapter: {}",
+                class.name()
+            );
+        }
+        state.accesskit = Some(adapter);
+    }
+
+    fn a11y_tree_update(&mut self, tree_update: accesskit::TreeUpdate) {
+        let mut state = self.0.lock();
+
+        let Some(adapter) = &mut state.accesskit else {
+            panic!("cannot update accesskit tree - not initialized");
+        };
+
+        adapter.update_if_active(|| tree_update);
+    }
+
+    fn a11y_update_window_bounds(&self) {
+        // todo! figure this out
+    }
+
+    fn is_a11y_active(&self) -> bool {
+        true
+    }
 }
 
 impl rwh::HasWindowHandle for MacWindow {

crates/workspace/src/welcome.rs 🔗

@@ -102,6 +102,9 @@ impl RenderOnce for SectionButton {
                 h_flex()
                     .w_full()
                     .justify_between()
+                    .child(
+                        div().id("test-a11y-id").role(gpui::Role::Button).child("hello")
+                    )
                     .child(
                         h_flex()
                             .gap_2()