azul_winit/platform_impl/macos/
window_delegate.rs

1use std::{
2    f64,
3    os::raw::c_void,
4    sync::{atomic::Ordering, Arc, Weak},
5};
6
7use cocoa::{
8    appkit::{self, NSApplicationPresentationOptions, NSView, NSWindow},
9    base::{id, nil},
10    foundation::{NSAutoreleasePool, NSUInteger},
11};
12use objc::{
13    declare::ClassDecl,
14    runtime::{Class, Object, Sel, BOOL, NO, YES},
15};
16
17use crate::{
18    dpi::{LogicalPosition, LogicalSize},
19    event::{Event, ModifiersState, WindowEvent},
20    platform_impl::platform::{
21        app_state::AppState,
22        app_state::INTERRUPT_EVENT_LOOP_EXIT,
23        event::{EventProxy, EventWrapper},
24        util::{self, IdRef},
25        view::ViewState,
26        window::{get_window_id, UnownedWindow},
27    },
28    window::{Fullscreen, WindowId},
29};
30
31pub struct WindowDelegateState {
32    ns_window: IdRef, // never changes
33    ns_view: IdRef,   // never changes
34
35    window: Weak<UnownedWindow>,
36
37    // TODO: It's possible for delegate methods to be called asynchronously,
38    // causing data races / `RefCell` panics.
39
40    // This is set when WindowBuilder::with_fullscreen was set,
41    // see comments of `window_did_fail_to_enter_fullscreen`
42    initial_fullscreen: bool,
43
44    // During `windowDidResize`, we use this to only send Moved if the position changed.
45    previous_position: Option<(f64, f64)>,
46
47    // Used to prevent redundant events.
48    previous_scale_factor: f64,
49}
50
51impl WindowDelegateState {
52    pub fn new(window: &Arc<UnownedWindow>, initial_fullscreen: bool) -> Self {
53        let scale_factor = window.scale_factor();
54        let mut delegate_state = WindowDelegateState {
55            ns_window: window.ns_window.clone(),
56            ns_view: window.ns_view.clone(),
57            window: Arc::downgrade(&window),
58            initial_fullscreen,
59            previous_position: None,
60            previous_scale_factor: scale_factor,
61        };
62
63        if scale_factor != 1.0 {
64            delegate_state.emit_static_scale_factor_changed_event();
65        }
66
67        delegate_state
68    }
69
70    fn with_window<F, T>(&mut self, callback: F) -> Option<T>
71    where
72        F: FnOnce(&UnownedWindow) -> T,
73    {
74        self.window.upgrade().map(|ref window| callback(window))
75    }
76
77    pub fn emit_event(&mut self, event: WindowEvent<'static>) {
78        let event = Event::WindowEvent {
79            window_id: WindowId(get_window_id(*self.ns_window)),
80            event,
81        };
82        AppState::queue_event(EventWrapper::StaticEvent(event));
83    }
84
85    pub fn emit_static_scale_factor_changed_event(&mut self) {
86        let scale_factor = self.get_scale_factor();
87        if scale_factor == self.previous_scale_factor {
88            return ();
89        };
90
91        self.previous_scale_factor = scale_factor;
92        let wrapper = EventWrapper::EventProxy(EventProxy::DpiChangedProxy {
93            ns_window: IdRef::retain(*self.ns_window),
94            suggested_size: self.view_size(),
95            scale_factor,
96        });
97        AppState::queue_event(wrapper);
98    }
99
100    pub fn emit_resize_event(&mut self) {
101        let rect = unsafe { NSView::frame(*self.ns_view) };
102        let scale_factor = self.get_scale_factor();
103        let logical_size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64);
104        let size = logical_size.to_physical(scale_factor);
105        self.emit_event(WindowEvent::Resized(size));
106    }
107
108    fn emit_move_event(&mut self) {
109        let rect = unsafe { NSWindow::frame(*self.ns_window) };
110        let x = rect.origin.x as f64;
111        let y = util::bottom_left_to_top_left(rect);
112        let moved = self.previous_position != Some((x, y));
113        if moved {
114            self.previous_position = Some((x, y));
115            let scale_factor = self.get_scale_factor();
116            let physical_pos = LogicalPosition::<f64>::from((x, y)).to_physical(scale_factor);
117            self.emit_event(WindowEvent::Moved(physical_pos));
118        }
119    }
120
121    fn get_scale_factor(&self) -> f64 {
122        (unsafe { NSWindow::backingScaleFactor(*self.ns_window) }) as f64
123    }
124
125    fn view_size(&self) -> LogicalSize<f64> {
126        let ns_size = unsafe { NSView::frame(*self.ns_view).size };
127        LogicalSize::new(ns_size.width as f64, ns_size.height as f64)
128    }
129}
130
131pub fn new_delegate(window: &Arc<UnownedWindow>, initial_fullscreen: bool) -> IdRef {
132    let state = WindowDelegateState::new(window, initial_fullscreen);
133    unsafe {
134        // This is free'd in `dealloc`
135        let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void;
136        let delegate: id = msg_send![WINDOW_DELEGATE_CLASS.0, alloc];
137        IdRef::new(msg_send![delegate, initWithWinit: state_ptr])
138    }
139}
140
141struct WindowDelegateClass(*const Class);
142unsafe impl Send for WindowDelegateClass {}
143unsafe impl Sync for WindowDelegateClass {}
144
145lazy_static! {
146    static ref WINDOW_DELEGATE_CLASS: WindowDelegateClass = unsafe {
147        let superclass = class!(NSResponder);
148        let mut decl = ClassDecl::new("WinitWindowDelegate", superclass).unwrap();
149
150        decl.add_method(sel!(dealloc), dealloc as extern "C" fn(&Object, Sel));
151        decl.add_method(
152            sel!(initWithWinit:),
153            init_with_winit as extern "C" fn(&Object, Sel, *mut c_void) -> id,
154        );
155
156        decl.add_method(
157            sel!(windowShouldClose:),
158            window_should_close as extern "C" fn(&Object, Sel, id) -> BOOL,
159        );
160        decl.add_method(
161            sel!(windowWillClose:),
162            window_will_close as extern "C" fn(&Object, Sel, id),
163        );
164        decl.add_method(
165            sel!(windowDidResize:),
166            window_did_resize as extern "C" fn(&Object, Sel, id),
167        );
168        decl.add_method(
169            sel!(windowDidMove:),
170            window_did_move as extern "C" fn(&Object, Sel, id),
171        );
172        decl.add_method(
173            sel!(windowDidChangeBackingProperties:),
174            window_did_change_backing_properties as extern "C" fn(&Object, Sel, id),
175        );
176        decl.add_method(
177            sel!(windowDidBecomeKey:),
178            window_did_become_key as extern "C" fn(&Object, Sel, id),
179        );
180        decl.add_method(
181            sel!(windowDidResignKey:),
182            window_did_resign_key as extern "C" fn(&Object, Sel, id),
183        );
184
185        decl.add_method(
186            sel!(draggingEntered:),
187            dragging_entered as extern "C" fn(&Object, Sel, id) -> BOOL,
188        );
189        decl.add_method(
190            sel!(prepareForDragOperation:),
191            prepare_for_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
192        );
193        decl.add_method(
194            sel!(performDragOperation:),
195            perform_drag_operation as extern "C" fn(&Object, Sel, id) -> BOOL,
196        );
197        decl.add_method(
198            sel!(concludeDragOperation:),
199            conclude_drag_operation as extern "C" fn(&Object, Sel, id),
200        );
201        decl.add_method(
202            sel!(draggingExited:),
203            dragging_exited as extern "C" fn(&Object, Sel, id),
204        );
205
206        decl.add_method(
207            sel!(window:willUseFullScreenPresentationOptions:),
208            window_will_use_fullscreen_presentation_options
209                as extern "C" fn(&Object, Sel, id, NSUInteger) -> NSUInteger,
210        );
211        decl.add_method(
212            sel!(windowDidEnterFullScreen:),
213            window_did_enter_fullscreen as extern "C" fn(&Object, Sel, id),
214        );
215        decl.add_method(
216            sel!(windowWillEnterFullScreen:),
217            window_will_enter_fullscreen as extern "C" fn(&Object, Sel, id),
218        );
219        decl.add_method(
220            sel!(windowDidExitFullScreen:),
221            window_did_exit_fullscreen as extern "C" fn(&Object, Sel, id),
222        );
223        decl.add_method(
224            sel!(windowWillExitFullScreen:),
225            window_will_exit_fullscreen as extern "C" fn(&Object, Sel, id),
226        );
227        decl.add_method(
228            sel!(windowDidFailToEnterFullScreen:),
229            window_did_fail_to_enter_fullscreen as extern "C" fn(&Object, Sel, id),
230        );
231
232        decl.add_ivar::<*mut c_void>("winitState");
233        WindowDelegateClass(decl.register())
234    };
235}
236
237// This function is definitely unsafe, but labeling that would increase
238// boilerplate and wouldn't really clarify anything...
239fn with_state<F: FnOnce(&mut WindowDelegateState) -> T, T>(this: &Object, callback: F) {
240    let state_ptr = unsafe {
241        let state_ptr: *mut c_void = *this.get_ivar("winitState");
242        &mut *(state_ptr as *mut WindowDelegateState)
243    };
244    callback(state_ptr);
245}
246
247extern "C" fn dealloc(this: &Object, _sel: Sel) {
248    with_state(this, |state| unsafe {
249        Box::from_raw(state as *mut WindowDelegateState);
250    });
251}
252
253extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> id {
254    unsafe {
255        let this: id = msg_send![this, init];
256        if this != nil {
257            (*this).set_ivar("winitState", state);
258            with_state(&*this, |state| {
259                let () = msg_send![*state.ns_window, setDelegate: this];
260            });
261        }
262        this
263    }
264}
265
266extern "C" fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL {
267    trace!("Triggered `windowShouldClose:`");
268    with_state(this, |state| state.emit_event(WindowEvent::CloseRequested));
269    trace!("Completed `windowShouldClose:`");
270    NO
271}
272
273extern "C" fn window_will_close(this: &Object, _: Sel, _: id) {
274    trace!("Triggered `windowWillClose:`");
275    with_state(this, |state| unsafe {
276        // `setDelegate:` retains the previous value and then autoreleases it
277        let pool = NSAutoreleasePool::new(nil);
278        // Since El Capitan, we need to be careful that delegate methods can't
279        // be called after the window closes.
280        let () = msg_send![*state.ns_window, setDelegate: nil];
281        pool.drain();
282        state.emit_event(WindowEvent::Destroyed);
283    });
284    trace!("Completed `windowWillClose:`");
285}
286
287extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
288    trace!("Triggered `windowDidResize:`");
289    with_state(this, |state| {
290        state.emit_resize_event();
291        state.emit_move_event();
292    });
293    trace!("Completed `windowDidResize:`");
294}
295
296// This won't be triggered if the move was part of a resize.
297extern "C" fn window_did_move(this: &Object, _: Sel, _: id) {
298    trace!("Triggered `windowDidMove:`");
299    with_state(this, |state| {
300        state.emit_move_event();
301    });
302    trace!("Completed `windowDidMove:`");
303}
304
305extern "C" fn window_did_change_backing_properties(this: &Object, _: Sel, _: id) {
306    trace!("Triggered `windowDidChangeBackingProperties:`");
307    with_state(this, |state| {
308        state.emit_static_scale_factor_changed_event();
309    });
310    trace!("Completed `windowDidChangeBackingProperties:`");
311}
312
313extern "C" fn window_did_become_key(this: &Object, _: Sel, _: id) {
314    trace!("Triggered `windowDidBecomeKey:`");
315    with_state(this, |state| {
316        // TODO: center the cursor if the window had mouse grab when it
317        // lost focus
318        state.emit_event(WindowEvent::Focused(true));
319    });
320    trace!("Completed `windowDidBecomeKey:`");
321}
322
323extern "C" fn window_did_resign_key(this: &Object, _: Sel, _: id) {
324    trace!("Triggered `windowDidResignKey:`");
325    with_state(this, |state| {
326        // It happens rather often, e.g. when the user is Cmd+Tabbing, that the
327        // NSWindowDelegate will receive a didResignKey event despite no event
328        // being received when the modifiers are released.  This is because
329        // flagsChanged events are received by the NSView instead of the
330        // NSWindowDelegate, and as a result a tracked modifiers state can quite
331        // easily fall out of synchrony with reality.  This requires us to emit
332        // a synthetic ModifiersChanged event when we lose focus.
333        //
334        // Here we (very unsafely) acquire the winitState (a ViewState) from the
335        // Object referenced by state.ns_view (an IdRef, which is dereferenced
336        // to an id)
337        let view_state: &mut ViewState = unsafe {
338            let ns_view: &Object = (*state.ns_view).as_ref().expect("failed to deref");
339            let state_ptr: *mut c_void = *ns_view.get_ivar("winitState");
340            &mut *(state_ptr as *mut ViewState)
341        };
342
343        // Both update the state and emit a ModifiersChanged event.
344        if !view_state.modifiers.is_empty() {
345            view_state.modifiers = ModifiersState::empty();
346            state.emit_event(WindowEvent::ModifiersChanged(view_state.modifiers));
347        }
348
349        state.emit_event(WindowEvent::Focused(false));
350    });
351    trace!("Completed `windowDidResignKey:`");
352}
353
354/// Invoked when the dragged image enters destination bounds or frame
355extern "C" fn dragging_entered(this: &Object, _: Sel, sender: id) -> BOOL {
356    trace!("Triggered `draggingEntered:`");
357
358    use cocoa::{appkit::NSPasteboard, foundation::NSFastEnumeration};
359    use std::path::PathBuf;
360
361    let pb: id = unsafe { msg_send![sender, draggingPasteboard] };
362    let filenames = unsafe { NSPasteboard::propertyListForType(pb, appkit::NSFilenamesPboardType) };
363
364    for file in unsafe { filenames.iter() } {
365        use cocoa::foundation::NSString;
366        use std::ffi::CStr;
367
368        unsafe {
369            let f = NSString::UTF8String(file);
370            let path = CStr::from_ptr(f).to_string_lossy().into_owned();
371
372            with_state(this, |state| {
373                state.emit_event(WindowEvent::HoveredFile(PathBuf::from(path)));
374            });
375        }
376    }
377
378    trace!("Completed `draggingEntered:`");
379    YES
380}
381
382/// Invoked when the image is released
383extern "C" fn prepare_for_drag_operation(_: &Object, _: Sel, _: id) -> BOOL {
384    trace!("Triggered `prepareForDragOperation:`");
385    trace!("Completed `prepareForDragOperation:`");
386    YES
387}
388
389/// Invoked after the released image has been removed from the screen
390extern "C" fn perform_drag_operation(this: &Object, _: Sel, sender: id) -> BOOL {
391    trace!("Triggered `performDragOperation:`");
392
393    use cocoa::{appkit::NSPasteboard, foundation::NSFastEnumeration};
394    use std::path::PathBuf;
395
396    let pb: id = unsafe { msg_send![sender, draggingPasteboard] };
397    let filenames = unsafe { NSPasteboard::propertyListForType(pb, appkit::NSFilenamesPboardType) };
398
399    for file in unsafe { filenames.iter() } {
400        use cocoa::foundation::NSString;
401        use std::ffi::CStr;
402
403        unsafe {
404            let f = NSString::UTF8String(file);
405            let path = CStr::from_ptr(f).to_string_lossy().into_owned();
406
407            with_state(this, |state| {
408                state.emit_event(WindowEvent::DroppedFile(PathBuf::from(path)));
409            });
410        }
411    }
412
413    trace!("Completed `performDragOperation:`");
414    YES
415}
416
417/// Invoked when the dragging operation is complete
418extern "C" fn conclude_drag_operation(_: &Object, _: Sel, _: id) {
419    trace!("Triggered `concludeDragOperation:`");
420    trace!("Completed `concludeDragOperation:`");
421}
422
423/// Invoked when the dragging operation is cancelled
424extern "C" fn dragging_exited(this: &Object, _: Sel, _: id) {
425    trace!("Triggered `draggingExited:`");
426    with_state(this, |state| {
427        state.emit_event(WindowEvent::HoveredFileCancelled)
428    });
429    trace!("Completed `draggingExited:`");
430}
431
432/// Invoked when before enter fullscreen
433extern "C" fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) {
434    trace!("Triggered `windowWillEnterFullscreen:`");
435
436    INTERRUPT_EVENT_LOOP_EXIT.store(true, Ordering::SeqCst);
437
438    with_state(this, |state| {
439        state.with_window(|window| {
440            trace!("Locked shared state in `window_will_enter_fullscreen`");
441            let mut shared_state = window.shared_state.lock().unwrap();
442            shared_state.maximized = window.is_zoomed();
443            match shared_state.fullscreen {
444                // Exclusive mode sets the state in `set_fullscreen` as the user
445                // can't enter exclusive mode by other means (like the
446                // fullscreen button on the window decorations)
447                Some(Fullscreen::Exclusive(_)) => (),
448                // `window_will_enter_fullscreen` was triggered and we're already
449                // in fullscreen, so we must've reached here by `set_fullscreen`
450                // as it updates the state
451                Some(Fullscreen::Borderless(_)) => (),
452                // Otherwise, we must've reached fullscreen by the user clicking
453                // on the green fullscreen button. Update state!
454                None => {
455                    let current_monitor = Some(window.current_monitor_inner());
456                    shared_state.fullscreen = Some(Fullscreen::Borderless(current_monitor))
457                }
458            }
459            shared_state.in_fullscreen_transition = true;
460            trace!("Unlocked shared state in `window_will_enter_fullscreen`");
461        })
462    });
463    trace!("Completed `windowWillEnterFullscreen:`");
464}
465
466/// Invoked when before exit fullscreen
467extern "C" fn window_will_exit_fullscreen(this: &Object, _: Sel, _: id) {
468    trace!("Triggered `windowWillExitFullScreen:`");
469
470    INTERRUPT_EVENT_LOOP_EXIT.store(true, Ordering::SeqCst);
471
472    with_state(this, |state| {
473        state.with_window(|window| {
474            trace!("Locked shared state in `window_will_exit_fullscreen`");
475            let mut shared_state = window.shared_state.lock().unwrap();
476            shared_state.in_fullscreen_transition = true;
477            trace!("Unlocked shared state in `window_will_exit_fullscreen`");
478        });
479    });
480    trace!("Completed `windowWillExitFullScreen:`");
481}
482
483extern "C" fn window_will_use_fullscreen_presentation_options(
484    _this: &Object,
485    _: Sel,
486    _: id,
487    _proposed_options: NSUInteger,
488) -> NSUInteger {
489    // Generally, games will want to disable the menu bar and the dock. Ideally,
490    // this would be configurable by the user. Unfortunately because of our
491    // `CGShieldingWindowLevel() + 1` hack (see `set_fullscreen`), our window is
492    // placed on top of the menu bar in exclusive fullscreen mode. This looks
493    // broken so we always disable the menu bar in exclusive fullscreen. We may
494    // still want to make this configurable for borderless fullscreen. Right now
495    // we don't, for consistency. If we do, it should be documented that the
496    // user-provided options are ignored in exclusive fullscreen.
497    (NSApplicationPresentationOptions::NSApplicationPresentationFullScreen
498        | NSApplicationPresentationOptions::NSApplicationPresentationHideDock
499        | NSApplicationPresentationOptions::NSApplicationPresentationHideMenuBar)
500        .bits()
501}
502
503/// Invoked when entered fullscreen
504extern "C" fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) {
505    INTERRUPT_EVENT_LOOP_EXIT.store(false, Ordering::SeqCst);
506
507    trace!("Triggered `windowDidEnterFullscreen:`");
508    with_state(this, |state| {
509        state.initial_fullscreen = false;
510        state.with_window(|window| {
511            trace!("Locked shared state in `window_did_enter_fullscreen`");
512            let mut shared_state = window.shared_state.lock().unwrap();
513            shared_state.in_fullscreen_transition = false;
514            let target_fullscreen = shared_state.target_fullscreen.take();
515            trace!("Unlocked shared state in `window_did_enter_fullscreen`");
516            drop(shared_state);
517            if let Some(target_fullscreen) = target_fullscreen {
518                window.set_fullscreen(target_fullscreen);
519            }
520        });
521    });
522    trace!("Completed `windowDidEnterFullscreen:`");
523}
524
525/// Invoked when exited fullscreen
526extern "C" fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id) {
527    INTERRUPT_EVENT_LOOP_EXIT.store(false, Ordering::SeqCst);
528
529    trace!("Triggered `windowDidExitFullscreen:`");
530    with_state(this, |state| {
531        state.with_window(|window| {
532            window.restore_state_from_fullscreen();
533            trace!("Locked shared state in `window_did_exit_fullscreen`");
534            let mut shared_state = window.shared_state.lock().unwrap();
535            shared_state.in_fullscreen_transition = false;
536            let target_fullscreen = shared_state.target_fullscreen.take();
537            trace!("Unlocked shared state in `window_did_exit_fullscreen`");
538            drop(shared_state);
539            if let Some(target_fullscreen) = target_fullscreen {
540                window.set_fullscreen(target_fullscreen);
541            }
542        })
543    });
544    trace!("Completed `windowDidExitFullscreen:`");
545}
546
547/// Invoked when fail to enter fullscreen
548///
549/// When this window launch from a fullscreen app (e.g. launch from VS Code
550/// terminal), it creates a new virtual destkop and a transition animation.
551/// This animation takes one second and cannot be disable without
552/// elevated privileges. In this animation time, all toggleFullscreen events
553/// will be failed. In this implementation, we will try again by using
554/// performSelector:withObject:afterDelay: until window_did_enter_fullscreen.
555/// It should be fine as we only do this at initialzation (i.e with_fullscreen
556/// was set).
557///
558/// From Apple doc:
559/// In some cases, the transition to enter full-screen mode can fail,
560/// due to being in the midst of handling some other animation or user gesture.
561/// This method indicates that there was an error, and you should clean up any
562/// work you may have done to prepare to enter full-screen mode.
563extern "C" fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) {
564    trace!("Triggered `windowDidFailToEnterFullscreen:`");
565    with_state(this, |state| {
566        state.with_window(|window| {
567            trace!("Locked shared state in `window_did_fail_to_enter_fullscreen`");
568            let mut shared_state = window.shared_state.lock().unwrap();
569            shared_state.in_fullscreen_transition = false;
570            shared_state.target_fullscreen = None;
571            trace!("Unlocked shared state in `window_did_fail_to_enter_fullscreen`");
572        });
573        if state.initial_fullscreen {
574            let _: () = unsafe {
575                msg_send![*state.ns_window,
576                    performSelector:sel!(toggleFullScreen:)
577                    withObject:nil
578                    afterDelay: 0.5
579                ]
580            };
581        } else {
582            state.with_window(|window| window.restore_state_from_fullscreen());
583        }
584    });
585    trace!("Completed `windowDidFailToEnterFullscreen:`");
586}