Skip to content

Commit

Permalink
feat(platforms/windows): Win32 subclassing support (#118)
Browse files Browse the repository at this point in the history
Co-authored-by: Arnold Loubriat <[email protected]>
  • Loading branch information
mwcampbell and DataTriny authored Jul 21, 2022
1 parent f2333c8 commit 60c69b7
Show file tree
Hide file tree
Showing 8 changed files with 1,101 additions and 10 deletions.
746 changes: 736 additions & 10 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions platforms/windows/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ crossbeam-utils = "0.8.5"
lazy_static = "1.4.0"
parking_lot = "0.11.2"
scopeguard = "1.1.0"
winit = "0.26.1"
179 changes: 179 additions & 0 deletions platforms/windows/examples/winit.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use accesskit::{
Action, ActionHandler, ActionRequest, Node, NodeId, Role, StringEncoding, Tree, TreeUpdate,
};
use accesskit_windows::{Adapter, WindowSubclass};
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<Mutex<Self>> {
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<Mutex<State>>);

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, StringEncoding::Utf8)),
focus: state.is_window_focused.then_some(state.focus),
}
}

pub struct WinitActionHandler(Mutex<EventLoopProxy<ActionRequest>>);

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: Arc<Adapter> = {
let state = Arc::clone(&state);
let proxy = Mutex::new(event_loop.create_proxy());
Arc::new(Adapter::new(
HWND(window.hwnd() as _),
Box::new(move || {
let state = state.lock().unwrap();
initial_tree_update(&state)
}),
Box::new(WinitActionHandler(proxy)),
))
};
let _subclass = WindowSubclass::new(&*adapter);

window.set_visible(true);

let adapter = Arc::clone(&adapter); // to move into the event handler
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);
}
_ => (),
}
});
}
4 changes: 4 additions & 0 deletions platforms/windows/src/adapter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,10 @@ impl<Source: Into<TreeUpdate>> Adapter<Source> {
}
}

pub fn window_handle(&self) -> HWND {
self.hwnd
}

fn get_or_create_tree(&self) -> &Arc<Tree> {
self.tree
.get_or_create(|(source, action_handler)| Tree::new(source.into(), action_handler))
Expand Down
3 changes: 3 additions & 0 deletions platforms/windows/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,8 @@ mod util;
mod adapter;
pub use adapter::{Adapter, QueuedEvents};

mod subclass;
pub use subclass::WindowSubclass;

#[cfg(test)]
mod tests;
99 changes: 99 additions & 0 deletions platforms/windows/src/subclass.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright 2022 The AccessKit Authors. All rights reserved.
// Licensed under the Apache License, Version 2.0 (found in
// the LICENSE-APACHE file) or the MIT license (found in
// the LICENSE-MIT file), at your option.

use accesskit::TreeUpdate;
use std::{cell::Cell, ffi::c_void, mem::transmute};
use windows::{
core::*,
Win32::{Foundation::*, UI::WindowsAndMessaging::*},
};

use crate::Adapter;

const PROP_NAME: &str = "AccessKitAdapter";

struct SubclassData<'a, Source: Into<TreeUpdate>> {
adapter: &'a Adapter<Source>,
prev_wnd_proc: WNDPROC,
window_destroyed: Cell<bool>,
}

extern "system" fn wnd_proc<Source: Into<TreeUpdate>>(
window: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
) -> LRESULT {
let handle = unsafe { GetPropW(window, PROP_NAME) };
let data_ptr = handle.0 as *const SubclassData<'_, Source>;
assert!(!data_ptr.is_null());
let data = unsafe { &*data_ptr };
if message == WM_GETOBJECT {
if let Some(result) = data.adapter.handle_wm_getobject(wparam, lparam) {
return result.into();
}
}
if message == WM_NCDESTROY {
data.window_destroyed.set(true);
}
unsafe { CallWindowProcW(data.prev_wnd_proc, window, message, wparam, lparam) }
}

/// Uses [Win32 subclassing] to handle `WM_GETOBJECT` messages on a window
/// that provides no other way of adding custom message handlers.
///
/// [Win32 subclassing]: https://docs.microsoft.com/en-us/windows/win32/controls/subclassing-overview
#[repr(transparent)]
pub struct WindowSubclass<'a, Source: Into<TreeUpdate> = Box<dyn FnOnce() -> TreeUpdate>>(
Box<SubclassData<'a, Source>>,
);

impl<'a, Source: Into<TreeUpdate>> WindowSubclass<'a, Source> {
pub fn new(adapter: &'a Adapter<Source>) -> Self {
let hwnd = adapter.window_handle();
let mut data = Box::new(SubclassData {
adapter,
prev_wnd_proc: None,
window_destroyed: Cell::new(false),
});
unsafe {
SetPropW(
hwnd,
PROP_NAME,
HANDLE(&*data as *const SubclassData<'_, Source> as _),
)
}
.unwrap();
let result = unsafe {
SetWindowLongPtrW(hwnd, GWLP_WNDPROC, wnd_proc::<Source> as *const c_void as _)
};
if result == 0 {
let result: Result<()> = Err(Error::from_win32());
result.unwrap();
}
data.prev_wnd_proc = unsafe { transmute::<isize, WNDPROC>(result) };
Self(data)
}
}

impl<Source: Into<TreeUpdate>> Drop for WindowSubclass<'_, Source> {
fn drop(&mut self) {
if !self.0.window_destroyed.get() {
let hwnd = self.0.adapter.window_handle();
let result = unsafe {
SetWindowLongPtrW(
hwnd,
GWLP_WNDPROC,
transmute::<WNDPROC, isize>(self.0.prev_wnd_proc),
)
};
if result == 0 {
let result: Result<()> = Err(Error::from_win32());
result.unwrap();
}
unsafe { RemovePropW(hwnd, PROP_NAME) }.unwrap();
}
}
}
1 change: 1 addition & 0 deletions platforms/windows/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -349,3 +349,4 @@ impl IUIAutomationFocusChangedEventHandler_Impl for FocusEventHandler {
}

mod simple;
mod subclassed;
78 changes: 78 additions & 0 deletions platforms/windows/src/tests/subclassed.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2022 The AccessKit Authors. All rights reserved.
// Licensed under the Apache License, Version 2.0 (found in
// the LICENSE-APACHE file) or the MIT license (found in
// the LICENSE-MIT file), at your option.

use std::num::NonZeroU128;

use accesskit::{
ActionHandler, ActionRequest, Node, NodeId, Role, StringEncoding, Tree, TreeUpdate,
};
use windows::Win32::{Foundation::*, UI::Accessibility::*};
use winit::{
event_loop::EventLoop,
platform::windows::{EventLoopExtWindows, WindowExtWindows},
window::WindowBuilder,
};

use crate::{Adapter, WindowSubclass};

const WINDOW_TITLE: &str = "Simple test";

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) });

fn make_button(id: NodeId, name: &str) -> Node {
Node {
name: Some(name.into()),
focusable: true,
..Node::new(id, Role::Button)
}
}

fn get_initial_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, StringEncoding::Utf8)),
focus: None,
}
}

pub struct NullActionHandler;

impl ActionHandler for NullActionHandler {
fn do_action(&self, _request: ActionRequest) {}
}

// This module uses winit for the purpose of testing with a real third-party
// window implementation that we don't control.

#[test]
fn has_native_uia() {
// This test is simple enough that we know it's fine to run entirely
// on one thread, so we don't need a full multithreaded test harness.
let event_loop = EventLoop::<()>::new_any_thread();
let window = WindowBuilder::new()
.with_title(WINDOW_TITLE)
.build(&event_loop)
.unwrap();
let hwnd = HWND(window.hwnd() as _);
assert!(!unsafe { UiaHasServerSideProvider(hwnd) }.as_bool());
let adapter = Adapter::new(hwnd, get_initial_state(), Box::new(NullActionHandler {}));
let subclass = WindowSubclass::new(&adapter);
assert!(unsafe { UiaHasServerSideProvider(hwnd) }.as_bool());
drop(subclass);
assert!(!unsafe { UiaHasServerSideProvider(hwnd) }.as_bool());
let subclass = WindowSubclass::new(&adapter);
assert!(unsafe { UiaHasServerSideProvider(hwnd) }.as_bool());
drop(window);
drop(subclass);
}

0 comments on commit 60c69b7

Please sign in to comment.