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

WIP: Iced accessibility #1849

Draft
wants to merge 8 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 4 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ jobs:
targets: wasm32-unknown-unknown
- uses: actions/checkout@master
- name: Run checks
run: cargo check --package iced --target wasm32-unknown-unknown
run: cargo check --package iced --target wasm32-unknown-unknown --no-default-features --features "wgpu"
- name: Check compilation of `tour` example
run: cargo build --package tour --target wasm32-unknown-unknown
run: cargo build --package tour --target wasm32-unknown-unknown --no-default-features --features "wgpu"
- name: Check compilation of `todos` example
run: cargo build --package todos --target wasm32-unknown-unknown
run: cargo build --package todos --target wasm32-unknown-unknown --no-default-features --features "wgpu"
- name: Check compilation of `integration` example
run: cargo build --package integration --target wasm32-unknown-unknown
run: cargo build --package integration --target wasm32-unknown-unknown --no-default-features --features "wgpu"
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ keywords = ["gui", "ui", "graphics", "interface", "widgets"]
categories = ["gui"]

[features]
default = ["wgpu"]
default = ["wgpu", "a11y"]
# Enable the `wgpu` GPU-accelerated renderer backend
wgpu = ["iced_renderer/wgpu"]
# Enables the `Image` widget
Expand All @@ -39,6 +39,9 @@ palette = ["iced_core/palette"]
system = ["iced_winit/system"]
# Enables the advanced module
advanced = []
# Enables the `accesskit` accessibility library
a11y = ["iced_accessibility", "iced_widget/a11y", "iced_core/a11y", "iced_winit/a11y"]


[badges]
maintenance = { status = "actively-developed" }
Expand All @@ -56,6 +59,7 @@ members = [
"widget",
"winit",
"examples/*",
"accessibility",
]

[dependencies]
Expand All @@ -64,6 +68,7 @@ iced_futures = { version = "0.6", path = "futures" }
iced_renderer = { version = "0.1", path = "renderer" }
iced_widget = { version = "0.1", path = "widget" }
iced_winit = { version = "0.9", path = "winit", features = ["application"] }
iced_accessibility = { version = "0.1", path = "accessibility", optional = true }
thiserror = "1"

[dependencies.image_rs]
Expand Down
19 changes: 19 additions & 0 deletions accessibility/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "iced_accessibility"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
# TODO Ashley re-export more platform adapters

[dependencies]
accesskit = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.11.0" }
accesskit_unix = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.4.0", optional = true }
accesskit_windows = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.14.0", optional = true}
accesskit_macos = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.7.0", optional = true}
accesskit_winit = { git = "https://github.com/wash2/accesskit.git", tag = "v0.11.0", version = "0.13.0", optional = true}
# accesskit = { path = "../../accesskit/common/", version = "0.11.0" }
# accesskit_unix = { path = "../../accesskit/platforms/unix/", version = "0.4.0", optional = true }
# accesskit_windows = { path = "../../accesskit/platforms/windows/", version = "0.14.0", optional = true}
# accesskit_macos = { path = "../../accesskit/platforms/macos/", version = "0.7.0", optional = true}
# accesskit_winit = { path = "../../accesskit/platforms/winit/", version = "0.13.0", optional = true}
80 changes: 80 additions & 0 deletions accessibility/src/a11y_tree.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
use crate::{A11yId, A11yNode};

#[derive(Debug, Clone, Default)]
/// Accessible tree of nodes
pub struct A11yTree {
/// The root of the current widget, children of the parent widget or the Window if there is no parent widget
root: Vec<A11yNode>,
/// The children of a widget and its children
children: Vec<A11yNode>,
}

impl A11yTree {
/// Create a new A11yTree
/// XXX if you use this method, you will need to manually add the children of the root nodes
pub fn new(root: Vec<A11yNode>, children: Vec<A11yNode>) -> Self {
Self { root, children }
}

pub fn leaf<T: Into<A11yId>>(node: accesskit::NodeBuilder, id: T) -> Self {
Self {
root: vec![A11yNode::new(node, id)],
children: vec![],
}
}

/// Helper for creating an A11y tree with a single root node and some children
pub fn node_with_child_tree(mut root: A11yNode, child_tree: Self) -> Self {
root.add_children(
child_tree.root.iter().map(|n| n.id()).cloned().collect(),
);
Self {
root: vec![root],
children: child_tree
.children
.into_iter()
.chain(child_tree.root)
.collect(),
}
}

/// Joins multiple trees into a single tree
pub fn join<T: Iterator<Item = Self>>(trees: T) -> Self {
trees.fold(Self::default(), |mut acc, A11yTree { root, children }| {
acc.root.extend(root);
acc.children.extend(children);
acc
})
}

pub fn root(&self) -> &Vec<A11yNode> {
&self.root
}

pub fn children(&self) -> &Vec<A11yNode> {
&self.children
}

pub fn root_mut(&mut self) -> &mut Vec<A11yNode> {
&mut self.root
}

pub fn children_mut(&mut self) -> &mut Vec<A11yNode> {
&mut self.children
}

pub fn contains(&self, id: &A11yId) -> bool {
self.root.iter().any(|n| n.id() == id)
|| self.children.iter().any(|n| n.id() == id)
}
}

impl From<A11yTree> for Vec<(accesskit::NodeId, accesskit::Node)> {
fn from(tree: A11yTree) -> Vec<(accesskit::NodeId, accesskit::Node)> {
tree.root
.into_iter()
.map(|node| node.into())
.chain(tree.children.into_iter().map(|node| node.into()))
.collect()
}
}
188 changes: 188 additions & 0 deletions accessibility/src/id.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
//! Widget and Window IDs.

use std::hash::Hash;
use std::sync::atomic::{self, AtomicU64};
use std::{borrow, num::NonZeroU128};

#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub enum A11yId {
Window(NonZeroU128),
Widget(Id),
}

// impl A11yId {
// pub fn new_widget() -> Self {
// Self::Widget(Id::unique())
// }

// pub fn new_window() -> Self {
// Self::Window(window_node_id())
// }
// }

impl From<NonZeroU128> for A11yId {
fn from(id: NonZeroU128) -> Self {
Self::Window(id)
}
}

impl From<Id> for A11yId {
fn from(id: Id) -> Self {
assert!(!matches!(id.0, Internal::Set(_)));
Self::Widget(id)
}
}

impl From<accesskit::NodeId> for A11yId {
fn from(value: accesskit::NodeId) -> Self {
let val = u128::from(value.0);
if val > u64::MAX as u128 {
Self::Window(value.0)
} else {
Self::Widget(Id::from(val as u64))
}
}
}

impl From<A11yId> for accesskit::NodeId {
fn from(value: A11yId) -> Self {
let node_id = match value {
A11yId::Window(id) => id,
A11yId::Widget(id) => id.into(),
};
accesskit::NodeId(node_id)
}
}

static NEXT_ID: AtomicU64 = AtomicU64::new(1);
static NEXT_WINDOW_ID: AtomicU64 = AtomicU64::new(1);

/// The identifier of a generic widget.
#[derive(Debug, Clone, PartialEq, Hash, Eq)]
pub struct Id(pub Internal);

impl Id {
/// Creates a custom [`Id`].
pub fn new(id: impl Into<borrow::Cow<'static, str>>) -> Self {
Self(Internal::Custom(Self::next(), id.into()))
}

/// resets the id counter
pub fn reset() {
NEXT_ID.store(1, atomic::Ordering::Relaxed);
}

fn next() -> u64 {
NEXT_ID.fetch_add(1, atomic::Ordering::Relaxed)
}

/// Creates a unique [`Id`].
///
/// This function produces a different [`Id`] every time it is called.
pub fn unique() -> Self {
let id = Self::next();

Self(Internal::Unique(id))
}
}

// Not meant to be used directly
impl From<u64> for Id {
fn from(value: u64) -> Self {
Self(Internal::Unique(value))
}
}

// Not meant to be used directly
impl Into<NonZeroU128> for Id {
fn into(self) -> NonZeroU128 {
match &self.0 {
Internal::Unique(id) => NonZeroU128::try_from(*id as u128).unwrap(),
Internal::Custom(id, _) => {
NonZeroU128::try_from(*id as u128).unwrap()
}
// this is a set id, which is not a valid id and will not ever be converted to a NonZeroU128
// so we panic
Internal::Set(_) => {
panic!("Cannot convert a set id to a NonZeroU128")
}
}
}
}

impl ToString for Id {
fn to_string(&self) -> String {
match &self.0 {
Internal::Unique(_) => "Undefined".to_string(),
Internal::Custom(_, id) => id.to_string(),
Internal::Set(_) => "Set".to_string(),
}
}
}

// XXX WIndow IDs are made unique by adding u64::MAX to them
/// get window node id that won't conflict with other node ids for the duration of the program
pub fn window_node_id() -> NonZeroU128 {
std::num::NonZeroU128::try_from(
u64::MAX as u128
+ NEXT_WINDOW_ID.fetch_add(1, atomic::Ordering::Relaxed) as u128,
)
.unwrap()
}

// TODO refactor to make panic impossible?
#[derive(Debug, Clone, Eq)]
/// Internal representation of an [`Id`].
pub enum Internal {
/// a unique id
Unique(u64),
/// a custom id, which is equal to any [`Id`] with a matching number or string
Custom(u64, borrow::Cow<'static, str>),
/// XXX Do not use this as an id for an accessibility node, it will panic!
/// XXX Only meant to be used for widgets that have multiple accessibility nodes, each with a
/// unique or custom id
/// an Id Set, which is equal to any [`Id`] with a matching number or string
Set(Vec<Self>),
}

impl PartialEq for Internal {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Self::Unique(l0), Self::Unique(r0)) => l0 == r0,
(Self::Custom(l0, l1), Self::Custom(r0, r1)) => {
l0 == r0 || l1 == r1
}
// allow custom ids to be equal to unique ids
(Self::Unique(l0), Self::Custom(r0, _))
| (Self::Custom(l0, _), Self::Unique(r0)) => l0 == r0,
(Self::Set(l0), Self::Set(r0)) => l0 == r0,
// allow set ids to just be equal to any of their members
(Self::Set(l0), r) | (r, Self::Set(l0)) => {
l0.iter().any(|l| l == r)
}
}
}
}

impl Hash for Internal {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
match self {
Self::Unique(id) => id.hash(state),
Self::Custom(name, _) => name.hash(state),
Self::Set(ids) => ids.hash(state),
}
}
}

#[cfg(test)]
mod tests {
use super::Id;

#[test]
fn unique_generates_different_ids() {
let a = Id::unique();
let b = Id::unique();

assert_ne!(a, b);
}
}
19 changes: 19 additions & 0 deletions accessibility/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
mod a11y_tree;
pub mod id;
mod node;
mod traits;

pub use a11y_tree::*;
pub use accesskit;
pub use id::*;
pub use node::*;
pub use traits::*;

#[cfg(feature = "accesskit_macos")]
pub use accesskit_macos;
#[cfg(feature = "accesskit_unix")]
pub use accesskit_unix;
#[cfg(feature = "accesskit_windows")]
pub use accesskit_windows;
#[cfg(feature = "accesskit_winit")]
pub use accesskit_winit;
Loading