use crcbl_core::input::{ButtonState, DeviceId, Keysym, Scancode};
use crcbl_core::{EventTime, KeyCode};
use crate::{
CursorIcon, PhysicalPoint, PointerMode, ShellError, ShellEvent, WindowId,
appkit::events::RawEvent,
};
use super::ffi::{self, Id, NSRect, NSSize, Sel};
use super::geometry::Flip;
use super::keys;
use super::pointer::{self, Show};
use super::shell::AppKitShell;
pub(super) const KEYBOARD_DEVICE: DeviceId = DeviceId(1);
pub(super) const POINTER_DEVICE: DeviceId = DeviceId(2);
#[must_use]
pub(super) fn now_seconds() -> f64 {
unsafe { ffi::CACurrentMediaTime() }
}
impl AppKitShell {
pub(super) fn event_time(&self, seconds: f64) -> EventTime {
self.time.event_time_at(now_seconds(), seconds)
}
fn pointer_mode_of(&self, window: WindowId) -> PointerMode {
self.window(window)
.map_or(PointerMode::Free, |state| state.pointer_mode)
}
fn view_metrics(&self, window: WindowId) -> Option<(f64, f64)> {
let state = self.window(window).ok()?;
let bounds = unsafe { ffi::msg_rect(state.view, ffi::sel(c"bounds")) };
Some((bounds.size.height, state.scale))
}
fn position_of(&self, window: WindowId, location: ffi::NSPoint) -> Option<PhysicalPoint> {
if self.pointer_mode_of(window) == PointerMode::Locked {
return None;
}
let (height, scale) = self.view_metrics(window)?;
Some(pointer::view_point(location, height, scale))
}
pub(super) fn translate_input(&mut self, event: RawEvent, window: Option<WindowId>) {
let Some(window) = window else { return };
match event {
RawEvent::Key {
keycode,
character,
flags,
state,
repeat,
seconds,
..
} => {
let time = self.event_time(seconds);
let key_code = keys::key_code(keycode);
let event = ShellEvent::Key {
window,
device: KEYBOARD_DEVICE,
time,
scancode: Scancode(u32::from(keycode)),
key_code,
keysym: keysym_of(key_code, character),
state,
repeat,
modifiers: keys::modifiers(flags),
};
let popup = self
.window(window)
.ok()
.and_then(|state| state.popup)
.map(|popup| popup.input);
let dismissed = crate::role::escape_dismisses(popup, &event);
self.queue_event(event);
if dismissed {
self.close_requested(window);
}
}
RawEvent::FlagsChanged {
keycode,
flags,
seconds,
..
} => {
let Some(key_code) = keys::key_code(keycode) else {
return;
};
let Some(down) = keys::modifier_is_down(key_code, flags) else {
return;
};
let time = self.event_time(seconds);
self.queue_event(ShellEvent::Key {
window,
device: KEYBOARD_DEVICE,
time,
scancode: Scancode(u32::from(keycode)),
key_code: Some(key_code),
keysym: keys::named_keysym(key_code).unwrap_or(Keysym::NONE),
state: ButtonState::from_pressed(down),
repeat: false,
modifiers: keys::modifiers(flags),
});
}
RawEvent::TextCommitted {
window: key,
seconds,
} => {
let time = self.event_time(seconds);
let Some(text) = self.shared_text(key) else {
return;
};
self.queue_event(ShellEvent::TextCommit { window, time, text });
}
RawEvent::Preedit {
window: key,
seconds,
} => {
let Some((text, cursor)) = self.shared_preedit(key) else {
return;
};
if !self.composing.report(window, &text) {
return;
}
let time = self.event_time(seconds);
self.queue_event(ShellEvent::TextPreedit {
window,
time,
text,
cursor,
});
}
RawEvent::PointerMotion {
location,
dx,
dy,
seconds,
..
} => {
let time = self.event_time(seconds);
self.queue_event(ShellEvent::PointerMotion {
window,
device: POINTER_DEVICE,
time,
abs: self.position_of(window, location),
raw_delta: Some((dx, dy)),
});
}
RawEvent::PointerFocus {
entered,
location,
seconds,
..
} => {
let time = self.event_time(seconds);
let position = if entered {
self.position_of(window, location)
} else {
None
};
self.queue_event(ShellEvent::PointerFocus {
window,
device: POINTER_DEVICE,
time,
entered,
position,
});
}
RawEvent::Button {
number,
state,
location,
flags,
seconds,
..
} => {
let time = self.event_time(seconds);
self.queue_event(ShellEvent::Button {
window,
device: POINTER_DEVICE,
time,
button: pointer::button(number),
state,
position: self.position_of(window, location),
modifiers: keys::modifiers(flags),
});
}
RawEvent::FilesDropped {
window: key,
location,
seconds,
} => {
let Some(paths) = self.shared_drop(key) else {
return;
};
if !self.window(window).is_ok_and(|state| state.accept_drops) {
crcbl_core::log::debug!(
"discarding a drop of {} file(s) on a window created without accept_drops",
paths.len()
);
return;
}
let time = self.event_time(seconds);
let position = self
.view_metrics(window)
.map(|(height, scale)| pointer::view_point(location, height, scale));
for path in paths {
self.queue_event(ShellEvent::DroppedFile {
window,
time,
path,
position,
});
}
}
RawEvent::Scroll {
dx,
dy,
precise,
location,
flags,
seconds,
..
} => {
let time = self.event_time(seconds);
self.queue_event(ShellEvent::Wheel {
window,
device: POINTER_DEVICE,
time,
delta: pointer::scroll(precise, dx, dy),
position: self.position_of(window, location),
modifiers: keys::modifiers(flags),
});
}
RawEvent::Resized { .. }
| RawEvent::BackingChanged { .. }
| RawEvent::Focus { .. }
| RawEvent::CloseRequested { .. }
| RawEvent::Closed { .. }
| RawEvent::ScreensChanged => {}
}
}
pub(super) fn refresh_pointer_capture(&mut self) {
let wanted = self
.windows_iter()
.any(|(_, state)| state.focused && state.pointer_mode == PointerMode::Locked);
if wanted == self.pointer_frozen {
return;
}
self.pointer_frozen = wanted;
let error = unsafe { ffi::CGAssociateMouseAndMouseCursorPosition(i32::from(!wanted)) };
if error != 0 {
crcbl_core::log::warn!(
"CGAssociateMouseAndMouseCursorPosition({}) failed with CGError {error}; the \
pointer will keep moving while it is locked",
!wanted
);
self.pointer_frozen = !wanted;
}
}
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 selector = match show {
Show::Nothing => return,
Show::Hide => c"hide",
Show::Reveal => c"unhide",
};
let Some(class) = ffi::class(c"NSCursor") else {
return;
};
unsafe { ffi::msg_void(class, ffi::sel(selector)) };
}
pub(super) fn apply_cursor_shape(&mut self, window: WindowId, icon: CursorIcon) {
let Ok(state) = self.window(window) else {
return;
};
let key = state.key;
let Some(class) = ffi::class(c"NSCursor") else {
return;
};
let cursor = unsafe { ffi::msg(class, ffi::sel(pointer::cursor_selector(icon))) };
if cursor.is_null() {
crcbl_core::log::warn!(
"+[NSCursor {icon:?}] returned nil; the cursor shape is unchanged"
);
return;
}
self.record_cursor(key, cursor as usize);
unsafe { ffi::msg_void(cursor, ffi::sel(c"set")) };
}
pub(super) fn warp_to_window(
&self,
window: WindowId,
position: PhysicalPoint,
) -> Result<(), ShellError> {
let state = self.window(window)?;
let native = state.window;
let (height, scale) = self
.view_metrics(window)
.ok_or_else(|| ShellError::invalid_window(window))?;
let flip = self
.screens
.first()
.map(|screen| Flip::about_primary(screen.frame))
.ok_or_else(|| {
ShellError::Backend(
"there is no display to express a pointer position on".to_string(),
)
})?;
let local = pointer::warp_source(position, height, scale);
let screen = unsafe {
let convert: unsafe extern "C" fn(Id, Sel, NSRect) -> NSRect = ffi::msg_send_stret();
convert(
native,
ffi::sel(c"convertRectToScreen:"),
NSRect {
origin: local,
size: NSSize::new(0.0, 0.0),
},
)
};
let error = unsafe { ffi::CGWarpMouseCursorPosition(flip.point(screen.origin)) };
if error != 0 {
return Err(ShellError::Backend(format!(
"CGWarpMouseCursorPosition failed with CGError {error}"
)));
}
Ok(())
}
pub(super) fn centre_pointer(&self, window: WindowId) {
let Ok(state) = self.window(window) else {
return;
};
let scale = state.scale;
let bounds = unsafe { ffi::msg_rect(state.view, ffi::sel(c"bounds")) };
if let Err(error) = self.warp_to_window(window, pointer::centre_of(bounds.size, scale)) {
crcbl_core::log::debug!("could not centre the pointer before locking it: {error}");
}
}
}
fn keysym_of(key_code: Option<KeyCode>, character: Option<char>) -> Keysym {
if let Some(named) = key_code.and_then(keys::named_keysym) {
return named;
}
character.map_or(Keysym::NONE, keys::keysym_from_character)
}
#[cfg(all(test, target_os = "macos"))]
mod tests {
use super::*;
use core::ptr;
fn cursor_position() -> Option<ffi::NSPoint> {
unsafe {
let event = ffi::CGEventCreate(ptr::null_mut());
if event.is_null() {
return None;
}
let at = ffi::CGEventGetLocation(event);
ffi::CFRelease(event);
Some(at)
}
}
fn warp(to: ffi::NSPoint) -> i32 {
unsafe { ffi::CGWarpMouseCursorPosition(to) }
}
#[test]
fn a_warp_moves_the_cursor_to_the_point_it_names() {
let Some(original) = cursor_position() else {
panic!("CGEventCreate answered nil, so this runner has no input state to read");
};
let near = ffi::NSPoint { x: 40.0, y: 30.0 };
let far = ffi::NSPoint { x: 40.0, y: 130.0 };
assert_eq!(warp(near), 0, "CGWarpMouseCursorPosition refused {near:?}");
let at_near = cursor_position().expect("the cursor has a position");
assert_eq!(warp(far), 0, "CGWarpMouseCursorPosition refused {far:?}");
let at_far = cursor_position().expect("the cursor has a position");
warp(original);
assert!(
(at_near.x - near.x).abs() < 1.0 && (at_near.y - near.y).abs() < 1.0,
"warping to {near:?} left the cursor at {at_near:?}"
);
assert!(
(at_far.x - far.x).abs() < 1.0 && (at_far.y - far.y).abs() < 1.0,
"warping to {far:?} left the cursor at {at_far:?}"
);
assert!(
at_far.y - at_near.y > 0.0,
"y increases downwards in Quartz's space: {at_near:?} then {at_far:?}"
);
}
#[test]
fn disconnecting_the_mouse_is_accepted_and_can_be_given_back() {
let disconnected = unsafe { ffi::CGAssociateMouseAndMouseCursorPosition(0) };
let reconnected = unsafe { ffi::CGAssociateMouseAndMouseCursorPosition(1) };
assert_eq!(
disconnected, 0,
"CGAssociateMouseAndMouseCursorPosition(false) failed with CGError {disconnected}, \
so pointer lock cannot work on this runner"
);
assert_eq!(reconnected, 0, "and it would not give the cursor back");
}
}