tauri_nspanel/
event.rs

1/// Macro to create a custom event handler for panels
2///
3/// This creates an NSWindowDelegate that responds to window events with type-safe callbacks.
4///
5/// This macro generates a type-safe delegate class with strongly typed callbacks.
6/// Users specify parameter types directly in the macro, and callbacks receive
7/// properly typed arguments instead of raw pointers.
8///
9/// # References
10///
11/// - [objc2 NSWindowDelegate trait documentation](https://docs.rs/objc2-app-kit/0.3.1/objc2_app_kit/trait.NSWindowDelegate.html)
12/// - [Apple NSWindowDelegate documentation](https://developer.apple.com/documentation/appkit/nswindowdelegate)
13///
14/// # Selector Generation Rules
15///
16/// The macro generates Objective-C selectors based on how you declare the methods:
17///
18/// - **Single parameter**: `method_name(param: Type)` → `methodName:`
19///   - Example: `window_did_become_key(notification: &NSNotification)` → `windowDidBecomeKey:`
20///
21/// - **Multiple parameters**: `method_name(first: Type1, second: Type2)` → `methodName:second:`
22///   - Example: `window_will_resize(window: &NSWindow, to_size: &NSSize)` → `windowWillResize:toSize:`
23///   - The first parameter is always anonymous (`:`) in the selector
24///   - Subsequent parameters use their names as selector parts
25///
26/// **Parameter Naming**: The macro automatically converts snake_case to camelCase:
27/// - `to_size` → `toSize`
28/// - `to_object` → `toObject`
29/// - `for_client` → `forClient`
30///
31/// You can use Rust's conventional snake_case naming and the macro will generate
32/// the correct camelCase selectors to match Apple's NSWindowDelegate signatures.
33///
34/// **Important**: Always check the references above to ensure your method names and
35/// parameter names match the exact NSWindowDelegate protocol signatures.
36///
37/// Usage:
38/// ```
39/// use tauri_nspanel::tauri_panel;
40///
41/// tauri_panel! {
42///     panel_event!(MyPanelEventHandler {
43///         window_did_become_key(notification: &NSNotification) -> (),
44///         window_should_close(window: &NSWindow) -> Bool,
45///         window_will_resize(window: &NSWindow, to_size: &NSSize) -> NSSize,
46///         window_will_return_field_editor(sender: &NSWindow, client: Option<&AnyObject>) -> Option<Retained<NSObject>>
47///     })
48/// }
49///
50/// let handler = MyPanelEventHandler::new();
51///
52/// handler.window_did_become_key(|notification| {
53///     // notification is already typed as &NSNotification
54///     println!("Window became key: {:?}", notification);
55/// });
56///
57/// handler.window_should_close(|window| {
58///     // window is already typed as &NSWindow
59///     println!("Should close window: {:?}?", window);
60///     Bool::new(true) // Allow closing
61/// });
62/// ```
63///
64/// # Type Specification
65///
66/// You must specify the exact types for parameters, including references:
67/// - Object types usually take references: `&NSWindow`, `&NSNotification`
68/// - Value types also take references: `&NSSize`, `&NSRect`, `&NSPoint`
69/// - Optional parameters: `Option<&AnyObject>`
70/// - The macro does NOT automatically add references
71///
72/// ## Return Values
73/// Methods must specify their return type explicitly:
74/// - `-> ()` for void methods (no return value)
75/// - `-> Bool` for BOOL returns (objc2 Bool type)
76/// - `-> Option<Retained<NSObject>>` for nullable object returns
77/// - `-> NSSize` for NSSize value returns
78/// - `-> NSRect` for NSRect value returns
79/// - `-> NSPoint` for NSPoint value returns
80/// - Other types as needed by the delegate method
81///
82/// The macro handles conversion between Rust types and Objective-C types automatically.
83#[macro_export]
84macro_rules! panel_event {
85    (
86        $handler_name:ident {
87            $(
88                $method:ident ( $first_param:ident : $first_type:ty $(, $param:ident : $param_type:ty)* $(,)? ) -> $return_type:ty
89            ),* $(,)?
90        }
91    ) => {
92        $crate::pastey::paste! {
93                // Generate typed callback signatures for each method
94                $(
95                    pub type [<$handler_name $method:camel Callback>] = std::boxed::Box<
96                        dyn Fn($first_type $(, $param_type)*) -> $return_type
97                    >;
98                )*
99
100                struct [<$handler_name Ivars>] {
101                   $(
102                       [<$method:snake>]: std::cell::Cell<Option<[<$handler_name $method:camel Callback>]>>,
103                   )*
104                   // Mouse event callbacks
105                   mouse_entered_callback: std::cell::Cell<Option<Box<dyn Fn(&$crate::objc2_app_kit::NSEvent)>>>,
106                   mouse_exited_callback: std::cell::Cell<Option<Box<dyn Fn(&$crate::objc2_app_kit::NSEvent)>>>,
107                   mouse_moved_callback: std::cell::Cell<Option<Box<dyn Fn(&$crate::objc2_app_kit::NSEvent)>>>,
108                   cursor_update_callback: std::cell::Cell<Option<Box<dyn Fn(&$crate::objc2_app_kit::NSEvent)>>>,
109                }
110
111                define_class!(
112                    #[unsafe(super(NSObject))]
113                    #[name = stringify!($handler_name)]
114                    #[thread_kind = MainThreadOnly]
115
116                    #[ivars = [<$handler_name Ivars>]]
117                    struct $handler_name;
118
119                    unsafe impl NSObjectProtocol for $handler_name {}
120
121                    unsafe impl NSWindowDelegate for $handler_name {
122                        $(
123                            #[doc = concat!(" Objective-C delegate method: ", stringify!($method), "_:", $(stringify!([<$param:lower_camel>]), ":"),*)]
124                            #[allow(non_snake_case)]
125                            #[unsafe(method([<$method:lower_camel>]:$([<$param:lower_camel>]:)*))]
126                            fn [<__ $method:snake>](&self, [<$first_param:lower_camel>]: $first_type $(, [<$param:lower_camel>]: $param_type )* ) -> $return_type {
127                                // Take the callback from the cell temporarily
128                                let callback = self.ivars().[<$method:snake>].take();
129                                if let Some(callback) = callback {
130                                    // Call the callback with typed parameters
131                                    let result = callback([<$first_param:lower_camel>] $(, [<$param:lower_camel>])*);
132
133                                    // Put the callback back
134                                    self.ivars().[<$method:snake>].set(Some(callback));
135
136                                    result
137                                } else {
138                                    // Return default value for the type
139                                    Default::default()
140                                }
141                            }
142                        )*
143                    }
144
145                    impl $handler_name {
146                        // Mouse event methods
147                        #[unsafe(method(mouseEntered:))]
148                        fn mouse_entered(&self, event: &$crate::objc2_app_kit::NSEvent) {
149                            let ivars = self.ivars();
150                            if let Some(callback) = ivars.mouse_entered_callback.take() {
151                                callback(event);
152                                ivars.mouse_entered_callback.set(Some(callback));
153                            }
154                        }
155
156                        #[unsafe(method(mouseExited:))]
157                        fn mouse_exited(&self, event: &$crate::objc2_app_kit::NSEvent) {
158                            let ivars = self.ivars();
159                            if let Some(callback) = ivars.mouse_exited_callback.take() {
160                                callback(event);
161                                ivars.mouse_exited_callback.set(Some(callback));
162                            }
163                        }
164
165                        #[unsafe(method(mouseMoved:))]
166                        fn mouse_moved(&self, event: &$crate::objc2_app_kit::NSEvent) {
167                            let ivars = self.ivars();
168                            if let Some(callback) = ivars.mouse_moved_callback.take() {
169                                callback(event);
170                                ivars.mouse_moved_callback.set(Some(callback));
171                            }
172                        }
173
174                        #[unsafe(method(cursorUpdate:))]
175                        fn cursor_update(&self, event: &$crate::objc2_app_kit::NSEvent) {
176                            let ivars = self.ivars();
177                            if let Some(callback) = ivars.cursor_update_callback.take() {
178                                callback(event);
179                                ivars.cursor_update_callback.set(Some(callback));
180                            }
181                        }
182                    }
183                );
184
185                impl $handler_name {
186                    /// Create a new event handler instance
187                    pub fn new() -> Retained<Self> {
188                        unsafe {
189                            // Get main thread marker
190                            let mtm = MainThreadMarker::new().expect("Must be on main thread");
191
192                            // Allocate instance
193                            let this = Self::alloc(mtm);
194                            // Set ivars
195                            let this = this.set_ivars([<$handler_name Ivars>] {
196                                $(
197                                    [<$method:snake>]: std::cell::Cell::new(None),
198                                )*
199                                mouse_entered_callback: std::cell::Cell::new(None),
200                                mouse_exited_callback: std::cell::Cell::new(None),
201                                mouse_moved_callback: std::cell::Cell::new(None),
202                                cursor_update_callback: std::cell::Cell::new(None),
203                            });
204                            // Initialize
205                            msg_send![super(this), init]
206                        }
207                    }
208
209                    $(
210                        #[doc = " A callback for the `" $method "` event"]
211                        pub fn [<$method:snake>]<F>(&self, callback: F)
212                        where
213                            F: Fn($first_type $(, $param_type)*) -> $return_type + 'static
214                        {
215                            let boxed_callback: [<$handler_name $method:camel Callback>] = std::boxed::Box::new(callback);
216
217                            // Store new callback
218                            self.ivars().[<$method:snake>].set(Some(boxed_callback));
219                        }
220                    )*
221
222                    // Mouse event handlers
223                    /// Set the mouse entered callback
224                    pub fn on_mouse_entered<F>(&self, callback: F)
225                    where
226                        F: Fn(&$crate::objc2_app_kit::NSEvent) + 'static
227                    {
228                        self.ivars().mouse_entered_callback.set(Some(Box::new(callback)));
229                    }
230
231                    /// Set the mouse exited callback
232                    pub fn on_mouse_exited<F>(&self, callback: F)
233                    where
234                        F: Fn(&$crate::objc2_app_kit::NSEvent) + 'static
235                    {
236                        self.ivars().mouse_exited_callback.set(Some(Box::new(callback)));
237                    }
238
239                    /// Set the mouse moved callback
240                    pub fn on_mouse_moved<F>(&self, callback: F)
241                    where
242                        F: Fn(&$crate::objc2_app_kit::NSEvent) + 'static
243                    {
244                        self.ivars().mouse_moved_callback.set(Some(Box::new(callback)));
245                    }
246
247                    /// Set the cursor update callback
248                    pub fn on_cursor_update<F>(&self, callback: F)
249                    where
250                        F: Fn(&$crate::objc2_app_kit::NSEvent) + 'static
251                    {
252                        self.ivars().cursor_update_callback.set(Some(Box::new(callback)));
253                    }
254                }
255
256                /// Implement AsRef for idiomatic conversion to ProtocolObject
257                impl std::convert::AsRef<ProtocolObject<dyn NSWindowDelegate>> for $handler_name {
258                    fn as_ref(&self) -> &ProtocolObject<dyn NSWindowDelegate> {
259                        unsafe {
260                            ProtocolObject::from_ref(self)
261                        }
262                    }
263                }
264        }
265    };
266}
267
268// Example usage:
269//
270// use tauri_nspanel::tauri_panel;
271//
272// tauri_panel! {
273//     panel_event!(MyPanelEventHandler {
274//         window_did_become_key(notification: &NSNotification) -> (),
275//         window_will_close(window: &NSWindow) -> (),
276//         window_should_close(window: &NSWindow) -> Bool,
277//         window_will_resize(window: &NSWindow, to_size: &NSSize) -> NSSize
278//     })
279// }
280//
281// let handler = MyPanelEventHandler::new();
282//
283// // Example: Handle window_did_become_key notification
284// handler.window_did_become_key(|notification| {
285//     println!("Window became key with notification: {:?}", notification);
286// });
287//
288// // Example: Handle window_should_close with bool return
289// handler.window_should_close(|window| {
290//     println!("Should close window?");
291//     Bool::new(true) // Allow closing
292// });
293//
294// // Example: Handle window_will_resize with NSSize return
295// handler.window_will_resize(|window, proposed_size| {
296//     // Enforce minimum size
297//     NSSize {
298//         width: proposed_size.width.max(200.0),
299//         height: proposed_size.height.max(100.0),
300//     }
301// });
302//
303// // Use with panel
304// panel.set_event_handler(Some(handler.as_ref()));