pub mod decode;
#[cfg(target_os = "windows")]
pub use imp::WindowsPenSource;
#[cfg(target_os = "windows")]
mod imp {
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::sync::atomic::{AtomicUsize, Ordering};
use raw_window_handle::RawWindowHandle;
use teksilo_canvas::Size;
use teksilo_core::raw_handle::ParentHandle;
use teksilo_core::trace_input;
use windows::Win32::Foundation::{HWND, LPARAM, LRESULT, POINT, WPARAM};
use windows::Win32::Graphics::Gdi::ScreenToClient;
use windows::Win32::UI::HiDpi::GetDpiForWindow;
use windows::Win32::UI::Input::Pointer::{
GetPointerPenInfo, GetPointerTouchInfo, GetPointerType, POINTER_PEN_INFO,
POINTER_TOUCH_INFO,
};
use windows::Win32::UI::Shell::{DefSubclassProc, RemoveWindowSubclass, SetWindowSubclass};
use windows::Win32::UI::WindowsAndMessaging::{
POINTER_INPUT_TYPE, WM_POINTERDOWN, WM_POINTERENTER, WM_POINTERLEAVE, WM_POINTERUP,
WM_POINTERUPDATE,
};
use super::decode;
use crate::pen::{PenCaps, PenPacket, PenSource};
static NEXT_SUBCLASS_ID: AtomicUsize = AtomicUsize::new(0xFE_112_000);
const MAX_REMEMBERED_CONTACTS: usize = 16;
#[derive(Debug, Default)]
struct PenShared {
packets: RefCell<Vec<PenPacket>>,
contacts: RefCell<HashMap<u64, Size>>,
}
#[derive(Debug)]
pub struct WindowsPenSource {
hwnd: HWND,
subclass_id: usize,
shared: Rc<PenShared>,
}
impl WindowsPenSource {
pub fn attach(parent: &ParentHandle) -> Option<Self> {
let RawWindowHandle::Win32(handle) = parent.raw_window_handle() else {
return None;
};
let hwnd = HWND(handle.hwnd.get() as *mut core::ffi::c_void);
let shared = Rc::new(PenShared::default());
let subclass_id = NEXT_SUBCLASS_ID.fetch_add(1, Ordering::Relaxed);
let installed = unsafe {
SetWindowSubclass(
hwnd,
Some(teksilo_pen_proc),
subclass_id,
Rc::as_ptr(&shared) as usize,
)
};
if !installed.as_bool() {
trace_input!(
Samples,
"pen: SetWindowSubclass returned FALSE; this window has no pen input"
);
return None;
}
Some(Self {
hwnd,
subclass_id,
shared,
})
}
}
impl Drop for WindowsPenSource {
fn drop(&mut self) {
unsafe {
let _ = RemoveWindowSubclass(self.hwnd, Some(teksilo_pen_proc), self.subclass_id);
}
}
}
impl PenSource for WindowsPenSource {
fn poll(&mut self, out: &mut Vec<PenPacket>) {
if let Ok(mut packets) = self.shared.packets.try_borrow_mut() {
out.append(&mut packets);
}
}
fn capabilities(&self) -> PenCaps {
PenCaps {
touch_contact: true,
..PenCaps::FULL_PEN
}
}
fn touch_contact(&self, os_contact_id: u64) -> Option<Size> {
self.shared
.contacts
.try_borrow()
.ok()?
.get(&os_contact_id)
.copied()
}
}
fn scale_factor(hwnd: HWND) -> f32 {
let dpi = unsafe { GetDpiForWindow(hwnd) };
if dpi == 0 { 1.0 } else { dpi as f32 / 96.0 }
}
fn client_origin(hwnd: HWND, screen: (i32, i32)) -> (i32, i32) {
let mut point = POINT {
x: screen.0,
y: screen.1,
};
let ok = unsafe { ScreenToClient(hwnd, &mut point) };
if !ok.as_bool() {
return (0, 0);
}
(screen.0 - point.x, screen.1 - point.y)
}
unsafe fn as_bytes<T>(value: &T) -> &[u8] {
unsafe {
std::slice::from_raw_parts(value as *const T as *const u8, std::mem::size_of::<T>())
}
}
fn read_pen(hwnd: HWND, pointer_id: u32, left_window: bool) -> Option<PenPacket> {
let mut info: POINTER_PEN_INFO = unsafe { std::mem::zeroed() };
unsafe { GetPointerPenInfo(pointer_id, &mut info) }.ok()?;
let raw = decode::decode_pen_info(unsafe { as_bytes(&info) })?;
let mut packet = raw.to_packet(client_origin(hwnd, raw.screen), scale_factor(hwnd));
if left_window {
packet.in_proximity = false;
packet.down = false;
}
Some(packet)
}
fn read_touch_contact(hwnd: HWND, pointer_id: u32) -> Option<Size> {
let mut info: POINTER_TOUCH_INFO = unsafe { std::mem::zeroed() };
unsafe { GetPointerTouchInfo(pointer_id, &mut info) }.ok()?;
decode::decode_touch_info(unsafe { as_bytes(&info) })?.contact_size(scale_factor(hwnd))
}
unsafe extern "system" fn teksilo_pen_proc(
hwnd: HWND,
msg: u32,
wparam: WPARAM,
lparam: LPARAM,
_uid: usize,
dw_ref_data: usize,
) -> LRESULT {
if dw_ref_data != 0 {
let shared: &PenShared = unsafe { &*(dw_ref_data as *const PenShared) };
observe(hwnd, msg, wparam, shared);
}
unsafe { DefSubclassProc(hwnd, msg, wparam, lparam) }
}
fn observe(hwnd: HWND, msg: u32, wparam: WPARAM, shared: &PenShared) {
if !matches!(
msg,
WM_POINTERUPDATE | WM_POINTERDOWN | WM_POINTERUP | WM_POINTERENTER | WM_POINTERLEAVE
) {
return;
}
let pointer_id = (wparam.0 & 0xFFFF) as u32;
let mut kind = POINTER_INPUT_TYPE::default();
if unsafe { GetPointerType(pointer_id, &mut kind) }.is_err() {
return;
}
match kind.0 as u32 {
decode::PT_PEN => {
let left = msg == WM_POINTERLEAVE;
if let Some(packet) = read_pen(hwnd, pointer_id, left)
&& let Ok(mut packets) = shared.packets.try_borrow_mut()
{
packets.push(packet);
}
}
decode::PT_TOUCH => {
let Ok(mut contacts) = shared.contacts.try_borrow_mut() else {
return;
};
if msg == WM_POINTERUP || msg == WM_POINTERLEAVE {
contacts.remove(&(pointer_id as u64));
return;
}
if let Some(size) = read_touch_contact(hwnd, pointer_id) {
if contacts.len() >= MAX_REMEMBERED_CONTACTS
&& !contacts.contains_key(&(pointer_id as u64))
{
contacts.clear();
}
contacts.insert(pointer_id as u64, size);
}
}
_ => {}
}
}
}