use std::cell::Cell;
use std::io;
use std::ptr::null_mut;
use std::rc::Rc;
use thiserror::Error;
use windows_sys::Win32::Foundation::{HWND, LPARAM, LRESULT, WPARAM};
use windows_sys::Win32::System::Threading::GetCurrentThreadId;
use windows_sys::Win32::UI::Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass};
use windows_sys::Win32::UI::WindowsAndMessaging::{
GetWindowThreadProcessId, HTTRANSPARENT, MA_NOACTIVATE, WM_MOUSEACTIVATE, WM_NCDESTROY,
WM_NCHITTEST,
};
use winit::raw_window_handle::{HasWindowHandle, RawWindowHandle};
use winit::window::{Window, WindowId};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct NativeWindowPolicy {
pub accepts_pointer_input: bool,
pub no_focus_on_click: bool,
}
impl Default for NativeWindowPolicy {
fn default() -> Self {
Self {
accepts_pointer_input: true,
no_focus_on_click: false,
}
}
}
#[derive(Clone, Debug, Eq, Error, PartialEq)]
#[non_exhaustive]
pub enum WindowPolicyError {
#[error("the Winit window handle is unavailable: {message}")]
WindowHandleUnavailable { message: String },
#[error("Winit returned a non-Win32 window handle on Windows")]
UnexpectedHandleKind,
#[error("the native window owner thread is unavailable")]
WindowOwnerUnavailable,
#[error(
"the native window belongs to thread {owner_thread_id}, but the current thread is {current_thread_id}"
)]
WrongWindowThread {
owner_thread_id: u32,
current_thread_id: u32,
},
#[error("SetWindowSubclass failed: {message}")]
InstallFailed { message: String },
#[error("the native policy hook is no longer installed on its exact window")]
HookDetached,
#[error("the native window has already been destroyed")]
WindowDestroyed,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
enum LeasePhase {
Installed,
Abandoned,
Destroying,
Detached,
Destroyed,
}
struct LeaseState {
hwnd: HWND,
window_id: WindowId,
subclass_id: usize,
owner_thread_id: u32,
phase: Cell<LeasePhase>,
callback_ref_owned: Cell<bool>,
accepts_pointer_input: Cell<bool>,
no_focus_on_click: Cell<bool>,
}
pub struct WindowPolicyLease {
state: Rc<LeaseState>,
}
impl WindowPolicyLease {
pub fn install(window: &Window, policy: NativeWindowPolicy) -> Result<Self, WindowPolicyError> {
let (hwnd, window_id) = window_handle(window)?;
let owner_thread_id = window_owner_thread(hwnd)?;
validate_owner_thread(owner_thread_id, current_thread_id())?;
let state = Rc::new(LeaseState {
hwnd,
window_id,
subclass_id: next_subclass_id(),
owner_thread_id,
phase: Cell::new(LeasePhase::Installed),
callback_ref_owned: Cell::new(true),
accepts_pointer_input: Cell::new(policy.accepts_pointer_input),
no_focus_on_click: Cell::new(policy.no_focus_on_click),
});
let callback_ref = Rc::into_raw(Rc::clone(&state)) as usize;
let installed = unsafe {
SetWindowSubclass(
hwnd,
Some(window_policy_subclass),
state.subclass_id,
callback_ref,
)
};
if installed == 0 {
unsafe { drop(Rc::from_raw(callback_ref as *const LeaseState)) };
state.callback_ref_owned.set(false);
return Err(WindowPolicyError::InstallFailed {
message: io::Error::last_os_error().to_string(),
});
}
Ok(Self { state })
}
pub fn update(&mut self, policy: NativeWindowPolicy) -> Result<(), WindowPolicyError> {
match self.state.phase.get() {
LeasePhase::Destroyed | LeasePhase::Destroying => {
return Err(WindowPolicyError::WindowDestroyed);
}
LeasePhase::Detached | LeasePhase::Abandoned => {
return Err(WindowPolicyError::HookDetached);
}
LeasePhase::Installed => {}
}
validate_state_thread(&self.state)?;
self.state
.accepts_pointer_input
.set(policy.accepts_pointer_input);
self.state.no_focus_on_click.set(policy.no_focus_on_click);
Ok(())
}
pub fn matches_window(&self, window: &Window) -> bool {
self.state.phase.get() == LeasePhase::Installed
&& current_thread_id() == self.state.owner_thread_id
&& window.id() == self.state.window_id
&& window_handle(window).is_ok_and(|(hwnd, _)| hwnd == self.state.hwnd)
&& window_owner_thread(self.state.hwnd)
.is_ok_and(|owner| owner == self.state.owner_thread_id)
}
}
impl Drop for WindowPolicyLease {
fn drop(&mut self) {
if self.state.phase.get() != LeasePhase::Installed {
return;
}
if validate_state_thread(&self.state).is_err() {
self.state.phase.set(LeasePhase::Abandoned);
return;
}
let removed = unsafe {
RemoveWindowSubclass(
self.state.hwnd,
Some(window_policy_subclass),
self.state.subclass_id,
)
} != 0;
if removed {
self.state.phase.set(LeasePhase::Detached);
release_callback_ref(&self.state);
} else {
self.state.phase.set(LeasePhase::Abandoned);
}
}
}
fn release_callback_ref(state: &LeaseState) {
if state.callback_ref_owned.replace(false) {
let raw = state as *const LeaseState;
unsafe { drop(Rc::from_raw(raw)) };
}
}
fn current_thread_id() -> u32 {
unsafe { GetCurrentThreadId() }
}
fn window_owner_thread(hwnd: HWND) -> Result<u32, WindowPolicyError> {
let owner = unsafe { GetWindowThreadProcessId(hwnd, null_mut()) };
(owner != 0)
.then_some(owner)
.ok_or(WindowPolicyError::WindowOwnerUnavailable)
}
fn validate_owner_thread(
owner_thread_id: u32,
current_thread_id: u32,
) -> Result<(), WindowPolicyError> {
(owner_thread_id == current_thread_id).then_some(()).ok_or(
WindowPolicyError::WrongWindowThread {
owner_thread_id,
current_thread_id,
},
)
}
fn validate_state_thread(state: &LeaseState) -> Result<(), WindowPolicyError> {
let current = current_thread_id();
validate_owner_thread(state.owner_thread_id, current)?;
let owner = window_owner_thread(state.hwnd)?;
if owner != state.owner_thread_id {
return Err(WindowPolicyError::WrongWindowThread {
owner_thread_id: owner,
current_thread_id: current,
});
}
Ok(())
}
fn window_handle(window: &Window) -> Result<(HWND, WindowId), WindowPolicyError> {
let handle = window
.window_handle()
.map_err(|error| WindowPolicyError::WindowHandleUnavailable {
message: error.to_string(),
})?
.as_raw();
let RawWindowHandle::Win32(handle) = handle else {
return Err(WindowPolicyError::UnexpectedHandleKind);
};
Ok((handle.hwnd.get() as HWND, window.id()))
}
fn next_subclass_id() -> usize {
static NEXT_ID: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(1);
loop {
let id = NEXT_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
if id != 0 {
return id;
}
}
}
unsafe extern "system" fn window_policy_subclass(
hwnd: HWND,
message: u32,
wparam: WPARAM,
lparam: LPARAM,
subclass_id: usize,
reference_data: usize,
) -> LRESULT {
let raw = reference_data as *const LeaseState;
if raw.is_null() {
return unsafe { DefSubclassProc(hwnd, message, wparam, lparam) };
}
let state = unsafe { &*raw };
if state.hwnd != hwnd || state.subclass_id != subclass_id {
return unsafe { DefSubclassProc(hwnd, message, wparam, lparam) };
}
unsafe { Rc::increment_strong_count(raw) };
let callback_state = unsafe { Rc::from_raw(raw) };
if message == WM_NCDESTROY {
let terminal = matches!(
callback_state.phase.replace(LeasePhase::Destroying),
LeasePhase::Installed | LeasePhase::Abandoned
);
let _ = unsafe { RemoveWindowSubclass(hwnd, Some(window_policy_subclass), subclass_id) };
let result = unsafe { DefSubclassProc(hwnd, message, wparam, lparam) };
if terminal {
callback_state.phase.set(LeasePhase::Destroyed);
release_callback_ref(&callback_state);
}
return result;
}
if matches!(
callback_state.phase.get(),
LeasePhase::Abandoned
| LeasePhase::Detached
| LeasePhase::Destroyed
| LeasePhase::Destroying
) {
return unsafe { DefSubclassProc(hwnd, message, wparam, lparam) };
}
if message == WM_NCHITTEST && !callback_state.accepts_pointer_input.get() {
return HTTRANSPARENT as LRESULT;
}
if message == WM_MOUSEACTIVATE && callback_state.no_focus_on_click.get() {
return MA_NOACTIVATE as LRESULT;
}
unsafe { DefSubclassProc(hwnd, message, wparam, lparam) }
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn policy_defaults_to_interactive_and_focusable() {
assert_eq!(
NativeWindowPolicy::default(),
NativeWindowPolicy {
accepts_pointer_input: true,
no_focus_on_click: false,
}
);
}
#[test]
fn wrong_thread_is_rejected_by_the_pure_seam() {
assert_eq!(
validate_owner_thread(7, 8),
Err(WindowPolicyError::WrongWindowThread {
owner_thread_id: 7,
current_thread_id: 8,
})
);
assert!(validate_owner_thread(7, 7).is_ok());
}
#[test]
fn callback_state_changes_are_visible_to_the_owner_thread() {
let state = LeaseState {
hwnd: std::ptr::null_mut(),
window_id: WindowId::dummy(),
subclass_id: 1,
owner_thread_id: 1,
phase: Cell::new(LeasePhase::Installed),
callback_ref_owned: Cell::new(false),
accepts_pointer_input: Cell::new(true),
no_focus_on_click: Cell::new(false),
};
state.accepts_pointer_input.set(false);
state.no_focus_on_click.set(true);
assert!(!state.accepts_pointer_input.get());
assert!(state.no_focus_on_click.get());
}
#[test]
fn terminal_callback_ref_release_is_one_shot() {
let state = LeaseState {
hwnd: std::ptr::null_mut(),
window_id: WindowId::dummy(),
subclass_id: 1,
owner_thread_id: 1,
phase: Cell::new(LeasePhase::Destroyed),
callback_ref_owned: Cell::new(false),
accepts_pointer_input: Cell::new(true),
no_focus_on_click: Cell::new(false),
};
release_callback_ref(&state);
assert!(!state.callback_ref_owned.get());
}
}