Skip to content

Commit

Permalink
AccessKit integration (#78)
Browse files Browse the repository at this point in the history
* Connect the hot-reloading crate

* Apply template updates from the hot-reload CLI

* Run cargo fmt

* Make hot-reload a non-default feature

* Fix panic on root node and clean up

* Import accesskit

* Pipe accesskit events to View

* Build example accesskit tree

* Add Document::visit method and build initial accesskit tree from View

* Create mapping from accesskit id to node id

* Send basic tree updates on dom change

* Clean up and map ids to node ids

* Rebuild tree until observability is figured out

* Clean up

* Fix tree hierarchy

* Create AccessibilityState struct and refactor

* Handle more accesskit roles

* Move accessibility to new module

* Rename accesskit feature flag to accessibility

* Set element node name instead of html tag

* Add labelled_by relationship for text nodes and elements
  • Loading branch information
matthunz authored Jul 9, 2024
1 parent 3e58462 commit 57f9f5b
Show file tree
Hide file tree
Showing 8 changed files with 267 additions and 52 deletions.
20 changes: 20 additions & 0 deletions examples/accessibility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use dioxus::prelude::*;

fn main() {
dioxus_blitz::launch(app);
}

fn app() -> Element {
rsx! {
body {
App {}
}
}
}

#[component]
fn App() -> Element {
rsx! {
div { "Dioxus for all" }
}
}
2 changes: 0 additions & 2 deletions packages/blitz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ name = "blitz"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
slab = "0.4.9"
style = { workspace = true, features = ["servo"] }
Expand Down
5 changes: 4 additions & 1 deletion packages/dioxus-blitz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,14 @@ version = "0.0.0"
edition = "2021"

[features]
default = ["hot-reload", "menu"]
accessibility = ["dep:accesskit", "dep:accesskit_winit"]
hot-reload = ["dep:dioxus-cli-config", "dep:dioxus-hot-reload"]
menu = ["dep:muda"]
default = ["accessibility", "menu"]

[dependencies]
accesskit = { version = "0.15.0", optional = true }
accesskit_winit = { version = "0.21.1", optional = true }
winit = { version = "0.30.2", features = ["rwh_06"] }
muda = { version = "0.11.5", features = ["serde"], optional = true }
tokio = { workspace = true, features = ["full"] }
Expand Down
92 changes: 92 additions & 0 deletions packages/dioxus-blitz/src/accessibility.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
use crate::waker::UserEvent;
use accesskit::{NodeBuilder, NodeId, Role, Tree, TreeUpdate};
use blitz_dom::{local_name, Document, Node};
use winit::{event_loop::EventLoopProxy, window::Window};

/// State of the accessibility node tree and platform adapter.
pub struct AccessibilityState {
/// Adapter to connect to the [`EventLoop`](`winit::event_loop::EventLoop`).
adapter: accesskit_winit::Adapter,

/// Next ID to assign an an [`accesskit::Node`].
next_id: u64,
}

impl AccessibilityState {
pub fn new(window: &Window, proxy: EventLoopProxy<UserEvent>) -> Self {
Self {
adapter: accesskit_winit::Adapter::with_event_loop_proxy(window, proxy.clone()),
next_id: 1,
}
}
pub fn build_tree(&mut self, doc: &Document) {
let mut nodes = std::collections::HashMap::new();
let mut window = NodeBuilder::new(Role::Window);

doc.visit(|node_id, node| {
let parent = node
.parent
.and_then(|parent_id| nodes.get_mut(&parent_id))
.map(|(_, parent)| parent)
.unwrap_or(&mut window);
let (id, node_builder) = self.build_node(node, parent);

nodes.insert(node_id, (id, node_builder));
});

let mut nodes: Vec<_> = nodes
.into_iter()
.map(|(_, (id, node))| (id, node.build()))
.collect();
nodes.push((NodeId(0), window.build()));

let tree = Tree::new(NodeId(0));
let tree_update = TreeUpdate {
nodes,
tree: Some(tree),
focus: NodeId(0),
};

self.adapter.update_if_active(|| tree_update)
}

fn build_node(&mut self, node: &Node, parent: &mut NodeBuilder) -> (NodeId, NodeBuilder) {
let mut node_builder = NodeBuilder::default();

let id = NodeId(self.next_id);
self.next_id += 1;

if let Some(element_data) = node.element_data() {
let name = element_data.name.local.to_string();

// TODO match more roles
let role = match &*name {
"button" => Role::Button,
"div" => Role::GenericContainer,
"header" => Role::Header,
"h1" | "h2" | "h3" | "h4" | "h5" | "h6" => Role::Heading,
"p" => Role::Paragraph,
"section" => Role::Section,
"input" => {
let ty = element_data.attr(local_name!("type")).unwrap_or("text");
match ty {
"number" => Role::NumberInput,
_ => Role::TextInput,
}
}
_ => Role::Unknown,
};

node_builder.set_role(role);
node_builder.set_html_tag(name);
} else if node.is_text_node() {
node_builder.set_role(Role::StaticText);
node_builder.set_name(node.text_content());
parent.push_labelled_by(id)
}

parent.push_child(id);

(id, node_builder)
}
}
82 changes: 46 additions & 36 deletions packages/dioxus-blitz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ mod documents;
mod waker;
mod window;

#[cfg(feature = "accessibility")]
mod accessibility;

use crate::waker::{EventData, UserEvent};
use crate::{documents::HtmlDocument, window::View};

Expand Down Expand Up @@ -187,45 +190,52 @@ fn launch_with_window<Doc: DocumentLike + 'static>(window: View<'static, Doc>) {
};
}

Event::UserEvent(UserEvent::Window {
data: EventData::Poll,
window_id: id,
}) => {
if let Some(view) = windows.get_mut(&id) {
if view.poll() {
view.request_redraw();
Event::UserEvent(user_event) => match user_event {
UserEvent::Window {
data: EventData::Poll,
window_id: id,
} => {
if let Some(view) = windows.get_mut(&id) {
if view.poll() {
view.request_redraw();
}
};
}
#[cfg(feature = "accessibility")]
UserEvent::Accessibility(accessibility_event) => {
if let Some(window) = windows.get_mut(&accessibility_event.window_id) {
window.handle_accessibility_event(&accessibility_event.window_event);
}
};
}

#[cfg(all(
feature = "hot-reload",
debug_assertions,
not(target_os = "android"),
not(target_os = "ios")
))]
Event::UserEvent(UserEvent::HotReloadEvent(msg)) => match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
for window in windows.values_mut() {
if let Some(dx_doc) = window
.renderer
.dom
.as_any_mut()
.downcast_mut::<DioxusDocument>()
{
dx_doc.vdom.replace_template(template);

if window.poll() {
window.request_redraw();
}
#[cfg(all(
feature = "hot-reload",
debug_assertions,
not(target_os = "android"),
not(target_os = "ios")
))]
UserEvent::HotReloadEvent(msg) => match msg {
dioxus_hot_reload::HotReloadMsg::UpdateTemplate(template) => {
for window in windows.values_mut() {
if let Some(dx_doc) = window
.renderer
.dom
.as_any_mut()
.downcast_mut::<DioxusDocument>()
{
dx_doc.vdom.replace_template(template);

if window.poll() {
window.request_redraw();
}
}
}
}
}
dioxus_hot_reload::HotReloadMsg::Shutdown => event_loop.exit(),
dioxus_hot_reload::HotReloadMsg::UpdateAsset(asset) => {
// TODO dioxus-desktop seems to handle this by forcing a reload of all stylesheets.
dbg!("Update asset {:?}", asset);
}
dioxus_hot_reload::HotReloadMsg::Shutdown => event_loop.exit(),
dioxus_hot_reload::HotReloadMsg::UpdateAsset(asset) => {
// TODO dioxus-desktop seems to handle this by forcing a reload of all stylesheets.
dbg!("Update asset {:?}", asset);
}
},
},

// Event::UserEvent(_redraw) => {
Expand Down Expand Up @@ -255,7 +265,7 @@ fn launch_with_window<Doc: DocumentLike + 'static>(window: View<'static, Doc>) {
} => {
if let Some(window) = windows.get_mut(&window_id) {
window.handle_window_event(event);
};
}
}

_ => (),
Expand Down
17 changes: 16 additions & 1 deletion packages/dioxus-blitz/src/waker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@ use futures_util::task::ArcWake;
use std::sync::Arc;
use winit::{event_loop::EventLoopProxy, window::WindowId};

#[cfg(feature = "accessibility")]
use accesskit_winit::Event as AccessibilityEvent;

#[derive(Debug, Clone)]
pub enum UserEvent {
Window {
window_id: WindowId,
data: EventData,
},
/// Handle a hotreload event, basically telling us to update our templates

/// An accessibility event from `accesskit`.
#[cfg(feature = "accessibility")]
Accessibility(Arc<AccessibilityEvent>),

/// A hotreload event, basically telling us to update our templates.
#[cfg(all(
feature = "hot-reload",
debug_assertions,
Expand All @@ -18,6 +26,13 @@ pub enum UserEvent {
HotReloadEvent(dioxus_hot_reload::HotReloadMsg),
}

#[cfg(feature = "accessibility")]
impl From<AccessibilityEvent> for UserEvent {
fn from(value: AccessibilityEvent) -> Self {
Self::Accessibility(Arc::new(value))
}
}

#[derive(Debug, Clone)]
pub enum EventData {
Poll,
Expand Down
73 changes: 62 additions & 11 deletions packages/dioxus-blitz/src/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,16 @@ use winit::event::{ElementState, MouseButton};
use winit::event_loop::{ActiveEventLoop, EventLoopProxy};
use winit::{event::WindowEvent, keyboard::KeyCode, keyboard::ModifiersState, window::Window};

struct State {
#[cfg(feature = "accessibility")]
/// Accessibility adapter for `accesskit`.
accessibility: crate::accessibility::AccessibilityState,

/// Main menu bar of this view's window.
#[cfg(feature = "menu")]
_menu: muda::Menu,
}

pub(crate) struct View<'s, Doc: DocumentLike> {
pub(crate) renderer: Renderer<'s, Window, Doc>,
pub(crate) scene: Scene,
Expand All @@ -22,9 +32,8 @@ pub(crate) struct View<'s, Doc: DocumentLike> {
/// need to store them in order to have access to them when processing keypress events
keyboard_modifiers: ModifiersState,

/// Main menu bar of this view's window.
#[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
menu: Option<muda::Menu>,
#[cfg(any(feature = "accessibility", feature = "menu"))]
state: Option<State>,
}

impl<'a, Doc: DocumentLike> View<'a, Doc> {
Expand All @@ -34,8 +43,8 @@ impl<'a, Doc: DocumentLike> View<'a, Doc> {
scene: Scene::new(),
waker: None,
keyboard_modifiers: Default::default(),
#[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
menu: None,
#[cfg(any(feature = "accessibility", feature = "menu"))]
state: None,
}
}
}
Expand All @@ -46,7 +55,22 @@ impl<'a, Doc: DocumentLike> View<'a, Doc> {
None => false,
Some(waker) => {
let cx = std::task::Context::from_waker(waker);
self.renderer.poll(cx)
if self.renderer.poll(cx) {
#[cfg(feature = "accessibility")]
{
if let Some(ref mut state) = self.state {
// TODO send fine grained accessibility tree updates.
let changed = std::mem::take(&mut self.renderer.dom.as_mut().changed);
if !changed.is_empty() {
state.accessibility.build_tree(self.renderer.dom.as_ref());
}
}
}

true
} else {
false
}
}
}
}
Expand Down Expand Up @@ -274,6 +298,23 @@ impl<'a, Doc: DocumentLike> View<'a, Doc> {
}
}

#[cfg(feature = "accessibility")]
pub fn handle_accessibility_event(&mut self, event: &accesskit_winit::WindowEvent) {
match event {
accesskit_winit::WindowEvent::InitialTreeRequested => {
if let Some(ref mut state) = self.state {
state.accessibility.build_tree(self.renderer.dom.as_ref());
}
}
accesskit_winit::WindowEvent::AccessibilityDeactivated => {
// TODO
}
accesskit_winit::WindowEvent::ActionRequested(_req) => {
// TODO
}
}
}

pub fn resume(
&mut self,
event_loop: &ActiveEventLoop,
Expand All @@ -288,13 +329,23 @@ impl<'a, Doc: DocumentLike> View<'a, Doc> {
}))
.unwrap();

#[cfg(all(feature = "menu", not(any(target_os = "android", target_os = "ios"))))]
// Initialize the accessibility and menu bar state.
#[cfg(any(feature = "accessibility", feature = "menu"))]
{
self.menu = Some(init_menu(
#[cfg(target_os = "windows")]
&window,
));
self.state = Some(State {
#[cfg(feature = "accessibility")]
accessibility: crate::accessibility::AccessibilityState::new(
&window,
proxy.clone(),
),
#[cfg(feature = "menu")]
_menu: init_menu(
#[cfg(target_os = "windows")]
&window,
),
});
}

let size: winit::dpi::PhysicalSize<u32> = window.inner_size();
let mut viewport = Viewport::new((size.width, size.height));
viewport.set_hidpi_scale(window.scale_factor() as _);
Expand Down
Loading

0 comments on commit 57f9f5b

Please sign in to comment.