Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(platforms/macos): Basic macOS platform adapter #158

Merged
merged 47 commits into from
Nov 23, 2022
Merged
Show file tree
Hide file tree
Changes from 45 commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
508ca6a
Get the old macOS adapter to compile and pass clippy
mwcampbell Nov 15, 2022
348c25e
Preliminary winit integration (the simple example doesn't work yet)
mwcampbell Nov 15, 2022
a05074c
Try marking the root window as ignored; didn't help
mwcampbell Nov 15, 2022
49b96b2
Use parking_lot
mwcampbell Nov 15, 2022
0e950ca
Switch from lazy_static to once_cell
mwcampbell Nov 15, 2022
7388e40
Switch to objc2; still not working, but this prepares us for the next…
mwcampbell Nov 16, 2022
3f6ef64
Make Adapter::root_platform_node public again
mwcampbell Nov 16, 2022
fca7139
Flip the y direction in the bounding rect
mwcampbell Nov 16, 2022
2b8842c
move ns_role function; reformat
mwcampbell Nov 16, 2022
965dd3a
Implement dynamic view subclassing
mwcampbell Nov 17, 2022
feb6ca9
Directly return unignored children from the view. Now we can see the …
mwcampbell Nov 17, 2022
fa805da
Rearrange and add to filter
mwcampbell Nov 17, 2022
dd6240e
Switch to the macOS 10.10+ method-based protocol
mwcampbell Nov 17, 2022
b02b1f9
Expose node values
mwcampbell Nov 17, 2022
bd3573c
Expose focus on request (no notification yet)
mwcampbell Nov 17, 2022
dea5883
First action: set focus
mwcampbell Nov 17, 2022
192dfb7
Clean up how we return boolean results
mwcampbell Nov 17, 2022
b640641
Remove a leftover TODO comment
mwcampbell Nov 17, 2022
91274ba
Implement the press action
mwcampbell Nov 17, 2022
7ae9689
Fix version numbers after rebase
mwcampbell Nov 18, 2022
859c978
Events
mwcampbell Nov 18, 2022
c1d7b13
Add a context struct to reduce the number of pointers passed around a…
mwcampbell Nov 18, 2022
f5087be
Post 'destroyed' notifications for remaining platform nodes when the …
mwcampbell Nov 18, 2022
6fc0101
Modify SubclassingAdapter to take and hold its own reference to the view
mwcampbell Nov 18, 2022
4bf08fe
Don't hold a persistent strong reference to the view except in the su…
mwcampbell Nov 18, 2022
7122822
Relax the version requirement for once_cell to avoid an issue in the …
mwcampbell Nov 19, 2022
cd6094a
Support lazy initialization and conditional updates
mwcampbell Nov 20, 2022
856fb91
Be explicit about forcing the context to the initialized state
mwcampbell Nov 20, 2022
bb7119c
Hit-testing
mwcampbell Nov 20, 2022
737aaaf
Support increment and decrement actions
mwcampbell Nov 20, 2022
bf2b845
Expose min and max values as required by VoiceOver control of sliders
mwcampbell Nov 20, 2022
f78097e
Change return type of `PlatformNode::notifies_when_destroyed`
mwcampbell Nov 22, 2022
29922dc
Eliminate other uses of the Bool type
mwcampbell Nov 22, 2022
17d8656
Don't force SubclassingAdapter to be Send and Sync. Our one real inte…
mwcampbell Nov 22, 2022
ecd4789
Eliminate PlatformNodePtr
mwcampbell Nov 22, 2022
8a793a5
Don't force QueuedEvents to implement Send. Don't actually need it (a…
mwcampbell Nov 22, 2022
dac340a
Don't directly depend on objc-sys
mwcampbell Nov 22, 2022
1fec8c6
Fix out-of-date documentation.
mwcampbell Nov 22, 2022
502dbbb
Go all the way in tying the adapter and context to the main thread. I…
mwcampbell Nov 22, 2022
b0dd794
We don't really need SubclassingAdapter::into_inner; that lets us eli…
mwcampbell Nov 23, 2022
96a2d65
Eliminate another unnecessary use of Option
mwcampbell Nov 23, 2022
dece18d
Another refactor of subclassing
mwcampbell Nov 23, 2022
84e9afc
Mention when the PlatformNode ivar is set
mwcampbell Nov 23, 2022
94241fc
Final subclassing refactor: use an associated object
mwcampbell Nov 23, 2022
8579156
Add a safety comment, and change the capitalization of others
mwcampbell Nov 23, 2022
f7d2fb5
Address more CR feedback on subclassing
mwcampbell Nov 23, 2022
390fce1
Require unsafe code to be marked even in unsafe functions
mwcampbell Nov 23, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"common",
"consumer",
"platforms/macos",
"platforms/windows",
"platforms/winit",
]
Expand Down
20 changes: 19 additions & 1 deletion consumer/src/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::{iter::FusedIterator, ops::Deref, sync::Arc};

use accesskit::kurbo::{Affine, Point, Rect};
use accesskit::{
CheckedState, DefaultActionVerb, Live, Node as NodeData, NodeId, Role, TextSelection,
Action, CheckedState, DefaultActionVerb, Live, Node as NodeData, NodeId, Role, TextSelection,
};

use crate::iterators::{
Expand Down Expand Up @@ -275,6 +275,10 @@ impl<'a> Node<'a> {
parent_transform * self.direct_transform()
}

pub fn has_bounds(&self) -> bool {
self.data().bounds.is_some()
}

/// Returns the node's transformed bounding box relative to the tree's
/// container (e.g. window).
pub fn bounding_box(&self) -> Option<Rect> {
Expand Down Expand Up @@ -464,6 +468,20 @@ impl NodeState {
&& !self.supports_toggle()
&& !self.supports_expand_collapse()
}

// The future of the `Action` enum is undecided, so keep the following
// function private for now.
fn supports_action(&self, action: Action) -> bool {
self.data().actions.contains(action)
}

pub fn supports_increment(&self) -> bool {
self.supports_action(Action::Increment)
}

pub fn supports_decrement(&self) -> bool {
self.supports_action(Action::Decrement)
}
}

impl<'a> Node<'a> {
Expand Down
16 changes: 16 additions & 0 deletions consumer/src/tree.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,22 @@ impl Tree {
data: Some(ActionData::SetTextSelection(selection)),
})
}

pub fn increment(&self, target: NodeId) {
self.action_handler.do_action(ActionRequest {
action: Action::Increment,
target,
data: None,
})
}

pub fn decrement(&self, target: NodeId) {
self.action_handler.do_action(ActionRequest {
action: Action::Decrement,
target,
data: None,
})
}
}

#[cfg(test)]
Expand Down
18 changes: 18 additions & 0 deletions platforms/macos/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[package]
name = "accesskit_macos"
version = "0.0.0"
authors = ["Matt Campbell <[email protected]>"]
license = "MIT/Apache-2.0"
description = "AccessKit UI accessibility infrastructure: macOS adapter"
categories = ["gui"]
keywords = ["gui", "ui", "accessibility"]
repository = "https://github.com/AccessKit/accesskit"
readme = "README.md"
edition = "2021"

[dependencies]
accesskit = { version = "0.8.0", path = "../../common" }
accesskit_consumer = { version = "0.8.0", path = "../../consumer" }
objc2 = "0.3.0-beta.3"
once_cell = "1.13.0"
parking_lot = "0.12.1"
3 changes: 3 additions & 0 deletions platforms/macos/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# AccessKit macOS adapter

This is the macOS adapter for [AccessKit](https://accesskit.dev/). It exposes an AccessKit accessibility tree through the Cocoa `NSAccessibility` protocol.
127 changes: 127 additions & 0 deletions platforms/macos/src/adapter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// Copyright 2022 The AccessKit Authors. All rights reserved.
// Licensed under the Apache License, Version 2.0 (found in
// the LICENSE-APACHE file) or the MIT license (found in
// the LICENSE-MIT file), at your option.

use accesskit::{kurbo::Point, ActionHandler, TreeUpdate};
use accesskit_consumer::{FilterResult, Tree};
use objc2::{
foundation::{MainThreadMarker, NSArray, NSObject, NSPoint},
rc::{Id, Shared, WeakId},
};
use once_cell::unsync::Lazy;
use std::{ffi::c_void, ptr::null_mut, rc::Rc};

use crate::{appkit::NSView, context::Context, event::QueuedEvents, node::filter};

pub struct Adapter {
context: Lazy<Rc<Context>, Box<dyn FnOnce() -> Rc<Context>>>,
}

impl Adapter {
/// Create a new macOS adapter. This function must be called on
/// the main thread.
///
/// # Safety
///
/// `view` must be a valid, unreleased pointer to an `NSView`.
pub unsafe fn new(
view: *mut c_void,
source: Box<dyn FnOnce() -> TreeUpdate>,
action_handler: Box<dyn ActionHandler>,
) -> Self {
let view = Id::retain(view as *mut NSView).unwrap();
let view = WeakId::new(&view);
let mtm = MainThreadMarker::new().unwrap();
Self {
context: Lazy::new(Box::new(move || {
let tree = Tree::new(source(), action_handler);
Context::new(view, tree, mtm)
})),
}
}

/// Initialize the tree if it hasn't been initialized already, then apply
/// the provided update.
///
/// The caller must call [`QueuedEvents::raise`] on the return value.
///
/// This method may be safely called on any thread, but refer to
/// [`QueuedEvents::raise`] for restrictions on the context in which
/// it should be called.
pub fn update(&self, update: TreeUpdate) -> QueuedEvents {
let context = Lazy::force(&self.context);
context.update(update)
}

/// If and only if the tree has been initialized, call the provided function
/// and apply the resulting update.
///
/// If a [`QueuedEvents`] instance is returned, the caller must call
/// [`QueuedEvents::raise`] on it.
///
/// This method may be safely called on any thread, but refer to
/// [`QueuedEvents::raise`] for restrictions on the context in which
/// it should be called.
pub fn update_if_active(&self, updater: impl FnOnce() -> TreeUpdate) -> Option<QueuedEvents> {
Lazy::get(&self.context).map(|context| context.update(updater()))
}

pub fn view_children(&self) -> *mut NSArray<NSObject> {
let context = Lazy::force(&self.context);
let state = context.tree.read();
let node = state.root();
let platform_nodes = if filter(&node) == FilterResult::Include {
vec![Id::into_super(Id::into_super(
context.get_or_create_platform_node(node.id()),
))]
} else {
node.filtered_children(filter)
.map(|node| {
Id::into_super(Id::into_super(
context.get_or_create_platform_node(node.id()),
))
})
.collect::<Vec<Id<NSObject, Shared>>>()
};
let array = NSArray::from_vec(platform_nodes);
Id::autorelease_return(array)
}

pub fn focus(&self) -> *mut NSObject {
let context = Lazy::force(&self.context);
let state = context.tree.read();
if let Some(node) = state.focus() {
if filter(&node) == FilterResult::Include {
return Id::autorelease_return(context.get_or_create_platform_node(node.id()))
as *mut _;
}
}
null_mut()
}

pub fn hit_test(&self, point: NSPoint) -> *mut NSObject {
let context = Lazy::force(&self.context);
let view = match context.view.load() {
Some(view) => view,
None => {
return null_mut();
}
};

let window = view.window().unwrap();
let point = window.convert_point_from_screen(point);
let point = view.convert_point_from_view(point, None);
let view_bounds = view.bounds();
let point = Point::new(point.x, view_bounds.size.height - point.y);

let state = context.tree.read();
let root = state.root();
let point = root.transform().inverse() * point;
if let Some(node) = root.node_at_point(point, &filter) {
return Id::autorelease_return(context.get_or_create_platform_node(node.id()))
as *mut _;
}
null_mut()
}
}
47 changes: 47 additions & 0 deletions platforms/macos/src/appkit/accessibility_constants.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Copyright 2022 The AccessKit Authors. All rights reserved.
// Licensed under the Apache License, Version 2.0 (found in
// the LICENSE-APACHE file) or the MIT license (found in
// the LICENSE-MIT file), at your option.

use objc2::foundation::NSString;

#[link(name = "AppKit", kind = "framework")]
extern "C" {
// Notifications
pub(crate) static NSAccessibilityUIElementDestroyedNotification: &'static NSString;
pub(crate) static NSAccessibilityFocusedUIElementChangedNotification: &'static NSString;
pub(crate) static NSAccessibilityTitleChangedNotification: &'static NSString;
pub(crate) static NSAccessibilityValueChangedNotification: &'static NSString;

// Roles
pub(crate) static NSAccessibilityButtonRole: &'static NSString;
pub(crate) static NSAccessibilityCheckBoxRole: &'static NSString;
pub(crate) static NSAccessibilityCellRole: &'static NSString;
pub(crate) static NSAccessibilityColorWellRole: &'static NSString;
pub(crate) static NSAccessibilityColumnRole: &'static NSString;
pub(crate) static NSAccessibilityComboBoxRole: &'static NSString;
pub(crate) static NSAccessibilityGroupRole: &'static NSString;
pub(crate) static NSAccessibilityImageRole: &'static NSString;
pub(crate) static NSAccessibilityIncrementorRole: &'static NSString;
pub(crate) static NSAccessibilityLevelIndicatorRole: &'static NSString;
pub(crate) static NSAccessibilityLinkRole: &'static NSString;
pub(crate) static NSAccessibilityListRole: &'static NSString;
pub(crate) static NSAccessibilityMenuRole: &'static NSString;
pub(crate) static NSAccessibilityMenuBarRole: &'static NSString;
pub(crate) static NSAccessibilityMenuItemRole: &'static NSString;
pub(crate) static NSAccessibilityOutlineRole: &'static NSString;
pub(crate) static NSAccessibilityPopUpButtonRole: &'static NSString;
pub(crate) static NSAccessibilityProgressIndicatorRole: &'static NSString;
pub(crate) static NSAccessibilityRadioButtonRole: &'static NSString;
pub(crate) static NSAccessibilityRadioGroupRole: &'static NSString;
pub(crate) static NSAccessibilityRowRole: &'static NSString;
pub(crate) static NSAccessibilityScrollBarRole: &'static NSString;
pub(crate) static NSAccessibilitySliderRole: &'static NSString;
pub(crate) static NSAccessibilitySplitterRole: &'static NSString;
pub(crate) static NSAccessibilityStaticTextRole: &'static NSString;
pub(crate) static NSAccessibilityTabGroupRole: &'static NSString;
pub(crate) static NSAccessibilityTableRole: &'static NSString;
pub(crate) static NSAccessibilityTextFieldRole: &'static NSString;
pub(crate) static NSAccessibilityToolbarRole: &'static NSString;
pub(crate) static NSAccessibilityUnknownRole: &'static NSString;
}
15 changes: 15 additions & 0 deletions platforms/macos/src/appkit/accessibility_element.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright 2022 The AccessKit Authors. All rights reserved.
// Licensed under the Apache License, Version 2.0 (found in
// the LICENSE-APACHE file) or the MIT license (found in
// the LICENSE-MIT file), at your option.

use objc2::{extern_class, foundation::NSObject, ClassType};

extern_class!(
#[derive(Debug)]
pub struct NSAccessibilityElement;

unsafe impl ClassType for NSAccessibilityElement {
type Super = NSObject;
}
);
11 changes: 11 additions & 0 deletions platforms/macos/src/appkit/accessibility_functions.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2022 The AccessKit Authors. All rights reserved.
// Licensed under the Apache License, Version 2.0 (found in
// the LICENSE-APACHE file) or the MIT license (found in
// the LICENSE-MIT file), at your option.

use objc2::foundation::{NSObject, NSString};

#[link(name = "AppKit", kind = "framework")]
extern "C" {
pub(crate) fn NSAccessibilityPostNotification(element: &NSObject, notification: &NSString);
}
Loading