diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a2225c476..febd13845 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -95,7 +95,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: cargo clippy (common packages) - run: cargo clippy -p accesskit -p accesskit_consumer -p accesskit_winit ${{ matrix.target && format('--target {0}', matrix.target) }} --all-targets -- -D warnings + run: cargo clippy -p accesskit -p accesskit_consumer -p accesskit_winit -p example_common ${{ matrix.target && format('--target {0}', matrix.target) }} --all-targets -- -D warnings - name: cargo clippy (adapters) run: cargo clippy ${{ matrix.adapters }} ${{ matrix.target && format('--target {0}', matrix.target) }} --all-targets -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index dbf8334f9..38ffa0661 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,8 +108,10 @@ version = "0.35.0" dependencies = [ "accesskit", "accesskit_consumer", + "example_common", "hashbrown", "parking_lot", + "raw-window-handle 0.6.2", "scopeguard", "static_assertions", "windows", @@ -127,9 +129,9 @@ dependencies = [ "accesskit_macos", "accesskit_unix", "accesskit_windows", + "example_common", "raw-window-handle 0.5.2", "raw-window-handle 0.6.2", - "softbuffer", "winit", ] @@ -716,6 +718,15 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "example_common" +version = "0.0.0" +dependencies = [ + "accesskit", + "raw-window-handle 0.6.2", + "softbuffer", +] + [[package]] name = "fastrand" version = "2.1.1" diff --git a/Cargo.toml b/Cargo.toml index 475c37529..836898309 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ "adapters/unix", "adapters/windows", "adapters/winit", + "example_common", ] default-members = [ "accesskit", diff --git a/adapters/windows/Cargo.toml b/adapters/windows/Cargo.toml index 254f01fd6..564eb0209 100644 --- a/adapters/windows/Cargo.toml +++ b/adapters/windows/Cargo.toml @@ -8,6 +8,7 @@ categories.workspace = true keywords = ["gui", "ui", "accessibility"] repository.workspace = true readme = "README.md" +exclude = ["examples"] edition.workspace = true rust-version.workspace = true @@ -38,6 +39,8 @@ features = [ ] [dev-dependencies] +example_common = { path = "../../example_common" } parking_lot = "0.12.4" +raw-window-handle = "0.6.2" scopeguard = "1.1.0" winit = "0.30" diff --git a/adapters/windows/examples/hello_world.rs b/adapters/windows/examples/hello_world.rs index ceded26a9..b3ae7fe4d 100644 --- a/adapters/windows/examples/hello_world.rs +++ b/adapters/windows/examples/hello_world.rs @@ -1,11 +1,17 @@ // Based on the create_window sample in windows-samples-rs. -use accesskit::{ - Action, ActionHandler, ActionRequest, ActivationHandler, Live, Node, NodeId, Rect, Role, - TreeId, TreeInfo, TreeUpdate, -}; +use accesskit::{ActionHandler, ActionRequest, ActivationHandler, TreeUpdate}; use accesskit_windows::Adapter; -use std::{cell::RefCell, sync::LazyLock}; +use example_common::{Key, KeyEvent, KeyState, Modifiers, Renderer, UiState, WINDOW_TITLE}; +use raw_window_handle::{ + DisplayHandle, HandleError, HasDisplayHandle, HasWindowHandle, Win32WindowHandle, WindowHandle, +}; +use std::{ + cell::RefCell, + num::NonZeroIsize, + ops::{Deref, DerefMut}, + sync::LazyLock, +}; use windows::{ Win32::{ Foundation::*, @@ -35,141 +41,106 @@ static WINDOW_CLASS_ATOM: LazyLock = LazyLock::new(|| { atom }); -const WINDOW_TITLE: &str = "Hello world"; +const ACTION_REQUEST_MSG: u32 = WM_USER; -const WINDOW_ID: NodeId = NodeId(0); -const BUTTON_1_ID: NodeId = NodeId(1); -const BUTTON_2_ID: NodeId = NodeId(2); -const ANNOUNCEMENT_ID: NodeId = NodeId(3); -const INITIAL_FOCUS: NodeId = BUTTON_1_ID; +const ANNOUNCEMENT_TIMER_ID: usize = 1; -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, -}; +struct Ui(UiState); -const SET_FOCUS_MSG: u32 = WM_USER; -const CLICK_MSG: u32 = WM_USER + 1; +impl ActivationHandler for Ui { + fn request_initial_tree(&mut self) -> Option { + Some(self.0.build_tree_update()) + } +} -fn build_button(id: NodeId, label: &str) -> Node { - let rect = match id { - BUTTON_1_ID => BUTTON_1_RECT, - BUTTON_2_ID => BUTTON_2_RECT, - _ => unreachable!(), - }; +impl Deref for Ui { + type Target = UiState; - let mut node = Node::new(Role::Button); - node.set_bounds(rect); - node.set_label(label); - node.add_action(Action::Focus); - node.add_action(Action::Click); - node + fn deref(&self) -> &UiState { + &self.0 + } } -fn build_announcement(text: &str) -> Node { - let mut node = Node::new(Role::Label); - node.set_value(text); - node.set_live(Live::Polite); - node +impl DerefMut for Ui { + fn deref_mut(&mut self) -> &mut UiState { + &mut self.0 + } } -struct InnerWindowState { - focus: NodeId, - announcement: Option, -} +#[derive(Clone)] +struct RenderTarget(HWND); -impl InnerWindowState { - fn build_root(&mut self) -> Node { - let mut node = Node::new(Role::Window); - node.set_children(vec![BUTTON_1_ID, BUTTON_2_ID]); - if self.announcement.is_some() { - node.push_child(ANNOUNCEMENT_ID); - } - node.set_language("en"); - node +impl HasWindowHandle for RenderTarget { + fn window_handle(&self) -> std::result::Result, HandleError> { + let hwnd = NonZeroIsize::new(self.0.0 as isize).unwrap(); + // SAFETY: The window outlives this target, which the window itself + // owns, and it belongs to the thread that draws to it. + Ok(unsafe { WindowHandle::borrow_raw(Win32WindowHandle::new(hwnd).into()) }) } } -impl ActivationHandler for InnerWindowState { - fn request_initial_tree(&mut self) -> Option { - println!("Initial tree requested"); - let root = self.build_root(); - let button_1 = build_button(BUTTON_1_ID, "Button 1"); - let button_2 = build_button(BUTTON_2_ID, "Button 2"); - let tree = TreeInfo::new(WINDOW_ID); - - let mut result = TreeUpdate { - nodes: vec![ - (WINDOW_ID, root), - (BUTTON_1_ID, button_1), - (BUTTON_2_ID, button_2), - ], - tree: Some(tree), - tree_id: TreeId::ROOT, - focus: self.focus, - }; - if let Some(announcement) = &self.announcement { - result - .nodes - .push((ANNOUNCEMENT_ID, build_announcement(announcement))); - } - Some(result) +impl HasDisplayHandle for RenderTarget { + fn display_handle(&self) -> std::result::Result, HandleError> { + Ok(DisplayHandle::windows()) } } struct WindowState { adapter: RefCell, - inner_state: RefCell, + ui: RefCell, + renderer: RefCell>, } impl WindowState { - fn set_focus(&self, focus: NodeId) { - self.inner_state.borrow_mut().focus = focus; + fn update_accessibility_tree(&self) { let mut adapter = self.adapter.borrow_mut(); - if let Some(events) = adapter.update_if_active(|| TreeUpdate { - nodes: vec![], - tree: None, - tree_id: TreeId::ROOT, - focus, - }) { + let mut ui = self.ui.borrow_mut(); + if let Some(events) = adapter.update_if_active(|| ui.build_tree_update()) { + drop(ui); drop(adapter); events.raise(); } } - fn press_button(&self, id: NodeId) { - let mut inner_state = self.inner_state.borrow_mut(); - let text = if id == BUTTON_1_ID { - "You pressed button 1" - } else { - "You pressed button 2" + fn after_input(&self, window: HWND) { + self.update_accessibility_tree(); + let Some(delay) = self.ui.borrow().time_until_announcement() else { + return; }; - inner_state.announcement = Some(text.into()); - let mut adapter = self.adapter.borrow_mut(); - if let Some(events) = adapter.update_if_active(|| { - let announcement = build_announcement(text); - let root = inner_state.build_root(); - TreeUpdate { - nodes: vec![(ANNOUNCEMENT_ID, announcement), (WINDOW_ID, root)], - tree: None, - tree_id: TreeId::ROOT, - focus: inner_state.focus, - } - }) { - drop(adapter); - drop(inner_state); - events.raise(); + let timer = unsafe { + SetTimer( + Some(window), + ANNOUNCEMENT_TIMER_ID, + delay.as_millis() as u32, + None, + ) + }; + if timer == 0 { + panic!("{}", Error::from_thread()); } } + + fn flush_announcement(&self, window: HWND) { + let _ = unsafe { KillTimer(Some(window), ANNOUNCEMENT_TIMER_ID) }; + if self.ui.borrow_mut().flush_announcement() { + self.update_accessibility_tree(); + } + } +} + +fn modifiers() -> Modifiers { + Modifiers { + shift: unsafe { GetKeyState(VK_SHIFT.0 as i32) } < 0, + } +} + +fn translate_key(key: VIRTUAL_KEY) -> Option { + match key { + VK_RETURN => Some(Key::Enter), + VK_SPACE => Some(Key::Space), + VK_TAB => Some(Key::Tab), + _ => None, + } } unsafe fn get_window_state(window: HWND) -> *const WindowState { @@ -185,8 +156,6 @@ fn update_window_focus_state(window: HWND, is_focused: bool) { } } -struct WindowCreateParams(NodeId); - struct SimpleActionHandler { window: HWND, } @@ -196,54 +165,39 @@ unsafe impl Sync for SimpleActionHandler {} impl ActionHandler for SimpleActionHandler { fn do_action(&mut self, request: ActionRequest) { - match request.action { - Action::Focus => { - unsafe { - PostMessageW( - Some(self.window), - SET_FOCUS_MSG, - WPARAM(0), - LPARAM(request.target_node.0 as _), - ) - } - .unwrap(); - } - Action::Click => { - unsafe { - PostMessageW( - Some(self.window), - CLICK_MSG, - WPARAM(0), - LPARAM(request.target_node.0 as _), - ) - } - .unwrap(); - } - _ => (), + let request = Box::into_raw(Box::new(request)); + unsafe { + PostMessageW( + Some(self.window), + ACTION_REQUEST_MSG, + WPARAM(0), + LPARAM(request as _), + ) } + .unwrap(); } } extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: LPARAM) -> LRESULT { match message { WM_NCCREATE => { - let create_struct: &CREATESTRUCTW = unsafe { &mut *(lparam.0 as *mut _) }; - let create_params: Box = - unsafe { Box::from_raw(create_struct.lpCreateParams as _) }; - let WindowCreateParams(initial_focus) = *create_params; - let inner_state = RefCell::new(InnerWindowState { - focus: initial_focus, - announcement: None, - }); let adapter = Adapter::new(window, false, SimpleActionHandler { window }); let state = Box::new(WindowState { adapter: RefCell::new(adapter), - inner_state, + ui: RefCell::new(Ui(UiState::new())), + renderer: RefCell::new(Renderer::new(RenderTarget(window))), }); unsafe { SetWindowLongPtrW(window, GWLP_USERDATA, Box::into_raw(state) as _) }; unsafe { DefWindowProcW(window, message, wparam, lparam) } } WM_PAINT => { + let state = unsafe { &*get_window_state(window) }; + let mut rect = RECT::default(); + unsafe { GetClientRect(window, &mut rect) }.unwrap(); + state.renderer.borrow_mut().draw( + (rect.right - rect.left) as u32, + (rect.bottom - rect.top) as u32, + ); unsafe { ValidateRect(Some(window), None) }.unwrap(); LRESULT(0) } @@ -265,9 +219,9 @@ extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: L } let state = unsafe { &*state_ptr }; let mut adapter = state.adapter.borrow_mut(); - let mut inner_state = state.inner_state.borrow_mut(); - let result = adapter.handle_wm_getobject(wparam, lparam, &mut *inner_state); - drop(inner_state); + let mut ui = state.ui.borrow_mut(); + let result = adapter.handle_wm_getobject(wparam, lparam, &mut *ui); + drop(ui); drop(adapter); result.map_or_else( || unsafe { DefWindowProcW(window, message, wparam, lparam) }, @@ -282,48 +236,46 @@ extern "system" fn wndproc(window: HWND, message: u32, wparam: WPARAM, lparam: L update_window_focus_state(window, false); LRESULT(0) } - WM_KEYDOWN => match VIRTUAL_KEY(wparam.0 as u16) { - VK_TAB => { - let state = unsafe { &*get_window_state(window) }; - let old_focus = state.inner_state.borrow().focus; - let new_focus = if old_focus == BUTTON_1_ID { - BUTTON_2_ID - } else { - BUTTON_1_ID - }; - state.set_focus(new_focus); - LRESULT(0) - } - VK_SPACE => { - let state = unsafe { &*get_window_state(window) }; - let id = state.inner_state.borrow().focus; - state.press_button(id); - LRESULT(0) - } - _ => unsafe { DefWindowProcW(window, message, wparam, lparam) }, - }, - SET_FOCUS_MSG => { - let id = NodeId(lparam.0 as _); - if id == BUTTON_1_ID || id == BUTTON_2_ID { - let state = unsafe { &*get_window_state(window) }; - state.set_focus(id); - } + WM_KEYDOWN | WM_KEYUP => { + let Some(key) = translate_key(VIRTUAL_KEY(wparam.0 as u16)) else { + return unsafe { DefWindowProcW(window, message, wparam, lparam) }; + }; + let key_state = if message == WM_KEYDOWN { + KeyState::Pressed + } else { + KeyState::Released + }; + let state = unsafe { &*get_window_state(window) }; + state.ui.borrow_mut().handle_key(KeyEvent { + key, + state: key_state, + modifiers: modifiers(), + }); + state.after_input(window); LRESULT(0) } - CLICK_MSG => { - let id = NodeId(lparam.0 as _); - if id == BUTTON_1_ID || id == BUTTON_2_ID { - let state = unsafe { &*get_window_state(window) }; - state.press_button(id); + WM_TIMER => { + if wparam.0 != ANNOUNCEMENT_TIMER_ID { + return unsafe { DefWindowProcW(window, message, wparam, lparam) }; } + let state = unsafe { &*get_window_state(window) }; + state.flush_announcement(window); + LRESULT(0) + } + ACTION_REQUEST_MSG => { + // SAFETY: The action handler boxed this request and posted it + // here, and nothing else handles this message. + let request = unsafe { Box::from_raw(lparam.0 as *mut ActionRequest) }; + let state = unsafe { &*get_window_state(window) }; + state.ui.borrow_mut().do_action(&request); + state.after_input(window); LRESULT(0) } _ => unsafe { DefWindowProcW(window, message, wparam, lparam) }, } } -fn create_window(title: &str, initial_focus: NodeId) -> Result { - let create_params = Box::new(WindowCreateParams(initial_focus)); +fn create_window(title: &str) -> Result { let module = HINSTANCE::from(unsafe { GetModuleHandleW(None)? }); let window = unsafe { @@ -339,7 +291,7 @@ fn create_window(title: &str, initial_focus: NodeId) -> Result { None, None, Some(module), - Some(Box::into_raw(create_params) as _), + None, )? }; if window.is_invalid() { @@ -350,16 +302,9 @@ fn create_window(title: &str, initial_focus: NodeId) -> Result { } fn main() -> Result<()> { - println!("This example has no visible GUI, and a keyboard interface:"); - println!("- [Tab] switches focus between two logical buttons."); - println!( - "- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed." - ); - println!( - "Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows)." - ); - - let window = create_window(WINDOW_TITLE, INITIAL_FOCUS)?; + example_common::print_instructions(); + + let window = create_window(WINDOW_TITLE)?; let _ = unsafe { ShowWindow(window, SW_SHOW) }; let mut message = MSG::default(); diff --git a/adapters/winit/Cargo.toml b/adapters/winit/Cargo.toml index 2d6e84d68..637c7004f 100644 --- a/adapters/winit/Cargo.toml +++ b/adapters/winit/Cargo.toml @@ -8,6 +8,7 @@ categories.workspace = true keywords = ["gui", "ui", "accessibility", "winit"] repository.workspace = true readme = "README.md" +exclude = ["examples"] edition.workspace = true rust-version.workspace = true @@ -39,22 +40,11 @@ accesskit_android = { version = "0.8.0", path = "../android", optional = true, f [target.'cfg(target_os = "ios")'.dependencies] accesskit_ios = { version = "0.2.0", path = "../ios" } +[dev-dependencies] +example_common = { path = "../../example_common" } + [dev-dependencies.winit] version = "0.30.5" default-features = false features = ["x11", "wayland", "wayland-dlopen", "wayland-csd-adwaita"] -[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dev-dependencies.softbuffer] -version = "0.4.8" -default-features = false -features = [ - "x11", - "x11-dlopen", - "wayland", - "wayland-dlopen", -] - -[target.'cfg(target_os = "ios")'.dev-dependencies.softbuffer] -version = "0.4.8" -default-features = false - diff --git a/adapters/winit/README.md b/adapters/winit/README.md index 2eae18328..2eb06e4a5 100644 --- a/adapters/winit/README.md +++ b/adapters/winit/README.md @@ -19,8 +19,8 @@ The Android implementation of this adapter currently only works with [GameActivi The `examples/` directory contains two runnable examples: -- `simple` — a minimal window exposing a single accessible label. -- `mixed_handlers` — demonstrates combining AccessKit's action handling with winit event handling. +- `simple` — a window with two logical buttons and a live region, using a single winit event loop for both the initial tree request and action requests. +- `mixed_handlers` — the same UI, but with a tear-off activation handler that builds the initial tree on whichever thread the platform asks on, while action requests still go through the event loop. On desktop platforms, run them with `cargo run --example simple` or `cargo run --example mixed_handlers` from this crate's directory. diff --git a/adapters/winit/examples/mixed_handlers.rs b/adapters/winit/examples/mixed_handlers.rs index b3e87ab24..ffedde888 100644 --- a/adapters/winit/examples/mixed_handlers.rs +++ b/adapters/winit/examples/mixed_handlers.rs @@ -1,218 +1,53 @@ -#[path = "util/fill.rs"] -mod fill; +mod util; -use accesskit::{ - Action, ActionRequest, ActivationHandler, Affine, Live, Node, NodeId, Rect, Role, TreeId, - TreeInfo, TreeUpdate, Vec2, -}; +use accesskit::{ActivationHandler, TreeUpdate}; use accesskit_winit::{Adapter, Event as AccessKitEvent, WindowEvent as AccessKitWindowEvent}; +use example_common::{Modifiers, Renderer, UiState, WINDOW_TITLE}; use std::{ error::Error, sync::{Arc, Mutex}, - time::{Duration, Instant}, }; use winit::{ application::ApplicationHandler, - event::{ElementState, KeyEvent, WindowEvent}, - event_loop::ControlFlow, + event::{KeyEvent as WinitKeyEvent, WindowEvent}, event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}, - keyboard::Key, window::{Window, WindowId}, }; -const WINDOW_TITLE: &str = "Hello world"; - -const WINDOW_ID: NodeId = NodeId(0); -const BUTTON_1_ID: NodeId = NodeId(1); -const BUTTON_2_ID: NodeId = NodeId(2); -const ANNOUNCEMENT_ID: NodeId = NodeId(3); -const INITIAL_FOCUS: NodeId = BUTTON_1_ID; - -const WINDOW_RECT: Rect = Rect { - x0: 0.0, - y0: 0.0, - x1: 393.0, - y1: 759.0, -}; - -const BUTTON_1_RECT: Rect = Rect { - x0: 20.0, - y0: 20.0, - x1: 200.0, - y1: 64.0, -}; - -const BUTTON_2_RECT: Rect = Rect { - x0: 20.0, - y0: 84.0, - x1: 200.0, - y1: 128.0, -}; - -#[cfg(target_os = "ios")] -fn safe_area_inset(window: &Window) -> Vec2 { - let Ok(outer) = window.outer_position() else { - return Vec2::ZERO; - }; - let Ok(inner) = window.inner_position() else { - return Vec2::ZERO; - }; - Vec2::new((inner.x - outer.x) as f64, (inner.y - outer.y) as f64) -} - -#[cfg(not(target_os = "ios"))] -fn safe_area_inset(_: &Window) -> Vec2 { - Vec2::ZERO -} - -fn build_button(id: NodeId, label: &str) -> Node { - let rect = match id { - BUTTON_1_ID => BUTTON_1_RECT, - BUTTON_2_ID => BUTTON_2_RECT, - _ => unreachable!(), - }; - let mut node = Node::new(Role::Button); - node.set_bounds(rect); - node.set_label(label); - node.add_action(Action::Focus); - node.add_action(Action::Click); - node -} - -fn build_announcement(text: &str) -> Node { - let mut node = Node::new(Role::Label); - node.set_value(text); - node.set_live(Live::Polite); - node -} - -const ANNOUNCEMENT_DELAY: Duration = Duration::from_millis(150); - -struct UiState { - focus: NodeId, - announcement: Option<&'static str>, - pending_announcement: Option<(&'static str, Instant)>, - scale_factor: f64, - safe_area_inset: Vec2, -} - -impl UiState { - fn new(scale_factor: f64, safe_area_inset: Vec2) -> Arc> { - Arc::new(Mutex::new(Self { - focus: INITIAL_FOCUS, - announcement: None, - pending_announcement: None, - scale_factor, - safe_area_inset, - })) - } - - fn build_root(&mut self) -> Node { - let mut node = Node::new(Role::Window); - node.set_bounds(WINDOW_RECT); - node.set_transform( - Affine::translate(self.safe_area_inset) * Affine::scale(self.scale_factor), - ); - node.set_children(vec![BUTTON_1_ID, BUTTON_2_ID]); - if self.announcement.is_some() { - node.push_child(ANNOUNCEMENT_ID); - } - node.set_label(WINDOW_TITLE); - node - } - - fn build_initial_tree(&mut self) -> TreeUpdate { - let root = self.build_root(); - let button_1 = build_button(BUTTON_1_ID, "Button 1"); - let button_2 = build_button(BUTTON_2_ID, "Button 2"); - let tree = TreeInfo::new(WINDOW_ID); - let mut result = TreeUpdate { - nodes: vec![ - (WINDOW_ID, root), - (BUTTON_1_ID, button_1), - (BUTTON_2_ID, button_2), - ], - tree: Some(tree), - tree_id: TreeId::ROOT, - focus: self.focus, - }; - if let Some(announcement) = &self.announcement { - result - .nodes - .push((ANNOUNCEMENT_ID, build_announcement(announcement))); - } - result - } - - fn set_focus(&mut self, adapter: &mut Adapter, focus: NodeId) { - self.focus = focus; - adapter.update_if_active(|| TreeUpdate { - nodes: vec![], - tree: None, - tree_id: TreeId::ROOT, - focus, - }); - } - - fn press_button(&mut self, id: NodeId) { - let text = if id == BUTTON_1_ID { - "You pressed button 1" - } else { - "You pressed button 2" - }; - // On iOS, VoiceOver announces the label of the activated button. - // Postpone the live region update so the messages don't overlap. - self.pending_announcement = Some((text, Instant::now())); - } - - fn flush_announcement(&mut self, adapter: &mut Adapter) -> bool { - let Some((_, queued_at)) = &self.pending_announcement else { - return false; - }; - if queued_at.elapsed() < ANNOUNCEMENT_DELAY { - return true; - } - if let Some((text, _)) = self.pending_announcement.take() { - self.announcement = Some(text); - adapter.update_if_active(|| { - let announcement = build_announcement(text); - let root = self.build_root(); - TreeUpdate { - nodes: vec![(ANNOUNCEMENT_ID, announcement), (WINDOW_ID, root)], - tree: None, - tree_id: TreeId::ROOT, - focus: self.focus, - } - }); - } - false - } -} - struct TearoffActivationHandler { - state: Arc>, + ui: Arc>, } impl ActivationHandler for TearoffActivationHandler { fn request_initial_tree(&mut self) -> Option { - Some(self.state.lock().unwrap().build_initial_tree()) + Some(self.ui.lock().unwrap().build_tree_update()) } } struct WindowState { - window: Window, + // Declared first so that the renderer is dropped before the window. + renderer: Renderer>, + window: Arc, adapter: Adapter, ui: Arc>, + modifiers: Modifiers, } impl WindowState { - fn new(window: Window, adapter: Adapter, ui: Arc>) -> Self { + fn new(window: Arc, adapter: Adapter, ui: Arc>) -> Self { Self { + renderer: Renderer::new(Arc::clone(&window)), window, adapter, ui, + modifiers: Modifiers::default(), } } + + fn update_accessibility_tree(&mut self) { + let mut ui = self.ui.lock().unwrap(); + self.adapter.update_if_active(|| ui.build_tree_update()); + } } struct Application { @@ -233,10 +68,12 @@ impl Application { .with_title(WINDOW_TITLE) .with_visible(false); - let window = event_loop.create_window(window_attributes)?; - let ui = UiState::new(window.scale_factor(), safe_area_inset(&window)); + let window = Arc::new(event_loop.create_window(window_attributes)?); + let mut ui = UiState::new(); + ui.set_viewport(window.scale_factor(), util::safe_area_inset(&window)); + let ui = Arc::new(Mutex::new(ui)); let activation_handler = TearoffActivationHandler { - state: Arc::clone(&ui), + ui: Arc::clone(&ui), }; let adapter = Adapter::with_mixed_handlers( event_loop, @@ -253,92 +90,61 @@ impl Application { impl ApplicationHandler for Application { fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, event: WindowEvent) { - let window = match &mut self.window { - Some(window) => window, - None => return, + let Some(window) = &mut self.window else { + return; }; - let adapter = &mut window.adapter; - let state = &mut window.ui; - adapter.process_event(&window.window, &event); + window.adapter.process_event(&window.window, &event); match event { WindowEvent::CloseRequested => { - fill::cleanup_window(&window.window); self.window = None; } WindowEvent::Resized(_) => { - let factor = window.window.scale_factor(); - let inset = safe_area_inset(&window.window); - let mut state = state.lock().unwrap(); - state.scale_factor = factor; - state.safe_area_inset = inset; - adapter.update_if_active(|| state.build_initial_tree()); + let scale_factor = window.window.scale_factor(); + let inset = util::safe_area_inset(&window.window); + window.ui.lock().unwrap().set_viewport(scale_factor, inset); + window.update_accessibility_tree(); window.window.request_redraw(); } WindowEvent::RedrawRequested => { - fill::fill_window(&window.window); + let size = window.window.inner_size(); + window.renderer.draw(size.width, size.height); + } + WindowEvent::ModifiersChanged(modifiers) => { + window.modifiers = util::modifiers(&modifiers); } WindowEvent::KeyboardInput { - event: - KeyEvent { - logical_key: virtual_code, - state: ElementState::Pressed, - .. - }, + event: WinitKeyEvent { + logical_key, state, .. + }, .. - } => match virtual_code { - Key::Named(winit::keyboard::NamedKey::Tab) => { - let mut state = state.lock().unwrap(); - let new_focus = if state.focus == BUTTON_1_ID { - BUTTON_2_ID - } else { - BUTTON_1_ID - }; - state.set_focus(adapter, new_focus); + } => { + if let Some(event) = util::key_event(&logical_key, state, window.modifiers) { + window.ui.lock().unwrap().handle_key(event); + window.update_accessibility_tree(); window.window.request_redraw(); } - Key::Named(winit::keyboard::NamedKey::Space) => { - let mut state = state.lock().unwrap(); - let id = state.focus; - state.press_button(id); - window.window.request_redraw(); - } - _ => (), - }, + } _ => (), } } fn user_event(&mut self, _: &ActiveEventLoop, user_event: AccessKitEvent) { - let window = match &mut self.window { - Some(window) => window, - None => return, + let Some(window) = &mut self.window else { + return; }; - let adapter = &mut window.adapter; - let state = &mut window.ui; match user_event.window_event { + // The tearoff activation handler takes care of this. AccessKitWindowEvent::InitialTreeRequested => unreachable!(), - AccessKitWindowEvent::ActionRequested(ActionRequest { - action, - target_node, - .. - }) => { - if target_node == BUTTON_1_ID || target_node == BUTTON_2_ID { - let mut state = state.lock().unwrap(); - match action { - Action::Focus => { - state.set_focus(adapter, target_node); - } - Action::Click => { - state.press_button(target_node); - } - _ => (), - } - } + AccessKitWindowEvent::ActionRequested(request) => { + window.ui.lock().unwrap().do_action(&request); + window.update_accessibility_tree(); window.window.request_redraw(); } - AccessKitWindowEvent::AccessibilityDeactivated => {} + AccessKitWindowEvent::AccessibilityDeactivated => { + window.ui.lock().unwrap().deactivated(); + } } } @@ -353,42 +159,20 @@ impl ApplicationHandler for Application { } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { - if let Some(window) = &mut self.window { - if window - .ui - .lock() - .unwrap() - .flush_announcement(&mut window.adapter) - { - event_loop.set_control_flow(ControlFlow::wait_duration(ANNOUNCEMENT_DELAY)); - } - } else { + let Some(window) = &mut self.window else { event_loop.exit(); + return; + }; + + let flushed = util::flush_announcement(event_loop, &mut window.ui.lock().unwrap()); + if flushed { + window.update_accessibility_tree(); } } } fn main() -> Result<(), Box> { - println!("This example has no visible GUI, and a keyboard interface:"); - println!("- [Tab] switches focus between two logical buttons."); - println!( - "- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed." - ); - #[cfg(target_os = "windows")] - println!( - "Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows)." - ); - #[cfg(all( - feature = "accesskit_unix", - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ) - ))] - println!("Enable Orca with [Super]+[Alt]+[S]."); + example_common::print_instructions(); let event_loop = EventLoop::with_user_event().build()?; let mut state = Application::new(event_loop.create_proxy()); diff --git a/adapters/winit/examples/simple.rs b/adapters/winit/examples/simple.rs index 68a653f5b..050dd223a 100644 --- a/adapters/winit/examples/simple.rs +++ b/adapters/winit/examples/simple.rs @@ -1,203 +1,39 @@ -#[path = "util/fill.rs"] -mod fill; +mod util; -use accesskit::{ - Action, ActionRequest, Affine, Live, Node, NodeId, Rect, Role, TreeId, TreeInfo, TreeUpdate, - Vec2, -}; use accesskit_winit::{Adapter, Event as AccessKitEvent, WindowEvent as AccessKitWindowEvent}; -use std::error::Error; -use std::time::{Duration, Instant}; +use example_common::{Modifiers, Renderer, UiState, WINDOW_TITLE}; +use std::{error::Error, sync::Arc}; use winit::{ application::ApplicationHandler, - event::{ElementState, KeyEvent, WindowEvent}, - event_loop::ControlFlow, + event::{KeyEvent as WinitKeyEvent, WindowEvent}, event_loop::{ActiveEventLoop, EventLoop, EventLoopProxy}, - keyboard::Key, window::{Window, WindowId}, }; -const WINDOW_TITLE: &str = "Hello world"; - -const WINDOW_ID: NodeId = NodeId(0); -const BUTTON_1_ID: NodeId = NodeId(1); -const BUTTON_2_ID: NodeId = NodeId(2); -const ANNOUNCEMENT_ID: NodeId = NodeId(3); -const INITIAL_FOCUS: NodeId = BUTTON_1_ID; - -const WINDOW_RECT: Rect = Rect { - x0: 0.0, - y0: 0.0, - x1: 393.0, - y1: 759.0, -}; - -const BUTTON_1_RECT: Rect = Rect { - x0: 20.0, - y0: 20.0, - x1: 200.0, - y1: 64.0, -}; - -const BUTTON_2_RECT: Rect = Rect { - x0: 20.0, - y0: 84.0, - x1: 200.0, - y1: 128.0, -}; - -#[cfg(target_os = "ios")] -fn safe_area_inset(window: &Window) -> Vec2 { - let Ok(outer) = window.outer_position() else { - return Vec2::ZERO; - }; - let Ok(inner) = window.inner_position() else { - return Vec2::ZERO; - }; - Vec2::new((inner.x - outer.x) as f64, (inner.y - outer.y) as f64) -} - -#[cfg(not(target_os = "ios"))] -fn safe_area_inset(_: &Window) -> Vec2 { - Vec2::ZERO -} - -fn build_button(id: NodeId, label: &str) -> Node { - let rect = match id { - BUTTON_1_ID => BUTTON_1_RECT, - BUTTON_2_ID => BUTTON_2_RECT, - _ => unreachable!(), - }; - let mut node = Node::new(Role::Button); - node.set_bounds(rect); - node.set_label(label); - node.add_action(Action::Focus); - node.add_action(Action::Click); - node -} - -fn build_announcement(text: &str) -> Node { - let mut node = Node::new(Role::Label); - node.set_value(text); - node.set_live(Live::Polite); - node -} - -const ANNOUNCEMENT_DELAY: Duration = Duration::from_millis(150); - -struct UiState { - focus: NodeId, - announcement: Option<&'static str>, - pending_announcement: Option<(&'static str, Instant)>, - scale_factor: f64, - inset: Vec2, -} - -impl UiState { - fn new(scale_factor: f64, inset: Vec2) -> Self { - Self { - focus: INITIAL_FOCUS, - announcement: None, - pending_announcement: None, - scale_factor, - inset, - } - } - - fn build_root(&mut self) -> Node { - let mut node = Node::new(Role::Window); - node.set_bounds(WINDOW_RECT); - node.set_transform(Affine::translate(self.inset) * Affine::scale(self.scale_factor)); - node.set_children(vec![BUTTON_1_ID, BUTTON_2_ID]); - if self.announcement.is_some() { - node.push_child(ANNOUNCEMENT_ID); - } - node.set_label(WINDOW_TITLE); - node - } - - fn build_initial_tree(&mut self) -> TreeUpdate { - let root = self.build_root(); - let button_1 = build_button(BUTTON_1_ID, "Button 1"); - let button_2 = build_button(BUTTON_2_ID, "Button 2"); - let tree = TreeInfo::new(WINDOW_ID); - let mut result = TreeUpdate { - nodes: vec![ - (WINDOW_ID, root), - (BUTTON_1_ID, button_1), - (BUTTON_2_ID, button_2), - ], - tree: Some(tree), - tree_id: TreeId::ROOT, - focus: self.focus, - }; - if let Some(announcement) = &self.announcement { - result - .nodes - .push((ANNOUNCEMENT_ID, build_announcement(announcement))); - } - result - } - - fn set_focus(&mut self, adapter: &mut Adapter, focus: NodeId) { - self.focus = focus; - adapter.update_if_active(|| TreeUpdate { - nodes: vec![], - tree: None, - tree_id: TreeId::ROOT, - focus, - }); - } - - fn press_button(&mut self, id: NodeId) { - let text = if id == BUTTON_1_ID { - "You pressed button 1" - } else { - "You pressed button 2" - }; - // On iOS, VoiceOver announces the label of the activated button. - // Postpone the live region update so the messages don't overlap. - self.pending_announcement = Some((text, Instant::now())); - } - - fn flush_announcement(&mut self, adapter: &mut Adapter) -> bool { - let Some((_, queued_at)) = &self.pending_announcement else { - return false; - }; - if queued_at.elapsed() < ANNOUNCEMENT_DELAY { - return true; - } - if let Some((text, _)) = self.pending_announcement.take() { - self.announcement = Some(text); - adapter.update_if_active(|| { - let announcement = build_announcement(text); - let root = self.build_root(); - TreeUpdate { - nodes: vec![(ANNOUNCEMENT_ID, announcement), (WINDOW_ID, root)], - tree: None, - tree_id: TreeId::ROOT, - focus: self.focus, - } - }); - } - false - } -} - struct WindowState { - window: Window, + // Declared first so that the renderer is dropped before the window. + renderer: Renderer>, + window: Arc, adapter: Adapter, ui: UiState, + modifiers: Modifiers, } impl WindowState { - fn new(window: Window, adapter: Adapter, ui: UiState) -> Self { + fn new(window: Arc, adapter: Adapter, ui: UiState) -> Self { Self { + renderer: Renderer::new(Arc::clone(&window)), window, adapter, ui, + modifiers: Modifiers::default(), } } + + fn update_accessibility_tree(&mut self) { + let ui = &mut self.ui; + self.adapter.update_if_active(|| ui.build_tree_update()); + } } struct Application { @@ -218,8 +54,9 @@ impl Application { .with_title(WINDOW_TITLE) .with_visible(false); - let window = event_loop.create_window(window_attributes)?; - let ui = UiState::new(window.scale_factor(), safe_area_inset(&window)); + let window = Arc::new(event_loop.create_window(window_attributes)?); + let mut ui = UiState::new(); + ui.set_viewport(window.scale_factor(), util::safe_area_inset(&window)); let adapter = Adapter::with_event_loop_proxy(event_loop, &window, self.event_loop_proxy.clone()); window.set_visible(true); @@ -231,88 +68,62 @@ impl Application { impl ApplicationHandler for Application { fn window_event(&mut self, _: &ActiveEventLoop, _: WindowId, event: WindowEvent) { - let window = match &mut self.window { - Some(window) => window, - None => return, + let Some(window) = &mut self.window else { + return; }; - let adapter = &mut window.adapter; - let state = &mut window.ui; - adapter.process_event(&window.window, &event); + window.adapter.process_event(&window.window, &event); match event { WindowEvent::CloseRequested => { - fill::cleanup_window(&window.window); self.window = None; } WindowEvent::Resized(_) => { - state.scale_factor = window.window.scale_factor(); - state.inset = safe_area_inset(&window.window); - adapter.update_if_active(|| state.build_initial_tree()); + let scale_factor = window.window.scale_factor(); + let inset = util::safe_area_inset(&window.window); + window.ui.set_viewport(scale_factor, inset); + window.update_accessibility_tree(); window.window.request_redraw(); } WindowEvent::RedrawRequested => { - fill::fill_window(&window.window); + let size = window.window.inner_size(); + window.renderer.draw(size.width, size.height); + } + WindowEvent::ModifiersChanged(modifiers) => { + window.modifiers = util::modifiers(&modifiers); } WindowEvent::KeyboardInput { - event: - KeyEvent { - logical_key: virtual_code, - state: ElementState::Pressed, - .. - }, + event: WinitKeyEvent { + logical_key, state, .. + }, .. - } => match virtual_code { - Key::Named(winit::keyboard::NamedKey::Tab) => { - let new_focus = if state.focus == BUTTON_1_ID { - BUTTON_2_ID - } else { - BUTTON_1_ID - }; - state.set_focus(adapter, new_focus); + } => { + if let Some(event) = util::key_event(&logical_key, state, window.modifiers) { + window.ui.handle_key(event); + window.update_accessibility_tree(); window.window.request_redraw(); } - Key::Named(winit::keyboard::NamedKey::Space) => { - let id = state.focus; - state.press_button(id); - window.window.request_redraw(); - } - _ => (), - }, + } _ => (), } } fn user_event(&mut self, _: &ActiveEventLoop, user_event: AccessKitEvent) { - let window = match &mut self.window { - Some(window) => window, - None => return, + let Some(window) = &mut self.window else { + return; }; - let adapter = &mut window.adapter; - let state = &mut window.ui; match user_event.window_event { AccessKitWindowEvent::InitialTreeRequested => { - adapter.update_if_active(|| state.build_initial_tree()); + window.update_accessibility_tree(); } - AccessKitWindowEvent::ActionRequested(ActionRequest { - action, - target_node, - .. - }) => { - if target_node == BUTTON_1_ID || target_node == BUTTON_2_ID { - match action { - Action::Focus => { - state.set_focus(adapter, target_node); - } - Action::Click => { - state.press_button(target_node); - } - _ => (), - } - } + AccessKitWindowEvent::ActionRequested(request) => { + window.ui.do_action(&request); + window.update_accessibility_tree(); window.window.request_redraw(); } - AccessKitWindowEvent::AccessibilityDeactivated => (), + AccessKitWindowEvent::AccessibilityDeactivated => { + window.ui.deactivated(); + } } } @@ -327,37 +138,19 @@ impl ApplicationHandler for Application { } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { - if let Some(window) = &mut self.window { - if window.ui.flush_announcement(&mut window.adapter) { - event_loop.set_control_flow(ControlFlow::wait_duration(ANNOUNCEMENT_DELAY)); - } - } else { + let Some(window) = &mut self.window else { event_loop.exit(); + return; + }; + + if util::flush_announcement(event_loop, &mut window.ui) { + window.update_accessibility_tree(); } } } fn main() -> Result<(), Box> { - println!("This example has no visible GUI, and a keyboard interface:"); - println!("- [Tab] switches focus between two logical buttons."); - println!( - "- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed." - ); - #[cfg(target_os = "windows")] - println!( - "Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows)." - ); - #[cfg(all( - feature = "accesskit_unix", - any( - target_os = "linux", - target_os = "dragonfly", - target_os = "freebsd", - target_os = "netbsd", - target_os = "openbsd" - ) - ))] - println!("Enable Orca with [Super]+[Alt]+[S]."); + example_common::print_instructions(); let event_loop = EventLoop::with_user_event().build()?; let mut state = Application::new(event_loop.create_proxy()); diff --git a/adapters/winit/examples/util/fill.rs b/adapters/winit/examples/util/fill.rs deleted file mode 100644 index bc104c1c0..000000000 --- a/adapters/winit/examples/util/fill.rs +++ /dev/null @@ -1,123 +0,0 @@ -// Adapted from winit's examples/util/fill.rs. - -//! Fill the window buffer with a solid color. -//! -//! Launching a window without drawing to it has unpredictable results varying from platform to -//! platform. In order to have well-defined examples, this module provides an easy way to -//! fill the window buffer with a solid color. -//! -//! The `softbuffer` crate is used, largely because of its ease of use. `glutin` or `wgpu` could -//! also be used to fill the window buffer, but they are more complicated to use. - -pub use platform::cleanup_window; -pub use platform::fill_window; - -#[cfg(not(target_os = "android"))] -mod platform { - use std::cell::RefCell; - use std::collections::HashMap; - use std::mem; - use std::mem::ManuallyDrop; - use std::num::NonZeroU32; - - use softbuffer::{Context, Surface}; - use winit::window::{Window, WindowId}; - - thread_local! { - // NOTE: You should never do things like that, create context and drop it before - // you drop the event loop. We do this for brevity to not blow up examples. We use - // ManuallyDrop to prevent destructors from running. - // - // A static, thread-local map of graphics contexts to open windows. - static GC: ManuallyDrop>> = const { ManuallyDrop::new(RefCell::new(None)) }; - } - - /// The graphics context used to draw to a window. - struct GraphicsContext { - /// The global softbuffer context. - context: RefCell>, - - /// The hash map of window IDs to surfaces. - surfaces: HashMap>, - } - - impl GraphicsContext { - fn new(w: &Window) -> Self { - Self { - context: RefCell::new( - Context::new(unsafe { mem::transmute::<&'_ Window, &'static Window>(w) }) - .expect("Failed to create a softbuffer context"), - ), - surfaces: HashMap::new(), - } - } - - fn create_surface( - &mut self, - window: &Window, - ) -> &mut Surface<&'static Window, &'static Window> { - self.surfaces.entry(window.id()).or_insert_with(|| { - Surface::new(&self.context.borrow(), unsafe { - mem::transmute::<&'_ Window, &'static Window>(window) - }) - .expect("Failed to create a softbuffer surface") - }) - } - - fn destroy_surface(&mut self, window: &Window) { - self.surfaces.remove(&window.id()); - } - } - - pub fn fill_window(window: &Window) { - GC.with(|gc| { - let size = window.inner_size(); - let (Some(width), Some(height)) = - (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) - else { - return; - }; - - // Either get the last context used or create a new one. - let mut gc = gc.borrow_mut(); - let surface = gc - .get_or_insert_with(|| GraphicsContext::new(window)) - .create_surface(window); - - // Fill a buffer with a solid color. - const DARK_GRAY: u32 = 0xff181818; - - surface - .resize(width, height) - .expect("Failed to resize the softbuffer surface"); - - let mut buffer = surface - .buffer_mut() - .expect("Failed to get the softbuffer buffer"); - buffer.fill(DARK_GRAY); - buffer - .present() - .expect("Failed to present the softbuffer buffer"); - }) - } - - pub fn cleanup_window(window: &Window) { - GC.with(|gc| { - let mut gc = gc.borrow_mut(); - if let Some(context) = gc.as_mut() { - context.destroy_surface(window); - } - }); - } -} - -#[cfg(target_os = "android")] -mod platform { - pub fn fill_window(_window: &winit::window::Window) { - // No-op on Android platform. - } - - pub fn cleanup_window(_window: &winit::window::Window) { - // No-op on Android platform. - } -} diff --git a/adapters/winit/examples/util/mod.rs b/adapters/winit/examples/util/mod.rs new file mode 100644 index 000000000..04eba33b9 --- /dev/null +++ b/adapters/winit/examples/util/mod.rs @@ -0,0 +1,63 @@ +use accesskit::Vec2; +use example_common::{Key, KeyEvent, KeyState, Modifiers, UiState}; +use winit::{ + event::{ElementState, Modifiers as WinitModifiers}, + event_loop::{ActiveEventLoop, ControlFlow}, + keyboard::{Key as WinitKey, NamedKey}, + window::Window, +}; + +#[cfg(target_os = "ios")] +pub fn safe_area_inset(window: &Window) -> Vec2 { + let Ok(outer) = window.outer_position() else { + return Vec2::ZERO; + }; + let Ok(inner) = window.inner_position() else { + return Vec2::ZERO; + }; + Vec2::new((inner.x - outer.x) as f64, (inner.y - outer.y) as f64) +} + +#[cfg(not(target_os = "ios"))] +pub fn safe_area_inset(_: &Window) -> Vec2 { + Vec2::ZERO +} + +pub fn key_event(key: &WinitKey, state: ElementState, modifiers: Modifiers) -> Option { + let key = match key { + WinitKey::Named(NamedKey::Enter) => Key::Enter, + WinitKey::Named(NamedKey::Space) => Key::Space, + WinitKey::Named(NamedKey::Tab) => Key::Tab, + _ => return None, + }; + let state = match state { + ElementState::Pressed => KeyState::Pressed, + ElementState::Released => KeyState::Released, + }; + Some(KeyEvent { + key, + state, + modifiers, + }) +} + +pub fn modifiers(modifiers: &WinitModifiers) -> Modifiers { + Modifiers { + shift: modifiers.state().shift_key(), + } +} + +pub fn flush_announcement(event_loop: &ActiveEventLoop, ui: &mut UiState) -> bool { + match ui.time_until_announcement() { + Some(remaining) if !remaining.is_zero() => { + event_loop.set_control_flow(ControlFlow::wait_duration(remaining)); + false + } + Some(_) => { + let flushed = ui.flush_announcement(); + event_loop.set_control_flow(ControlFlow::Wait); + flushed + } + None => false, + } +} diff --git a/deny.toml b/deny.toml index 9f584ece8..6a04b0ccc 100644 --- a/deny.toml +++ b/deny.toml @@ -11,6 +11,7 @@ targets = [ { triple = "x86_64-unknown-linux-gnu" }, ] all-features = true +exclude = ["example_common"] [advisories] db-path = "~/.cargo/advisory-db" diff --git a/example_common/Cargo.toml b/example_common/Cargo.toml new file mode 100644 index 000000000..49bf6c558 --- /dev/null +++ b/example_common/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "example_common" +version = "0.0.0" +authors.workspace = true +repository.workspace = true +edition.workspace = true +rust-version.workspace = true +publish = false + +[dependencies] +accesskit = { path = "../accesskit" } +raw-window-handle = "0.6.2" + +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies.softbuffer] +version = "0.4.8" +default-features = false +features = [ + "x11", + "x11-dlopen", + "wayland", + "wayland-dlopen", +] + +[target.'cfg(target_os = "ios")'.dependencies.softbuffer] +version = "0.4.8" +default-features = false diff --git a/example_common/src/key.rs b/example_common/src/key.rs new file mode 100644 index 000000000..8df372cd8 --- /dev/null +++ b/example_common/src/key.rs @@ -0,0 +1,24 @@ +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Key { + Enter, + Space, + Tab, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum KeyState { + Pressed, + Released, +} + +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct Modifiers { + pub shift: bool, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct KeyEvent { + pub key: Key, + pub state: KeyState, + pub modifiers: Modifiers, +} diff --git a/example_common/src/lib.rs b/example_common/src/lib.rs new file mode 100644 index 000000000..83077d649 --- /dev/null +++ b/example_common/src/lib.rs @@ -0,0 +1,265 @@ +mod key; +mod render; + +pub use key::{Key, KeyEvent, KeyState, Modifiers}; +pub use render::Renderer; + +use accesskit::{ + Action, ActionRequest, Affine, Live, Node, NodeId, Rect, Role, TreeId, TreeInfo, TreeUpdate, + Vec2, +}; +use std::{ + collections::BTreeSet, + mem::take, + time::{Duration, Instant}, +}; + +pub const WINDOW_TITLE: &str = "Hello world"; + +const WINDOW_ID: NodeId = NodeId(0); +const BUTTON_1_ID: NodeId = NodeId(1); +const BUTTON_2_ID: NodeId = NodeId(2); +const ANNOUNCEMENT_ID: NodeId = NodeId(3); +const INITIAL_FOCUS: NodeId = BUTTON_1_ID; +const FOCUS_ORDER: [NodeId; 2] = [BUTTON_1_ID, BUTTON_2_ID]; + +const WINDOW_RECT: Rect = Rect { + x0: 0.0, + y0: 0.0, + x1: 393.0, + y1: 759.0, +}; + +const BUTTON_1_RECT: Rect = Rect { + x0: 20.0, + y0: 20.0, + x1: 200.0, + y1: 64.0, +}; + +const BUTTON_2_RECT: Rect = Rect { + x0: 20.0, + y0: 84.0, + x1: 200.0, + y1: 128.0, +}; + +fn build_button(id: NodeId, label: &str) -> Node { + let rect = match id { + BUTTON_1_ID => BUTTON_1_RECT, + BUTTON_2_ID => BUTTON_2_RECT, + _ => unreachable!(), + }; + let mut node = Node::new(Role::Button); + node.set_bounds(rect); + node.set_label(label); + node.add_action(Action::Focus); + node.add_action(Action::Click); + node +} + +fn build_announcement(text: &str) -> Node { + let mut node = Node::new(Role::Label); + node.set_value(text); + node.set_live(Live::Polite); + node +} + +const ANNOUNCEMENT_DELAY: Duration = Duration::from_millis(150); + +struct Viewport { + scale_factor: f64, + safe_area_inset: Vec2, +} + +pub struct UiState { + focus: NodeId, + announcement: Option<&'static str>, + pending_announcement: Option<(&'static str, Instant)>, + viewport: Option, + adapter_has_tree: bool, + dirty_nodes: BTreeSet, +} + +impl Default for UiState { + fn default() -> Self { + Self::new() + } +} + +impl UiState { + pub fn new() -> Self { + Self { + focus: INITIAL_FOCUS, + announcement: None, + pending_announcement: None, + viewport: None, + adapter_has_tree: false, + dirty_nodes: BTreeSet::new(), + } + } + + pub fn set_viewport(&mut self, scale_factor: f64, safe_area_inset: Vec2) { + self.viewport = Some(Viewport { + scale_factor, + safe_area_inset, + }); + self.dirty_nodes.insert(WINDOW_ID); + } + + fn build_root(&self) -> Node { + let mut node = Node::new(Role::Window); + if let Some(viewport) = &self.viewport { + node.set_bounds(WINDOW_RECT); + node.set_transform( + Affine::translate(viewport.safe_area_inset) * Affine::scale(viewport.scale_factor), + ); + } + node.set_children(vec![BUTTON_1_ID, BUTTON_2_ID]); + if self.announcement.is_some() { + node.push_child(ANNOUNCEMENT_ID); + } + node.set_label(WINDOW_TITLE); + node.set_language("en"); + node + } + + fn build_node(&self, id: NodeId) -> Node { + match id { + WINDOW_ID => self.build_root(), + BUTTON_1_ID => build_button(BUTTON_1_ID, "Button 1"), + BUTTON_2_ID => build_button(BUTTON_2_ID, "Button 2"), + ANNOUNCEMENT_ID => build_announcement(self.announcement.unwrap()), + _ => unreachable!(), + } + } + + pub fn build_tree_update(&mut self) -> TreeUpdate { + if self.adapter_has_tree { + self.build_update_for_dirty_nodes() + } else { + self.build_full_tree() + } + } + + fn build_full_tree(&mut self) -> TreeUpdate { + self.adapter_has_tree = true; + self.dirty_nodes.clear(); + let mut ids = vec![WINDOW_ID, BUTTON_1_ID, BUTTON_2_ID]; + if self.announcement.is_some() { + ids.push(ANNOUNCEMENT_ID); + } + TreeUpdate { + nodes: ids + .into_iter() + .map(|id| (id, self.build_node(id))) + .collect(), + tree: Some(TreeInfo::new(WINDOW_ID)), + tree_id: TreeId::ROOT, + focus: self.focus, + } + } + + fn build_update_for_dirty_nodes(&mut self) -> TreeUpdate { + let ids = take(&mut self.dirty_nodes); + TreeUpdate { + nodes: ids + .into_iter() + .map(|id| (id, self.build_node(id))) + .collect(), + tree: None, + tree_id: TreeId::ROOT, + focus: self.focus, + } + } + + pub fn deactivated(&mut self) { + self.adapter_has_tree = false; + self.dirty_nodes.clear(); + } + + fn set_focus(&mut self, focus: NodeId) { + self.focus = focus; + } + + fn move_focus(&mut self, forward: bool) { + let count = FOCUS_ORDER.len(); + let current = FOCUS_ORDER + .iter() + .position(|id| *id == self.focus) + .unwrap_or(0); + let next = if forward { + (current + 1) % count + } else { + (current + count - 1) % count + }; + self.focus = FOCUS_ORDER[next]; + } + + fn press_button(&mut self, id: NodeId) { + let text = if id == BUTTON_1_ID { + "You pressed button 1" + } else { + "You pressed button 2" + }; + // On iOS, VoiceOver announces the label of the activated button. + // Postpone the live region update so the messages don't overlap. + self.pending_announcement = Some((text, Instant::now())); + } + + pub fn handle_key(&mut self, event: KeyEvent) { + if event.state != KeyState::Pressed { + return; + } + match event.key { + Key::Enter | Key::Space => self.press_button(self.focus), + Key::Tab => self.move_focus(!event.modifiers.shift), + } + } + + pub fn do_action(&mut self, request: &ActionRequest) { + if request.target_node != BUTTON_1_ID && request.target_node != BUTTON_2_ID { + return; + } + match request.action { + Action::Focus => self.set_focus(request.target_node), + Action::Click => self.press_button(request.target_node), + _ => (), + } + } + + pub fn time_until_announcement(&self) -> Option { + self.pending_announcement + .map(|(_, queued_at)| ANNOUNCEMENT_DELAY.saturating_sub(queued_at.elapsed())) + } + + pub fn flush_announcement(&mut self) -> bool { + let Some((text, _)) = self.pending_announcement.take() else { + return false; + }; + self.announcement = Some(text); + self.dirty_nodes.insert(WINDOW_ID); + self.dirty_nodes.insert(ANNOUNCEMENT_ID); + true + } +} + +pub fn print_instructions() { + println!("This example has no visible GUI, and a keyboard interface:"); + println!("- [Tab] switches focus between two logical buttons."); + println!( + "- [Space] 'presses' the button, adding static text in a live region announcing that it was pressed." + ); + #[cfg(target_os = "windows")] + println!( + "Enable Narrator with [Win]+[Ctrl]+[Enter] (or [Win]+[Enter] on older versions of Windows)." + ); + #[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" + ))] + println!("Enable Orca with [Super]+[Alt]+[S]."); +} diff --git a/example_common/src/render.rs b/example_common/src/render.rs new file mode 100644 index 000000000..42861bb70 --- /dev/null +++ b/example_common/src/render.rs @@ -0,0 +1,76 @@ +//! Adapted from winit's `examples/util/fill.rs`. + +pub use platform::Renderer; + +#[cfg(not(target_os = "android"))] +mod platform { + use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; + use softbuffer::{Context, Surface}; + use std::num::NonZeroU32; + + const DARK_GRAY: u32 = 0xff181818; + + pub struct Renderer { + // Declared first so that it's dropped before the context it came from. + surface: Surface, + // Kept alive for as long as the surface that was made from it. + _context: Context, + } + + impl Renderer { + /// Create a renderer for a window. + /// + /// Drop this before the window itself goes away. + pub fn new(window: W) -> Self { + let context = + Context::new(window.clone()).expect("failed to create a softbuffer context"); + let surface = + Surface::new(&context, window).expect("failed to create a softbuffer surface"); + Self { + surface, + _context: context, + } + } + + /// Fill the window with a solid color. + /// + /// The size is in physical pixels. Nothing is drawn if either + /// dimension is zero, as happens when a window is minimized. + pub fn draw(&mut self, width: u32, height: u32) { + let (Some(width), Some(height)) = (NonZeroU32::new(width), NonZeroU32::new(height)) + else { + return; + }; + + self.surface + .resize(width, height) + .expect("failed to resize the softbuffer surface"); + + let mut buffer = self + .surface + .buffer_mut() + .expect("failed to get the softbuffer buffer"); + buffer.fill(DARK_GRAY); + buffer + .present() + .expect("failed to present the softbuffer buffer"); + } + } +} + +#[cfg(target_os = "android")] +mod platform { + use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; + use std::marker::PhantomData; + + /// Drawing is a no-op on Android, which softbuffer doesn't support. + pub struct Renderer(PhantomData); + + impl Renderer { + pub fn new(_window: W) -> Self { + Self(PhantomData) + } + + pub fn draw(&mut self, _width: u32, _height: u32) {} + } +}