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()));