summaryrefslogtreecommitdiffhomepage
path: root/desktop/packages/nseventforwarder/nseventforwarder-rs/lib.rs
blob: bd73c450d52a1d277e9ba7205767d3e1aebdd0ac (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
//! Forward [NSEvent]s from macOS to node.
#![cfg(target_os = "macos")]

use std::ptr::NonNull;
use std::sync::{Arc, Mutex, mpsc};
use std::thread::JoinHandle;

use block2::RcBlock;
use neon::prelude::{
    Context, FunctionContext, JsFunction, JsNull, JsResult, ModuleContext, NeonResult, Object, Root,
};
use objc2_app_kit::{NSEvent, NSEventMask};

#[neon::main]
fn main(mut cx: ModuleContext<'_>) -> NeonResult<()> {
    cx.export_function("start", start)?;
    Ok(())
}

/// Register a callback to fire every time a [NSEventMask::LeftMouseDown] or [NSEventMask::RightMouseDown] event occur.
///
/// Returns a stop function to call when the original callback shouldn't be called anymore. This
/// stop function returns a `true` value when called the first time and the callback is
/// deregistered. If it were to be called yet again, it will keep returning `false`.
fn start(mut cx: FunctionContext<'_>) -> JsResult<'_, JsFunction> {
    // Set up neon stuff.
    // These will be moved into the spawned thread
    let nodejs_callback = cx.argument::<JsFunction>(0)?.root(&mut cx);
    let channel = cx.channel();
    // Start a long-running thread which handles incoming NS events
    // When a new event is received, call the callback passed to us from the JavaScript caller
    let (stop_tx, stop_rx) = mpsc::channel();
    let join_handle = std::thread::spawn(move || {
        // Each time the nodejs callback is triggered, we need to reference the nodejs
        // function reference. As such, we keep it from being garbage-collected with the
        // Root handle type and allow sharing it with the RCBlock via an Arc.
        let nodesjs_callback: Arc<Root<JsFunction>> = Arc::new(nodejs_callback);
        // Create a callback which will be called on the registered NSEvents.
        // When called schedules a closure to execute in nodejs thread that invoked start.
        let nsevent_callback = move |_nsevent: NonNull<NSEvent>| {
            let nodejs_callback = Arc::clone(&nodesjs_callback);
            channel.send(move |mut cx| {
                let this = JsNull::new(&mut cx);
                let _ = nodejs_callback.to_inner(&mut cx).call(&mut cx, this, []);
                Ok(())
            });
        };
        // Start monitoring incoming NS events
        // SAFETY: This function is trivially safe to call.
        // Note: Make sure to cancel this handler with [NSEvent::removeMonitor] to unregister the
        // listener.
        let mut handler = unsafe {
            NSEvent::addGlobalMonitorForEventsMatchingMask_handler(
                NSEventMask::LeftMouseDown | NSEventMask::RightMouseDown,
                &RcBlock::new(nsevent_callback),
            )
        };
        // Listen for stop signal
        let _ = stop_rx.recv();
        if let Some(handler) = handler.take() {
            // SAFETY: handler is removed only once.
            // See https://developer.apple.com/documentation/appkit/nsevent/1533709-removemonitor#discussion
            unsafe { NSEvent::removeMonitor(&handler) }
        }
        // The thread's execution will stop when this function returns
    });

    // NSEventForwarder instance. It must be cleaned up by the callback
    // function returned from `start` (aka `stop`). We use an Option here
    // because we can not enforce the Nodejs caller to only call `stop` once.
    let nseventforwarder = Mutex::new(Some(NSEventForwarder {
        thread: join_handle,
        stop: stop_tx,
    }));

    // Return a stop function to be invoked from the node runtime to deregister the NSEvent
    // callback.
    JsFunction::new(&mut cx, move |mut cx: FunctionContext<'_>| {
        // Stop this NSEventForwarder
        // Returns whether NSEventForwarder was stopped on this invocation of the stop function
        let mut stopped = false;
        if let Some(context) = nseventforwarder.lock().unwrap().take() {
            context.stop();
            stopped = true;
        }
        Ok(cx.boolean(stopped))
    })
}

struct NSEventForwarder {
    /// The thread listening for incoming [NSEvent]s.
    thread: JoinHandle<()>,
    /// Signal for the current execution context to stop.
    stop: mpsc::Sender<()>,
}

impl NSEventForwarder {
    fn stop(self) {
        // Tell the thread to stop running
        let _ = self.stop.send(());
        // Wait for the thread to shutdown
        self.thread
            .join()
            .expect("Couldn't join the NSEventForwarder thread");
    }
}