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

AccessKit integration #78

Merged
merged 27 commits into from
Jul 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d611f85
Connect the hot-reloading crate
matthunz Jun 18, 2024
e710cdc
Apply template updates from the hot-reload CLI
matthunz Jun 18, 2024
860e80d
Run cargo fmt
matthunz Jun 18, 2024
46a48b4
Make hot-reload a non-default feature
matthunz Jun 18, 2024
21e11b6
Merge with main
matthunz Jun 20, 2024
b09b2f9
Fix panic on root node and clean up
matthunz Jun 20, 2024
9228169
Merge with main
matthunz Jun 20, 2024
b270203
Import accesskit
matthunz Jun 20, 2024
e108b15
Merge branch 'hot-reload' of https://github.com/matthunz/blitz into a…
matthunz Jun 21, 2024
a6c643c
Pipe accesskit events to View
matthunz Jun 21, 2024
f4d03f7
Build example accesskit tree
matthunz Jun 21, 2024
dde3fcb
Add Document::visit method and build initial accesskit tree from View
matthunz Jun 21, 2024
dcbfb91
Create mapping from accesskit id to node id
matthunz Jun 21, 2024
5b4e544
Send basic tree updates on dom change
matthunz Jun 21, 2024
f2be9f3
Clean up and map ids to node ids
matthunz Jun 21, 2024
a419383
Rebuild tree until observability is figured out
matthunz Jun 21, 2024
d407e0b
Clean up
matthunz Jun 21, 2024
05b04a2
Fix tree hierarchy
matthunz Jun 21, 2024
3d3e67d
Create AccessibilityState struct and refactor
matthunz Jun 26, 2024
6a009f5
Handle more accesskit roles
matthunz Jun 26, 2024
7f7e91b
Move accessibility to new module
matthunz Jun 26, 2024
0ce74dd
Rename accesskit feature flag to accessibility
matthunz Jun 26, 2024
709d505
Merge branch 'main' of https://github.com/matthunz/blitz into accesskit
matthunz Jun 30, 2024
f657a96
Merge with main
matthunz Jul 3, 2024
c2f984b
Fix merge
matthunz Jul 3, 2024
9ae0fb7
Set element node name instead of html tag
matthunz Jul 8, 2024
a882185
Add labelled_by relationship for text nodes and elements
matthunz Jul 8, 2024
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
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
}
Comment on lines +312 to +314
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Might it be worth at least implementing "click" here (which of course is the only event we support generally. I think the code could just be copied from the regular click handler (skipping the bit which determines which node to generate the event for as we should be given a node id directly)

}
}

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