From 0e07c28c40a583b548e1480d66e0b14f263c3989 Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 6 Dec 2022 11:49:55 -0600 Subject: [PATCH 01/50] Integrate AccessKit. --- crates/bevy_window/src/window.rs | 6 +- crates/bevy_winit/Cargo.toml | 4 ++ crates/bevy_winit/src/accessibility.rs | 97 ++++++++++++++++++++++++++ crates/bevy_winit/src/lib.rs | 9 ++- crates/bevy_winit/src/winit_windows.rs | 37 +++++++++- 5 files changed, 149 insertions(+), 4 deletions(-) create mode 100644 crates/bevy_winit/src/accessibility.rs diff --git a/crates/bevy_window/src/window.rs b/crates/bevy_window/src/window.rs index 3a84983f11f69..b0e8891adaf92 100644 --- a/crates/bevy_window/src/window.rs +++ b/crates/bevy_window/src/window.rs @@ -93,12 +93,16 @@ impl WindowId { } /// The [`WindowId`] for the primary window. pub const fn primary() -> Self { - WindowId(Uuid::from_u128(0)) + WindowId(Uuid::from_u128(1)) } /// Get whether or not this [`WindowId`] is for the primary window. pub fn is_primary(&self) -> bool { *self == WindowId::primary() } + /// Get this [`WindowId`] as a `u128`. + pub fn as_u128(&self) -> u128 { + self.0.as_u128() + } } use crate::CursorIcon; diff --git a/crates/bevy_winit/Cargo.toml b/crates/bevy_winit/Cargo.toml index 96864623f1113..876333c07c7be 100644 --- a/crates/bevy_winit/Cargo.toml +++ b/crates/bevy_winit/Cargo.toml @@ -16,6 +16,7 @@ x11 = ["winit/x11"] [dependencies] # bevy bevy_app = { path = "../bevy_app", version = "0.9.0" } +bevy_derive = { path = "../bevy_derive", version = "0.9.0" } bevy_ecs = { path = "../bevy_ecs", version = "0.9.0" } bevy_input = { path = "../bevy_input", version = "0.9.0" } bevy_math = { path = "../bevy_math", version = "0.9.0" } @@ -24,6 +25,9 @@ bevy_utils = { path = "../bevy_utils", version = "0.9.0" } # other winit = { version = "0.27", default-features = false } +accesskit = "0.8" +accesskit_winit = "0.7" +crossbeam-channel = "0.5" approx = { version = "0.5", default-features = false } raw-window-handle = "0.5" diff --git a/crates/bevy_winit/src/accessibility.rs b/crates/bevy_winit/src/accessibility.rs new file mode 100644 index 0000000000000..f18e79b1267ec --- /dev/null +++ b/crates/bevy_winit/src/accessibility.rs @@ -0,0 +1,97 @@ +use std::num::NonZeroU128; + +use accesskit::{ActionHandler, ActionRequest, Node, NodeId, TreeUpdate}; +use accesskit_winit::Adapter; +use bevy_app::{App, CoreStage, Plugin}; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + prelude::{Component, Entity, EventReader, EventWriter}, + system::{NonSend, NonSendMut, Res, ResMut, Resource}, +}; +use bevy_utils::{default, HashMap}; +use bevy_window::{WindowClosed, WindowFocused, WindowId}; +use crossbeam_channel::{Receiver, Sender}; + +#[derive(Component, Clone, Default, Deref, DerefMut)] +pub struct AccessibilityNode(pub Node); + +impl From for AccessibilityNode { + fn from(node: Node) -> Self { + Self(node) + } +} + +#[derive(Default, Deref, DerefMut)] +pub struct Adapters(pub HashMap); + +impl Adapters { + pub fn get_primary_adapter(&self) -> Option<&Adapter> { + self.get(&WindowId::primary()) + } +} + +#[derive(Resource, Default, Deref, DerefMut)] +pub struct Receivers(pub HashMap>); + +pub struct WinitActionHandler(pub Sender); + +impl ActionHandler for WinitActionHandler { + fn do_action(&self, request: ActionRequest) { + self.0.send(request).expect("Failed to send"); + } +} + +trait EntityExt { + fn to_node_id(&self) -> NodeId; +} + +impl EntityExt for Entity { + fn to_node_id(&self) -> NodeId { + let id = NonZeroU128::new((self.to_bits() + 1) as u128); + NodeId(id.unwrap().into()) + } +} + +fn handle_focus(adapters: NonSend, mut focus: EventReader) { + let root_id = NodeId(NonZeroU128::new(WindowId::primary().as_u128()).unwrap()); + for event in focus.iter() { + if let Some(adapter) = adapters.get_primary_adapter() { + adapter.update(TreeUpdate { + focus: if event.focused { Some(root_id) } else { None }, + ..default() + }); + } + } +} + +fn window_closed( + mut adapters: NonSendMut, + mut receivers: ResMut, + mut events: EventReader, +) { + for WindowClosed { id, .. } in events.iter() { + adapters.remove(id); + receivers.remove(id); + } +} + +fn poll_receivers(receivers: Res, mut actions: EventWriter) { + for (_id, receiver) in receivers.iter() { + if let Ok(event) = receiver.try_recv() { + actions.send(event); + } + } +} + +pub struct AccessibilityPlugin; + +impl Plugin for AccessibilityPlugin { + fn build(&self, app: &mut App) { + app.init_non_send_resource::() + .init_resource::() + .add_event::() + .add_system_to_stage(CoreStage::PreUpdate, handle_focus) + .add_system_to_stage(CoreStage::PreUpdate, window_closed) + .add_system_to_stage(CoreStage::PreUpdate, poll_receivers); + } +} diff --git a/crates/bevy_winit/src/lib.rs b/crates/bevy_winit/src/lib.rs index 749d256882500..b1728faca49ee 100644 --- a/crates/bevy_winit/src/lib.rs +++ b/crates/bevy_winit/src/lib.rs @@ -1,9 +1,11 @@ +mod accessibility; mod converters; #[cfg(target_arch = "wasm32")] mod web_resize; mod winit_config; mod winit_windows; +pub use accesskit; use winit::window::CursorGrabMode; pub use winit_config::*; pub use winit_windows::*; @@ -41,7 +43,8 @@ impl Plugin for WinitPlugin { app.init_non_send_resource::() .init_resource::() .set_runner(winit_runner) - .add_system_to_stage(CoreStage::PostUpdate, change_window.label(ModifiesWindows)); + .add_system_to_stage(CoreStage::PostUpdate, change_window.label(ModifiesWindows)) + .add_plugin(accessibility::AccessibilityPlugin); #[cfg(target_arch = "wasm32")] app.add_plugin(web_resize::CanvasParentResizePlugin); let event_loop = EventLoop::new(); @@ -665,11 +668,15 @@ fn handle_create_window_events( let mut winit_windows = world.non_send_resource_mut::(); let mut windows = world.resource_mut::(); let create_window_events = world.resource::>(); + let mut adapters = world.non_send_resource_mut::(); + let mut receivers = world.resource_mut::(); for create_window_event in create_window_event_reader.iter(&create_window_events) { let window = winit_windows.create_window( event_loop, create_window_event.id, &create_window_event.descriptor, + &mut adapters, + &mut receivers, ); // This event is already sent on windows, x11, and xwayland. // TODO: we aren't yet sure about native wayland, so we might be able to exclude it, diff --git a/crates/bevy_winit/src/winit_windows.rs b/crates/bevy_winit/src/winit_windows.rs index 3364c850b3c28..1a8bc285d018c 100644 --- a/crates/bevy_winit/src/winit_windows.rs +++ b/crates/bevy_winit/src/winit_windows.rs @@ -1,6 +1,11 @@ +use std::{num::NonZeroU128, sync::Arc}; + +use crate::accessibility::{Adapters, Receivers}; use crate::converters::convert_cursor_grab_mode; +use accesskit::{Node, NodeId, Role, Tree, TreeUpdate}; +use accesskit_winit::Adapter; use bevy_math::{DVec2, IVec2}; -use bevy_utils::HashMap; +use bevy_utils::{default, HashMap}; use bevy_window::{ CursorGrabMode, MonitorSelection, RawHandleWrapper, Window, WindowDescriptor, WindowId, WindowMode, @@ -28,6 +33,8 @@ impl WinitWindows { event_loop: &winit::event_loop::EventLoopWindowTarget<()>, window_id: WindowId, window_descriptor: &WindowDescriptor, + adapters: &mut Adapters, + receivers: &mut Receivers, ) -> Window { let mut winit_window_builder = winit::window::WindowBuilder::new(); @@ -95,8 +102,14 @@ impl WinitWindows { winit_window_builder.with_min_inner_size(min_inner_size) }; + let window_title = window_descriptor.title.as_str(); + #[allow(unused_mut)] - let mut winit_window_builder = winit_window_builder.with_title(&window_descriptor.title); + // Due to a UIA limitation, winit windows need to be invisible for the + // AccessKit adapter is initialized. + let winit_window_builder = winit_window_builder + .with_visible(false) + .with_title(window_title); #[cfg(target_arch = "wasm32")] { @@ -120,6 +133,26 @@ impl WinitWindows { let winit_window = winit_window_builder.build(event_loop).unwrap(); + let root = Arc::new(Node { + role: Role::Window, + name: Some(window_title.into()), + ..default() + }); + let accesskit_window_id = NodeId(NonZeroU128::new(window_id.as_u128()).unwrap()); + let (sender, receiver) = crossbeam_channel::unbounded(); + let adapter = Adapter::with_action_handler( + &winit_window, + Box::new(move || TreeUpdate { + nodes: vec![(accesskit_window_id, root)], + tree: Some(Tree::new(accesskit_window_id)), + focus: Some(accesskit_window_id), + }), + Box::new(crate::accessibility::WinitActionHandler(sender)), + ); + adapters.insert(window_id, adapter); + receivers.insert(window_id, receiver); + winit_window.set_visible(true); + if window_descriptor.mode == WindowMode::Windowed { use bevy_window::WindowPosition::*; match position { From 8b67099857737f53f783388733e3dd8319c5b87d Mon Sep 17 00:00:00 2001 From: Nolan Darilek Date: Tue, 15 Nov 2022 11:54:38 -0600 Subject: [PATCH 02/50] Add support for non-container UI widgets. --- crates/bevy_ui/Cargo.toml | 2 + crates/bevy_ui/src/accessibility.rs | 120 +++++++++++++++++++++++++ crates/bevy_ui/src/lib.rs | 6 +- crates/bevy_ui/src/widget/label.rs | 9 ++ crates/bevy_ui/src/widget/mod.rs | 2 + crates/bevy_winit/src/accessibility.rs | 65 +++++++++++++- crates/bevy_winit/src/lib.rs | 2 +- examples/ui/ui.rs | 35 +++++--- 8 files changed, 221 insertions(+), 20 deletions(-) create mode 100644 crates/bevy_ui/src/accessibility.rs create mode 100644 crates/bevy_ui/src/widget/label.rs diff --git a/crates/bevy_ui/Cargo.toml b/crates/bevy_ui/Cargo.toml index 8b85a40b088a3..ed89b2ef60be7 100644 --- a/crates/bevy_ui/Cargo.toml +++ b/crates/bevy_ui/Cargo.toml @@ -27,9 +27,11 @@ bevy_sprite = { path = "../bevy_sprite", version = "0.9.0" } bevy_text = { path = "../bevy_text", version = "0.9.0" } bevy_transform = { path = "../bevy_transform", version = "0.9.0" } bevy_window = { path = "../bevy_window", version = "0.9.0" } +bevy_winit = { path = "../bevy_winit", version = "0.9.0" } bevy_utils = { path = "../bevy_utils", version = "0.9.0" } # other +accesskit = "0.7" taffy = "0.1.0" serde = { version = "1", features = ["derive"] } smallvec = { version = "1.6", features = ["union", "const_generics"] } diff --git a/crates/bevy_ui/src/accessibility.rs b/crates/bevy_ui/src/accessibility.rs new file mode 100644 index 0000000000000..67499762a4d3e --- /dev/null +++ b/crates/bevy_ui/src/accessibility.rs @@ -0,0 +1,120 @@ +use bevy_app::{App, CoreStage, Plugin}; +use bevy_ecs::{ + prelude::Entity, + query::Changed, + system::{Commands, Query}, +}; +use bevy_hierarchy::Children; +use bevy_text::Text; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::default; +use bevy_winit::{ + accessibility::AccessibilityNode, + accesskit::{kurbo::Rect, DefaultActionVerb, Node as AccessKitNode, Role}, +}; + +use crate::{ + prelude::{Button, Label}, + Node, UiImage, +}; + +fn calc_name(texts: &Query<&Text>, children: &Children) -> Option> { + let mut name = None; + for child in children.iter() { + if let Ok(text) = texts.get(*child) { + let values = text + .sections + .iter() + .map(|v| v.value.to_string()) + .collect::>(); + name = Some(values.join(" ")); + } + } + name.map(|v| v.into_boxed_str()) +} + +fn calc_bounds(transform: &GlobalTransform, node: &Node) -> Rect { + Rect::new( + transform.translation().x.into(), + transform.translation().y.into(), + (transform.translation().x + node.calculated_size.x).into(), + (transform.translation().y + node.calculated_size.y).into(), + ) +} + +fn button_changed( + mut commands: Commands, + query: Query<(Entity, &GlobalTransform, &Node, &Children), Changed