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 23 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
7 changes: 1 addition & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,14 @@ edition = "2021"
description = "Top level crate for Blitz"
license = "MIT OR Apache-2.0"
keywords = ["dom", "ui", "gui", "react", "wasm"]
rust-version = "1.60.0"
rust-version = "1.70.0"
publish = false

[profile.dev.package."*"]
opt-level = 2

# Need to force specific versions of these dependencies
[dependencies]
# webrender = "0.61.0"
euclid = { version = "0.22", features = ["serde"] }
# mozbuild = "0.1.0"

[dev-dependencies]
Copy link
Collaborator

Choose a reason for hiding this comment

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

What's the reason for moving all of these deps from "dev-dependencies" to "dependencies"?

blitz = { path = "./packages/blitz" }
blitz-dom = { path = "./packages/dom" }
comrak = { version = "0.21.0", default-features = false }
Expand Down
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
9 changes: 8 additions & 1 deletion packages/dioxus-blitz/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,20 @@ name = "dioxus-blitz"
version = "0.0.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
accessibility = ["dep:accesskit", "dep:accesskit_winit"]
hot-reload = []
default = ["accessibility"]

[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"] }
tokio = { workspace = true, features = ["full"] }
dioxus = { workspace = true }
dioxus-cli-config = { git = "https://github.com/dioxuslabs/dioxus", rev = "a3aa6ae771a2d0a4d8cb6055c41efc0193b817ef"}
dioxus-hot-reload = { git = "https://github.com/dioxuslabs/dioxus", rev = "a3aa6ae771a2d0a4d8cb6055c41efc0193b817ef"}
futures-util = "0.3.30"
vello = { workspace = true }
wgpu = { workspace = true }
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::Role;
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 = accesskit::NodeBuilder::new(accesskit::Role::Window);

doc.visit(|node_id, node| {
let (id, node_builder) = self.build_node(node);

if let Some(parent_id) = node.parent {
let (_, parent_node): &mut (_, accesskit::NodeBuilder) =
nodes.get_mut(&parent_id).unwrap();
parent_node.push_child(id)
} else {
window.push_child(id)
}

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

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

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

self.adapter.update_if_active(|| tree_update)
}

#[cfg(feature = "accessibility")]
fn build_node(&mut self, node: &Node) -> (accesskit::NodeId, accesskit::NodeBuilder) {
let mut node_builder = accesskit::NodeBuilder::default();
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_name(name);

Choose a reason for hiding this comment

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

In this scope, the variable name seem to always contain the "HTML element type" which is not something the user should see. You could call NodeBuilder::set_html_tag (although we currently don't expose this property to assistive technologies) but you should not use it for the AccessKit node name.

} else if node.is_text_node() {
node_builder.set_role(accesskit::Role::StaticText);
node_builder.set_name(node.text_content());
}

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

(id, node_builder)
}
}
2 changes: 1 addition & 1 deletion packages/dioxus-blitz/src/documents/dioxus_document.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ fn qual_name(local_name: &str, namespace: Option<&str>) -> QualName {
}

pub struct DioxusDocument {
vdom: VirtualDom,
pub(crate) vdom: VirtualDom,
vdom_state: DioxusState,
inner: Document,
}
Expand Down
94 changes: 81 additions & 13 deletions packages/dioxus-blitz/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ mod documents;
mod waker;
mod window;

use crate::waker::{EventData, UserWindowEvent};
#[cfg(feature = "accessibility")]
mod accessibility;

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

use blitz::RenderState;
Expand Down Expand Up @@ -93,9 +96,7 @@ fn launch_with_window<Doc: DocumentLike + 'static>(window: View<'static, Doc>) {
let _guard = rt.enter();

// Build an event loop for the application
let event_loop = EventLoop::<UserWindowEvent>::with_user_event()
.build()
.unwrap();
let event_loop = EventLoop::<UserEvent>::with_user_event().build().unwrap();
let proxy = event_loop.create_proxy();

// Multiwindow ftw
Expand All @@ -108,6 +109,29 @@ fn launch_with_window<Doc: DocumentLike + 'static>(window: View<'static, Doc>) {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
let mut initial = true;

// Setup hot-reloading if enabled.
#[cfg(all(
feature = "hot-reload",
debug_assertions,
not(target_os = "android"),
not(target_os = "ios")
))]
{
let Ok(cfg) = dioxus_cli_config::CURRENT_CONFIG.as_ref() else {
return;
};

dioxus_hot_reload::connect_at(cfg.target_dir.join("dioxusin"), {
let proxy = proxy.clone();
move |template| {
let _ = proxy.send_event(UserEvent::HotReloadEvent(template));
}
});
}

// the move to winit wants us to use a struct with a run method instead of the callback approach
// we want to just keep the callback approach for now
#[allow(deprecated)]
// the move to winit wants us to use a struct with a run method instead of the callback approach
// we want to just keep the callback approach for now
#[allow(deprecated)]
Expand Down Expand Up @@ -156,21 +180,65 @@ fn launch_with_window<Doc: DocumentLike + 'static>(window: View<'static, Doc>) {
};
}

Event::UserEvent(UserWindowEvent(EventData::Poll, 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")
))]
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);
}
},
},

// Event::UserEvent(_redraw) => {
// for (_, view) in windows.iter() {
// view.request_redraw();
// }
// }
Event::NewEvents(_) => {
for id in windows.keys() {
_ = proxy.send_event(UserWindowEvent(EventData::Poll, *id));
for window_id in windows.keys().copied() {
_ = proxy.send_event(UserEvent::Window {
data: EventData::Poll,
window_id,
});
}
}

Expand All @@ -187,7 +255,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
41 changes: 35 additions & 6 deletions packages/dioxus-blitz/src/waker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,36 @@ 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 struct UserWindowEvent(pub EventData, pub WindowId);
pub enum UserEvent {
Window {
window_id: WindowId,
data: EventData,
},

/// 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,
not(target_os = "android"),
not(target_os = "ios")
))]
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 {
Expand All @@ -17,9 +45,9 @@ pub enum EventData {
/// This lets the VirtualDom "come up for air" and process events while the main thread is blocked by the WebView.
///
/// All other IO lives in the Tokio runtime,
pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::task::Waker {
pub fn tao_waker(proxy: &EventLoopProxy<UserEvent>, id: WindowId) -> std::task::Waker {
struct DomHandle {
proxy: EventLoopProxy<UserWindowEvent>,
proxy: EventLoopProxy<UserEvent>,
id: WindowId,
}

Expand All @@ -30,9 +58,10 @@ pub fn tao_waker(proxy: &EventLoopProxy<UserWindowEvent>, id: WindowId) -> std::

impl ArcWake for DomHandle {
fn wake_by_ref(arc_self: &Arc<Self>) {
_ = arc_self
.proxy
.send_event(UserWindowEvent(EventData::Poll, arc_self.id));
_ = arc_self.proxy.send_event(UserEvent::Window {
data: EventData::Poll,
window_id: arc_self.id,
})
}
}

Expand Down
Loading