From d4261ed958b1c69144e3d2a41b42cd30c9bc33ae Mon Sep 17 00:00:00 2001 From: Matt Campbell Date: Thu, 21 Jul 2022 19:44:42 -0500 Subject: [PATCH] feat(platforms/winit): New winit adapter --- Cargo.lock | 11 + Cargo.toml | 2 + platforms/windows/examples/winit.rs | 176 ---------------- platforms/winit/Cargo.toml | 25 +++ platforms/winit/README.md | 3 + platforms/winit/examples/simple.rs | 208 +++++++++++++++++++ platforms/winit/src/lib.rs | 60 ++++++ platforms/winit/src/platform_impl/mod.rs | 15 ++ platforms/winit/src/platform_impl/null.rs | 22 ++ platforms/winit/src/platform_impl/windows.rs | 33 +++ release-please-config.json | 3 +- 11 files changed, 381 insertions(+), 177 deletions(-) delete mode 100644 platforms/windows/examples/winit.rs create mode 100644 platforms/winit/Cargo.toml create mode 100644 platforms/winit/README.md create mode 100644 platforms/winit/examples/simple.rs create mode 100644 platforms/winit/src/lib.rs create mode 100644 platforms/winit/src/platform_impl/mod.rs create mode 100644 platforms/winit/src/platform_impl/null.rs create mode 100644 platforms/winit/src/platform_impl/windows.rs diff --git a/Cargo.lock b/Cargo.lock index 12754b0b7..f7c2092d1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -39,6 +39,17 @@ dependencies = [ "winit", ] +[[package]] +name = "accesskit_winit" +version = "0.0.0" +dependencies = [ + "accesskit", + "accesskit_windows", + "parking_lot", + "windows", + "winit", +] + [[package]] name = "arrayvec" version = "0.7.1" diff --git a/Cargo.toml b/Cargo.toml index ca1182de3..2d946f798 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,10 +3,12 @@ members = [ "common", "consumer", "platforms/windows", + "platforms/winit", ] default-members = [ "common", "consumer", + "platforms/winit", ] [profile.release] diff --git a/platforms/windows/examples/winit.rs b/platforms/windows/examples/winit.rs deleted file mode 100644 index c2d68cf0a..000000000 --- a/platforms/windows/examples/winit.rs +++ /dev/null @@ -1,176 +0,0 @@ -use accesskit::{Action, ActionHandler, ActionRequest, Node, NodeId, Role, Tree, TreeUpdate}; -use accesskit_windows::{Adapter, SubclassingAdapter}; -use std::{ - num::NonZeroU128, - sync::{Arc, Mutex}, -}; -use windows::Win32::Foundation::HWND; -use winit::{ - event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, - event_loop::{ControlFlow, EventLoop, EventLoopProxy}, - platform::windows::WindowExtWindows, - window::WindowBuilder, -}; - -const WINDOW_TITLE: &str = "Hello world"; - -const WINDOW_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(1) }); -const BUTTON_1_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(2) }); -const BUTTON_2_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(3) }); -const INITIAL_FOCUS: NodeId = BUTTON_1_ID; - -fn make_button(id: NodeId, name: &str) -> Node { - Node { - name: Some(name.into()), - focusable: true, - ..Node::new(id, Role::Button) - } -} - -#[derive(Debug)] -struct State { - focus: NodeId, - is_window_focused: bool, -} - -impl State { - fn new() -> Arc> { - Arc::new(Mutex::new(Self { - focus: INITIAL_FOCUS, - is_window_focused: false, - })) - } - - fn update_focus(&mut self, adapter: &Adapter) { - adapter - .update_if_active(|| TreeUpdate { - nodes: vec![], - tree: None, - focus: self.is_window_focused.then_some(self.focus), - }) - .raise(); - } -} - -#[derive(Debug)] -struct MyAccessKitFactory(Arc>); - -fn initial_tree_update(state: &State) -> TreeUpdate { - let root = Node { - children: vec![BUTTON_1_ID, BUTTON_2_ID], - name: Some(WINDOW_TITLE.into()), - ..Node::new(WINDOW_ID, Role::Window) - }; - let button_1 = make_button(BUTTON_1_ID, "Button 1"); - let button_2 = make_button(BUTTON_2_ID, "Button 2"); - TreeUpdate { - nodes: vec![root, button_1, button_2], - tree: Some(Tree::new(WINDOW_ID)), - focus: state.is_window_focused.then_some(state.focus), - } -} - -pub struct WinitActionHandler(Mutex>); - -impl ActionHandler for WinitActionHandler { - fn do_action(&self, request: ActionRequest) { - let proxy = self.0.lock().unwrap(); - proxy.send_event(request).unwrap(); - } -} - -fn main() { - let event_loop = EventLoop::with_user_event(); - - let state = State::new(); - let window = WindowBuilder::new() - .with_title(WINDOW_TITLE) - .with_visible(false) - .build(&event_loop) - .unwrap(); - - let adapter = { - let state = Arc::clone(&state); - let proxy = Mutex::new(event_loop.create_proxy()); - Adapter::new( - HWND(window.hwnd() as _), - Box::new(move || { - let state = state.lock().unwrap(); - initial_tree_update(&state) - }), - Box::new(WinitActionHandler(proxy)), - ) - }; - let adapter = SubclassingAdapter::new(adapter); - - window.set_visible(true); - - event_loop.run(move |event, _, control_flow| { - *control_flow = ControlFlow::Wait; - - match event { - Event::WindowEvent { event, .. } => { - match event { - WindowEvent::CloseRequested => { - *control_flow = ControlFlow::Exit; - } - WindowEvent::Focused(is_window_focused) => { - let mut state = state.lock().unwrap(); - state.is_window_focused = is_window_focused; - state.update_focus(&*adapter); - } - WindowEvent::KeyboardInput { - input: - KeyboardInput { - virtual_keycode: Some(virtual_code), - state: ElementState::Pressed, - .. - }, - .. - } => { - match virtual_code { - VirtualKeyCode::Tab => { - let mut state = state.lock().unwrap(); - state.focus = if state.focus == BUTTON_1_ID { - BUTTON_2_ID - } else { - BUTTON_1_ID - }; - state.update_focus(&*adapter); - } - VirtualKeyCode::Space => { - // This is a pretty hacky way of updating a node. - // A real GUI framework would have a consistent - // way of building a node from underlying data. - let focus = state.lock().unwrap().focus; - let node = if focus == BUTTON_1_ID { - make_button(BUTTON_1_ID, "You pressed button 1") - } else { - make_button(BUTTON_2_ID, "You pressed button 2") - }; - let update = TreeUpdate { - nodes: vec![node], - tree: None, - focus: Some(focus), - }; - adapter.update(update).raise(); - } - _ => (), - } - } - _ => (), - } - } - Event::UserEvent(ActionRequest { - action: Action::Focus, - target, - data: None, - }) if target == BUTTON_1_ID || target == BUTTON_2_ID => { - let mut state = state.lock().unwrap(); - state.focus = target; - state.update_focus(&*adapter); - } - _ => (), - } - }); -} diff --git a/platforms/winit/Cargo.toml b/platforms/winit/Cargo.toml new file mode 100644 index 000000000..2f1c82baa --- /dev/null +++ b/platforms/winit/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "accesskit_winit" +version = "0.0.0" +authors = ["Matt Campbell "] +license = "Apache-2.0" +description = "AccessKit UI accessibility infrastructure: winit adapter" +categories = ["gui"] +keywords = ["gui", "ui", "accessibility", "winit"] +repository = "https://github.com/AccessKit/accesskit" +readme = "README.md" +edition = "2021" + +[dependencies] +accesskit = { version = "0.3.0", path = "../../common" } +parking_lot = "0.11.2" +winit = "0.26.1" + +[target.'cfg(target_os = "windows")'.dependencies] +accesskit_windows = { version = "0.3.0", path = "../windows" } + +[target.'cfg(target_os = "windows")'.dependencies.windows] +version = "0.37.0" +features = [ + "Win32_Foundation", +] diff --git a/platforms/winit/README.md b/platforms/winit/README.md new file mode 100644 index 000000000..37ee59153 --- /dev/null +++ b/platforms/winit/README.md @@ -0,0 +1,3 @@ +# AccessKit winit adapter + +This is the winit adapter for [AccessKit](https://accesskit.dev/). It exposes an AccessKit accessibility tree through the platform-native accessibility API on any platform supported by AccessKit. On platforms not supported by AccessKit, this adapter does nothing, but still compiles. diff --git a/platforms/winit/examples/simple.rs b/platforms/winit/examples/simple.rs new file mode 100644 index 000000000..f5f486509 --- /dev/null +++ b/platforms/winit/examples/simple.rs @@ -0,0 +1,208 @@ +use accesskit::kurbo::Rect; +use accesskit::{Action, ActionRequest, DefaultActionVerb, Node, NodeId, Role, Tree, TreeUpdate}; +use accesskit_winit::{ActionRequestEvent, Adapter}; +use std::{ + num::NonZeroU128, + sync::{Arc, Mutex}, +}; +use winit::{ + event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, +}; + +const WINDOW_TITLE: &str = "Hello world"; + +const WINDOW_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(1) }); +const BUTTON_1_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(2) }); +const BUTTON_2_ID: NodeId = NodeId(unsafe { NonZeroU128::new_unchecked(3) }); +const INITIAL_FOCUS: NodeId = BUTTON_1_ID; + +const BUTTON_1_RECT: Rect = Rect { + x0: 20.0, + y0: 20.0, + x1: 100.0, + y1: 60.0, +}; + +const BUTTON_2_RECT: Rect = Rect { + x0: 20.0, + y0: 60.0, + x1: 100.0, + y1: 100.0, +}; + +fn make_button(id: NodeId, name: &str) -> Node { + let rect = match id { + BUTTON_1_ID => BUTTON_1_RECT, + BUTTON_2_ID => BUTTON_2_RECT, + _ => unreachable!(), + }; + + Node { + bounds: Some(rect), + name: Some(name.into()), + focusable: true, + default_action_verb: Some(DefaultActionVerb::Click), + ..Node::new(id, Role::Button) + } +} + +#[derive(Debug)] +struct State { + focus: NodeId, + is_window_focused: bool, +} + +impl State { + fn new() -> Arc> { + Arc::new(Mutex::new(Self { + focus: INITIAL_FOCUS, + is_window_focused: false, + })) + } + + fn update_focus(&mut self, adapter: &Adapter) { + adapter.update_if_active(|| TreeUpdate { + nodes: vec![], + tree: None, + focus: self.is_window_focused.then_some(self.focus), + }); + } + + fn press_button(&self, adapter: &Adapter, id: NodeId) { + // This is a pretty hacky way of updating a node. + // A real GUI framework would have a consistent way + // of building a node from underlying data. + // Also, this update isn't as lazy as it could be; + // we force the AccessKit tree to be initialized. + // This is expedient in this case, because that tree + // is the only place where the state of the buttons + // is stored. It's not a problem because we're really + // only concerned with testing lazy updates in the context + // of focus changes. + let name = if id == BUTTON_1_ID { + "You pressed button 1" + } else { + "You pressed button 2" + }; + let node = make_button(id, name); + let update = TreeUpdate { + nodes: vec![node], + tree: None, + focus: self.is_window_focused.then_some(self.focus), + }; + adapter.update(update); + } +} + +fn initial_tree_update(state: &State) -> TreeUpdate { + let root = Node { + children: vec![BUTTON_1_ID, BUTTON_2_ID], + name: Some(WINDOW_TITLE.into()), + ..Node::new(WINDOW_ID, Role::Window) + }; + let button_1 = make_button(BUTTON_1_ID, "Button 1"); + let button_2 = make_button(BUTTON_2_ID, "Button 2"); + TreeUpdate { + nodes: vec![root, button_1, button_2], + tree: Some(Tree::new(WINDOW_ID)), + focus: state.is_window_focused.then_some(state.focus), + } +} + +fn main() { + println!("This example has no visible GUI, and a keyboard interface:"); + println!("- [Tab] switches focus between two logical buttons."); + println!("- [Space] 'presses' the button, permanently renaming it."); + #[cfg(target_os = "windows")] + println!("Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows)."); + + let event_loop = EventLoop::with_user_event(); + + let state = State::new(); + + let window = WindowBuilder::new() + .with_title(WINDOW_TITLE) + .with_visible(false) + .build(&event_loop) + .unwrap(); + + let adapter = { + let state = Arc::clone(&state); + Adapter::new( + &window, + Box::new(move || { + let state = state.lock().unwrap(); + initial_tree_update(&state) + }), + event_loop.create_proxy(), + ) + }; + + window.set_visible(true); + + event_loop.run(move |event, _, control_flow| { + *control_flow = ControlFlow::Wait; + + match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::CloseRequested => { + *control_flow = ControlFlow::Exit; + } + WindowEvent::Focused(is_window_focused) => { + let mut state = state.lock().unwrap(); + state.is_window_focused = is_window_focused; + state.update_focus(&adapter); + } + WindowEvent::KeyboardInput { + input: + KeyboardInput { + virtual_keycode: Some(virtual_code), + state: ElementState::Pressed, + .. + }, + .. + } => match virtual_code { + VirtualKeyCode::Tab => { + let mut state = state.lock().unwrap(); + state.focus = if state.focus == BUTTON_1_ID { + BUTTON_2_ID + } else { + BUTTON_1_ID + }; + state.update_focus(&adapter); + } + VirtualKeyCode::Space => { + let state = state.lock().unwrap(); + state.press_button(&adapter, state.focus); + } + _ => (), + }, + _ => (), + }, + Event::UserEvent(ActionRequestEvent { + request: + ActionRequest { + action, + target, + data: None, + }, + .. + }) if target == BUTTON_1_ID || target == BUTTON_2_ID => { + let mut state = state.lock().unwrap(); + match action { + Action::Focus => { + state.focus = target; + state.update_focus(&adapter); + } + Action::Default => { + state.press_button(&adapter, target); + } + _ => (), + } + } + _ => (), + } + }); +} diff --git a/platforms/winit/src/lib.rs b/platforms/winit/src/lib.rs new file mode 100644 index 000000000..8ed12470f --- /dev/null +++ b/platforms/winit/src/lib.rs @@ -0,0 +1,60 @@ +// Copyright 2022 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file). + +use accesskit::{ActionHandler, ActionRequest, TreeUpdate}; +use parking_lot::Mutex; +use winit::{ + event_loop::EventLoopProxy, + window::{Window, WindowId}, +}; + +mod platform_impl; + +pub struct ActionRequestEvent { + pub window_id: WindowId, + pub request: ActionRequest, +} + +struct WinitActionHandler + Send + 'static> { + window_id: WindowId, + proxy: Mutex>, +} + +impl + Send + 'static> ActionHandler for WinitActionHandler { + fn do_action(&self, request: ActionRequest) { + let proxy = self.proxy.lock(); + let event = ActionRequestEvent { + window_id: self.window_id, + request, + }; + proxy.send_event(event.into()).ok(); + } +} + +pub struct Adapter { + adapter: platform_impl::Adapter, +} + +impl Adapter { + pub fn new + Send + 'static>( + window: &Window, + source: Box TreeUpdate + Send>, + event_loop_proxy: EventLoopProxy, + ) -> Self { + let action_handler = WinitActionHandler { + window_id: window.id(), + proxy: Mutex::new(event_loop_proxy), + }; + let adapter = platform_impl::Adapter::new(window, source, Box::new(action_handler)); + Self { adapter } + } + + pub fn update(&self, update: TreeUpdate) { + self.adapter.update(update) + } + + pub fn update_if_active(&self, updater: impl FnOnce() -> TreeUpdate) { + self.adapter.update_if_active(updater) + } +} diff --git a/platforms/winit/src/platform_impl/mod.rs b/platforms/winit/src/platform_impl/mod.rs new file mode 100644 index 000000000..bd07552f0 --- /dev/null +++ b/platforms/winit/src/platform_impl/mod.rs @@ -0,0 +1,15 @@ +// Copyright 2022 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file). + +// Based loosely on winit's src/platform_impl/mod.rs. + +pub use self::platform::*; + +#[cfg(target_os = "windows")] +#[path = "windows.rs"] +mod platform; + +#[cfg(all(not(target_os = "windows"),))] +#[path = "null.rs"] +mod platform; diff --git a/platforms/winit/src/platform_impl/null.rs b/platforms/winit/src/platform_impl/null.rs new file mode 100644 index 000000000..c620a33f1 --- /dev/null +++ b/platforms/winit/src/platform_impl/null.rs @@ -0,0 +1,22 @@ +// Copyright 2022 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file). + +use accesskit::{ActionHandler, TreeUpdate}; +use winit::window::Window; + +pub struct Adapter; + +impl Adapter { + pub fn new( + _window: &Window, + _source: Box TreeUpdate>, + _action_handler: Box, + ) -> Self { + Self {} + } + + pub fn update(&self, _update: TreeUpdate) {} + + pub fn update_if_active(&self, _updater: impl FnOnce() -> TreeUpdate) {} +} diff --git a/platforms/winit/src/platform_impl/windows.rs b/platforms/winit/src/platform_impl/windows.rs new file mode 100644 index 000000000..5c93c68b0 --- /dev/null +++ b/platforms/winit/src/platform_impl/windows.rs @@ -0,0 +1,33 @@ +// Copyright 2022 The AccessKit Authors. All rights reserved. +// Licensed under the Apache License, Version 2.0 (found in +// the LICENSE-APACHE file). + +use accesskit::{ActionHandler, TreeUpdate}; +use accesskit_windows::{Adapter as WindowsAdapter, SubclassingAdapter}; +use windows::Win32::Foundation::HWND; +use winit::{platform::windows::WindowExtWindows, window::Window}; + +pub struct Adapter { + adapter: SubclassingAdapter, +} + +impl Adapter { + pub fn new( + window: &Window, + source: Box TreeUpdate>, + action_handler: Box, + ) -> Self { + let hwnd = HWND(window.hwnd() as _); + let adapter = WindowsAdapter::new(hwnd, source, action_handler); + let adapter = SubclassingAdapter::new(adapter); + Self { adapter } + } + + pub fn update(&self, update: TreeUpdate) { + self.adapter.update(update).raise(); + } + + pub fn update_if_active(&self, updater: impl FnOnce() -> TreeUpdate) { + self.adapter.update_if_active(updater).raise(); + } +} diff --git a/release-please-config.json b/release-please-config.json index 4de056d2a..2942c7b10 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -6,6 +6,7 @@ "packages": { "common": {}, "consumer": {}, - "platforms/windows": {} + "platforms/windows": {}, + "platforms/winit": {} } }