From f7e672ade3636f2a7b3ea02c7324b4891aef2232 Mon Sep 17 00:00:00 2001 From: chris Date: Tue, 17 Mar 2026 11:10:50 -0700 Subject: [PATCH] [wip] macos implementation (broken due to strange NSView subclassing issues) Co-authored-by: cameron --- 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(-) diff --git a/Cargo.lock b/Cargo.lock index a02c4f9f8cf944e0a2c5e2082f9e6ec942e50afa..c6641717422c9f30174fe83e60395b7ffbee4e1a 100644 --- a/Cargo.lock +++ b/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", diff --git a/Cargo.toml b/Cargo.toml index abf1f50ae6e43c1cd6a89a2b9666aed4c4a15fbf..a661f84028acb96f3389eb322a0fa4d99b65dc1c 100644 --- a/Cargo.toml +++ b/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" } diff --git a/crates/gpui/src/element.rs b/crates/gpui/src/element.rs index 38e1d612e629c210c92d1334ef9a9fd3b377f2cc..a6191cd6aab9fc725b00038f5b6cf3b5fe255600 100644 --- a/crates/gpui/src/element.rs +++ b/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 diff --git a/crates/gpui/src/window.rs b/crates/gpui/src/window.rs index 32542f6a03e6d254799bf32d5305d1717dda247c..e328035e520f9adb7af9215644b6112aaa26d4e4 100644 --- a/crates/gpui/src/window.rs +++ b/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); } } diff --git a/crates/gpui_macos/Cargo.toml b/crates/gpui_macos/Cargo.toml index 06e5d0e7321af523a249f19ec0d5ac50e2da5d3f..208978fd6552ec659ea71e8040fb42bfc07a59cb 100644 --- a/crates/gpui_macos/Cargo.toml +++ b/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 diff --git a/crates/gpui_macos/src/window.rs b/crates/gpui_macos/src/window.rs index b783a4d083131fac70095d22718796ef761adee3..2586ba8b9130b2e39fe2640f209d8de6409b8b56 100644 --- a/crates/gpui_macos/src/window.rs +++ b/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, + accesskit: Option, } 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 { diff --git a/crates/workspace/src/welcome.rs b/crates/workspace/src/welcome.rs index 92f1cb4840731bedda5b0b6751f44bfdcdb8ea52..7050a9ba16ff7fc8f7405e17a1f12e47735aaf10 100644 --- a/crates/workspace/src/welcome.rs +++ b/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()