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: Factor out core AT-SPI translation layer #352

Merged
merged 31 commits into from
Feb 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
9e1a457
fix: Factor out core translation layer between AccessKit and AT-SPI
mwcampbell Feb 17, 2024
5667a84
Refactor unregister_interfaces
mwcampbell Feb 18, 2024
fd6e602
Refactor register_interfaces
mwcampbell Feb 18, 2024
e475b29
Prepare for event refactoring; eliminate most PlatformNode constructi…
mwcampbell Feb 18, 2024
7a0846b
Do the event refactor
mwcampbell Feb 18, 2024
bf52f1b
Eliminate churn in get_child_at_index and get_children on root
mwcampbell Feb 18, 2024
1c16e77
Just one adapter ID
mwcampbell Feb 18, 2024
f79343a
AdapterIdToken, not AdapterIdGuard
mwcampbell Feb 18, 2024
35e590f
Refactor the callback
mwcampbell Feb 18, 2024
f30a58e
move adapter ID out of context; pull out a helper function for creati…
mwcampbell Feb 18, 2024
6e663a4
Eliminate intermediate Vecs
mwcampbell Feb 18, 2024
3593564
Always use a stored bus address rather than looking at the message he…
mwcampbell Feb 19, 2024
29d3146
fix one clippy warning
mwcampbell Feb 19, 2024
ca8527f
fix more clippy warnings
mwcampbell Feb 19, 2024
66e6d6f
Drop `get_` prefix on some `PlatformNode` methods, for consistency, n…
mwcampbell Feb 20, 2024
77a6d69
derive Debug on events
mwcampbell Feb 21, 2024
c33fb71
Add a way for an event handler to create a PlatformRoot
mwcampbell Feb 21, 2024
b8fe547
make the fields of Rect public, since they need to be accessed direct…
mwcampbell Feb 22, 2024
62d89d6
In `PlatformNode::state`, translate errors to `State::Defunct`
mwcampbell Feb 23, 2024
6457f62
Add PlatformNode methods to check support for single interfaces
mwcampbell Feb 23, 2024
92690cd
Add a way to get to the root from a PlatformNode
mwcampbell Feb 23, 2024
ead59e5
Add a new `UnsupportedInterface` error; I was previously abusing `Def…
mwcampbell Feb 23, 2024
fa2ba93
Add direct toolkit_name and toolkit_version methods to PlatformNode
mwcampbell Feb 23, 2024
1bedc27
Implement PartialEq, Eq, and Hash for PlatformNode
mwcampbell Feb 23, 2024
4d784ba
Implement PartialEq and Hash for PlatformRoot. Not sure about Eq when…
mwcampbell Feb 23, 2024
8d64af6
Derive PartialEq and Eq on Rect
mwcampbell Feb 23, 2024
eb4ac6e
Consistently use `supports_value`
mwcampbell Feb 23, 2024
12053df
Add the simplified, libatspi-like API that I implemented for the newt…
mwcampbell Feb 24, 2024
ab8e8d0
forgot a reexport
mwcampbell Feb 24, 2024
b75f6c1
Add the action and component interfaces (the subsets that we already …
mwcampbell Feb 24, 2024
1909440
Back out changes to accesskit_unix; will open a second PR for that
mwcampbell Feb 24, 2024
File filter

Filter by extension

Filter by extension


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

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"
members = [
"common",
"consumer",
"platforms/atspi-common",
"platforms/macos",
"platforms/unix",
"platforms/windows",
Expand All @@ -13,6 +14,7 @@ members = [
default-members = [
"common",
"consumer",
"platforms/atspi-common",
"platforms/winit",
"bindings/c",
"bindings/python",
Expand Down
20 changes: 20 additions & 0 deletions platforms/atspi-common/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "accesskit_atspi_common"
version = "0.1.0"
authors.workspace = true
license.workspace = true
description = "AccessKit UI accessibility infrastructure: core AT-SPI translation layer"
categories.workspace = true
keywords = ["gui", "ui", "accessibility"]
repository.workspace = true
readme = "README.md"
edition.workspace = true
rust-version.workspace = true

[dependencies]
accesskit = { version = "0.12.2", path = "../../common" }
accesskit_consumer = { version = "0.17.0", path = "../../consumer" }
atspi-common = { version = "0.3.0", default-features = false }
serde = "1.0"
thiserror = "1.0.39"
zvariant = { version = "3", default-features = false }
14 changes: 14 additions & 0 deletions platforms/atspi-common/src/action.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// 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 serde::{Deserialize, Serialize};
use zvariant::Type;

#[derive(Deserialize, Serialize, Type)]
pub struct Action {
pub localized_name: String,
pub description: String,
pub key_binding: String,
}
325 changes: 325 additions & 0 deletions platforms/atspi-common/src/adapter.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,325 @@
// 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::{ActionHandler, NodeId, Role, TreeUpdate};
use accesskit_consumer::{DetachedNode, FilterResult, Node, Tree, TreeChangeHandler, TreeState};
use atspi_common::{InterfaceSet, Live, State};
use std::sync::{
atomic::{AtomicUsize, Ordering},
Arc, RwLock,
};

use crate::{
context::{AppContext, Context},
filters::{filter, filter_detached},
node::{NodeIdOrRoot, NodeWrapper, PlatformNode, PlatformRoot},
util::WindowBounds,
AdapterCallback, Event, ObjectEvent, WindowEvent,
};

struct AdapterChangeHandler<'a> {
adapter: &'a Adapter,
}

impl AdapterChangeHandler<'_> {
fn add_node(&mut self, node: &Node) {
let role = node.role();
let is_root = node.is_root();
let node = NodeWrapper::Node(node);
let interfaces = node.interfaces();
self.adapter.register_interfaces(node.id(), interfaces);
if is_root && role == Role::Window {
let adapter_index = self
.adapter
.context
.read_app_context()
.adapter_index(self.adapter.id)
.unwrap();
self.adapter.window_created(adapter_index, node.id());
}

let live = node.live();
if live != Live::None {
if let Some(name) = node.name() {
self.adapter
.emit_object_event(node.id(), ObjectEvent::Announcement(name, live));
}
}
}

fn remove_node(&mut self, node: &DetachedNode) {
let role = node.role();
let is_root = node.is_root();
let node = NodeWrapper::DetachedNode(node);
if is_root && role == Role::Window {
self.adapter.window_destroyed(node.id());
}
self.adapter
.emit_object_event(node.id(), ObjectEvent::StateChanged(State::Defunct, true));
self.adapter
.unregister_interfaces(node.id(), node.interfaces());
}
}

impl TreeChangeHandler for AdapterChangeHandler<'_> {
fn node_added(&mut self, node: &Node) {
if filter(node) == FilterResult::Include {
self.add_node(node);
}
}

fn node_updated(&mut self, old_node: &DetachedNode, new_node: &Node) {
let filter_old = filter_detached(old_node);
let filter_new = filter(new_node);
if filter_new != filter_old {
if filter_new == FilterResult::Include {
self.add_node(new_node);
} else if filter_old == FilterResult::Include {
self.remove_node(old_node);
}
} else if filter_new == FilterResult::Include {
let old_wrapper = NodeWrapper::DetachedNode(old_node);
let new_wrapper = NodeWrapper::Node(new_node);
let old_interfaces = old_wrapper.interfaces();
let new_interfaces = new_wrapper.interfaces();
let kept_interfaces = old_interfaces & new_interfaces;
self.adapter
.unregister_interfaces(new_wrapper.id(), old_interfaces ^ kept_interfaces);
self.adapter
.register_interfaces(new_node.id(), new_interfaces ^ kept_interfaces);
let bounds = *self.adapter.context.read_root_window_bounds();
new_wrapper.notify_changes(&bounds, self.adapter, &old_wrapper);
}
}

fn focus_moved(
&mut self,
old_node: Option<&DetachedNode>,
new_node: Option<&Node>,
current_state: &TreeState,
) {
if let Some(root_window) = root_window(current_state) {
if old_node.is_none() && new_node.is_some() {
self.adapter
.window_activated(&NodeWrapper::Node(&root_window));
} else if old_node.is_some() && new_node.is_none() {
self.adapter
.window_deactivated(&NodeWrapper::Node(&root_window));
}
}
}

fn node_removed(&mut self, node: &DetachedNode, _: &TreeState) {
if filter_detached(node) == FilterResult::Include {
self.remove_node(node);
}
}
}

static NEXT_ADAPTER_ID: AtomicUsize = AtomicUsize::new(0);

pub struct AdapterIdToken(usize);

impl AdapterIdToken {
pub fn next() -> Self {
let id = NEXT_ADAPTER_ID.fetch_add(1, Ordering::Relaxed);
Self(id)
}

pub fn id(&self) -> usize {
self.0
}
}

pub struct Adapter {
id: usize,
callback: Box<dyn AdapterCallback + Send + Sync>,
context: Arc<Context>,
}

impl Adapter {
pub fn new(
app_context: &Arc<RwLock<AppContext>>,
callback: Box<dyn AdapterCallback + Send + Sync>,
initial_state: TreeUpdate,
is_window_focused: bool,
root_window_bounds: WindowBounds,
action_handler: Box<dyn ActionHandler + Send>,
) -> Self {
let id_token = AdapterIdToken::next();
Self::with_id(
id_token,
app_context,
callback,
initial_state,
is_window_focused,
root_window_bounds,
action_handler,
)
}

pub fn with_id(
id_token: AdapterIdToken,
app_context: &Arc<RwLock<AppContext>>,
callback: Box<dyn AdapterCallback + Send + Sync>,
initial_state: TreeUpdate,
is_window_focused: bool,
root_window_bounds: WindowBounds,
action_handler: Box<dyn ActionHandler + Send>,
) -> Self {
let id = id_token.0;
let tree = Tree::new(initial_state, is_window_focused);
let context = Context::new(app_context, tree, action_handler, root_window_bounds);
context.write_app_context().push_adapter(id, &context);
let adapter = Self {
id,
callback,
context,
};
adapter.register_tree();
adapter
}

fn register_tree(&self) {
fn add_children(node: Node<'_>, to_add: &mut Vec<(NodeId, InterfaceSet)>) {
for child in node.filtered_children(&filter) {
let child_id = child.id();
let wrapper = NodeWrapper::Node(&child);
let interfaces = wrapper.interfaces();
to_add.push((child_id, interfaces));
add_children(child, to_add);
}
}

let mut objects_to_add = Vec::new();

let (adapter_index, root_id) = {
let tree = self.context.read_tree();
let tree_state = tree.state();
let mut app_context = self.context.write_app_context();
app_context.name = tree_state.app_name();
app_context.toolkit_name = tree_state.toolkit_name();
app_context.toolkit_version = tree_state.toolkit_version();
let adapter_index = app_context.adapter_index(self.id).unwrap();
let root = tree_state.root();
let root_id = root.id();
let wrapper = NodeWrapper::Node(&root);
objects_to_add.push((root_id, wrapper.interfaces()));
add_children(root, &mut objects_to_add);
(adapter_index, root_id)
};

for (id, interfaces) in objects_to_add {
self.register_interfaces(id, interfaces);
if id == root_id {
self.window_created(adapter_index, id);
}
}
}

pub fn platform_node(&self, id: NodeId) -> PlatformNode {
PlatformNode::new(&self.context, self.id, id)
}

pub fn platform_root(&self) -> PlatformRoot {
PlatformRoot::new(&self.context.app_context)
}

fn register_interfaces(&self, id: NodeId, new_interfaces: InterfaceSet) {
self.callback.register_interfaces(self, id, new_interfaces);
}

fn unregister_interfaces(&self, id: NodeId, old_interfaces: InterfaceSet) {
self.callback
.unregister_interfaces(self, id, old_interfaces);
}

pub(crate) fn emit_object_event(&self, target: NodeId, event: ObjectEvent) {
let target = NodeIdOrRoot::Node(target);
self.callback
.emit_event(self, Event::Object { target, event });
}

fn emit_root_object_event(&self, event: ObjectEvent) {
let target = NodeIdOrRoot::Root;
self.callback
.emit_event(self, Event::Object { target, event });
}

pub fn set_root_window_bounds(&self, new_bounds: WindowBounds) {
let mut bounds = self.context.root_window_bounds.write().unwrap();
*bounds = new_bounds;
}

pub fn update(&self, update: TreeUpdate) {
let mut handler = AdapterChangeHandler { adapter: self };
let mut tree = self.context.tree.write().unwrap();
tree.update_and_process_changes(update, &mut handler);
}

pub fn update_window_focus_state(&self, is_focused: bool) {
let mut handler = AdapterChangeHandler { adapter: self };
let mut tree = self.context.tree.write().unwrap();
tree.update_host_focus_state_and_process_changes(is_focused, &mut handler);
}

fn window_created(&self, adapter_index: usize, window: NodeId) {
self.emit_root_object_event(ObjectEvent::ChildAdded(adapter_index, window));
}

fn window_activated(&self, window: &NodeWrapper<'_>) {
self.callback.emit_event(
self,
Event::Window {
target: window.id(),
name: window.name().unwrap_or_default(),
event: WindowEvent::Activated,
},
);
self.emit_object_event(window.id(), ObjectEvent::StateChanged(State::Active, true));
self.emit_root_object_event(ObjectEvent::ActiveDescendantChanged(window.id()));
}

fn window_deactivated(&self, window: &NodeWrapper<'_>) {
self.callback.emit_event(
self,
Event::Window {
target: window.id(),
name: window.name().unwrap_or_default(),
event: WindowEvent::Deactivated,
},
);
self.emit_object_event(window.id(), ObjectEvent::StateChanged(State::Active, false));
}

fn window_destroyed(&self, window: NodeId) {
self.emit_root_object_event(ObjectEvent::ChildRemoved(window));
}

pub fn id(&self) -> usize {
self.id
}
}

fn root_window(current_state: &TreeState) -> Option<Node> {
const WINDOW_ROLES: &[Role] = &[Role::AlertDialog, Role::Dialog, Role::Window];
let root = current_state.root();
if WINDOW_ROLES.contains(&root.role()) {
Some(root)
} else {
None
}
}

impl Drop for Adapter {
fn drop(&mut self) {
let root_id = self.context.read_tree().state().root_id();
self.window_destroyed(root_id);
// Note: We deliberately do the following here, not in a Drop
// implementation on context, because AppContext owns a second
// strong reference to Context, and we need that to be released.
self.context.write_app_context().remove_adapter(self.id);
}
}