Compare commits

...

2 Commits

Author SHA1 Message Date
Mikayla Maki
30c4337b29 WIP 2025-05-19 08:55:29 -07:00
Mikayla Maki
020f8169e3 Start off acceskit integration
Co-authored-by: Sanay <fatullayevasanay@gmail.com>
2025-05-16 11:51:46 +02:00
21 changed files with 744 additions and 96 deletions

186
Cargo.lock generated
View File

@@ -2,6 +2,30 @@
# It is not intended for manual editing.
version = 4
[[package]]
name = "accesskit"
version = "0.19.0"
[[package]]
name = "accesskit_consumer"
version = "0.28.0"
dependencies = [
"accesskit",
"hashbrown 0.15.2",
]
[[package]]
name = "accesskit_macos"
version = "0.20.0"
dependencies = [
"accesskit",
"accesskit_consumer",
"hashbrown 0.15.2",
"objc2 0.5.2",
"objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2",
]
[[package]]
name = "activity_indicator"
version = "0.1.0"
@@ -2046,12 +2070,12 @@ dependencies = [
"log",
"mint",
"naga",
"objc2",
"objc2-app-kit",
"objc2 0.6.1",
"objc2-app-kit 0.3.1",
"objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
"objc2-quartz-core",
"objc2-foundation 0.3.1",
"objc2-metal 0.3.1",
"objc2-quartz-core 0.3.1",
"objc2-ui-kit",
"raw-window-handle",
"slab",
@@ -2117,13 +2141,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.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "340d2f0bdb2a43c1d3cd40513185b2bd7def0aa1052f956455114bc98f82dcf2"
dependencies = [
"objc2",
"objc2 0.6.1",
]
[[package]]
@@ -3898,6 +3931,10 @@ dependencies = [
"windows-sys 0.59.0",
]
[[package]]
name = "current_commit_sha"
version = "0.1.0"
[[package]]
name = "cursor-icon"
version = "1.1.0"
@@ -4440,7 +4477,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89a09f22a6c6069a18470eb92d2298acf25463f14256d24778e1230d789a2aec"
dependencies = [
"bitflags 2.9.0",
"objc2",
"objc2 0.6.1",
]
[[package]]
@@ -6156,6 +6193,8 @@ dependencies = [
name = "gpui"
version = "0.1.0"
dependencies = [
"accesskit",
"accesskit_macos",
"anyhow",
"as-raw-xcb-connection",
"ashpd",
@@ -6179,6 +6218,7 @@ dependencies = [
"core-video",
"cosmic-text",
"ctor",
"current_commit_sha",
"derive_more",
"embed-resource",
"env_logger 0.11.8",
@@ -6200,8 +6240,8 @@ dependencies = [
"naga",
"num_cpus",
"objc",
"objc2",
"objc2-metal",
"objc2 0.6.1",
"objc2-metal 0.3.1",
"oo7",
"open",
"parking",
@@ -9368,6 +9408,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.1"
@@ -9377,6 +9433,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.9.0",
"block2 0.5.1",
"libc",
"objc2 0.5.2",
"objc2-core-data",
"objc2-core-image",
"objc2-foundation 0.2.2",
"objc2-quartz-core 0.2.2",
]
[[package]]
name = "objc2-app-kit"
version = "0.3.1"
@@ -9384,10 +9456,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6f29f568bec459b0ddff777cec4fe3fd8666d82d5a40ebd0ff7e66134f89bcc"
dependencies = [
"bitflags 2.9.0",
"objc2",
"objc2 0.6.1",
"objc2-core-foundation",
"objc2-foundation",
"objc2-quartz-core",
"objc2-foundation 0.3.1",
"objc2-quartz-core 0.3.1",
]
[[package]]
name = "objc2-core-data"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [
"bitflags 2.9.0",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
@@ -9398,7 +9482,19 @@ checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166"
dependencies = [
"bitflags 2.9.0",
"dispatch2",
"objc2",
"objc2 0.6.1",
]
[[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]]
@@ -9407,6 +9503,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.9.0",
"block2 0.5.1",
"libc",
"objc2 0.5.2",
]
[[package]]
name = "objc2-foundation"
version = "0.3.1"
@@ -9414,10 +9522,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "900831247d2fe1a09a683278e5384cfb8c80c79fe6b166f9d14bfdde0ea1b03c"
dependencies = [
"bitflags 2.9.0",
"objc2",
"objc2 0.6.1",
"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.9.0",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
]
[[package]]
name = "objc2-metal"
version = "0.3.1"
@@ -9425,11 +9545,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f246c183239540aab1782457b35ab2040d4259175bd1d0c58e46ada7b47a874"
dependencies = [
"bitflags 2.9.0",
"block2",
"block2 0.6.1",
"dispatch2",
"objc2",
"objc2 0.6.1",
"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.9.0",
"block2 0.5.1",
"objc2 0.5.2",
"objc2-foundation 0.2.2",
"objc2-metal 0.2.2",
]
[[package]]
@@ -9439,10 +9572,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ffb6a0cd5f182dc964334388560b12a57f7b74b3e2dec5e2722aa2dfb2ccd5"
dependencies = [
"bitflags 2.9.0",
"objc2",
"objc2 0.6.1",
"objc2-core-foundation",
"objc2-foundation",
"objc2-metal",
"objc2-foundation 0.3.1",
"objc2-metal 0.3.1",
]
[[package]]
@@ -9452,10 +9585,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25b1312ad7bc8a0e92adae17aa10f90aae1fb618832f9b993b022b591027daed"
dependencies = [
"bitflags 2.9.0",
"objc2",
"objc2 0.6.1",
"objc2-core-foundation",
"objc2-foundation",
"objc2-quartz-core",
"objc2-foundation 0.3.1",
"objc2-quartz-core 0.3.1",
]
[[package]]
@@ -18108,9 +18241,9 @@ dependencies = [
"num-iter",
"num-rational",
"num-traits",
"objc2",
"objc2-foundation",
"objc2-metal",
"objc2 0.6.1",
"objc2-foundation 0.3.1",
"objc2-metal 0.3.1",
"object",
"once_cell",
"percent-encoding",
@@ -18554,6 +18687,7 @@ dependencies = [
"command_palette",
"component",
"copilot",
"current_commit_sha",
"dap",
"dap_adapters",
"db",

View File

@@ -34,6 +34,7 @@ members = [
"crates/context_server",
"crates/copilot",
"crates/credentials_provider",
"crates/current_commit_sha",
"crates/dap",
"crates/dap_adapters",
"crates/db",
@@ -232,6 +233,7 @@ cli = { path = "crates/cli" }
client = { path = "crates/client" }
clock = { path = "crates/clock" }
collab = { path = "crates/collab" }
current_commit_sha = { path = "crates/current_commit_sha" }
collab_ui = { path = "crates/collab_ui" }
collections = { path = "crates/collections" }
command_palette = { path = "crates/command_palette" }
@@ -380,6 +382,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
accesskit = { path = "../accesskit/common" }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"

127
accesskit.md Normal file
View File

@@ -0,0 +1,127 @@
# Adding AccessKit to your immediate mode UI framework
AccessKit is a cool rust project that provides a cross-platform library for interfacing with accessibility APIs.
It's a great project, but when setting out to add it to GPUI, Zed's UI framework, I had trouble figuring out how to even start.
So here's the tutorial I wish I had when I started:
## Step 0: Acquiring AccessKit
AccessKit is available on [crates.io](https://crates.io/crates/accesskit)
If you're using macOS, you'll also need to bundle your app.
## Step 1: Turning it On
AccessKit works by creating an "Adapter" object that does the work of communicating
with the platform. This interface is in a seperate, platform-specific crate.
At Zed, we're going to build our own accesskit-winit style general adapter, so we'll use
it's API as a model for the rest of this document. But first, let's make the macOS adapter
First, you need to have some way of implementing `accesskit::ActionHandler`, that works for your
framework. In gpui, we have a `MacWindowState` struct behind an `Arc<Mutex>` with a callback pointing
up to the general puprose framework, so let's use `Arc::new_cyclic` to capture a pointer to that struct in
the adapter:
```rs
struct MacWindow(Arc<Mutex<MacWindowState>>);
struct MacWindowState {
//...
accesskit_adapter: accesskit_macos::Adapter,
accesskit_action_handler: Option<Box<dyn FnMut(ActionRequest)>>
}
struct MacActionHandler(Weak<Mutex<MacWindowState>>);
impl accesskit::ActionHandler for MacActionHandler {
fn do_action(&mut self, request: accesskit::ActionRequest) {
if let Some(this) = self.0.upgrade() {
let mut lock = this.lock();
if let Some(mut callback) = lock.accesskit_action_handler_callback.take() {
drop(lock);
callback(request);
this.lock().accesskit_action_handler_callback = Some(callback);
};
}
}
}
// Later
let window = MacWindow(Arc::new_cyclic(|weak| {
Mutex::new(MacWindowState {
//....
accesskit_action_handler: None,
acceskit_adapter: accesskit_macos::Adapter::new(
native_view as *mut _,
focus,
MacActionHandler(weak.clone()),
)
})
}))
// Later still
impl PlatformWindow for MacWindow {
//.....
fn on_accesskit_action(&self, callback: Box<dyn FnMut(accesskit::ActionRequest)>) {
self.0.lock().accesskit_action_handler_callback = Some(callback);
}
}
```
And then we can handle it in the UI framework with just a little bit of wiring:
```rs
// in gpui::Window::new(handle)
let handle: AnyWindowHandle;
let mut window = cx.platform.open_window(/*...*/);
platform_window.on_accesskit_action({
let mut cx = cx.to_async();
Box::new(move |action| {
handle
.update(&mut cx, |_, window, cx| {
window.dispatch_accessibility_action(action, cx)
})
.log_err();
})
});
```
----
QUESTIONS:
- re: `update_view_focus_state` on macOS, is this for the window focus???
-----
- Nodes: these are the accesibility units
- NodeID: Used for references (just a number)
- Generate the tree, and then publish minimal updates to the tree as you render
- use the `Node` struct to create and provide data
- `Role` struct is important for Aria stuff
- `TreeUpdate` consists of nodes that have changed (NodeId, Node)
- `TreeUpdate.focus` -> The element that is currently focused
- Winit specific: `Adapter` The thing that we need to create to interface with accessibility
- These "adapters" are per-platform, and since we're not using winit, we need to pull in each individual
adapter (e.g.https://crates.io/crates/accesskit_macos)
- `update_if_active` is very important, RESEARCH THIS
- Actually testing on macOS:
- 1 use voice over to see how it reads
- 2 use accessibility inspector to read the data structures
- Only works if you've bundled
- Core issue: Bidirectional communication based on AcccesKit NodeIds
- Core issue: Need minimal acceskit tree updates, with stable node IDs
- for GPUI, essentially use Winit's approach to abstracting AcceskitAdapters
- OR look at how Glazier adopted AccessKit: https://github.com/linebender/glazier
- GOAL: Get the winit simple example of accesskit working
- Issue: AccessKit panics, rather than fails silently. This causes issues for things like dangling NodeIds
- !!!
- KitTest
-

View File

@@ -0,0 +1,14 @@
[package]
name = "current_commit_sha"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "Apache-2.0"
[lints]
workspace = true
[lib]
path = "src/current_commit_sha.rs"
[dependencies]

View File

@@ -0,0 +1,29 @@
use std::process::Command;
pub fn generate_current_commit_sha(prefix: &'static str, repo_root: &str, print_warning: bool) {
println!("cargo:rerun-if-changed={repo_root}/.git/logs/HEAD");
if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
if output.status.success() {
let git_sha = String::from_utf8_lossy(&output.stdout);
let git_sha = git_sha.trim();
println!("cargo:rustc-env={prefix}_COMMIT_SHA={git_sha}");
if print_warning {
if let Ok(build_profile) = std::env::var("PROFILE") {
if build_profile == "release" {
// This is currently the best way to make `cargo build ...`'s build script
// to print something to stdout without extra verbosity.
println!(
"cargo:warning=Info: using '{git_sha}' hash for {prefix}_COMMIT_SHA env var"
);
}
}
}
}
}
}
pub fn current_commit_sha(prefix: &'static str) -> Option<String> {
std::env::var(format!("{prefix}_COMMIT_SHA")).ok()
}

View File

@@ -75,6 +75,7 @@ path = "src/gpui.rs"
doctest = false
[dependencies]
accesskit.workspace = true
anyhow.workspace = true
async-task = "4.7"
backtrace = { version = "0.3", optional = true }
@@ -84,6 +85,7 @@ blade-util = { workspace = true, optional = true }
bytemuck = { version = "1", optional = true }
collections.workspace = true
ctor.workspace = true
current_commit_sha.workspace = true
derive_more.workspace = true
etagere = "0.2"
futures.workspace = true
@@ -143,6 +145,7 @@ objc2 = { version = "0.6", optional = true }
objc2-metal = { version = "0.3", optional = true }
#TODO: replace with "objc2"
metal.workspace = true
accesskit_macos = { path = "../../../accesskit/platforms/macos"}
[target.'cfg(any(target_os = "linux", target_os = "freebsd", target_os = "macos"))'.dependencies]
pathfinder_geometry = "0.5"
@@ -244,6 +247,8 @@ naga.workspace = true
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.build-dependencies]
naga.workspace = true
[build-dependencies]
current_commit_sha.workspace = true
[[example]]
name = "hello_world"

View File

@@ -12,6 +12,8 @@ fn main() {
#[cfg(any(not(target_os = "macos"), feature = "macos-blade"))]
check_wgsl_shaders();
current_commit_sha::generate_current_commit_sha("GPUI", "../../", false);
match target.as_deref() {
Ok("macos") => {
#[cfg(target_os = "macos")]

View File

@@ -0,0 +1,60 @@
use gpui::{
App, Application, Bounds, Context, Window, WindowBounds, WindowOptions, div, prelude::*, px,
rgb, size,
};
struct HelloWorld {}
impl Render for HelloWorld {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
div()
.flex()
.flex_col()
.gap_3()
.bg(rgb(0x505050))
.size(px(500.0))
.justify_center()
.items_center()
.shadow_lg()
.border_1()
.border_color(rgb(0x0000ff))
.text_xl()
.text_color(rgb(0xffffff))
.child(format!("Hello, world!"))
.child(
div()
.child(
div().child("Button label"), // .aria_role(accesskit::Role::Label),
)
.flex()
.gap_2()
.child(
div()
.id("button")
.size_8()
.bg(gpui::red())
.border_1()
.border_dashed()
.rounded_md()
.border_color(gpui::white())
// .aria_role(accesskit::Role::Button)
.on_click(|_, _, _| println!("button clicked")),
),
)
}
}
fn main() {
Application::new().run(|cx: &mut App| {
let bounds = Bounds::centered(None, size(px(500.), px(500.0)), cx);
cx.open_window(
WindowOptions {
window_bounds: Some(WindowBounds::Windowed(bounds)),
..Default::default()
},
|_, cx| cx.new(|_| HelloWorld {}),
)
.unwrap();
cx.activate(true);
});
}

View File

@@ -0,0 +1,100 @@
// pub const NULL_NODE_ID: accesskit::NodeId = accesskit::NodeId(0);
// pub fn null_node() -> accesskit::Node {
// let node = accesskit::Node::new(accesskit::Role::Window);
// node
// }
use std::mem;
use collections::HashMap;
use crate::{DispatchTree, EntityId, FocusId};
pub struct AccessibilityData {
next_node_id: u64,
entity_ids: HashMap<EntityId, accesskit::NodeId>,
focus_ids: HashMap<FocusId, accesskit::NodeId>,
last_nodes: HashMap<accesskit::NodeId, accesskit::Node>,
current_nodes: HashMap<accesskit::NodeId, accesskit::Node>,
node_stack: Vec<accesskit::NodeId>,
}
impl AccessibilityData {
pub fn new() -> Self {
AccessibilityData {
next_node_id: 0,
entity_ids: HashMap::default(),
last_nodes: HashMap::default(),
current_nodes: HashMap::default(),
focus_ids: HashMap::default(),
node_stack: Vec::new(),
}
}
// TODO: This API should work better,
pub fn node_id_for_entity(&mut self, entity_id: EntityId) -> accesskit::NodeId {
*self
.entity_ids
.entry(entity_id)
.or_insert_with(|| accesskit::NodeId(util::post_inc(&mut self.next_node_id)))
}
/// Get the nearest ancestor node ID for a given focus id
pub fn nearest_node_id(
&mut self,
focus_id: FocusId,
dispatch_tree: &DispatchTree,
) -> Option<accesskit::NodeId> {
let mut focus_path = dispatch_tree.focus_path(focus_id);
focus_path.reverse();
for focus_id in focus_path.into_iter() {
if let Some(node_id) = self.focus_ids.get(&focus_id) {
return Some(*node_id);
}
}
None
}
pub fn push_node(&mut self, node_id: accesskit::NodeId) {
debug_assert!(self.current_nodes.get(&node_id).is_some());
self.node_stack.push(node_id);
}
pub fn pop_node(&mut self) {
if let Some(child_id) = self.node_stack.pop() {
if let Some(parent_id) = self.node_stack.last_mut() {
let parent_node = self
.current_nodes
.get_mut(parent_id)
.expect("Nodes must be inserted before using");
parent_node.push_child(child_id);
}
}
}
pub fn insert_node(&mut self, node_id: accesskit::NodeId, node: accesskit::Node) {
self.current_nodes.insert(node_id, node);
}
pub fn updated_nodes(&mut self) -> Vec<(accesskit::NodeId, accesskit::Node)> {
debug_assert!(self.node_stack.is_empty());
let mut changed_ids = Vec::new();
for id in self.current_nodes.keys() {
if let Some(node) = self.last_nodes.get(id) {
if self.current_nodes[id] != *node {
changed_ids.push((*id, self.current_nodes[id].clone()));
}
} else {
changed_ids.push((*id, self.current_nodes[id].clone()));
}
}
mem::swap(&mut self.current_nodes, &mut self.last_nodes);
self.current_nodes.clear();
changed_ids
}
}

View File

@@ -13,6 +13,7 @@ use std::{
marker::PhantomData,
mem,
num::NonZeroU64,
rc::Rc,
sync::{
Arc, Weak,
atomic::{AtomicU64, AtomicUsize, Ordering::SeqCst},

View File

@@ -117,6 +117,11 @@ impl<T: IntoElement> FluentBuilder for T {}
/// An object that can be drawn to the screen. This is the trait that distinguishes "views" from
/// other entities. Views are `Entity`'s which `impl Render` and drawn to the screen.
pub trait Render: 'static + Sized {
/// The accessibility node for this view, if nescessary.
fn accessibility_node(&self, _window: &mut Window, _cx: &mut Context<Self>) -> accesskit::Node {
accesskit::Node::new(accesskit::Role::GenericContainer)
}
/// Render this view into an element tree.
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement;
}

View File

@@ -68,6 +68,7 @@
mod action;
mod app;
mod accessibility;
mod arena;
mod asset_cache;
mod assets;

View File

@@ -38,6 +38,7 @@ use crate::{
ShapedGlyph, ShapedRun, SharedString, Size, SvgRenderer, SvgSize, Task, TaskLabel, Window,
hash, point, px, size,
};
use anyhow::Result;
use async_task::Runnable;
use futures::channel::oneshot;
@@ -437,6 +438,7 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn on_should_close(&self, callback: Box<dyn FnMut() -> bool>);
fn on_close(&self, callback: Box<dyn FnOnce()>);
fn on_appearance_changed(&self, callback: Box<dyn FnMut()>);
fn on_accesskit_action(&self, callback: Box<dyn FnMut(accesskit::ActionRequest)>);
fn draw(&self, scene: &Scene);
fn completed_frame(&self) {}
fn sprite_atlas(&self) -> Arc<dyn PlatformAtlas>;
@@ -471,6 +473,9 @@ pub(crate) trait PlatformWindow: HasWindowHandle + HasDisplayHandle {
fn update_ime_position(&self, _bounds: Bounds<ScaledPixels>);
fn accesskit_active(&self) -> bool;
fn accesskit_update(&self, tree: accesskit::TreeUpdate);
#[cfg(any(test, feature = "test-support"))]
fn as_test(&mut self) -> Option<&mut TestWindow> {
None

View File

@@ -7,6 +7,7 @@ use crate::{
WindowAppearance, WindowBackgroundAppearance, WindowBounds, WindowKind, WindowParams,
platform::PlatformInputHandler, point, px, size,
};
use accesskit::ActivationHandler;
use block::ConcreteBlock;
use cocoa::{
appkit::{
@@ -336,6 +337,7 @@ struct MacWindowState {
native_view: NonNull<Object>,
display_link: Option<DisplayLink>,
renderer: renderer::Renderer,
accesskit_adapter: accesskit_macos::Adapter,
request_frame_callback: Option<Box<dyn FnMut(RequestFrameOptions)>>,
event_callback: Option<Box<dyn FnMut(PlatformInput) -> crate::DispatchEventResult>>,
activate_callback: Option<Box<dyn FnMut(bool)>>,
@@ -344,6 +346,7 @@ struct MacWindowState {
should_close_callback: Option<Box<dyn FnMut() -> bool>>,
close_callback: Option<Box<dyn FnOnce()>>,
appearance_changed_callback: Option<Box<dyn FnMut()>>,
accesskit_action_handler_callback: Option<Box<dyn FnMut(accesskit::ActionRequest)>>,
input_handler: Option<PlatformInputHandler>,
last_key_equivalent: Option<KeyDownEvent>,
synthetic_drag_counter: usize,
@@ -499,6 +502,21 @@ impl MacWindowState {
}
}
struct MacActionHandler(Weak<Mutex<MacWindowState>>);
impl accesskit::ActionHandler for MacActionHandler {
fn do_action(&mut self, request: accesskit::ActionRequest) {
if let Some(this) = self.0.upgrade() {
let mut lock = this.lock();
if let Some(mut callback) = lock.accesskit_action_handler_callback.take() {
drop(lock);
callback(request);
this.lock().accesskit_action_handler_callback = Some(callback);
};
}
}
}
unsafe impl Send for MacWindowState {}
pub(crate) struct MacWindow(Arc<Mutex<MacWindowState>>);
@@ -603,43 +621,51 @@ impl MacWindow {
let native_view = NSView::init(native_view);
assert!(!native_view.is_null());
let mut window = Self(Arc::new(Mutex::new(MacWindowState {
handle,
executor,
native_window,
native_view: NonNull::new_unchecked(native_view),
display_link: None,
renderer: renderer::new_renderer(
renderer_context,
native_window as *mut _,
native_view as *mut _,
bounds.size.map(|pixels| pixels.0),
false,
),
request_frame_callback: None,
event_callback: None,
activate_callback: None,
resize_callback: None,
moved_callback: None,
should_close_callback: None,
close_callback: None,
appearance_changed_callback: None,
input_handler: None,
last_key_equivalent: None,
synthetic_drag_counter: 0,
traffic_light_position: titlebar
.as_ref()
.and_then(|titlebar| titlebar.traffic_light_position),
transparent_titlebar: titlebar
.as_ref()
.map_or(true, |titlebar| titlebar.appears_transparent),
previous_modifiers_changed_event: None,
keystroke_for_do_command: None,
do_command_handled: None,
external_files_dragged: false,
first_mouse: false,
fullscreen_restore_bounds: Bounds::default(),
})));
let mut window = Self(Arc::new_cyclic(|weak| {
Mutex::new(MacWindowState {
handle,
executor,
native_window,
native_view: NonNull::new_unchecked(native_view),
display_link: None,
renderer: renderer::new_renderer(
renderer_context,
native_window as *mut _,
native_view as *mut _,
bounds.size.map(|pixels| pixels.0),
false,
),
accesskit_adapter: accesskit_macos::Adapter::new(
native_view as *mut _,
focus,
MacActionHandler(weak.clone()),
),
request_frame_callback: None,
event_callback: None,
activate_callback: None,
resize_callback: None,
moved_callback: None,
should_close_callback: None,
close_callback: None,
appearance_changed_callback: None,
accesskit_action_handler_callback: None,
input_handler: None,
last_key_equivalent: None,
synthetic_drag_counter: 0,
traffic_light_position: titlebar
.as_ref()
.and_then(|titlebar| titlebar.traffic_light_position),
transparent_titlebar: titlebar
.as_ref()
.map_or(true, |titlebar| titlebar.appears_transparent),
previous_modifiers_changed_event: None,
keystroke_for_do_command: None,
do_command_handled: None,
external_files_dragged: false,
first_mouse: false,
fullscreen_restore_bounds: Bounds::default(),
})
}));
(*native_window).set_ivar(
WINDOW_STATE_IVAR,
@@ -721,6 +747,8 @@ impl MacWindow {
}
}
window.activate_accessibility();
if focus && show {
native_window.makeKeyAndOrderFront_(nil);
} else if show {
@@ -775,6 +803,19 @@ impl MacWindow {
window_handles
}
}
pub fn activate_accessibility(&self) {
// We'll send the first tree update on the next frame draw
struct NoActivation;
impl ActivationHandler for NoActivation {
fn request_initial_tree(&mut self) -> Option<accesskit::TreeUpdate> {
None
}
}
let mut window = self.0.lock();
window.accesskit_adapter.focus(&mut NoActivation);
}
}
impl Drop for MacWindow {
@@ -1138,6 +1179,21 @@ impl PlatformWindow for MacWindow {
self.0.lock().appearance_changed_callback = Some(callback);
}
fn on_accesskit_action(&self, callback: Box<dyn FnMut(accesskit::ActionRequest)>) {
self.0.lock().accesskit_action_handler_callback = Some(callback);
}
fn accesskit_active(&self) -> bool {
self.0.lock().accesskit_adapter.active()
}
fn accesskit_update(&self, update: accesskit::TreeUpdate) {
dbg!(&update);
if let Some(events) = self.0.lock().accesskit_adapter.update(update) {
events.raise();
}
}
fn draw(&self, scene: &crate::Scene) {
let mut this = self.0.lock();
this.renderer.draw(scene);

View File

@@ -254,6 +254,8 @@ impl PlatformWindow for TestWindow {
fn on_close(&self, _callback: Box<dyn FnOnce()>) {}
fn on_accesskit_action(&self, _callback: Box<dyn FnMut(accesskit::ActionRequest)>) {}
fn on_appearance_changed(&self, _callback: Box<dyn FnMut()>) {}
fn draw(&self, _scene: &crate::Scene) {}
@@ -284,6 +286,12 @@ impl PlatformWindow for TestWindow {
fn gpu_specs(&self) -> Option<GpuSpecs> {
None
}
fn accesskit_active(&self) -> bool {
false
}
fn accesskit_update(&self, _update: accesskit::TreeUpdate) {}
}
pub(crate) struct TestAtlasState {

View File

@@ -1,7 +1,7 @@
use crate::{
AnyElement, AnyEntity, AnyWeakEntity, App, Bounds, ContentMask, Context, Element, ElementId,
Entity, EntityId, GlobalElementId, IntoElement, LayoutId, PaintIndex, Pixels,
PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity,
PrepaintStateIndex, Render, Style, StyleRefinement, TextStyle, WeakEntity, element,
};
use crate::{Empty, Window};
use anyhow::Result;
@@ -26,7 +26,7 @@ struct ViewCacheKey {
}
impl<V: Render> Element for Entity<V> {
type RequestLayoutState = AnyElement;
type RequestLayoutState = (AnyElement, accesskit::Node);
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
@@ -39,30 +39,39 @@ impl<V: Render> Element for Entity<V> {
window: &mut Window,
cx: &mut App,
) -> (LayoutId, Self::RequestLayoutState) {
let mut element = self.update(cx, |view, cx| view.render(window, cx).into_any_element());
let layout_id = window.with_rendered_view(self.entity_id(), |window| {
element.request_layout(window, cx)
let node_id = window.accessibility.node_id_for_entity(self.entity_id);
let (mut node, mut element) = self.update(cx, |view, cx| {
let node = view.accessibility_node(window, cx);
let element = view.render(window, cx).into_any_element();
(node, element)
});
(layout_id, element)
let layout_id = window.with_rendered_view(self.entity_id(), |window| {
window.with_accessibility_node(node_id, node.clone(), |window| {
element.request_layout(window, cx)
})
});
(layout_id, (element, node))
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
(element, node): &mut Self::RequestLayoutState,
window: &mut Window,
cx: &mut App,
) {
window.set_view_id(self.entity_id());
window.with_rendered_view(self.entity_id(), |window| element.prepaint(window, cx));
window.with_rendered_view(self.entity_id(), |window| {
element.prepaint(window, cx)
});
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_: Bounds<Pixels>,
element: &mut Self::RequestLayoutState,
(element, node): &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
window: &mut Window,
cx: &mut App,
@@ -75,6 +84,7 @@ impl<V: Render> Element for Entity<V> {
#[derive(Clone, Debug)]
pub struct AnyView {
entity: AnyEntity,
accessibility_node: fn(&AnyView, &mut Window, &mut App) -> accesskit::Node,
render: fn(&AnyView, &mut Window, &mut App) -> AnyElement,
cached_style: Option<Rc<StyleRefinement>>,
}
@@ -84,6 +94,7 @@ impl<V: Render> From<Entity<V>> for AnyView {
AnyView {
entity: value.into_any(),
render: any_view::render::<V>,
accessibility_node: any_view::accessibility_node::<V>,
cached_style: None,
}
}
@@ -103,6 +114,7 @@ impl AnyView {
AnyWeakView {
entity: self.entity.downgrade(),
render: self.render,
accessibility_node: self.accessibility_node,
}
}
@@ -114,6 +126,7 @@ impl AnyView {
Err(entity) => Err(Self {
entity,
render: self.render,
accessibility_node: self.accessibility_node,
cached_style: self.cached_style,
}),
}
@@ -296,6 +309,7 @@ impl IntoElement for AnyView {
pub struct AnyWeakView {
entity: AnyWeakEntity,
render: fn(&AnyView, &mut Window, &mut App) -> AnyElement,
accessibility_node: fn(&AnyView, &mut Window, &mut App) -> accesskit::Node,
}
impl AnyWeakView {
@@ -305,6 +319,7 @@ impl AnyWeakView {
Some(AnyView {
entity,
render: self.render,
accessibility_node: self.accessibility_node,
cached_style: None,
})
}
@@ -315,6 +330,7 @@ impl<V: 'static + Render> From<WeakEntity<V>> for AnyWeakView {
AnyWeakView {
entity: view.into(),
render: any_view::render::<V>,
accessibility_node: any_view::accessibility_node::<V>,
}
}
}
@@ -344,6 +360,14 @@ mod any_view {
let view = view.clone().downcast::<V>().unwrap();
view.update(cx, |view, cx| view.render(window, cx).into_any_element())
}
pub(crate) fn accessibility_node<V: 'static + Render>(
view: &AnyView,
window: &mut Window,
cx: &mut App,
) -> accesskit::Node {
let view = view.clone().downcast::<V>().unwrap();
view.update(cx, |view, cx| view.accessibility_node(window, cx))
}
}
/// A view that renders nothing

View File

@@ -13,8 +13,9 @@ use crate::{
SubscriberSet, Subscription, TaffyLayoutEngine, Task, TextStyle, TextStyleRefinement,
TransformationMatrix, Underline, UnderlineStyle, WindowAppearance, WindowBackgroundAppearance,
WindowBounds, WindowControls, WindowDecorations, WindowOptions, WindowParams, WindowTextSystem,
point, prelude::*, px, size, transparent_black,
accessibility::AccessibilityData, point, prelude::*, px, size, transparent_black,
};
use accesskit::Tree;
use anyhow::{Context as _, Result, anyhow};
use collections::{FxHashMap, FxHashSet};
#[cfg(target_os = "macos")]
@@ -31,7 +32,7 @@ use std::{
any::{Any, TypeId},
borrow::Cow,
cell::{Cell, RefCell},
cmp,
cmp, env,
fmt::{Debug, Display},
hash::{Hash, Hasher},
marker::PhantomData,
@@ -648,6 +649,7 @@ pub struct Window {
pub(crate) pending_input_observers: SubscriberSet<(), AnyObserver>,
prompt: Option<RenderablePromptHandle>,
pub(crate) client_inset: Option<Pixels>,
pub(crate) accessibility: AccessibilityData,
}
#[derive(Clone, Debug, Default)]
@@ -877,6 +879,16 @@ impl Window {
.unwrap_or(DispatchEventResult::default())
})
});
platform_window.on_accesskit_action({
let mut cx = cx.to_async();
Box::new(move |action| {
handle
.update(&mut cx, |_, window, cx| {
window.dispatch_accessibility_action(action, cx)
})
.log_err();
})
});
if let Some(app_id) = app_id {
platform_window.set_app_id(&app_id);
@@ -935,6 +947,7 @@ impl Window {
prompt: None,
client_inset: None,
image_cache_stack: Vec::new(),
accessibility: AccessibilityData::new(),
})
}
@@ -1619,11 +1632,45 @@ impl Window {
debug_assert!(self.rendered_entity_stack.is_empty());
self.record_entities_accessed(cx);
self.reset_cursor_style(cx);
if self.platform_window.accesskit_active() {
let update = self.accesskit_tree_diff();
self.platform_window.accesskit_update(update);
}
self.refreshing = false;
self.invalidator.set_phase(DrawPhase::None);
self.needs_present.set(true);
}
fn accesskit_tree_diff(&mut self) -> accesskit::TreeUpdate {
let root = self.root.clone().unwrap();
let root_id = self.accessibility.node_id_for_entity(root.entity_id());
let focus_node_id = self
.focus
.as_ref()
.and_then(|focus_id| {
self.accessibility
.nearest_node_id(*focus_id, &self.rendered_frame.dispatch_tree)
})
.unwrap_or(root_id);
let new_nodes = self.accessibility.updated_nodes();
accesskit::TreeUpdate {
focus: focus_node_id,
nodes: new_nodes,
tree: Some(Tree {
root: root_id,
toolkit_name: Some("GPUI".to_string()),
toolkit_version: current_commit_sha::current_commit_sha("GPUI")
.or_else(|| env::var("CARGO_PKG_VERSION").ok())
.or_else(|| Some("<unknown>".to_string())),
}),
}
}
fn record_entities_accessed(&mut self, cx: &mut App) {
let mut entities_ref = cx.entities.accessed_entities.borrow_mut();
let mut entities = mem::take(entities_ref.deref_mut());
@@ -2877,10 +2924,13 @@ impl Window {
pub(crate) fn with_rendered_view<R>(
&mut self,
id: EntityId,
node: accesskit::Node,
f: impl FnOnce(&mut Self) -> R,
) -> R {
self.rendered_entity_stack.push(id);
let node_id = self.accessibility.node_id_for_entity(id);
let result = f(self);
self.accessibility.pop_node();
self.rendered_entity_stack.pop();
result
}
@@ -3597,6 +3647,28 @@ impl Window {
}
}
fn dispatch_accessibility_action(
&mut self,
action_request: accesskit::ActionRequest,
_cx: &mut App,
) {
dbg!(action_request);
}
/// Run the given closure with a given accessibility node.
pub fn with_accessibility_node<R>(
&mut self,
node_id: accesskit::NodeId,
node: accesskit::Node,
f: impl FnOnce(&mut Window) -> R,
) -> R {
node.push_child(item);
self.accessibility.push_node(node_id, node);
let result = f(self);
self.accessibility.pop_node();
result
}
/// Register the given handler to be invoked whenever the global of the given type
/// is updated.
pub fn observe_global<G: Global>(

View File

@@ -154,6 +154,9 @@ winresource = "0.1"
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
ashpd.workspace = true
[build-dependencies]
current_commit_sha.workspace = true
[dev-dependencies]
call = { workspace = true, features = ["test-support"] }
dap = { workspace = true, features = ["test-support"] }

View File

@@ -1,5 +1,3 @@
use std::process::Command;
fn main() {
if cfg!(target_os = "macos") {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15.7");
@@ -18,29 +16,11 @@ fn main() {
}
// Populate git sha environment variable if git is available
println!("cargo:rerun-if-changed=../../.git/logs/HEAD");
current_commit_sha::generate_current_commit_sha("ZED", "../../", true);
println!(
"cargo:rustc-env=TARGET={}",
std::env::var("TARGET").unwrap()
);
if let Ok(output) = Command::new("git").args(["rev-parse", "HEAD"]).output() {
if output.status.success() {
let git_sha = String::from_utf8_lossy(&output.stdout);
let git_sha = git_sha.trim();
println!("cargo:rustc-env=ZED_COMMIT_SHA={git_sha}");
if let Ok(build_profile) = std::env::var("PROFILE") {
if build_profile == "release" {
// This is currently the best way to make `cargo build ...`'s build script
// to print something to stdout without extra verbosity.
println!(
"cargo:warning=Info: using '{git_sha}' hash for ZED_COMMIT_SHA env var"
);
}
}
}
}
#[cfg(target_os = "windows")]
{

View File

@@ -55,6 +55,8 @@ use zed::{
open_paths_with_positions,
};
use crate::zed::component_preview;
#[cfg(feature = "mimalloc")]
#[global_allocator]
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
@@ -660,7 +662,7 @@ fn main() {
let app_state = app_state.clone();
crate::zed::component_preview::init(app_state.clone(), cx);
component_preview::init(app_state.clone(), cx);
cx.spawn(async move |cx| {
while let Some(urls) = open_rx.next().await {
@@ -1034,6 +1036,10 @@ struct Args {
#[cfg(target_os = "windows")]
#[arg(hide = true)]
dock_action: Option<usize>,
#[arg(long)]
#[arg(hide = true)]
component_preview: Option<String>,
}
#[derive(Clone, Debug)]

13
tes.md Normal file
View File

@@ -0,0 +1,13 @@
See Miro for current: https://miro.com/welcomeonboard/OXl3SFBwOUFManhuN25tV1dkU2E0UVgzYnVIcHRRVWZkT0RJY1JqMm1PRmVmSTU3MlcweTRsRG9tb0dHWml3dlh5NTN6aSthcVlMSW9ZSlcvZWlxUVQ1NkhveXdJUjRzaVBhanFqRld5MFhOd2Fjcmh5MTlqR1diRHRGbzhNSHl0R2lncW1vRmFBVnlLcVJzTmdFdlNRPT0hdjE=?share_link_id=220464759562
Monetization follow ups:
- For new user flows:
- Create transition from free to trial
- Transition trial to free instead of "no plan"
- Transition from cancel-pro to free instead of "no plan"
- For existing users:
- Move from "no plan" to free plan
- UI in general:
- "Subscribe to free" button should not exist, free plan is the default
-