use crate::{ButtonMask, Key, Modifiers, MouseButton, NamedKey, Point, ScrollDelta, UiEvent};
use super::msg::WHEEL_DELTA;
pub(crate) const WM_LBUTTONDOWN: u32 = 0x0201;
pub(crate) const WM_LBUTTONUP: u32 = 0x0202;
pub(crate) const WM_RBUTTONDOWN: u32 = 0x0204;
pub(crate) const WM_RBUTTONUP: u32 = 0x0205;
pub(crate) const WM_MBUTTONDOWN: u32 = 0x0207;
pub(crate) const WM_MBUTTONUP: u32 = 0x0208;
pub(crate) const WM_XBUTTONDOWN: u32 = 0x020B;
pub(crate) const WM_XBUTTONUP: u32 = 0x020C;
pub fn win_modifiers(ctrl: bool, shift: bool, alt: bool, win: bool) -> Modifiers {
Modifiers {
ctrl,
shift,
alt,
cmd: win,
}
}
pub fn win_mouse_button_for_message(msg: u32, wparam: usize) -> Option<MouseButton> {
Some(match msg {
WM_LBUTTONDOWN | WM_LBUTTONUP => MouseButton::Left,
WM_RBUTTONDOWN | WM_RBUTTONUP => MouseButton::Right,
WM_MBUTTONDOWN | WM_MBUTTONUP => MouseButton::Middle,
WM_XBUTTONDOWN | WM_XBUTTONUP => match (wparam >> 16) & 0xFFFF {
1 => MouseButton::X1,
2 => MouseButton::X2,
n => MouseButton::Other(n.min(255) as u8),
},
_ => return None,
})
}
fn to_dip(px: i16, scale: f32) -> f32 {
if scale == 0.0 {
px as f32
} else {
px as f32 / scale
}
}
pub fn win_button_down(
button: MouseButton,
x: i16,
y: i16,
scale: f32,
modifiers: Modifiers,
) -> UiEvent {
UiEvent::MouseDown {
widget: None,
button,
position: Point::new(to_dip(x, scale), to_dip(y, scale)),
modifiers,
}
}
pub fn win_button_up(button: MouseButton, x: i16, y: i16, scale: f32) -> UiEvent {
UiEvent::MouseUp {
widget: None,
button,
position: Point::new(to_dip(x, scale), to_dip(y, scale)),
}
}
pub fn win_mouse_moved(x: i16, y: i16, scale: f32, buttons: ButtonMask) -> UiEvent {
UiEvent::MouseMoved {
position: Point::new(to_dip(x, scale), to_dip(y, scale)),
buttons,
}
}
pub fn win_files_dropped(paths: Vec<std::path::PathBuf>, x: i16, y: i16, scale: f32) -> UiEvent {
crate::event::files_dropped(paths, to_dip(x, scale), to_dip(y, scale))
}
pub fn win_wheel_to_uievent(
raw_delta: i16,
x: i16,
y: i16,
scale: f32,
horizontal: bool,
) -> UiEvent {
let notches = raw_delta as f32 / WHEEL_DELTA;
let delta = if horizontal {
ScrollDelta::new(notches, 0.0)
} else {
ScrollDelta::new(0.0, notches)
};
UiEvent::Scroll {
widget: None,
delta,
position: Point::new(to_dip(x, scale), to_dip(y, scale)),
}
}
pub fn vk_to_named_key(vk: u32) -> Option<NamedKey> {
Some(match vk {
0x1B => NamedKey::Escape, 0x09 => NamedKey::Tab, 0x0D => NamedKey::Enter, 0x08 => NamedKey::Backspace, 0x2E => NamedKey::Delete, 0x2D => NamedKey::Insert, 0x24 => NamedKey::Home, 0x23 => NamedKey::End, 0x21 => NamedKey::PageUp, 0x22 => NamedKey::PageDown, 0x25 => NamedKey::Left, 0x26 => NamedKey::Up, 0x27 => NamedKey::Right, 0x28 => NamedKey::Down, 0x14 => NamedKey::CapsLock, 0x90 => NamedKey::NumLock, 0x91 => NamedKey::ScrollLock, 0x5D => NamedKey::Menu, 0x70..=0x87 => NamedKey::F((vk - 0x70 + 1) as u8), _ => return None,
})
}
pub fn wm_keydown_to_uievent(vk: u32, modifiers: Modifiers, repeat: bool) -> Option<UiEvent> {
let named = vk_to_named_key(vk)?;
let named = if matches!(named, NamedKey::Tab) && modifiers.shift {
NamedKey::BackTab
} else {
named
};
Some(UiEvent::KeyPressed {
key: Key::Named(named),
modifiers,
repeat,
})
}
pub fn wm_char_to_uievent(c: char, modifiers: Modifiers, repeat: bool) -> Option<UiEvent> {
if c.is_control() {
if modifiers.ctrl && (1..=26).contains(&(c as u32)) {
let base = (b'a' + (c as u8 - 1)) as char;
return Some(UiEvent::KeyPressed {
key: Key::Char(base),
modifiers,
repeat,
});
}
return None;
}
Some(UiEvent::KeyPressed {
key: Key::Char(c),
modifiers,
repeat,
})
}
pub fn win_resize_to_uievent(width: u32, height: u32, scale: f32) -> UiEvent {
crate::event::window_resized(width as f32, height as f32, scale)
}
pub fn win_focus_to_uievent(focused: bool) -> UiEvent {
UiEvent::WindowFocused(focused)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn modifiers_all_false() {
let m = win_modifiers(false, false, false, false);
assert!(!m.ctrl && !m.shift && !m.alt && !m.cmd);
}
#[test]
fn modifiers_ctrl_shift() {
let m = win_modifiers(true, true, false, false);
assert!(m.ctrl);
assert!(m.shift);
assert!(!m.alt);
assert!(!m.cmd);
}
#[test]
fn modifiers_win_key_maps_to_cmd() {
let m = win_modifiers(false, false, false, true);
assert!(m.cmd);
assert!(!m.ctrl && !m.shift && !m.alt);
}
#[test]
fn modifiers_all_true() {
let m = win_modifiers(true, true, true, true);
assert!(m.ctrl && m.shift && m.alt && m.cmd);
}
#[test]
fn button_for_message_left_and_right() {
assert_eq!(
win_mouse_button_for_message(WM_LBUTTONDOWN, 0),
Some(MouseButton::Left)
);
assert_eq!(
win_mouse_button_for_message(WM_LBUTTONUP, 0),
Some(MouseButton::Left)
);
assert_eq!(
win_mouse_button_for_message(WM_RBUTTONDOWN, 0),
Some(MouseButton::Right)
);
assert_eq!(
win_mouse_button_for_message(WM_RBUTTONUP, 0),
Some(MouseButton::Right)
);
}
#[test]
fn button_for_message_middle() {
assert_eq!(
win_mouse_button_for_message(WM_MBUTTONDOWN, 0),
Some(MouseButton::Middle)
);
assert_eq!(
win_mouse_button_for_message(WM_MBUTTONUP, 0),
Some(MouseButton::Middle)
);
}
#[test]
fn button_for_message_xbutton_reads_high_word() {
let xbutton1 = 1usize << 16;
let xbutton2 = 2usize << 16;
assert_eq!(
win_mouse_button_for_message(WM_XBUTTONDOWN, xbutton1),
Some(MouseButton::X1)
);
assert_eq!(
win_mouse_button_for_message(WM_XBUTTONUP, xbutton2),
Some(MouseButton::X2)
);
}
#[test]
fn button_for_message_unknown_returns_none() {
assert_eq!(win_mouse_button_for_message(0x9999, 0), None);
}
#[test]
fn mouse_down_carries_button_position_and_modifiers() {
let ev = win_button_down(
MouseButton::Left,
50,
100,
1.0,
win_modifiers(true, false, false, false),
);
match ev {
UiEvent::MouseDown {
widget,
button,
position,
modifiers,
} => {
assert!(widget.is_none());
assert_eq!(button, MouseButton::Left);
assert_eq!(position.x, 50.0);
assert_eq!(position.y, 100.0);
assert!(modifiers.ctrl);
}
other => panic!("expected MouseDown, got {other:?}"),
}
}
#[test]
fn mouse_up_translation() {
let ev = win_button_up(MouseButton::Right, 200, 300, 1.0);
match ev {
UiEvent::MouseUp {
button, position, ..
} => {
assert_eq!(button, MouseButton::Right);
assert_eq!(position.x, 200.0);
assert_eq!(position.y, 300.0);
}
other => panic!("expected MouseUp, got {other:?}"),
}
}
#[test]
fn mouse_moved_carries_button_mask() {
let buttons = ButtonMask {
left: true,
..Default::default()
};
let ev = win_mouse_moved(10, 20, 1.0, buttons);
match ev {
UiEvent::MouseMoved { position, buttons } => {
assert_eq!(position.x, 10.0);
assert_eq!(position.y, 20.0);
assert!(buttons.left);
assert!(!buttons.middle && !buttons.right);
}
_ => panic!(),
}
}
#[test]
fn files_dropped_scales_position_and_carries_paths() {
let paths = vec![
std::path::PathBuf::from(r"C:\tmp\a.txt"),
std::path::PathBuf::from(r"C:\tmp\b.txt"),
];
let ev = win_files_dropped(paths.clone(), 100, 100, 2.0);
match ev {
UiEvent::FilesDropped {
paths: got,
position,
} => {
assert_eq!(got, paths);
assert_eq!(position.x, 50.0);
assert_eq!(position.y, 50.0);
}
other => panic!("expected UiEvent::FilesDropped, got {other:?}"),
}
}
#[test]
fn mouse_position_scales_device_pixels_to_dips() {
let ev = win_button_down(MouseButton::Left, 100, 100, 2.0, Modifiers::default());
match ev {
UiEvent::MouseDown { position, .. } => {
assert_eq!(position.x, 50.0);
assert_eq!(position.y, 50.0);
}
_ => panic!(),
}
}
#[test]
fn mouse_position_at_100_percent_scale_is_unchanged() {
let ev = win_button_up(MouseButton::Left, 77, 33, 1.0);
match ev {
UiEvent::MouseUp { position, .. } => {
assert_eq!(position.x, 77.0);
assert_eq!(position.y, 33.0);
}
_ => panic!(),
}
}
#[test]
fn mouse_position_never_divides_by_zero_scale() {
let ev = win_mouse_moved(10, 20, 0.0, ButtonMask::default());
match ev {
UiEvent::MouseMoved { position, .. } => {
assert!(position.x.is_finite());
assert!(position.y.is_finite());
assert_eq!(position.x, 10.0);
assert_eq!(position.y, 20.0);
}
_ => panic!(),
}
}
#[test]
fn wheel_forward_notch_is_positive_y_no_negation() {
let ev = win_wheel_to_uievent(120, 10, 20, 1.0, false);
match ev {
UiEvent::Scroll {
delta, position, ..
} => {
assert_eq!(delta.y, 1.0);
assert_eq!(delta.x, 0.0);
assert_eq!(position.x, 10.0);
assert_eq!(position.y, 20.0);
}
_ => panic!(),
}
}
#[test]
fn wheel_backward_notch_is_negative_y() {
let ev = win_wheel_to_uievent(-120, 0, 0, 1.0, false);
match ev {
UiEvent::Scroll { delta, .. } => assert_eq!(delta.y, -1.0),
_ => panic!(),
}
}
#[test]
fn horizontal_wheel_maps_to_delta_x() {
let ev = win_wheel_to_uievent(120, 0, 0, 1.0, true);
match ev {
UiEvent::Scroll { delta, .. } => {
assert_eq!(delta.x, 1.0);
assert_eq!(delta.y, 0.0);
}
_ => panic!(),
}
}
#[test]
fn wheel_fractional_notch_normalises_correctly() {
let ev = win_wheel_to_uievent(40, 0, 0, 1.0, false);
match ev {
UiEvent::Scroll { delta, .. } => {
assert!((delta.y - (40.0 / 120.0)).abs() < f32::EPSILON)
}
_ => panic!(),
}
}
#[test]
fn vk_navigation_and_editing_keys() {
for (vk, expected) in &[
(0x1B_u32, NamedKey::Escape),
(0x09, NamedKey::Tab),
(0x0D, NamedKey::Enter),
(0x08, NamedKey::Backspace),
(0x2E, NamedKey::Delete),
(0x2D, NamedKey::Insert),
(0x24, NamedKey::Home),
(0x23, NamedKey::End),
(0x21, NamedKey::PageUp),
(0x22, NamedKey::PageDown),
(0x25, NamedKey::Left),
(0x26, NamedKey::Up),
(0x27, NamedKey::Right),
(0x28, NamedKey::Down),
(0x14, NamedKey::CapsLock),
(0x90, NamedKey::NumLock),
(0x91, NamedKey::ScrollLock),
(0x5D, NamedKey::Menu),
] {
assert_eq!(vk_to_named_key(*vk), Some(*expected), "for VK 0x{vk:02X}");
}
}
#[test]
fn vk_function_keys() {
assert_eq!(vk_to_named_key(0x70), Some(NamedKey::F(1)));
assert_eq!(vk_to_named_key(0x7B), Some(NamedKey::F(12)));
assert_eq!(vk_to_named_key(0x87), Some(NamedKey::F(24)));
}
#[test]
fn vk_letters_and_digits_are_not_named_keys() {
assert_eq!(vk_to_named_key(0x41), None);
assert_eq!(vk_to_named_key(0x30), None);
}
#[test]
fn vk_unknown_returns_none() {
assert_eq!(vk_to_named_key(0xFF), None);
}
#[test]
fn keydown_named_key_translates() {
let ev = wm_keydown_to_uievent(0x1B, Modifiers::default(), false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Named(NamedKey::Escape),
repeat,
..
}) => assert!(!repeat),
other => panic!("expected KeyPressed(Named(Escape)), got {other:?}"),
}
}
#[test]
fn keydown_shift_tab_promotes_to_backtab() {
let mods = win_modifiers(false, true, false, false);
let ev = wm_keydown_to_uievent(0x09, mods, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Named(NamedKey::BackTab),
modifiers,
..
}) => assert!(modifiers.shift),
other => panic!("expected KeyPressed(Named(BackTab)), got {other:?}"),
}
}
#[test]
fn keydown_plain_tab_unchanged() {
let ev = wm_keydown_to_uievent(0x09, Modifiers::default(), false);
assert!(matches!(
ev,
Some(UiEvent::KeyPressed {
key: Key::Named(NamedKey::Tab),
..
})
));
}
#[test]
fn keydown_unmapped_vk_returns_none() {
assert_eq!(
wm_keydown_to_uievent(0x41, Modifiers::default(), false),
None
);
}
#[test]
fn keydown_repeat_flag_passes_through() {
let ev = wm_keydown_to_uievent(0x25, Modifiers::default(), true);
match ev {
Some(UiEvent::KeyPressed { repeat, .. }) => assert!(repeat),
_ => panic!(),
}
}
#[test]
fn keydown_alt_modifier_passes_through() {
let mods = win_modifiers(false, false, true, false);
let ev = wm_keydown_to_uievent(0x73, mods, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Named(NamedKey::F(4)),
modifiers,
..
}) => assert!(modifiers.alt),
other => panic!("expected KeyPressed{{Named(F(4)), alt}}, got {other:?}"),
}
}
#[test]
fn char_alt_modifier_passes_through() {
let mods = win_modifiers(false, false, true, false);
let ev = wm_char_to_uievent('a', mods, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Char('a'),
modifiers,
..
}) => assert!(modifiers.alt),
other => panic!("expected KeyPressed{{Char('a'), alt}}, got {other:?}"),
}
}
#[test]
fn char_printable_translates() {
let ev = wm_char_to_uievent('a', Modifiers::default(), false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Char(c),
repeat,
..
}) => {
assert_eq!(c, 'a');
assert!(!repeat);
}
other => panic!("expected KeyPressed(Char('a')), got {other:?}"),
}
}
#[test]
fn char_ctrl_letter_recovers_base_letter() {
let mods = win_modifiers(true, false, false, false);
let ev = wm_char_to_uievent('\u{0003}', mods, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Char(c),
modifiers,
..
}) => {
assert_eq!(c, 'c');
assert!(modifiers.ctrl);
}
other => panic!("expected KeyPressed(Char('c'), ctrl), got {other:?}"),
}
}
#[test]
fn char_ctrl_a_through_z_all_recover() {
for i in 1u32..=26 {
let ctrl_char = char::from_u32(i).unwrap();
let ev = wm_char_to_uievent(ctrl_char, win_modifiers(true, false, false, false), false);
let expected = (b'a' + (i as u8 - 1)) as char;
match ev {
Some(UiEvent::KeyPressed {
key: Key::Char(c), ..
}) => assert_eq!(c, expected, "for control char {i}"),
other => panic!("expected KeyPressed(Char), got {other:?}"),
}
}
}
#[test]
fn char_control_without_ctrl_held_returns_none() {
assert_eq!(wm_char_to_uievent('\r', Modifiers::default(), false), None);
assert_eq!(wm_char_to_uievent('\t', Modifiers::default(), false), None);
assert_eq!(
wm_char_to_uievent('\x08', Modifiers::default(), false),
None
);
assert_eq!(
wm_char_to_uievent('\x1B', Modifiers::default(), false),
None
);
}
#[test]
fn char_control_with_ctrl_held_recovers_even_for_named_key_codes() {
let mods = win_modifiers(true, false, false, false);
let ev = wm_char_to_uievent('\r', mods, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Char(c),
modifiers,
..
}) => {
assert_eq!(c, 'm');
assert!(modifiers.ctrl);
}
other => panic!("expected KeyPressed(Char('m'), ctrl), got {other:?}"),
}
}
#[test]
fn char_repeat_flag_passes_through() {
let ev = wm_char_to_uievent('k', Modifiers::default(), true);
match ev {
Some(UiEvent::KeyPressed { repeat, .. }) => assert!(repeat),
_ => panic!(),
}
}
#[test]
fn ctrl_b_wm_char_recovers_the_base_letter_1674() {
let mods = win_modifiers(true, false, false, false);
let ev = wm_char_to_uievent('\u{0002}', mods, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Char(c),
modifiers,
..
}) => {
assert_eq!(c, 'b');
assert!(modifiers.ctrl);
}
other => panic!("expected KeyPressed(Char('b'), ctrl), got {other:?}"),
}
}
#[test]
fn ctrl_backtick_wm_char_passes_through_with_ctrl_held_1674() {
let mods = win_modifiers(true, false, false, false);
let ev = wm_char_to_uievent('`', mods, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Char(c),
modifiers,
..
}) => {
assert_eq!(c, '`');
assert!(modifiers.ctrl);
}
other => panic!("expected KeyPressed(Char('`'), ctrl), got {other:?}"),
}
}
#[test]
fn resize_translation_carries_device_pixel_size_and_scale() {
let ev = win_resize_to_uievent(1920, 1080, 2.0);
match ev {
UiEvent::WindowResized { viewport } => {
assert_eq!(viewport.width, 1920.0);
assert_eq!(viewport.height, 1080.0);
assert_eq!(viewport.scale, 2.0);
}
other => panic!("expected WindowResized, got {other:?}"),
}
}
#[test]
fn resize_translation_zero_size_round_trips() {
let ev = win_resize_to_uievent(0, 0, 1.0);
assert_eq!(
ev,
UiEvent::WindowResized {
viewport: crate::Viewport::new(0.0, 0.0, 1.0),
}
);
}
#[test]
fn focus_translation() {
assert_eq!(win_focus_to_uievent(true), UiEvent::WindowFocused(true));
assert_eq!(win_focus_to_uievent(false), UiEvent::WindowFocused(false));
}
}