Skip to content

Commit a06725e

Browse files
mwcampbellmadsmtm
andauthored
feat(platforms/macos): Basic macOS platform adapter (#158)
The major missing feature at this point is text editing support. Co-authored-by: Mads Marquart <[email protected]>
1 parent be88b64 commit a06725e

22 files changed

+1427
-2
lines changed

Cargo.lock

Lines changed: 57 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
members = [
33
"common",
44
"consumer",
5+
"platforms/macos",
56
"platforms/windows",
67
"platforms/winit",
78
]

consumer/src/node.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ use std::{iter::FusedIterator, ops::Deref, sync::Arc};
1212

1313
use accesskit::kurbo::{Affine, Point, Rect};
1414
use accesskit::{
15-
CheckedState, DefaultActionVerb, Live, Node as NodeData, NodeId, Role, TextSelection,
15+
Action, CheckedState, DefaultActionVerb, Live, Node as NodeData, NodeId, Role, TextSelection,
1616
};
1717

1818
use crate::iterators::{
@@ -275,6 +275,10 @@ impl<'a> Node<'a> {
275275
parent_transform * self.direct_transform()
276276
}
277277

278+
pub fn has_bounds(&self) -> bool {
279+
self.data().bounds.is_some()
280+
}
281+
278282
/// Returns the node's transformed bounding box relative to the tree's
279283
/// container (e.g. window).
280284
pub fn bounding_box(&self) -> Option<Rect> {
@@ -464,6 +468,20 @@ impl NodeState {
464468
&& !self.supports_toggle()
465469
&& !self.supports_expand_collapse()
466470
}
471+
472+
// The future of the `Action` enum is undecided, so keep the following
473+
// function private for now.
474+
fn supports_action(&self, action: Action) -> bool {
475+
self.data().actions.contains(action)
476+
}
477+
478+
pub fn supports_increment(&self) -> bool {
479+
self.supports_action(Action::Increment)
480+
}
481+
482+
pub fn supports_decrement(&self) -> bool {
483+
self.supports_action(Action::Decrement)
484+
}
467485
}
468486

469487
impl<'a> Node<'a> {

consumer/src/tree.rs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,6 +397,22 @@ impl Tree {
397397
data: Some(ActionData::SetTextSelection(selection)),
398398
})
399399
}
400+
401+
pub fn increment(&self, target: NodeId) {
402+
self.action_handler.do_action(ActionRequest {
403+
action: Action::Increment,
404+
target,
405+
data: None,
406+
})
407+
}
408+
409+
pub fn decrement(&self, target: NodeId) {
410+
self.action_handler.do_action(ActionRequest {
411+
action: Action::Decrement,
412+
target,
413+
data: None,
414+
})
415+
}
400416
}
401417

402418
#[cfg(test)]

platforms/macos/Cargo.toml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[package]
2+
name = "accesskit_macos"
3+
version = "0.0.0"
4+
authors = ["Matt Campbell <[email protected]>"]
5+
license = "MIT/Apache-2.0"
6+
description = "AccessKit UI accessibility infrastructure: macOS adapter"
7+
categories = ["gui"]
8+
keywords = ["gui", "ui", "accessibility"]
9+
repository = "https://github.com/AccessKit/accesskit"
10+
readme = "README.md"
11+
edition = "2021"
12+
13+
[dependencies]
14+
accesskit = { version = "0.8.0", path = "../../common" }
15+
accesskit_consumer = { version = "0.8.0", path = "../../consumer" }
16+
objc2 = "0.3.0-beta.3"
17+
once_cell = "1.13.0"
18+
parking_lot = "0.12.1"

platforms/macos/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# AccessKit macOS adapter
2+
3+
This is the macOS adapter for [AccessKit](https://accesskit.dev/). It exposes an AccessKit accessibility tree through the Cocoa `NSAccessibility` protocol.

platforms/macos/src/adapter.rs

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2022 The AccessKit Authors. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0 (found in
3+
// the LICENSE-APACHE file) or the MIT license (found in
4+
// the LICENSE-MIT file), at your option.
5+
6+
use accesskit::{kurbo::Point, ActionHandler, TreeUpdate};
7+
use accesskit_consumer::{FilterResult, Tree};
8+
use objc2::{
9+
foundation::{MainThreadMarker, NSArray, NSObject, NSPoint},
10+
rc::{Id, Shared, WeakId},
11+
};
12+
use once_cell::unsync::Lazy;
13+
use std::{ffi::c_void, ptr::null_mut, rc::Rc};
14+
15+
use crate::{appkit::NSView, context::Context, event::QueuedEvents, node::filter};
16+
17+
pub struct Adapter {
18+
context: Lazy<Rc<Context>, Box<dyn FnOnce() -> Rc<Context>>>,
19+
}
20+
21+
impl Adapter {
22+
/// Create a new macOS adapter. This function must be called on
23+
/// the main thread.
24+
///
25+
/// # Safety
26+
///
27+
/// `view` must be a valid, unreleased pointer to an `NSView`.
28+
pub unsafe fn new(
29+
view: *mut c_void,
30+
source: Box<dyn FnOnce() -> TreeUpdate>,
31+
action_handler: Box<dyn ActionHandler>,
32+
) -> Self {
33+
let view = unsafe { Id::retain(view as *mut NSView) }.unwrap();
34+
let view = WeakId::new(&view);
35+
let mtm = MainThreadMarker::new().unwrap();
36+
Self {
37+
context: Lazy::new(Box::new(move || {
38+
let tree = Tree::new(source(), action_handler);
39+
Context::new(view, tree, mtm)
40+
})),
41+
}
42+
}
43+
44+
/// Initialize the tree if it hasn't been initialized already, then apply
45+
/// the provided update.
46+
///
47+
/// The caller must call [`QueuedEvents::raise`] on the return value.
48+
///
49+
/// This method may be safely called on any thread, but refer to
50+
/// [`QueuedEvents::raise`] for restrictions on the context in which
51+
/// it should be called.
52+
pub fn update(&self, update: TreeUpdate) -> QueuedEvents {
53+
let context = Lazy::force(&self.context);
54+
context.update(update)
55+
}
56+
57+
/// If and only if the tree has been initialized, call the provided function
58+
/// and apply the resulting update.
59+
///
60+
/// If a [`QueuedEvents`] instance is returned, the caller must call
61+
/// [`QueuedEvents::raise`] on it.
62+
///
63+
/// This method may be safely called on any thread, but refer to
64+
/// [`QueuedEvents::raise`] for restrictions on the context in which
65+
/// it should be called.
66+
pub fn update_if_active(&self, updater: impl FnOnce() -> TreeUpdate) -> Option<QueuedEvents> {
67+
Lazy::get(&self.context).map(|context| context.update(updater()))
68+
}
69+
70+
pub fn view_children(&self) -> *mut NSArray<NSObject> {
71+
let context = Lazy::force(&self.context);
72+
let state = context.tree.read();
73+
let node = state.root();
74+
let platform_nodes = if filter(&node) == FilterResult::Include {
75+
vec![Id::into_super(Id::into_super(
76+
context.get_or_create_platform_node(node.id()),
77+
))]
78+
} else {
79+
node.filtered_children(filter)
80+
.map(|node| {
81+
Id::into_super(Id::into_super(
82+
context.get_or_create_platform_node(node.id()),
83+
))
84+
})
85+
.collect::<Vec<Id<NSObject, Shared>>>()
86+
};
87+
let array = NSArray::from_vec(platform_nodes);
88+
Id::autorelease_return(array)
89+
}
90+
91+
pub fn focus(&self) -> *mut NSObject {
92+
let context = Lazy::force(&self.context);
93+
let state = context.tree.read();
94+
if let Some(node) = state.focus() {
95+
if filter(&node) == FilterResult::Include {
96+
return Id::autorelease_return(context.get_or_create_platform_node(node.id()))
97+
as *mut _;
98+
}
99+
}
100+
null_mut()
101+
}
102+
103+
pub fn hit_test(&self, point: NSPoint) -> *mut NSObject {
104+
let context = Lazy::force(&self.context);
105+
let view = match context.view.load() {
106+
Some(view) => view,
107+
None => {
108+
return null_mut();
109+
}
110+
};
111+
112+
let window = view.window().unwrap();
113+
let point = window.convert_point_from_screen(point);
114+
let point = view.convert_point_from_view(point, None);
115+
let view_bounds = view.bounds();
116+
let point = Point::new(point.x, view_bounds.size.height - point.y);
117+
118+
let state = context.tree.read();
119+
let root = state.root();
120+
let point = root.transform().inverse() * point;
121+
if let Some(node) = root.node_at_point(point, &filter) {
122+
return Id::autorelease_return(context.get_or_create_platform_node(node.id()))
123+
as *mut _;
124+
}
125+
null_mut()
126+
}
127+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
// Copyright 2022 The AccessKit Authors. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0 (found in
3+
// the LICENSE-APACHE file) or the MIT license (found in
4+
// the LICENSE-MIT file), at your option.
5+
6+
use objc2::foundation::NSString;
7+
8+
#[link(name = "AppKit", kind = "framework")]
9+
extern "C" {
10+
// Notifications
11+
pub(crate) static NSAccessibilityUIElementDestroyedNotification: &'static NSString;
12+
pub(crate) static NSAccessibilityFocusedUIElementChangedNotification: &'static NSString;
13+
pub(crate) static NSAccessibilityTitleChangedNotification: &'static NSString;
14+
pub(crate) static NSAccessibilityValueChangedNotification: &'static NSString;
15+
16+
// Roles
17+
pub(crate) static NSAccessibilityButtonRole: &'static NSString;
18+
pub(crate) static NSAccessibilityCheckBoxRole: &'static NSString;
19+
pub(crate) static NSAccessibilityCellRole: &'static NSString;
20+
pub(crate) static NSAccessibilityColorWellRole: &'static NSString;
21+
pub(crate) static NSAccessibilityColumnRole: &'static NSString;
22+
pub(crate) static NSAccessibilityComboBoxRole: &'static NSString;
23+
pub(crate) static NSAccessibilityGroupRole: &'static NSString;
24+
pub(crate) static NSAccessibilityImageRole: &'static NSString;
25+
pub(crate) static NSAccessibilityIncrementorRole: &'static NSString;
26+
pub(crate) static NSAccessibilityLevelIndicatorRole: &'static NSString;
27+
pub(crate) static NSAccessibilityLinkRole: &'static NSString;
28+
pub(crate) static NSAccessibilityListRole: &'static NSString;
29+
pub(crate) static NSAccessibilityMenuRole: &'static NSString;
30+
pub(crate) static NSAccessibilityMenuBarRole: &'static NSString;
31+
pub(crate) static NSAccessibilityMenuItemRole: &'static NSString;
32+
pub(crate) static NSAccessibilityOutlineRole: &'static NSString;
33+
pub(crate) static NSAccessibilityPopUpButtonRole: &'static NSString;
34+
pub(crate) static NSAccessibilityProgressIndicatorRole: &'static NSString;
35+
pub(crate) static NSAccessibilityRadioButtonRole: &'static NSString;
36+
pub(crate) static NSAccessibilityRadioGroupRole: &'static NSString;
37+
pub(crate) static NSAccessibilityRowRole: &'static NSString;
38+
pub(crate) static NSAccessibilityScrollBarRole: &'static NSString;
39+
pub(crate) static NSAccessibilitySliderRole: &'static NSString;
40+
pub(crate) static NSAccessibilitySplitterRole: &'static NSString;
41+
pub(crate) static NSAccessibilityStaticTextRole: &'static NSString;
42+
pub(crate) static NSAccessibilityTabGroupRole: &'static NSString;
43+
pub(crate) static NSAccessibilityTableRole: &'static NSString;
44+
pub(crate) static NSAccessibilityTextFieldRole: &'static NSString;
45+
pub(crate) static NSAccessibilityToolbarRole: &'static NSString;
46+
pub(crate) static NSAccessibilityUnknownRole: &'static NSString;
47+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright 2022 The AccessKit Authors. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0 (found in
3+
// the LICENSE-APACHE file) or the MIT license (found in
4+
// the LICENSE-MIT file), at your option.
5+
6+
use objc2::{extern_class, foundation::NSObject, ClassType};
7+
8+
extern_class!(
9+
#[derive(Debug)]
10+
pub struct NSAccessibilityElement;
11+
12+
unsafe impl ClassType for NSAccessibilityElement {
13+
type Super = NSObject;
14+
}
15+
);
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Copyright 2022 The AccessKit Authors. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0 (found in
3+
// the LICENSE-APACHE file) or the MIT license (found in
4+
// the LICENSE-MIT file), at your option.
5+
6+
use objc2::foundation::{NSObject, NSString};
7+
8+
#[link(name = "AppKit", kind = "framework")]
9+
extern "C" {
10+
pub(crate) fn NSAccessibilityPostNotification(element: &NSObject, notification: &NSString);
11+
}

0 commit comments

Comments
 (0)