Skip to content

Commit

Permalink
feat(platforms/macos): Basic macOS platform adapter (#158)
Browse files Browse the repository at this point in the history
The major missing feature at this point is text editing support.

Co-authored-by: Mads Marquart <mads@marquart.dk>
  • Loading branch information
mwcampbell and madsmtm committed Nov 23, 2022
1 parent be88b64 commit a06725e
Show file tree
Hide file tree
Showing 22 changed files with 1,427 additions and 2 deletions.
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
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
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
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
@@ -0,0 +1,18 @@
[package]
name = "accesskit_macos"
version = "0.0.0"
authors = ["Matt Campbell <mattcampbell@pobox.com>"]
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
@@ -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
@@ -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 = unsafe { 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
@@ -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
@@ -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
@@ -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);
}

0 comments on commit a06725e

Please sign in to comment.