use crcbl_core::input::{ButtonState, PointerButton, ScrollDelta, TouchPhase};
use crate::CursorIcon;
use super::ffi::{msg, value};
#[must_use]
pub const fn point(l_param: isize) -> (i32, i32) {
let x = (l_param & 0xFFFF) as u16 as i16;
let y = ((l_param >> 16) & 0xFFFF) as u16 as i16;
(x as i32, y as i32)
}
#[must_use]
pub const fn button(message: u32, w_param: usize) -> Option<(PointerButton, ButtonState)> {
let state = match message {
msg::L_BUTTON_DOWN | msg::R_BUTTON_DOWN | msg::M_BUTTON_DOWN | msg::X_BUTTON_DOWN => {
ButtonState::Pressed
}
msg::L_BUTTON_UP | msg::R_BUTTON_UP | msg::M_BUTTON_UP | msg::X_BUTTON_UP => {
ButtonState::Released
}
_ => return None,
};
let button = match message {
msg::L_BUTTON_DOWN | msg::L_BUTTON_UP => PointerButton::Left,
msg::R_BUTTON_DOWN | msg::R_BUTTON_UP => PointerButton::Right,
msg::M_BUTTON_DOWN | msg::M_BUTTON_UP => PointerButton::Middle,
_ => match ((w_param >> 16) & 0xFFFF) as u32 {
value::XBUTTON1 => PointerButton::Back,
value::XBUTTON2 => PointerButton::Forward,
_ => return None,
},
};
Some((button, state))
}
#[must_use]
pub const fn touch(message: u32, w_param: usize) -> Option<(u32, TouchPhase)> {
let pointer_id = (w_param & 0xFFFF) as u32;
let flags = ((w_param >> 16) & 0xFFFF) as u32;
let phase = match message {
msg::POINTER_DOWN => TouchPhase::Began,
msg::POINTER_UPDATE if flags & value::POINTER_MESSAGE_FLAG_IN_CONTACT != 0 => {
TouchPhase::Moved
}
msg::POINTER_UP if flags & value::POINTER_MESSAGE_FLAG_CANCELED != 0 => {
TouchPhase::Cancelled
}
msg::POINTER_UP => TouchPhase::Ended,
msg::POINTER_CAPTURE_CHANGED => TouchPhase::Cancelled,
_ => return None,
};
Some((pointer_id, phase))
}
#[must_use]
pub const fn is_primary(w_param: usize) -> bool {
((w_param >> 16) as u32) & value::POINTER_MESSAGE_FLAG_PRIMARY != 0
}
#[must_use]
pub const fn button_bit(button: PointerButton) -> u32 {
match button {
PointerButton::Left => 1 << 0,
PointerButton::Right => 1 << 1,
PointerButton::Middle => 1 << 2,
PointerButton::Back => 1 << 3,
PointerButton::Forward => 1 << 4,
PointerButton::Other(_) => 1 << 5,
}
}
#[must_use]
pub fn wheel(horizontal: bool, ticks: i16) -> ScrollDelta {
let lines = f32::from(ticks) / f32::from(value::WHEEL_DELTA);
if horizontal {
ScrollDelta::Lines { x: lines, y: 0.0 }
} else {
ScrollDelta::Lines { x: 0.0, y: lines }
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct RawMotion {
last: Option<(u16, i32, i32)>,
}
impl RawMotion {
pub fn delta(&mut self, flags: u16, x: i32, y: i32, screen: (i32, i32)) -> Option<(f64, f64)> {
if flags & value::MOUSE_MOVE_ABSOLUTE == 0 {
self.last = None;
if x == 0 && y == 0 {
return None;
}
return Some((f64::from(x), f64::from(y)));
}
let space = flags & value::MOUSE_VIRTUAL_DESKTOP;
let to_pixels = |value: i32, extent: i32| -> i32 {
(i64::from(value) * i64::from(extent) / i64::from(value::ABSOLUTE_RANGE)) as i32
};
let position = (to_pixels(x, screen.0), to_pixels(y, screen.1));
let previous = self.last.replace((space, position.0, position.1));
let (previous_space, previous_x, previous_y) = previous?;
if previous_space != space {
return None;
}
let delta = (position.0 - previous_x, position.1 - previous_y);
if delta == (0, 0) {
return None;
}
Some((f64::from(delta.0), f64::from(delta.1)))
}
#[must_use]
pub const fn is_virtual_desktop(flags: u16) -> bool {
flags & value::MOUSE_VIRTUAL_DESKTOP != 0
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Visibility {
hidden: bool,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Show {
Nothing,
Hide,
Reveal,
}
impl Visibility {
pub const fn want(&mut self, hidden: bool) -> Show {
if hidden == self.hidden {
return Show::Nothing;
}
self.hidden = hidden;
if hidden { Show::Hide } else { Show::Reveal }
}
}
#[must_use]
pub const fn cursor_id(icon: CursorIcon) -> usize {
match icon {
CursorIcon::Default => value::IDC_ARROW,
CursorIcon::Pointer => value::IDC_HAND,
CursorIcon::Text => value::IDC_IBEAM,
CursorIcon::Crosshair => value::IDC_CROSS,
CursorIcon::Wait => value::IDC_WAIT,
CursorIcon::Progress => value::IDC_APP_STARTING,
CursorIcon::NotAllowed => value::IDC_NO,
CursorIcon::Grab | CursorIcon::Grabbing | CursorIcon::Move => value::IDC_SIZE_ALL,
CursorIcon::ResizeNorthSouth | CursorIcon::ResizeRow => value::IDC_SIZE_NS,
CursorIcon::ResizeEastWest | CursorIcon::ResizeColumn => value::IDC_SIZE_WE,
CursorIcon::ResizeNorthEastSouthWest => value::IDC_SIZE_NESW,
CursorIcon::ResizeNorthWestSouthEast => value::IDC_SIZE_NWSE,
}
}
#[cfg(test)]
mod tests {
use super::*;
const fn packed(x: i32, y: i32) -> isize {
(((y as u16 as usize) << 16) | (x as u16 as usize)) as isize
}
const fn x_button(which: u32) -> usize {
((which as usize) << 16) | 0x000C
}
#[test]
fn a_negative_coordinate_stays_negative_rather_than_becoming_sixty_five_thousand() {
assert_eq!(point(packed(100, 50)), (100, 50));
assert_eq!(point(packed(-3, -1)), (-3, -1));
assert_eq!(point(packed(-1920, 40)), (-1920, 40));
assert_eq!(point(packed(0, 0)), (0, 0));
assert_eq!(point(packed(32_767, -32_768)), (32_767, -32_768));
}
#[test]
fn the_five_buttons_decode_and_the_thumb_pair_comes_from_the_high_word() {
assert_eq!(
button(msg::L_BUTTON_DOWN, 0),
Some((PointerButton::Left, ButtonState::Pressed))
);
assert_eq!(
button(msg::L_BUTTON_UP, 0),
Some((PointerButton::Left, ButtonState::Released))
);
assert_eq!(
button(msg::R_BUTTON_DOWN, 0),
Some((PointerButton::Right, ButtonState::Pressed))
);
assert_eq!(
button(msg::M_BUTTON_DOWN, 0),
Some((PointerButton::Middle, ButtonState::Pressed))
);
assert_eq!(
button(msg::X_BUTTON_DOWN, x_button(value::XBUTTON1)),
Some((PointerButton::Back, ButtonState::Pressed))
);
assert_eq!(
button(msg::X_BUTTON_UP, x_button(value::XBUTTON2)),
Some((PointerButton::Forward, ButtonState::Released))
);
assert_eq!(button(msg::X_BUTTON_DOWN, x_button(3)), None);
assert_eq!(button(msg::MOUSE_MOVE, 0), None);
assert_eq!(button(msg::MOUSE_WHEEL, 0), None);
}
#[cfg(target_os = "linux")]
#[test]
fn the_thumb_buttons_agree_with_the_linux_backends() {
assert_eq!(
crate::linux::keymap::pointer_button(0x113),
PointerButton::Back,
"BTN_SIDE and XBUTTON1"
);
assert_eq!(
crate::linux::keymap::pointer_button(0x114),
PointerButton::Forward,
"BTN_EXTRA and XBUTTON2"
);
assert_eq!(
button(msg::X_BUTTON_DOWN, x_button(value::XBUTTON1)).map(|(button, _)| button),
Some(crate::linux::keymap::pointer_button(0x113))
);
}
#[test]
fn the_button_mask_gives_every_button_its_own_bit() {
let bits = [
button_bit(PointerButton::Left),
button_bit(PointerButton::Right),
button_bit(PointerButton::Middle),
button_bit(PointerButton::Back),
button_bit(PointerButton::Forward),
];
let mut unique = bits.to_vec();
unique.sort_unstable();
unique.dedup();
assert_eq!(unique.len(), bits.len(), "{bits:?}");
assert_eq!(bits.iter().fold(0, |all, bit| all | bit).count_ones(), 5);
}
#[test]
fn a_pointer_message_names_its_contact_and_a_cancelled_release_is_not_an_end() {
const IN_CONTACT: usize = (value::POINTER_MESSAGE_FLAG_IN_CONTACT as usize) << 16;
const CANCELED: usize = (value::POINTER_MESSAGE_FLAG_CANCELED as usize) << 16;
assert_eq!(
touch(msg::POINTER_DOWN, 7 | IN_CONTACT),
Some((7, TouchPhase::Began))
);
assert_eq!(
touch(msg::POINTER_UPDATE, 7 | IN_CONTACT),
Some((7, TouchPhase::Moved))
);
assert_eq!(touch(msg::POINTER_UP, 7), Some((7, TouchPhase::Ended)));
assert_eq!(
touch(msg::POINTER_UP, 7 | CANCELED),
Some((7, TouchPhase::Cancelled))
);
assert_eq!(
touch(msg::POINTER_CAPTURE_CHANGED, 7),
Some((7, TouchPhase::Cancelled))
);
assert_eq!(touch(msg::POINTER_UPDATE, 7), None);
assert_eq!(touch(msg::MOUSE_MOVE, 7 | IN_CONTACT), None);
}
#[test]
fn primary_is_read_from_the_flag_word_not_the_id() {
const PRIMARY: usize = (value::POINTER_MESSAGE_FLAG_PRIMARY as usize) << 16;
assert!(is_primary(3 | PRIMARY));
assert!(!is_primary(3));
assert!(!is_primary(value::POINTER_MESSAGE_FLAG_PRIMARY as usize));
}
#[test]
fn one_detent_is_one_line_and_a_high_resolution_wheel_keeps_its_fraction() {
assert_eq!(wheel(false, 120), ScrollDelta::Lines { x: 0.0, y: 1.0 });
assert_eq!(wheel(false, -120), ScrollDelta::Lines { x: 0.0, y: -1.0 });
assert_eq!(wheel(false, 240), ScrollDelta::Lines { x: 0.0, y: 2.0 });
assert_eq!(wheel(true, 120), ScrollDelta::Lines { x: 1.0, y: 0.0 });
assert_eq!(wheel(true, -120), ScrollDelta::Lines { x: -1.0, y: 0.0 });
let ScrollDelta::Lines { y, .. } = wheel(false, 40) else {
panic!("a wheel reports detents, not pixels");
};
assert!((y - 1.0 / 3.0).abs() < 1e-6, "{y}");
assert!(!wheel(false, 40).is_zero());
assert!(wheel(false, 0).is_zero());
}
#[test]
fn a_relative_report_is_the_delta_and_a_zero_one_is_not_an_event() {
let mut motion = RawMotion::default();
let screen = (1920, 1080);
assert_eq!(
motion.delta(0, 7, -3, screen),
Some((7.0, -3.0)),
"relative motion passes through unscaled"
);
assert_eq!(motion.delta(0, 0, 0, screen), None);
}
#[test]
fn an_absolute_report_is_differenced_instead_of_being_read_as_a_delta() {
let mut motion = RawMotion::default();
let screen = (1920, 1080);
let half = value::ABSOLUTE_RANGE / 2;
assert_eq!(
motion.delta(value::MOUSE_MOVE_ABSOLUTE, half, half, screen),
None,
"the first absolute sample has nothing to subtract from"
);
let tenth = value::ABSOLUTE_RANGE / 10;
let moved = motion
.delta(value::MOUSE_MOVE_ABSOLUTE, half + tenth, half, screen)
.expect("a second sample is a movement");
assert!((moved.0 - 192.0).abs() <= 1.0, "{moved:?}");
assert!(moved.1.abs() < 1.0, "{moved:?}");
assert_eq!(
motion.delta(value::MOUSE_MOVE_ABSOLUTE, half + tenth, half, screen),
None
);
let mut wide = RawMotion::default();
assert_eq!(
wide.delta(value::MOUSE_MOVE_ABSOLUTE, half, half, (3840, 2160)),
None
);
let moved = wide
.delta(value::MOUSE_MOVE_ABSOLUTE, half + tenth, half, (3840, 2160))
.expect("a movement");
assert!((moved.0 - 384.0).abs() <= 1.0, "{moved:?}");
}
#[test]
fn switching_between_absolute_and_relative_never_produces_a_jump() {
let mut motion = RawMotion::default();
let screen = (1920, 1080);
assert_eq!(motion.delta(value::MOUSE_MOVE_ABSOLUTE, 0, 0, screen), None);
assert_eq!(motion.delta(0, 5, 5, screen), Some((5.0, 5.0)));
assert_eq!(
motion.delta(value::MOUSE_MOVE_ABSOLUTE, 60_000, 60_000, screen),
None,
"the relative report cleared the absolute history"
);
let virtual_desktop = value::MOUSE_MOVE_ABSOLUTE | value::MOUSE_VIRTUAL_DESKTOP;
assert_eq!(motion.delta(virtual_desktop, 60_000, 60_000, screen), None);
assert!(RawMotion::is_virtual_desktop(virtual_desktop));
assert!(!RawMotion::is_virtual_desktop(value::MOUSE_MOVE_ABSOLUTE));
assert!(!RawMotion::is_virtual_desktop(0));
}
#[test]
fn hiding_the_cursor_twice_owes_one_show_and_not_two() {
let mut visibility = Visibility::default();
assert_eq!(visibility.want(true), Show::Hide);
assert_eq!(visibility.want(true), Show::Nothing, "already hidden");
assert_eq!(visibility.want(false), Show::Reveal);
assert_eq!(visibility.want(false), Show::Nothing);
assert_eq!(visibility.want(true), Show::Hide);
assert_eq!(Visibility::default(), Visibility { hidden: false });
}
#[test]
fn every_cursor_shape_maps_to_a_stock_windows_cursor() {
let shapes = [
CursorIcon::Default,
CursorIcon::Pointer,
CursorIcon::Text,
CursorIcon::Crosshair,
CursorIcon::Wait,
CursorIcon::Progress,
CursorIcon::NotAllowed,
CursorIcon::Grab,
CursorIcon::Grabbing,
CursorIcon::Move,
CursorIcon::ResizeNorthSouth,
CursorIcon::ResizeEastWest,
CursorIcon::ResizeNorthEastSouthWest,
CursorIcon::ResizeNorthWestSouthEast,
CursorIcon::ResizeColumn,
CursorIcon::ResizeRow,
];
assert_eq!(
shapes
.iter()
.filter(|shape| cursor_id(**shape) == value::IDC_ARROW)
.count(),
1,
"only the default is an arrow"
);
for shape in shapes {
let id = cursor_id(shape);
assert!(
(32_512..=32_651).contains(&id),
"{shape:?} is not an IDC_* id: {id}"
);
}
assert_eq!(cursor_id(CursorIcon::Grab), value::IDC_SIZE_ALL);
assert_eq!(cursor_id(CursorIcon::ResizeColumn), value::IDC_SIZE_WE);
assert_eq!(cursor_id(CursorIcon::ResizeRow), value::IDC_SIZE_NS);
}
}