1 use std::{
2     ops::Deref,
3     sync::{Mutex, Weak},
4 };
5 
6 use cocoa::{
7     appkit::{CGFloat, NSScreen, NSWindow, NSWindowStyleMask},
8     base::{id, nil},
9     foundation::{NSPoint, NSSize, NSString},
10 };
11 use dispatch::Queue;
12 use objc::rc::autoreleasepool;
13 
14 use crate::{
15     dpi::LogicalSize,
16     platform_impl::platform::{ffi, util::IdRef, window::SharedState},
17 };
18 
19 // Unsafe wrapper type that allows us to dispatch things that aren't Send.
20 // This should *only* be used to dispatch to the main queue.
21 // While it is indeed not guaranteed that these types can safely be sent to
22 // other threads, we know that they're safe to use on the main thread.
23 struct MainThreadSafe<T>(T);
24 
25 unsafe impl<T> Send for MainThreadSafe<T> {}
26 
27 impl<T> Deref for MainThreadSafe<T> {
28     type Target = T;
deref(&self) -> &T29     fn deref(&self) -> &T {
30         &self.0
31     }
32 }
33 
set_style_mask(ns_window: id, ns_view: id, mask: NSWindowStyleMask)34 unsafe fn set_style_mask(ns_window: id, ns_view: id, mask: NSWindowStyleMask) {
35     ns_window.setStyleMask_(mask);
36     // If we don't do this, key handling will break
37     // (at least until the window is clicked again/etc.)
38     ns_window.makeFirstResponder_(ns_view);
39 }
40 
41 // Always use this function instead of trying to modify `styleMask` directly!
42 // `setStyleMask:` isn't thread-safe, so we have to use Grand Central Dispatch.
43 // Otherwise, this would vomit out errors about not being on the main thread
44 // and fail to do anything.
set_style_mask_async(ns_window: id, ns_view: id, mask: NSWindowStyleMask)45 pub unsafe fn set_style_mask_async(ns_window: id, ns_view: id, mask: NSWindowStyleMask) {
46     let ns_window = MainThreadSafe(ns_window);
47     let ns_view = MainThreadSafe(ns_view);
48     Queue::main().exec_async(move || {
49         set_style_mask(*ns_window, *ns_view, mask);
50     });
51 }
set_style_mask_sync(ns_window: id, ns_view: id, mask: NSWindowStyleMask)52 pub unsafe fn set_style_mask_sync(ns_window: id, ns_view: id, mask: NSWindowStyleMask) {
53     if msg_send![class!(NSThread), isMainThread] {
54         set_style_mask(ns_window, ns_view, mask);
55     } else {
56         let ns_window = MainThreadSafe(ns_window);
57         let ns_view = MainThreadSafe(ns_view);
58         Queue::main().exec_sync(move || {
59             set_style_mask(*ns_window, *ns_view, mask);
60         })
61     }
62 }
63 
64 // `setContentSize:` isn't thread-safe either, though it doesn't log any errors
65 // and just fails silently. Anyway, GCD to the rescue!
set_content_size_async(ns_window: id, size: LogicalSize<f64>)66 pub unsafe fn set_content_size_async(ns_window: id, size: LogicalSize<f64>) {
67     let ns_window = MainThreadSafe(ns_window);
68     Queue::main().exec_async(move || {
69         ns_window.setContentSize_(NSSize::new(size.width as CGFloat, size.height as CGFloat));
70     });
71 }
72 
73 // `setFrameTopLeftPoint:` isn't thread-safe, but fortunately has the courtesy
74 // to log errors.
set_frame_top_left_point_async(ns_window: id, point: NSPoint)75 pub unsafe fn set_frame_top_left_point_async(ns_window: id, point: NSPoint) {
76     let ns_window = MainThreadSafe(ns_window);
77     Queue::main().exec_async(move || {
78         ns_window.setFrameTopLeftPoint_(point);
79     });
80 }
81 
82 // `setFrameTopLeftPoint:` isn't thread-safe, and fails silently.
set_level_async(ns_window: id, level: ffi::NSWindowLevel)83 pub unsafe fn set_level_async(ns_window: id, level: ffi::NSWindowLevel) {
84     let ns_window = MainThreadSafe(ns_window);
85     Queue::main().exec_async(move || {
86         ns_window.setLevel_(level as _);
87     });
88 }
89 
90 // `toggleFullScreen` is thread-safe, but our additional logic to account for
91 // window styles isn't.
toggle_full_screen_async( ns_window: id, ns_view: id, not_fullscreen: bool, shared_state: Weak<Mutex<SharedState>>, )92 pub unsafe fn toggle_full_screen_async(
93     ns_window: id,
94     ns_view: id,
95     not_fullscreen: bool,
96     shared_state: Weak<Mutex<SharedState>>,
97 ) {
98     let ns_window = MainThreadSafe(ns_window);
99     let ns_view = MainThreadSafe(ns_view);
100     let shared_state = MainThreadSafe(shared_state);
101     Queue::main().exec_async(move || {
102         // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we
103         // set a normal style temporarily. The previous state will be
104         // restored in `WindowDelegate::window_did_exit_fullscreen`.
105         if not_fullscreen {
106             let curr_mask = ns_window.styleMask();
107             let required =
108                 NSWindowStyleMask::NSTitledWindowMask | NSWindowStyleMask::NSResizableWindowMask;
109             if !curr_mask.contains(required) {
110                 set_style_mask(*ns_window, *ns_view, required);
111                 if let Some(shared_state) = shared_state.upgrade() {
112                     trace!("Locked shared state in `toggle_full_screen_callback`");
113                     let mut shared_state_lock = shared_state.lock().unwrap();
114                     (*shared_state_lock).saved_style = Some(curr_mask);
115                     trace!("Unlocked shared state in `toggle_full_screen_callback`");
116                 }
117             }
118         }
119         // Window level must be restored from `CGShieldingWindowLevel()
120         // + 1` back to normal in order for `toggleFullScreen` to do
121         // anything
122         ns_window.setLevel_(0);
123         ns_window.toggleFullScreen_(nil);
124     });
125 }
126 
restore_display_mode_async(ns_screen: u32)127 pub unsafe fn restore_display_mode_async(ns_screen: u32) {
128     Queue::main().exec_async(move || {
129         ffi::CGRestorePermanentDisplayConfiguration();
130         assert_eq!(ffi::CGDisplayRelease(ns_screen), ffi::kCGErrorSuccess);
131     });
132 }
133 
134 // `setMaximized` is not thread-safe
set_maximized_async( ns_window: id, is_zoomed: bool, maximized: bool, shared_state: Weak<Mutex<SharedState>>, )135 pub unsafe fn set_maximized_async(
136     ns_window: id,
137     is_zoomed: bool,
138     maximized: bool,
139     shared_state: Weak<Mutex<SharedState>>,
140 ) {
141     let ns_window = MainThreadSafe(ns_window);
142     let shared_state = MainThreadSafe(shared_state);
143     Queue::main().exec_async(move || {
144         if let Some(shared_state) = shared_state.upgrade() {
145             trace!("Locked shared state in `set_maximized`");
146             let mut shared_state_lock = shared_state.lock().unwrap();
147 
148             // Save the standard frame sized if it is not zoomed
149             if !is_zoomed {
150                 shared_state_lock.standard_frame = Some(NSWindow::frame(*ns_window));
151             }
152 
153             shared_state_lock.maximized = maximized;
154 
155             let curr_mask = ns_window.styleMask();
156             if shared_state_lock.fullscreen.is_some() {
157                 // Handle it in window_did_exit_fullscreen
158                 return;
159             } else if curr_mask.contains(NSWindowStyleMask::NSResizableWindowMask) {
160                 // Just use the native zoom if resizable
161                 ns_window.zoom_(nil);
162             } else {
163                 // if it's not resizable, we set the frame directly
164                 let new_rect = if maximized {
165                     let screen = NSScreen::mainScreen(nil);
166                     NSScreen::visibleFrame(screen)
167                 } else {
168                     shared_state_lock.saved_standard_frame()
169                 };
170                 ns_window.setFrame_display_(new_rect, 0);
171             }
172 
173             trace!("Unlocked shared state in `set_maximized`");
174         }
175     });
176 }
177 
178 // `orderOut:` isn't thread-safe. Calling it from another thread actually works,
179 // but with an odd delay.
order_out_async(ns_window: id)180 pub unsafe fn order_out_async(ns_window: id) {
181     let ns_window = MainThreadSafe(ns_window);
182     Queue::main().exec_async(move || {
183         ns_window.orderOut_(nil);
184     });
185 }
186 
187 // `makeKeyAndOrderFront:` isn't thread-safe. Calling it from another thread
188 // actually works, but with an odd delay.
make_key_and_order_front_async(ns_window: id)189 pub unsafe fn make_key_and_order_front_async(ns_window: id) {
190     let ns_window = MainThreadSafe(ns_window);
191     Queue::main().exec_async(move || {
192         ns_window.makeKeyAndOrderFront_(nil);
193     });
194 }
195 
196 // `setTitle:` isn't thread-safe. Calling it from another thread invalidates the
197 // window drag regions, which throws an exception when not done in the main
198 // thread
set_title_async(ns_window: id, title: String)199 pub unsafe fn set_title_async(ns_window: id, title: String) {
200     let ns_window = MainThreadSafe(ns_window);
201     Queue::main().exec_async(move || {
202         let title = IdRef::new(NSString::alloc(nil).init_str(&title));
203         ns_window.setTitle_(*title);
204     });
205 }
206 
207 // `close:` is thread-safe, but we want the event to be triggered from the main
208 // thread. Though, it's a good idea to look into that more...
close_async(ns_window: id)209 pub unsafe fn close_async(ns_window: id) {
210     let ns_window = MainThreadSafe(ns_window);
211     Queue::main().exec_async(move || {
212         autoreleasepool(move || {
213             ns_window.close();
214         });
215     });
216 }
217