use core::ffi::CStr;
use crcbl_core::input::{PointerButton, ScrollDelta};
use crate::{CursorIcon, PhysicalPoint};
use super::ffi::{NSPoint, NSSize};
#[must_use]
pub const fn button(number: isize) -> PointerButton {
match number {
0 => PointerButton::Left,
1 => PointerButton::Right,
2 => PointerButton::Middle,
3 => PointerButton::Back,
4 => PointerButton::Forward,
other if other > 0 => PointerButton::Other(other as u16),
_ => PointerButton::Other(u16::MAX),
}
}
#[must_use]
pub fn scroll(precise: bool, x: f64, y: f64) -> ScrollDelta {
if precise {
ScrollDelta::Pixels { x, y }
} else {
ScrollDelta::Lines {
x: x as f32,
y: y as f32,
}
}
}
#[must_use]
pub fn view_point(point: NSPoint, height: f64, scale: f64) -> PhysicalPoint {
let scale = super::geometry::usable_scale(scale);
PhysicalPoint::new(point.x * scale, (height - point.y) * scale)
}
#[must_use]
pub fn centre_of(size: NSSize, scale: f64) -> PhysicalPoint {
let scale = super::geometry::usable_scale(scale);
PhysicalPoint::new(size.width * scale / 2.0, size.height * scale / 2.0)
}
#[must_use]
pub fn warp_source(position: PhysicalPoint, height: f64, scale: f64) -> NSPoint {
let scale = super::geometry::usable_scale(scale);
NSPoint {
x: position.x / scale,
y: height - position.y / scale,
}
}
#[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_selector(icon: CursorIcon) -> &'static CStr {
match icon {
CursorIcon::Default => c"arrowCursor",
CursorIcon::Pointer => c"pointingHandCursor",
CursorIcon::Text => c"IBeamCursor",
CursorIcon::Crosshair => c"crosshairCursor",
CursorIcon::NotAllowed => c"operationNotAllowedCursor",
CursorIcon::Grab => c"openHandCursor",
CursorIcon::Grabbing | CursorIcon::Move => c"closedHandCursor",
CursorIcon::ResizeNorthSouth | CursorIcon::ResizeRow => c"resizeUpDownCursor",
CursorIcon::ResizeEastWest | CursorIcon::ResizeColumn => c"resizeLeftRightCursor",
CursorIcon::Wait
| CursorIcon::Progress
| CursorIcon::ResizeNorthEastSouthWest
| CursorIcon::ResizeNorthWestSouthEast => c"arrowCursor",
}
}
#[cfg(test)]
mod tests {
use super::*;
const SHAPES: [CursorIcon; 16] = [
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,
];
#[test]
fn the_five_named_buttons_decode_and_the_rest_stay_bindable() {
assert_eq!(button(0), PointerButton::Left);
assert_eq!(button(1), PointerButton::Right);
assert_eq!(button(2), PointerButton::Middle);
assert_eq!(button(3), PointerButton::Back);
assert_eq!(button(4), PointerButton::Forward);
assert_eq!(button(5), PointerButton::Other(5));
assert_eq!(button(31), PointerButton::Other(31));
assert_eq!(button(-1), PointerButton::Other(u16::MAX));
}
#[cfg(target_os = "linux")]
#[test]
fn the_thumb_buttons_agree_with_the_linux_backends() {
assert_eq!(
crate::linux::keymap::pointer_button(0x113),
button(3),
"BTN_SIDE and buttonNumber 3 are both Back"
);
assert_eq!(
crate::linux::keymap::pointer_button(0x114),
button(4),
"BTN_EXTRA and buttonNumber 4 are both Forward"
);
}
#[test]
fn a_trackpad_scrolls_in_pixels_and_a_wheel_in_lines() {
assert_eq!(
scroll(true, 0.0, -13.5),
ScrollDelta::Pixels { x: 0.0, y: -13.5 }
);
assert_eq!(
scroll(false, 0.0, 1.0),
ScrollDelta::Lines { x: 0.0, y: 1.0 }
);
assert_eq!(
scroll(false, 0.0, -1.0),
ScrollDelta::Lines { x: 0.0, y: -1.0 }
);
assert_eq!(
scroll(true, 40.0, 0.0),
ScrollDelta::Pixels { x: 40.0, y: 0.0 }
);
assert!(scroll(true, 0.0, 0.0).is_zero());
assert!(scroll(false, 0.0, 0.0).is_zero());
let ScrollDelta::Pixels { y, .. } = scroll(true, 0.0, 0.5) else {
panic!("a precise device reports pixels");
};
assert!((y - 0.5).abs() < f64::EPSILON);
}
#[test]
fn a_position_is_reflected_and_scaled_and_the_top_of_the_view_is_zero() {
let top = view_point(NSPoint { x: 100.0, y: 720.0 }, 720.0, 1.0);
assert_eq!(top, PhysicalPoint::new(100.0, 0.0));
let bottom = view_point(NSPoint { x: 0.0, y: 0.0 }, 720.0, 1.0);
assert_eq!(bottom, PhysicalPoint::new(0.0, 720.0));
let retina = view_point(NSPoint { x: 100.0, y: 620.0 }, 720.0, 2.0);
assert_eq!(retina, PhysicalPoint::new(200.0, 200.0));
let outside = view_point(NSPoint { x: -8.0, y: 740.0 }, 720.0, 1.0);
assert_eq!(outside, PhysicalPoint::new(-8.0, -20.0));
}
#[test]
fn a_warp_target_is_the_exact_inverse_of_a_reported_position() {
for (height, scale) in [(720.0, 1.0), (720.0, 2.0), (1055.5, 2.0)] {
for point in [
NSPoint { x: 0.0, y: 0.0 },
NSPoint { x: 640.0, y: 360.0 },
NSPoint {
x: 1279.5,
y: height,
},
] {
let reported = view_point(point, height, scale);
let back = warp_source(reported, height, scale);
assert!(
(back.x - point.x).abs() < 1e-9 && (back.y - point.y).abs() < 1e-9,
"{point:?} at {height}x{scale} came back as {back:?}"
);
}
}
}
#[test]
fn the_centre_of_a_retina_view_is_in_pixels_like_everything_else() {
assert_eq!(
centre_of(NSSize::new(1280.0, 720.0), 1.0),
PhysicalPoint::new(640.0, 360.0)
);
assert_eq!(
centre_of(NSSize::new(1280.0, 720.0), 2.0),
PhysicalPoint::new(1280.0, 720.0)
);
assert_eq!(
centre_of(NSSize::new(640.0, 480.0), 0.0),
PhysicalPoint::new(320.0, 240.0)
);
}
#[test]
fn hiding_the_cursor_twice_owes_one_unhide_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_names_a_public_nscursor_class_method() {
for shape in SHAPES {
let selector = cursor_selector(shape);
let name = selector.to_str().expect("an ASCII selector");
assert!(
!name.starts_with('_'),
"{shape:?} names {name}, a private selector"
);
assert!(name.ends_with("Cursor"), "{shape:?} names {name}");
}
}
#[test]
fn the_documented_approximations_are_asserted_rather_than_drifted_into() {
assert_eq!(cursor_selector(CursorIcon::Wait), c"arrowCursor");
assert_eq!(cursor_selector(CursorIcon::Progress), c"arrowCursor");
assert_eq!(
cursor_selector(CursorIcon::ResizeNorthEastSouthWest),
c"arrowCursor"
);
assert_eq!(
cursor_selector(CursorIcon::ResizeNorthWestSouthEast),
c"arrowCursor"
);
assert_eq!(cursor_selector(CursorIcon::Move), c"closedHandCursor");
assert_eq!(
cursor_selector(CursorIcon::ResizeColumn),
c"resizeLeftRightCursor"
);
assert_eq!(
cursor_selector(CursorIcon::ResizeRow),
c"resizeUpDownCursor"
);
let arrows = SHAPES
.iter()
.filter(|shape| cursor_selector(**shape) == c"arrowCursor")
.count();
assert_eq!(arrows, 5, "the default plus the four approximations");
}
}