use std::time::Duration;
use crate::{ButtonMask, Key, Modifiers, MouseButton, NamedKey, UiEvent};
const NS_FLAG_SHIFT: usize = 1 << 17;
const NS_FLAG_CONTROL: usize = 1 << 18;
const NS_FLAG_OPTION: usize = 1 << 19; const NS_FLAG_COMMAND: usize = 1 << 20;
pub fn ns_modifier_flags_to_quadraui(flags: usize) -> Modifiers {
Modifiers {
shift: flags & NS_FLAG_SHIFT != 0,
ctrl: flags & NS_FLAG_CONTROL != 0,
alt: flags & NS_FLAG_OPTION != 0,
cmd: flags & NS_FLAG_COMMAND != 0,
}
}
pub fn ns_button_number_to_quadraui(n: i64) -> MouseButton {
match n {
0 => MouseButton::Left,
1 => MouseButton::Right,
2 => MouseButton::Middle,
3 => MouseButton::X1,
4 => MouseButton::X2,
n if (0..=255).contains(&n) => MouseButton::Other(n as u8),
_ => MouseButton::Other(255),
}
}
pub fn ns_mouse_down(button: i64, x: f64, y: f64, flags: usize) -> UiEvent {
crate::event::mouse_down(
ns_button_number_to_quadraui(button),
x as f32,
y as f32,
ns_modifier_flags_to_quadraui(flags),
)
}
pub fn ns_mouse_up(button: i64, x: f64, y: f64) -> UiEvent {
crate::event::mouse_up(ns_button_number_to_quadraui(button), x as f32, y as f32)
}
pub fn ns_mouse_moved(x: f64, y: f64, buttons: ButtonMask) -> UiEvent {
crate::event::mouse_moved(x as f32, y as f32, buttons)
}
pub fn ns_scroll(dx: f64, dy: f64, x: f64, y: f64) -> UiEvent {
crate::event::scroll(dx as f32, dy as f32, x as f32, y as f32)
}
pub(crate) const WHEEL_MOMENTUM_DURATION: Duration = Duration::from_millis(220);
pub(crate) const WHEEL_MOMENTUM_TICK: Duration = Duration::from_millis(16);
pub(crate) fn ease_out_cubic(t: f32) -> f32 {
let t = t.clamp(0.0, 1.0);
1.0 - (1.0 - t).powi(3)
}
pub(crate) fn fold_wheel_momentum_remaining(
old_total: f32,
old_delivered: f32,
new_notch: f32,
) -> f32 {
(old_total - old_delivered) + new_notch
}
pub(crate) fn wheel_momentum_quantized_target(total: f32, eased: f32) -> f32 {
(total * eased).round()
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum WheelRouting {
Immediate(UiEvent),
Glide { dx: f32, dy: f32, x: f32, y: f32 },
}
pub(crate) fn route_wheel_event(precise: bool, dx: f64, dy: f64, x: f64, y: f64) -> WheelRouting {
let event = ns_scroll(dx, dy, x, y);
if precise {
return WheelRouting::Immediate(event);
}
match event {
UiEvent::Scroll {
delta, position, ..
} => WheelRouting::Glide {
dx: delta.x,
dy: delta.y,
x: position.x,
y: position.y,
},
other => WheelRouting::Immediate(other),
}
}
pub fn ns_key_to_uievent(
characters: Option<&str>,
key_code: u16,
flags: usize,
repeat: bool,
) -> Option<UiEvent> {
let modifiers = ns_modifier_flags_to_quadraui(flags);
if let Some(named) = ns_keycode_to_named_key(key_code) {
let named = if matches!(named, NamedKey::Tab) && modifiers.shift {
NamedKey::BackTab
} else {
named
};
return Some(UiEvent::KeyPressed {
key: Key::Named(named),
modifiers,
repeat,
});
}
let first = characters?.chars().next()?;
if first.is_control() {
if (1..=26).contains(&(first as u32)) {
let base = (b'a' + (first as u8 - 1)) as char;
return Some(UiEvent::KeyPressed {
key: Key::Char(base),
modifiers,
repeat,
});
}
return None;
}
Some(UiEvent::KeyPressed {
key: Key::Char(first),
modifiers,
repeat,
})
}
pub fn ns_keycode_to_named_key(key_code: u16) -> Option<NamedKey> {
Some(match key_code {
0x24 => NamedKey::Enter, 0x4C => NamedKey::Enter, 0x30 => NamedKey::Tab, 0x33 => NamedKey::Backspace, 0x75 => NamedKey::Delete, 0x72 => NamedKey::Insert, 0x35 => NamedKey::Escape,
0x39 => NamedKey::CapsLock,
0x47 => NamedKey::NumLock, 0x73 => NamedKey::Home,
0x77 => NamedKey::End,
0x74 => NamedKey::PageUp,
0x79 => NamedKey::PageDown,
0x7B => NamedKey::Left,
0x7C => NamedKey::Right,
0x7D => NamedKey::Down,
0x7E => NamedKey::Up,
0x7A => NamedKey::F(1),
0x78 => NamedKey::F(2),
0x63 => NamedKey::F(3),
0x76 => NamedKey::F(4),
0x60 => NamedKey::F(5),
0x61 => NamedKey::F(6),
0x62 => NamedKey::F(7),
0x64 => NamedKey::F(8),
0x65 => NamedKey::F(9),
0x6D => NamedKey::F(10),
0x67 => NamedKey::F(11),
0x6F => NamedKey::F(12),
0x69 => NamedKey::F(13),
0x6B => NamedKey::F(14),
0x71 => NamedKey::F(15),
0x6A => NamedKey::F(16),
0x40 => NamedKey::F(17),
0x4F => NamedKey::F(18),
0x50 => NamedKey::F(19),
0x5A => NamedKey::F(20),
_ => return None,
})
}
pub fn ns_resize_to_uievent(width: f64, height: f64, scale: f32) -> UiEvent {
crate::event::window_resized(width as f32, height as f32, scale)
}
pub fn ns_focus_to_uievent(focused: bool) -> UiEvent {
UiEvent::WindowFocused(focused)
}
pub fn ns_files_dropped(paths: Vec<std::path::PathBuf>, x: f64, y: f64) -> UiEvent {
crate::event::files_dropped(paths, x as f32, y as f32)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn modifier_shift_alone() {
let m = ns_modifier_flags_to_quadraui(NS_FLAG_SHIFT);
assert!(m.shift);
assert!(!m.ctrl);
assert!(!m.alt);
assert!(!m.cmd);
}
#[test]
fn modifier_option_maps_to_alt() {
let m = ns_modifier_flags_to_quadraui(NS_FLAG_OPTION);
assert!(m.alt);
assert!(!m.shift);
}
#[test]
fn modifier_command_maps_to_cmd() {
let m = ns_modifier_flags_to_quadraui(NS_FLAG_COMMAND);
assert!(m.cmd);
assert!(!m.ctrl);
}
#[test]
fn modifier_all_four_combined() {
let m = ns_modifier_flags_to_quadraui(
NS_FLAG_SHIFT | NS_FLAG_CONTROL | NS_FLAG_OPTION | NS_FLAG_COMMAND,
);
assert!(m.shift && m.ctrl && m.alt && m.cmd);
}
#[test]
fn modifier_ignores_unrelated_bits() {
let m = ns_modifier_flags_to_quadraui((1 << 16) | (1 << 21) | NS_FLAG_SHIFT);
assert!(m.shift);
assert!(!m.ctrl && !m.alt && !m.cmd);
}
#[test]
fn button_zero_to_left() {
assert_eq!(ns_button_number_to_quadraui(0), MouseButton::Left);
}
#[test]
fn button_one_to_right() {
assert_eq!(ns_button_number_to_quadraui(1), MouseButton::Right);
}
#[test]
fn button_two_to_middle() {
assert_eq!(ns_button_number_to_quadraui(2), MouseButton::Middle);
}
#[test]
fn button_three_four_to_x1_x2() {
assert_eq!(ns_button_number_to_quadraui(3), MouseButton::X1);
assert_eq!(ns_button_number_to_quadraui(4), MouseButton::X2);
}
#[test]
fn button_other_in_byte_range() {
assert_eq!(ns_button_number_to_quadraui(7), MouseButton::Other(7));
assert_eq!(ns_button_number_to_quadraui(200), MouseButton::Other(200));
}
#[test]
fn button_out_of_byte_range_clamps_to_255() {
assert_eq!(
ns_button_number_to_quadraui(99_999),
MouseButton::Other(255)
);
}
#[test]
fn mouse_down_carries_coords_and_modifiers() {
let ev = ns_mouse_down(0, 50.0, 100.0, NS_FLAG_CONTROL);
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);
assert!(!modifiers.shift);
}
other => panic!("expected MouseDown, got {other:?}"),
}
}
#[test]
fn mouse_up_translation() {
let ev = ns_mouse_up(1, 200.0, 300.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 = ns_mouse_moved(10.0, 20.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 scroll_negates_dy_for_quadraui_convention() {
let ev = ns_scroll(0.0, 1.0, 10.0, 20.0);
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 scroll_dx_is_not_negated() {
let ev = ns_scroll(2.5, 0.0, 0.0, 0.0);
match ev {
UiEvent::Scroll { delta, .. } => {
assert_eq!(delta.x, 2.5);
assert_eq!(delta.y, 0.0);
}
_ => panic!(),
}
}
#[test]
fn ease_out_cubic_endpoints_are_exact() {
assert_eq!(ease_out_cubic(0.0), 0.0);
assert_eq!(ease_out_cubic(1.0), 1.0);
}
#[test]
fn ease_out_cubic_clamps_out_of_range_input() {
assert_eq!(ease_out_cubic(-1.0), 0.0);
assert_eq!(ease_out_cubic(2.0), 1.0);
}
#[test]
fn ease_out_cubic_is_monotonically_non_decreasing() {
let mut prev = ease_out_cubic(0.0);
for i in 1..=20 {
let t = i as f32 / 20.0;
let next = ease_out_cubic(t);
assert!(next >= prev, "eased value decreased at t={t}");
prev = next;
}
}
#[test]
fn ease_out_cubic_front_loads_progress() {
assert!(ease_out_cubic(0.25) > 0.25);
}
#[test]
fn quantized_targets_sum_to_the_rounded_total_over_a_full_sweep() {
for total in [1.0_f32, -3.5, 42.0] {
let mut delivered = 0.0_f32;
for i in 1..=10 {
let t = i as f32 / 10.0;
let eased = ease_out_cubic(t);
let target = wheel_momentum_quantized_target(total, eased);
let tick_delta = target - delivered;
delivered += tick_delta;
}
assert_eq!(
delivered,
total.round(),
"expected {}, got {delivered}",
total.round()
);
}
}
#[test]
fn quantized_target_every_tick_is_a_whole_unit() {
for i in 0..=10 {
let t = i as f32 / 10.0;
let eased = ease_out_cubic(t);
let target = wheel_momentum_quantized_target(1.0, eased);
assert_eq!(target, target.round());
}
}
#[test]
fn quantized_target_is_stable_once_fully_eased() {
let first = wheel_momentum_quantized_target(10.0, 1.0);
let second = wheel_momentum_quantized_target(10.0, 1.0);
assert_eq!(first, second);
assert_eq!(first - second, 0.0);
}
#[test]
fn fold_remaining_adds_undelivered_portion_to_new_notch() {
let folded = fold_wheel_momentum_remaining(10.0, 5.0, 2.0);
assert_eq!(folded, 7.0);
}
#[test]
fn fold_remaining_with_finished_transition_is_just_the_new_notch() {
let folded = fold_wheel_momentum_remaining(10.0, 10.0, 3.0);
assert_eq!(folded, 3.0);
}
#[test]
fn fold_remaining_handles_negative_deltas() {
let folded = fold_wheel_momentum_remaining(-10.0, -5.0, -2.0);
assert_eq!(folded, -7.0);
}
#[test]
fn precise_delta_routes_immediate() {
match route_wheel_event(true, 1.5, -2.25, 10.0, 20.0) {
WheelRouting::Immediate(UiEvent::Scroll {
delta, position, ..
}) => {
assert_eq!(delta.x, 1.5);
assert_eq!(delta.y, 2.25);
assert_eq!(position.x, 10.0);
assert_eq!(position.y, 20.0);
}
other => panic!("expected Immediate(Scroll), got {other:?}"),
}
}
#[test]
fn discrete_delta_routes_glide() {
match route_wheel_event(false, 0.0, 1.0, 3.0, 4.0) {
WheelRouting::Glide { dx, dy, x, y } => {
assert_eq!(dx, 0.0);
assert_eq!(dy, -1.0); assert_eq!(x, 3.0);
assert_eq!(y, 4.0);
}
other => panic!("expected Glide, got {other:?}"),
}
}
#[test]
fn a_full_discrete_glide_applies_exactly_one_row_per_axis() {
use crate::primitives::scrollbar::wheel_row_delta;
for rows_per_notch in [1.0_f32, 3.0] {
let dy = -1.0_f32; let expected_rows = wheel_row_delta(dy, rows_per_notch);
let mut delivered = 0.0_f32;
let mut applied_rows = 0_isize;
for i in 1..=14 {
let t = i as f32 / 14.0;
let eased = ease_out_cubic(t);
let target = wheel_momentum_quantized_target(dy, eased);
let tick_delta = target - delivered;
delivered = target;
if tick_delta != 0.0 {
applied_rows += wheel_row_delta(tick_delta, rows_per_notch);
}
}
assert_eq!(
applied_rows, expected_rows,
"rows_per_notch={rows_per_notch}: expected {expected_rows}, got {applied_rows}"
);
}
}
#[test]
fn keycode_navigation_to_named() {
for (code, expected) in &[
(0x35_u16, NamedKey::Escape),
(0x30, NamedKey::Tab),
(0x24, NamedKey::Enter),
(0x4C, NamedKey::Enter), (0x33, NamedKey::Backspace),
(0x75, NamedKey::Delete),
(0x7B, NamedKey::Left),
(0x7C, NamedKey::Right),
(0x7D, NamedKey::Down),
(0x7E, NamedKey::Up),
(0x74, NamedKey::PageUp),
(0x79, NamedKey::PageDown),
(0x73, NamedKey::Home),
(0x77, NamedKey::End),
(0x39, NamedKey::CapsLock),
(0x72, NamedKey::Insert),
] {
assert_eq!(
ns_keycode_to_named_key(*code),
Some(*expected),
"for 0x{code:X}",
);
}
}
#[test]
fn keycode_function_keys() {
for (code, n) in &[
(0x7A_u16, 1_u8),
(0x78, 2),
(0x63, 3),
(0x76, 4),
(0x60, 5),
(0x61, 6),
(0x62, 7),
(0x64, 8),
(0x65, 9),
(0x6D, 10),
(0x67, 11),
(0x6F, 12),
(0x5A, 20),
] {
assert_eq!(
ns_keycode_to_named_key(*code),
Some(NamedKey::F(*n)),
"for 0x{code:X}",
);
}
}
#[test]
fn keycode_unknown_returns_none() {
assert_eq!(ns_keycode_to_named_key(0x0A), None);
assert_eq!(ns_keycode_to_named_key(0xFFFF), None);
}
#[test]
fn key_event_printable_via_characters() {
let ev = ns_key_to_uievent(Some("a"), 0x00, 0, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Char(c),
modifiers,
repeat,
}) => {
assert_eq!(c, 'a');
assert!(!modifiers.shift && !modifiers.cmd);
assert!(!repeat);
}
other => panic!("expected KeyPressed(Char), got {other:?}"),
}
}
#[test]
fn shift_tab_translates_to_backtab() {
let ev = ns_key_to_uievent(Some("\u{0019}"), 0x30, NS_FLAG_SHIFT, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Named(NamedKey::BackTab),
modifiers,
..
}) => {
assert!(modifiers.shift, "shift modifier should still be set");
}
other => panic!("expected KeyPressed(Named(BackTab)), got {other:?}"),
}
}
#[test]
fn plain_tab_unchanged() {
let ev = ns_key_to_uievent(Some("\t"), 0x30, 0, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Named(NamedKey::Tab),
modifiers,
..
}) => {
assert!(!modifiers.shift);
}
other => panic!("expected KeyPressed(Named(Tab)), got {other:?}"),
}
}
#[test]
fn key_event_arrow_via_keycode() {
let ev = ns_key_to_uievent(Some("\u{F702}"), 0x7B, NS_FLAG_SHIFT, false);
match ev {
Some(UiEvent::KeyPressed {
key: Key::Named(NamedKey::Left),
modifiers,
..
}) => {
assert!(modifiers.shift);
}
other => panic!("expected KeyPressed(Named(Left)), got {other:?}"),
}
}
#[test]
fn key_event_ctrl_letter_recovers_base() {
let ev = ns_key_to_uievent(Some("\u{0003}"), 0x08, NS_FLAG_CONTROL, 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 key_event_empty_characters_returns_none() {
let ev = ns_key_to_uievent(None, 0x00, 0, false);
assert!(ev.is_none());
}
#[test]
fn key_event_repeat_flag_passes_through() {
let ev = ns_key_to_uievent(Some("k"), 0x00, 0, true);
match ev {
Some(UiEvent::KeyPressed { repeat, .. }) => assert!(repeat),
_ => panic!(),
}
}
#[test]
fn resize_translation() {
let ev = ns_resize_to_uievent(1920.0, 1080.0, 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);
}
_ => panic!(),
}
}
#[test]
fn focus_translation() {
assert_eq!(ns_focus_to_uievent(true), UiEvent::WindowFocused(true));
assert_eq!(ns_focus_to_uievent(false), UiEvent::WindowFocused(false));
}
#[test]
fn files_dropped_translation() {
let paths = vec![
std::path::PathBuf::from("/tmp/a.txt"),
std::path::PathBuf::from("/tmp/b.txt"),
];
let ev = ns_files_dropped(paths.clone(), 12.0, 34.0);
match ev {
UiEvent::FilesDropped {
paths: got,
position,
} => {
assert_eq!(got, paths);
assert_eq!((position.x, position.y), (12.0, 34.0));
}
other => panic!("expected UiEvent::FilesDropped, got {other:?}"),
}
}
}