#![allow(non_snake_case, clippy::missing_safety_doc)]
use super::{Button, Mouse};
use std::cell::Cell;
use std::ffi::{c_char, c_void, CStr, CString};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{channel, Sender};
use std::sync::OnceLock;
use std::time::Duration;
use tauri::{Runtime, WebviewWindow};
type Id = *mut c_void;
type Sel = *const c_void;
#[repr(C)]
#[derive(Clone, Copy)]
struct NSPoint {
x: f64,
y: f64,
}
#[link(name = "objc")]
extern "C" {
fn objc_getClass(name: *const c_char) -> Id;
fn sel_registerName(name: *const c_char) -> Sel;
fn objc_msgSend();
fn class_getClassMethod(class: Id, name: Sel) -> Id;
fn class_getInstanceMethod(class: Id, name: Sel) -> Id;
fn method_getImplementation(method: Id) -> *const c_void;
fn method_setImplementation(method: Id, imp: *const c_void) -> *const c_void;
static _NSConcreteStackBlock: *const c_void;
}
#[link(name = "AppKit", kind = "framework")]
extern "C" {}
#[link(name = "Foundation", kind = "framework")]
extern "C" {}
#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
fn CGEventCreateScrollWheelEvent2(
src: *const c_void,
unit: u32,
n: u32,
a: i32,
b: i32,
c: i32,
) -> *const c_void;
fn CGEventSetLocation(e: *const c_void, p: NSPoint);
fn CGMainDisplayID() -> u32;
fn CGDisplayPixelsHigh(d: u32) -> usize;
}
#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
fn CFRelease(c: *const c_void);
}
unsafe fn class(n: &str) -> Id {
let c = CString::new(n).unwrap();
objc_getClass(c.as_ptr())
}
unsafe fn sel(n: &str) -> Sel {
let c = CString::new(n).unwrap();
sel_registerName(c.as_ptr())
}
macro_rules! msg {
($ret:ty; $obj:expr, $sel:expr $(, $t:ty = $a:expr)*) => {{
let f: unsafe extern "C" fn(Id, Sel $(, $t)*) -> $ret = std::mem::transmute(objc_msgSend as *const c_void);
f($obj, sel($sel) $(, $a)*)
}};
}
unsafe fn nsstring(s: &str) -> Id {
let c = CString::new(s.replace('\0', "")).unwrap();
msg!(Id; class("NSString"), "stringWithUTF8String:", *const c_char = c.as_ptr())
}
unsafe fn utf8(s: Id) -> Option<String> {
if s.is_null() {
return None;
}
let p = msg!(*const c_char; s, "UTF8String");
(!p.is_null()).then(|| CStr::from_ptr(p).to_string_lossy().into_owned())
}
unsafe fn uptime() -> f64 {
msg!(f64; msg!(Id; class("NSProcessInfo"), "processInfo"), "systemUptime")
}
thread_local! {
static POINTER_BUTTONS: Cell<Option<usize>> = const { Cell::new(None) };
static POINTER_VIEW: Cell<Id> = const { Cell::new(std::ptr::null_mut()) };
}
static POINTER_HOOKS: OnceLock<Result<(), String>> = OnceLock::new();
static ORIGINAL_BUTTONS: AtomicUsize = AtomicUsize::new(0);
static ORIGINAL_INPUT_CONTEXT: AtomicUsize = AtomicUsize::new(0);
extern "C" fn pressed_mouse_buttons(obj: Id, selector: Sel) -> usize {
if let Ok(Some(buttons)) = POINTER_BUTTONS.try_with(Cell::get) {
return buttons;
}
unsafe {
let imp = ORIGINAL_BUTTONS.load(Ordering::Acquire);
let original: unsafe extern "C" fn(Id, Sel) -> usize = std::mem::transmute(imp);
original(obj, selector)
}
}
extern "C" fn pointer_input_context(obj: Id, selector: Sel) -> Id {
if POINTER_VIEW.try_with(Cell::get).ok() == Some(obj) {
return std::ptr::null_mut();
}
unsafe {
let original: unsafe extern "C" fn(Id, Sel) -> Id =
std::mem::transmute(ORIGINAL_INPUT_CONTEXT.load(Ordering::Acquire));
original(obj, selector)
}
}
struct PointerButtons(Option<usize>, Id);
impl PointerButtons {
unsafe fn enter(wk: Id, buttons: usize) -> Result<Self, String> {
let hooks = POINTER_HOOKS.get_or_init(|| {
let method = class_getClassMethod(class("NSEvent"), sel("pressedMouseButtons"));
let context = class_getInstanceMethod(class("WKWebView"), sel("inputContext"));
if method.is_null() || context.is_null() {
return Err("AppKit pointer state methods unavailable".into());
}
let imp = method_getImplementation(method);
let context_imp = method_getImplementation(context);
if imp.is_null() || context_imp.is_null() {
return Err("AppKit pointer state has no implementation".into());
}
ORIGINAL_BUTTONS.store(imp as usize, Ordering::Release);
ORIGINAL_INPUT_CONTEXT.store(context_imp as usize, Ordering::Release);
method_setImplementation(method, pressed_mouse_buttons as *const c_void);
method_setImplementation(context, pointer_input_context as *const c_void);
Ok(())
});
hooks.as_ref().map_err(Clone::clone)?;
Ok(Self(
POINTER_BUTTONS.with(|state| state.replace(Some(buttons))),
POINTER_VIEW.with(|state| state.replace(wk)),
))
}
}
impl Drop for PointerButtons {
fn drop(&mut self) {
POINTER_BUTTONS.with(|state| state.set(self.0));
POINTER_VIEW.with(|state| state.set(self.1));
}
}
fn mouse_spec(kind: Mouse, button: Button) -> (u64, &'static str, f32, usize) {
match (kind, button) {
(Mouse::Down, Button::Left) => (1, "mouseDown:", 1.0, 1),
(Mouse::Up, Button::Left) => (2, "mouseUp:", 0.0, 0),
(Mouse::Down, Button::Right) => (3, "rightMouseDown:", 1.0, 2),
(Mouse::Up, Button::Right) => (4, "rightMouseUp:", 0.0, 0),
(Mouse::Move, _) => (6, "mouseDragged:", 0.0, 0),
(Mouse::Drag, Button::Left) => (6, "mouseDragged:", 1.0, 1),
(Mouse::Drag, Button::Right) => (7, "rightMouseDragged:", 1.0, 2),
}
}
#[repr(C)]
#[derive(Clone, Copy)]
struct NSSize {
width: f64,
height: f64,
}
#[repr(C)]
#[derive(Clone, Copy)]
struct NSRect {
origin: NSPoint,
size: NSSize,
}
fn css_viewport<R: Runtime>(win: &WebviewWindow<R>) -> Option<(f64, f64)> {
let s = eval(win, "innerWidth + ',' + innerHeight").ok()?;
let (w, h) = s.split_once(',')?;
Some((w.trim().parse().ok()?, h.trim().parse().ok()?))
}
unsafe fn window_point(wk: Id, viewport: Option<(f64, f64)>, x: f64, y: f64) -> NSPoint {
let b = msg!(NSRect; wk, "bounds");
let (scale, top) = match viewport {
Some((w, h)) if w > 0.0 && h > 0.0 => {
let scale = b.size.width / w;
(scale, (b.size.height - h * scale).max(0.0))
}
_ => (1.0, 0.0),
};
let p = NSPoint {
x: b.origin.x + x * scale,
y: b.origin.y + top + y * scale,
};
msg!(NSPoint; wk, "convertPoint:toView:", NSPoint = p, Id = std::ptr::null_mut())
}
fn on_view<R: Runtime, T: Send + 'static>(
win: &WebviewWindow<R>,
f: impl FnOnce(Id) -> T + Send + 'static,
) -> Result<T, String> {
let (tx, rx) = channel();
win.with_webview(move |pv| {
if !unsafe { msg!(bool; class("NSThread"), "isMainThread") } {
let _ = tx.send(Err("AppKit control requires the main thread".into()));
return;
}
let _ = tx.send(Ok(f(pv.inner())));
})
.map_err(|e| e.to_string())?;
rx.recv_timeout(Duration::from_secs(10))
.map_err(|_| "webview call timed out".to_string())?
}
pub fn mouse<R: Runtime>(
win: &WebviewWindow<R>,
kind: Mouse,
button: Button,
x: f64,
y: f64,
clicks: i64,
flags: u64,
) -> Result<(), String> {
let viewport = css_viewport(win);
on_view(win, move |wk| unsafe {
let window = msg!(Id; wk, "window");
if window.is_null() {
return Err("webview has no window".to_string());
}
let wp = window_point(wk, viewport, x, y);
let (ty, handler, pressure, buttons) = mouse_spec(kind, button);
let ev = msg!(Id; class("NSEvent"),
"mouseEventWithType:location:modifierFlags:timestamp:windowNumber:context:eventNumber:clickCount:pressure:",
u64 = ty, NSPoint = wp, u64 = flags, f64 = uptime(), i64 = msg!(i64; window, "windowNumber"),
Id = std::ptr::null_mut(), i64 = 0, i64 = clicks, f32 = pressure);
if ev.is_null() {
return Err("NSEvent creation failed".to_string());
}
let _buttons = PointerButtons::enter(wk, buttons)?;
msg!((); wk, handler, Id = ev);
Ok(())
})?
}
pub fn key<R: Runtime>(
win: &WebviewWindow<R>,
down: bool,
code: u16,
chars: &str,
flags: u64,
) -> Result<(), String> {
let chars = chars.to_string();
on_view(win, move |wk| unsafe {
let window = msg!(Id; wk, "window");
if window.is_null() {
return Err("webview has no window".to_string());
}
let s = nsstring(&chars);
let ev = msg!(Id; class("NSEvent"),
"keyEventWithType:location:modifierFlags:timestamp:windowNumber:context:characters:charactersIgnoringModifiers:isARepeat:keyCode:",
u64 = if down { 10 } else { 11 }, NSPoint = NSPoint { x: 0.0, y: 0.0 }, u64 = flags, f64 = uptime(),
i64 = msg!(i64; window, "windowNumber"), Id = std::ptr::null_mut(), Id = s, Id = s, u8 = 0, u16 = code);
if ev.is_null() {
return Err("NSEvent creation failed".to_string());
}
msg!((); wk, if down { "keyDown:" } else { "keyUp:" }, Id = ev);
Ok(())
})?
}
pub fn wheel<R: Runtime>(
win: &WebviewWindow<R>,
x: f64,
y: f64,
dx: f64,
dy: f64,
) -> Result<(), String> {
let viewport = css_viewport(win);
on_view(win, move |wk| unsafe {
let window = msg!(Id; wk, "window");
if window.is_null() {
return Err("webview has no window".to_string());
}
let wp = window_point(wk, viewport, x, y);
let top = CGDisplayPixelsHigh(CGMainDisplayID()) as f64;
let cg =
CGEventCreateScrollWheelEvent2(std::ptr::null(), 0, 2, (-dy) as i32, (-dx) as i32, 0);
if cg.is_null() {
return Err("CGEvent creation failed".to_string());
}
CGEventSetLocation(
cg,
NSPoint {
x: wp.x,
y: top - wp.y,
},
);
let ev = msg!(Id; class("NSEvent"), "eventWithCGEvent:", *const c_void = cg);
CFRelease(cg);
if ev.is_null() {
return Err("NSEvent from CGEvent failed".to_string());
}
msg!((); wk, "scrollWheel:", Id = ev);
Ok(())
})?
}
#[repr(C)]
struct Desc {
reserved: usize,
size: usize,
}
#[repr(C)]
struct Blk {
isa: *const c_void,
flags: i32,
reserved: i32,
invoke: *const c_void,
desc: *const Desc,
data: *mut c_void,
}
static DESC: Desc = Desc {
reserved: 0,
size: std::mem::size_of::<Blk>(),
};
unsafe fn block(invoke: *const c_void, data: *mut c_void) -> Blk {
Blk {
isa: &raw const _NSConcreteStackBlock as *const c_void,
flags: 0,
reserved: 0,
invoke,
desc: &DESC,
data,
}
}
extern "C" fn eval_done(blk: *mut Blk, result: Id, err: Id) {
unsafe {
let tx = Box::from_raw((*blk).data as *mut Sender<Result<String, String>>);
let out = if !err.is_null() {
Err(utf8(msg!(Id; err, "localizedDescription"))
.unwrap_or_else(|| "javascript error".into()))
} else if !result.is_null() && msg!(bool; result, "isKindOfClass:", Id = class("NSString"))
{
Ok(utf8(result).unwrap_or_default())
} else {
Ok(String::new())
};
let _ = tx.send(out);
}
}
extern "C" fn shot_done(blk: *mut Blk, image: Id, err: Id) {
unsafe {
let tx = Box::from_raw((*blk).data as *mut Sender<Result<Vec<u8>, String>>);
let out = (|| {
if image.is_null() || !err.is_null() {
return Err(utf8(msg!(Id; err, "localizedDescription"))
.unwrap_or_else(|| "snapshot failed".into()));
}
let tiff = msg!(Id; image, "TIFFRepresentation");
let rep = msg!(Id; class("NSBitmapImageRep"), "imageRepWithData:", Id = tiff);
let png = msg!(Id; rep, "representationUsingType:properties:", u64 = 4, Id = std::ptr::null_mut());
if png.is_null() {
return Err("png encode failed".to_string());
}
let len = msg!(usize; png, "length");
let p = msg!(*const u8; png, "bytes");
Ok(std::slice::from_raw_parts(p, len).to_vec())
})();
let _ = tx.send(out);
}
}
pub fn eval<R: Runtime>(win: &WebviewWindow<R>, js: &str) -> Result<String, String> {
let (tx, rx) = channel::<Result<String, String>>();
let js = js.to_string();
win.with_webview(move |pv| unsafe {
let wk = pv.inner();
let data = Box::into_raw(Box::new(tx)) as *mut c_void;
let mut b = block(eval_done as *const c_void, data);
msg!((); wk, "evaluateJavaScript:completionHandler:", Id = nsstring(&js), *mut Blk = &mut b);
})
.map_err(|e| e.to_string())?;
rx.recv_timeout(Duration::from_secs(15))
.map_err(|_| "eval timed out".to_string())?
}
pub fn screenshot<R: Runtime>(win: &WebviewWindow<R>) -> Result<Vec<u8>, String> {
let (tx, rx) = channel::<Result<Vec<u8>, String>>();
win.with_webview(move |pv| unsafe {
let wk = pv.inner();
let data = Box::into_raw(Box::new(tx)) as *mut c_void;
let mut b = block(shot_done as *const c_void, data);
msg!((); wk, "takeSnapshotWithConfiguration:completionHandler:", Id = std::ptr::null_mut(), *mut Blk = &mut b);
})
.map_err(|e| e.to_string())?;
rx.recv_timeout(Duration::from_secs(15))
.map_err(|_| "screenshot timed out".to_string())?
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn native_pointer_buttons_include_press_drag_and_release() {
for (button, mask) in [(Button::Left, 1), (Button::Right, 2)] {
assert_eq!(mouse_spec(Mouse::Down, button).3, mask);
assert_eq!(mouse_spec(Mouse::Drag, button).3, mask);
assert_eq!(mouse_spec(Mouse::Up, button).3, 0);
assert_eq!(mouse_spec(Mouse::Move, button).3, 0);
}
}
#[test]
fn pointer_state_is_nested_and_thread_local() {
assert_eq!(POINTER_BUTTONS.with(Cell::get), None);
let view = 1usize as Id;
let outer = PointerButtons(
POINTER_BUTTONS.with(|s| s.replace(Some(1))),
POINTER_VIEW.with(|s| s.replace(view)),
);
let inner = PointerButtons(
POINTER_BUTTONS.with(|s| s.replace(Some(0))),
POINTER_VIEW.with(|s| s.replace(std::ptr::null_mut())),
);
assert_eq!(POINTER_BUTTONS.with(Cell::get), Some(0));
drop(inner);
assert_eq!(POINTER_BUTTONS.with(Cell::get), Some(1));
assert_eq!(POINTER_VIEW.with(Cell::get), view);
std::thread::spawn(|| {
assert_eq!(POINTER_BUTTONS.with(Cell::get), None);
assert!(POINTER_VIEW.with(Cell::get).is_null());
})
.join()
.unwrap();
drop(outer);
assert_eq!(POINTER_BUTTONS.with(Cell::get), None);
assert!(POINTER_VIEW.with(Cell::get).is_null());
}
}