-
Notifications
You must be signed in to change notification settings - Fork 53
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(platforms/windows): Win32 subclassing support (#118)
Co-authored-by: Arnold Loubriat <[email protected]>
- Loading branch information
1 parent
f2333c8
commit 60c69b7
Showing
8 changed files
with
1,101 additions
and
10 deletions.
There are no files selected for viewing
Large diffs are not rendered by default.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
_ => (), | ||
} | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -349,3 +349,4 @@ impl IUIAutomationFocusChangedEventHandler_Impl for FocusEventHandler { | |
} | ||
|
||
mod simple; | ||
mod subclassed; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} |