use core::ptr;
use std::sync::OnceLock;
use crcbl_core::KeyCode;
use crcbl_core::input::{ContactId, DeviceId, Keysym, Modifiers, Scancode, TouchPhase};
use crate::{
CursorIcon, PhysicalPoint, PhysicalSize, PointerMode, ShellError, ShellEvent, WindowId,
};
use super::devices::{self, POINTER_DEVICE, TOUCH_DEVICE};
use super::events::RawEvent;
use super::ffi::{
self, Handle, Lparam, Point, RawInput, RawInputDevice, RawKeyboard, RawMouse, Rect, TrackMouse,
value,
};
use super::keys;
use super::pointer::{self, RawMotion, Show};
use super::proc::{Shape, Shared};
use super::shell::Win32Shell;
pub(super) enum RawReport {
Mouse { device: Handle, mouse: RawMouse },
Keyboard {
device: Handle,
keyboard: RawKeyboard,
},
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(super) struct Contact {
window: WindowId,
pointer_id: u32,
position: PhysicalPoint,
}
const LOCK_RECENTRE_FRACTION: f64 = 0.25;
static RAW_INPUT: OnceLock<bool> = OnceLock::new();
fn unshifted(character: char) -> char {
let mut lowered = character.to_lowercase();
match (lowered.next(), lowered.next()) {
(Some(single), None) => single,
_ => character,
}
}
fn register(usage: u16, what: &str) -> bool {
let device = RawInputDevice {
us_usage_page: value::HID_USAGE_PAGE_GENERIC,
us_usage: usage,
dw_flags: value::RIDEV_DEV_NOTIFY,
hwnd_target: ptr::null_mut(),
};
let ok = unsafe {
ffi::RegisterRawInputDevices(&raw const device, 1, size_of::<RawInputDevice>() as u32)
};
if ok == 0 {
let error = unsafe { ffi::GetLastError() };
crcbl_core::log::warn!(
"RegisterRawInputDevices for the {what} failed with Win32 error {error}"
);
return false;
}
crcbl_core::log::debug!("raw {what} input is registered for this process");
true
}
pub(super) fn register_raw_input() -> bool {
*RAW_INPUT.get_or_init(|| {
let mouse = register(value::HID_USAGE_GENERIC_MOUSE, "mouse");
if !mouse {
crcbl_core::log::warn!(
"relative pointer motion will not be reported and RAW_POINTER_MOTION is clear"
);
}
register(value::HID_USAGE_GENERIC_KEYBOARD, "keyboard");
mouse
})
}
pub(super) unsafe fn read_raw(l_param: Lparam) -> Option<RawReport> {
let mut report = RawInput::default();
let mut size = size_of::<RawInput>() as u32;
let written = unsafe {
ffi::GetRawInputData(
l_param as Handle,
value::RID_INPUT,
(&raw mut report).cast(),
&raw mut size,
size_of::<ffi::RawInputHeader>() as u32,
)
};
if written == u32::MAX || written == 0 {
return None;
}
let device = report.header.h_device;
match report.header.dw_type {
value::RIM_TYPE_MOUSE => Some(RawReport::Mouse {
device,
mouse: unsafe { report.data.mouse },
}),
value::RIM_TYPE_KEYBOARD => Some(RawReport::Keyboard {
device,
keyboard: unsafe { report.data.keyboard },
}),
_ => None,
}
}
fn with_input_context<T>(hwnd: Handle, use_it: impl FnOnce(Handle) -> T) -> Option<T> {
let context = unsafe { ffi::ImmGetContext(hwnd) };
if context.is_null() {
return None;
}
let answer = use_it(context);
unsafe { ffi::ImmReleaseContext(hwnd, context) };
Some(answer)
}
pub(super) fn composition(hwnd: Handle) -> (Vec<u16>, Option<usize>) {
with_input_context(hwnd, |context| {
let answer = unsafe {
ffi::ImmGetCompositionStringW(context, value::GCS_COMP_STR, ptr::null_mut(), 0)
};
let Ok(bytes) = u32::try_from(answer) else {
crcbl_core::log::debug!("the composition string's size was refused: {answer}");
return (Vec::new(), None);
};
let mut units = vec![0u16; (bytes as usize).div_ceil(size_of::<u16>())];
let copied = unsafe {
ffi::ImmGetCompositionStringW(
context,
value::GCS_COMP_STR,
units.as_mut_ptr().cast(),
bytes,
)
};
let Ok(copied) = usize::try_from(copied) else {
crcbl_core::log::debug!("the composition string was refused: {copied}");
return (Vec::new(), None);
};
units.truncate(copied / size_of::<u16>());
let cursor = unsafe {
ffi::ImmGetCompositionStringW(context, value::GCS_CURSOR_POS, ptr::null_mut(), 0)
};
(units, usize::try_from(cursor).ok())
})
.unwrap_or_default()
}
pub(super) fn enable_ime(hwnd: Handle, enabled: bool) -> Result<(), ShellError> {
let flags = if enabled { value::IACE_DEFAULT } else { 0 };
if unsafe { ffi::ImmAssociateContextEx(hwnd, ptr::null_mut(), flags) } == 0 {
return Err(ShellError::Backend(format!(
"ImmAssociateContextEx refused to turn the input method {}",
if enabled { "on" } else { "off" }
)));
}
Ok(())
}
pub(super) fn place_ime(hwnd: Handle, area: Option<Rect>) {
with_input_context(hwnd, |context| {
let Some(area) = area else {
let form = ffi::CompositionForm {
dw_style: value::CFS_DEFAULT,
..ffi::CompositionForm::default()
};
unsafe { ffi::ImmSetCompositionWindow(context, &raw const form) };
return;
};
let composition = ffi::CompositionForm {
dw_style: value::CFS_POINT,
pt_current_pos: Point {
x: area.left,
y: area.top,
},
rc_area: area,
};
let candidates = ffi::CandidateForm {
dw_index: 0,
dw_style: value::CFS_EXCLUDE,
pt_current_pos: Point {
x: area.left,
y: area.bottom,
},
rc_area: area,
};
unsafe {
ffi::ImmSetCompositionWindow(context, &raw const composition);
ffi::ImmSetCandidateWindow(context, &raw const candidates);
}
});
}
fn device_name(device: Handle) -> Option<String> {
let mut length = 0u32;
unsafe {
ffi::GetRawInputDeviceInfoW(
device,
value::RIDI_DEVICE_NAME,
ptr::null_mut(),
&raw mut length,
)
};
if length == 0 {
return None;
}
let mut name = vec![0u16; length as usize];
let written = unsafe {
ffi::GetRawInputDeviceInfoW(
device,
value::RIDI_DEVICE_NAME,
name.as_mut_ptr().cast(),
&raw mut length,
)
};
if written == u32::MAX || written == 0 {
return None;
}
name.truncate(written as usize);
let name = String::from_utf16_lossy(&name);
Some(name.trim_end_matches('\0').to_owned())
}
pub(super) fn track_leave(hwnd: Handle) {
let mut track = TrackMouse {
cb_size: size_of::<TrackMouse>() as u32,
dw_flags: value::TME_LEAVE,
hwnd_track: hwnd,
dw_hover_time: 0,
};
unsafe { ffi::TrackMouseEvent(&raw mut track) };
}
pub(super) fn client_screen_rect(hwnd: Handle) -> Option<Rect> {
let mut rect = Rect::default();
if unsafe { ffi::GetClientRect(hwnd, &raw mut rect) } == 0 {
return None;
}
let mut top_left = Point {
x: rect.left,
y: rect.top,
};
let mut bottom_right = Point {
x: rect.right,
y: rect.bottom,
};
unsafe {
if ffi::ClientToScreen(hwnd, &raw mut top_left) == 0
|| ffi::ClientToScreen(hwnd, &raw mut bottom_right) == 0
{
return None;
}
}
Some(Rect {
left: top_left.x,
top: top_left.y,
right: bottom_right.x,
bottom: bottom_right.y,
})
}
pub(super) fn apply_clip(hwnd: Handle) {
let Some(rect) = client_screen_rect(hwnd) else {
return;
};
unsafe { ffi::ClipCursor(&raw const rect) };
}
pub(super) fn release_clip() {
unsafe { ffi::ClipCursor(ptr::null()) };
}
pub(super) fn reclip(shared: &Shared, hwnd: Handle, window: isize) {
if shared.clipped() != window {
return;
}
apply_clip(hwnd);
}
fn absolute_screen(flags: u16) -> (i32, i32) {
let (width_index, height_index) = if RawMotion::is_virtual_desktop(flags) {
(value::SM_CX_VIRTUAL_SCREEN, value::SM_CY_VIRTUAL_SCREEN)
} else {
(value::SM_CX_SCREEN, value::SM_CY_SCREEN)
};
let (width, height) = unsafe {
(
ffi::GetSystemMetrics(width_index),
ffi::GetSystemMetrics(height_index),
)
};
(width.max(1), height.max(1))
}
impl Win32Shell {
pub(super) fn modifiers_now() -> Modifiers {
let mut state = [0u8; 256];
if unsafe { ffi::GetKeyboardState(state.as_mut_ptr()) } == 0 {
return Modifiers::empty();
}
keys::modifiers(&state)
}
fn keysym_of(key_code: Option<KeyCode>, virtual_key: u16) -> Keysym {
if let Some(named) = key_code.and_then(keys::named_keysym) {
return named;
}
let mapped =
unsafe { ffi::MapVirtualKeyW(u32::from(virtual_key), value::MAPVK_VK_TO_CHAR) };
if mapped == 0 {
return Keysym::NONE;
}
let Some(character) = char::from_u32(mapped & 0x7FFF_FFFF) else {
return Keysym::NONE;
};
Keysym::from_char(unshifted(character))
}
pub(super) fn translate_input(&mut self, event: RawEvent, window: Option<WindowId>) {
let Some(window) = window else { return };
match event {
RawEvent::Key {
scancode,
virtual_key,
state,
repeat,
millis,
..
} => {
let time = self.event_time(millis);
let key_code = keys::key_code(scancode);
let device = self.attribution.key(scancode, state, millis);
self.queue_event(ShellEvent::Key {
window,
device,
time,
scancode: Scancode(scancode),
key_code,
keysym: Self::keysym_of(key_code, virtual_key),
state,
repeat,
modifiers: Self::modifiers_now(),
});
}
RawEvent::Char { unit, millis, .. } => {
let time = self.event_time(millis);
let Some(character) = self.text.push(unit) else {
return;
};
if !keys::is_text(character) {
return;
}
self.queue_event(ShellEvent::TextCommit {
window,
time,
text: character.to_string(),
});
}
RawEvent::PointerMotion { x, y, millis, .. } => {
let time = self.event_time(millis);
if self.pointer_mode_of(window) == PointerMode::Locked {
self.recentre_if_near_edge(window, x, y);
return;
}
self.queue_event(ShellEvent::PointerMotion {
window,
device: self.attribution.pointer(),
time,
abs: Some(PhysicalPoint::new(f64::from(x), f64::from(y))),
raw_delta: None,
});
}
RawEvent::PointerFocus {
entered,
x,
y,
millis,
..
} => {
let time = self.event_time(millis);
self.queue_event(ShellEvent::PointerFocus {
window,
device: self.attribution.pointer(),
time,
entered,
position: entered.then(|| PhysicalPoint::new(f64::from(x), f64::from(y))),
});
}
RawEvent::Button {
button,
state,
x,
y,
millis,
..
} => {
let time = self.event_time(millis);
let device = self.attribution.button(button, state, millis);
self.queue_event(ShellEvent::Button {
window,
device,
time,
button,
state,
position: self.position_or_none(window, x, y),
modifiers: Self::modifiers_now(),
});
}
RawEvent::Wheel {
horizontal,
ticks,
x,
y,
millis,
..
} => {
let time = self.event_time(millis);
let device = self.attribution.wheel(horizontal, millis);
self.queue_event(ShellEvent::Wheel {
window,
device,
time,
delta: pointer::wheel(horizontal, ticks),
position: self.position_or_none(window, x, y),
modifiers: Self::modifiers_now(),
});
}
RawEvent::Touch {
pointer_id,
phase,
position,
millis,
..
} => self.translate_touch(window, pointer_id, phase, position, millis),
RawEvent::RawKey {
device,
make_code,
flags,
millis,
..
} => {
let device = self.device_id(device);
if let Some(scancode) = devices::raw_scancode(make_code, flags) {
let pressed = flags & value::RI_KEY_BREAK == 0;
self.attribution.raw_key(scancode, pressed, device, millis);
}
}
RawEvent::RawMotion {
device,
flags,
buttons,
x,
y,
millis,
..
} => {
let time = self.event_time(millis);
let device = self.device_id(device);
self.attribution.raw_mouse(buttons, device, millis);
let screen = absolute_screen(flags);
let Some(delta) = self.raw_motion.delta(flags, x, y, screen) else {
return;
};
let locked = self.pointer_mode_of(window) == PointerMode::Locked;
self.queue_event(ShellEvent::PointerMotion {
window,
device: device.unwrap_or(POINTER_DEVICE),
time,
abs: if locked {
None
} else {
self.pointer_position(window)
},
raw_delta: Some(delta),
});
}
RawEvent::Resized { .. }
| RawEvent::Minimized { .. }
| RawEvent::DpiChanged { .. }
| RawEvent::Focus { .. }
| RawEvent::CloseRequested { .. }
| RawEvent::Destroyed { .. }
| RawEvent::FilesDropped { .. }
| RawEvent::MonitorsChanged
| RawEvent::Preedit { .. }
| RawEvent::AppBar { .. }
| RawEvent::DeviceRemoved { .. } => {}
#[cfg(feature = "accesskit")]
RawEvent::AccessibilityActivated { .. } => {}
}
}
fn device_id(&mut self, handle: isize) -> Option<DeviceId> {
self.devices
.resolve(handle, || device_name(handle as Handle))
}
fn translate_touch(
&mut self,
window: WindowId,
pointer_id: u32,
phase: TouchPhase,
position: Option<(i32, i32)>,
millis: u32,
) {
let time = self.event_time(millis);
let tracked = self
.contacts
.iter()
.position(|contact| contact.window == window && contact.pointer_id == pointer_id);
let position = match (position, tracked) {
(Some((x, y)), _) => PhysicalPoint::new(f64::from(x), f64::from(y)),
(None, Some(index)) => self.contacts[index].position,
(None, None) => return,
};
let touch = |phase, position| ShellEvent::Touch {
window,
device: TOUCH_DEVICE,
time,
contact: ContactId(pointer_id),
phase,
position,
};
match (phase, tracked) {
(TouchPhase::Began, Some(index)) => {
let stale = self.contacts[index].position;
self.queue_event(touch(TouchPhase::Cancelled, stale));
self.contacts[index].position = position;
}
(TouchPhase::Began, None) => self.contacts.push(Contact {
window,
pointer_id,
position,
}),
(_, None) => return,
(TouchPhase::Moved, Some(index)) => self.contacts[index].position = position,
(TouchPhase::Ended | TouchPhase::Cancelled, Some(index)) => {
self.contacts.swap_remove(index);
}
}
self.queue_event(touch(phase, position));
}
pub(super) fn forget_input(&mut self, window: WindowId) {
self.contacts.retain(|contact| contact.window != window);
self.composing.forget(window);
}
fn position_or_none(&self, window: WindowId, x: i32, y: i32) -> Option<PhysicalPoint> {
if self.pointer_mode_of(window) == PointerMode::Locked {
return None;
}
Some(PhysicalPoint::new(f64::from(x), f64::from(y)))
}
fn pointer_position(&self, window: WindowId) -> Option<PhysicalPoint> {
let hwnd = self.window(window).ok()?.raw();
let mut point = Point::default();
if unsafe { ffi::GetCursorPos(&raw mut point) } == 0 {
return None;
}
if unsafe { ffi::ScreenToClient(hwnd, &raw mut point) } == 0 {
return None;
}
Some(PhysicalPoint::new(f64::from(point.x), f64::from(point.y)))
}
fn pointer_mode_of(&self, window: WindowId) -> PointerMode {
self.window(window)
.map_or(PointerMode::Free, |state| state.pointer_mode)
}
fn recentre_if_near_edge(&mut self, window: WindowId, x: i32, y: i32) {
let Some(size) = self.client_size(window) else {
return;
};
if size.is_empty() {
return;
}
let centre_x = i32::try_from(size.width / 2).unwrap_or(0);
let centre_y = i32::try_from(size.height / 2).unwrap_or(0);
let margin_x = (f64::from(size.width) * LOCK_RECENTRE_FRACTION) as i32;
let margin_y = (f64::from(size.height) * LOCK_RECENTRE_FRACTION) as i32;
if (x - centre_x).abs() < margin_x && (y - centre_y).abs() < margin_y {
return;
}
if let Err(why) = self.warp_to_client(window, centre_x, centre_y) {
crcbl_core::log::warn!("the locked pointer was not recentred: {why}");
}
}
fn client_size(&self, window: WindowId) -> Option<PhysicalSize> {
let hwnd = self.window(window).ok()?.raw();
Some(Self::client_size_of(hwnd))
}
pub(super) fn warp_to_client(
&self,
window: WindowId,
x: i32,
y: i32,
) -> Result<(), ShellError> {
let hwnd = self.window(window)?.raw();
let mut point = Point { x, y };
if unsafe { ffi::ClientToScreen(hwnd, &raw mut point) } == 0 {
return Err(ShellError::Backend(format!(
"ClientToScreen refused the conversion of ({x}, {y}) for window {window:?}"
)));
}
if unsafe { ffi::SetCursorPos(point.x, point.y) } == 0 {
return Err(ShellError::Backend(format!(
"SetCursorPos refused ({}, {}) — the usual cause is that this \
process is not in the foreground, which Windows requires of a \
caller that moves the cursor",
point.x, point.y
)));
}
Ok(())
}
pub(super) fn refresh_clip(&mut self) {
let wanted = self
.windows_iter()
.find(|(_, state)| state.pointer_mode.is_captured())
.map(|(_, state)| (state.raw(), state.key, state.focused));
match wanted {
Some((hwnd, key, focused)) => {
self.shared().set_clipped(key);
if focused {
apply_clip(hwnd);
} else {
release_clip();
}
}
None => {
self.shared().set_clipped(0);
release_clip();
}
}
}
pub(super) fn refresh_cursor_visibility(&mut self) {
let hidden = self.windows_iter().any(|(_, state)| {
state.focused && (state.pointer_mode == PointerMode::Locked || state.cursor_hidden())
});
Self::show_cursor(self.visibility.want(hidden));
}
pub(super) fn show_cursor(show: Show) {
let argument = match show {
Show::Nothing => return,
Show::Hide => 0,
Show::Reveal => 1,
};
unsafe { ffi::ShowCursor(argument) };
}
pub(super) fn apply_cursor_shape(
&mut self,
window: WindowId,
icon: CursorIcon,
) -> Result<(), ShellError> {
let key = self.window(window)?.key;
let cursor = unsafe { ffi::LoadCursorW(ptr::null_mut(), pointer::cursor_id(icon)) };
if cursor.is_null() {
crcbl_core::log::warn!(
"LoadCursorW returned null for {icon:?}; the cursor shape is unchanged"
);
return Ok(());
}
self.shared().set_shape(Shape { hwnd: key, cursor });
Ok(())
}
}