1use crate::{
2 executor,
3 geometry::vector::Vector2F,
4 platform::{self, Event},
5 Scene,
6};
7use anyhow::{anyhow, Result};
8use cocoa::{
9 appkit::{
10 NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable,
11 NSWindow, NSWindowStyleMask,
12 },
13 base::{id, nil},
14 foundation::{NSAutoreleasePool, NSSize, NSString},
15};
16use ctor::ctor;
17use objc::{
18 class,
19 declare::ClassDecl,
20 msg_send,
21 runtime::{Class, Object, Sel, BOOL, NO, YES},
22 sel, sel_impl,
23};
24use pathfinder_geometry::vector::vec2f;
25use smol::Timer;
26use std::{
27 cell::{Cell, RefCell},
28 ffi::c_void,
29 mem, ptr,
30 rc::Rc,
31 time::{Duration, Instant},
32};
33
34use super::geometry::RectFExt;
35
36const WINDOW_STATE_IVAR: &'static str = "windowState";
37
38static mut WINDOW_CLASS: *const Class = ptr::null();
39static mut VIEW_CLASS: *const Class = ptr::null();
40static mut DELEGATE_CLASS: *const Class = ptr::null();
41
42#[ctor]
43unsafe fn build_classes() {
44 WINDOW_CLASS = {
45 let mut decl = ClassDecl::new("GPUIWindow", class!(NSWindow)).unwrap();
46 decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
47 decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel));
48 decl.add_method(
49 sel!(canBecomeMainWindow),
50 yes as extern "C" fn(&Object, Sel) -> BOOL,
51 );
52 decl.add_method(
53 sel!(canBecomeKeyWindow),
54 yes as extern "C" fn(&Object, Sel) -> BOOL,
55 );
56 decl.add_method(
57 sel!(sendEvent:),
58 send_event as extern "C" fn(&Object, Sel, id),
59 );
60 decl.register()
61 };
62
63 VIEW_CLASS = {
64 let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap();
65 decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
66 decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel));
67 decl.add_method(
68 sel!(keyDown:),
69 handle_view_event as extern "C" fn(&Object, Sel, id),
70 );
71 decl.add_method(
72 sel!(mouseDown:),
73 handle_view_event as extern "C" fn(&Object, Sel, id),
74 );
75 decl.add_method(
76 sel!(mouseUp:),
77 handle_view_event as extern "C" fn(&Object, Sel, id),
78 );
79 decl.add_method(
80 sel!(mouseDragged:),
81 handle_view_event as extern "C" fn(&Object, Sel, id),
82 );
83 decl.add_method(
84 sel!(scrollWheel:),
85 handle_view_event as extern "C" fn(&Object, Sel, id),
86 );
87 decl.register()
88 };
89
90 DELEGATE_CLASS = {
91 let mut decl = ClassDecl::new("GPUIWindowDelegate", class!(NSObject)).unwrap();
92 decl.add_method(
93 sel!(dealloc),
94 dealloc_delegate as extern "C" fn(&Object, Sel),
95 );
96 decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR);
97 decl.add_method(
98 sel!(windowDidResize:),
99 window_did_resize as extern "C" fn(&Object, Sel, id),
100 );
101 decl.register()
102 };
103}
104
105pub struct Window(Rc<WindowState>);
106
107struct WindowState {
108 native_window: id,
109 event_callback: RefCell<Option<Box<dyn FnMut(Event) -> bool>>>,
110 resize_callback: RefCell<Option<Box<dyn FnMut(Vector2F, f32)>>>,
111 synthetic_drag_counter: Cell<usize>,
112 executor: Rc<executor::Foreground>,
113}
114
115impl Window {
116 pub fn open(
117 options: platform::WindowOptions,
118 executor: Rc<executor::Foreground>,
119 ) -> Result<Self> {
120 unsafe {
121 let pool = NSAutoreleasePool::new(nil);
122
123 let frame = options.bounds.to_ns_rect();
124 let style_mask = NSWindowStyleMask::NSClosableWindowMask
125 | NSWindowStyleMask::NSMiniaturizableWindowMask
126 | NSWindowStyleMask::NSResizableWindowMask
127 | NSWindowStyleMask::NSTitledWindowMask;
128
129 let native_window: id = msg_send![WINDOW_CLASS, alloc];
130 let native_window = native_window.initWithContentRect_styleMask_backing_defer_(
131 frame,
132 style_mask,
133 NSBackingStoreBuffered,
134 NO,
135 );
136
137 if native_window == nil {
138 return Err(anyhow!("window returned nil from initializer"));
139 }
140
141 let delegate: id = msg_send![DELEGATE_CLASS, alloc];
142 let delegate = delegate.init();
143 if native_window == nil {
144 return Err(anyhow!("delegate returned nil from initializer"));
145 }
146 native_window.setDelegate_(delegate);
147
148 let native_view: id = msg_send![VIEW_CLASS, alloc];
149 let native_view = NSView::init(native_view);
150 if native_view == nil {
151 return Err(anyhow!("view return nil from initializer"));
152 }
153
154 let window = Self(Rc::new(WindowState {
155 native_window,
156 event_callback: RefCell::new(None),
157 resize_callback: RefCell::new(None),
158 synthetic_drag_counter: Cell::new(0),
159 executor,
160 }));
161
162 (*native_window).set_ivar(
163 WINDOW_STATE_IVAR,
164 Rc::into_raw(window.0.clone()) as *const c_void,
165 );
166 (*native_view).set_ivar(
167 WINDOW_STATE_IVAR,
168 Rc::into_raw(window.0.clone()) as *const c_void,
169 );
170 (*delegate).set_ivar(
171 WINDOW_STATE_IVAR,
172 Rc::into_raw(window.0.clone()) as *const c_void,
173 );
174
175 if let Some(title) = options.title.as_ref() {
176 native_window.setTitle_(NSString::alloc(nil).init_str(title));
177 }
178 native_window.setAcceptsMouseMovedEvents_(YES);
179
180 native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable);
181 native_view.setWantsBestResolutionOpenGLSurface_(YES);
182
183 // From winit crate: On Mojave, views automatically become layer-backed shortly after
184 // being added to a native_window. Changing the layer-backedness of a view breaks the
185 // association between the view and its associated OpenGL context. To work around this,
186 // on we explicitly make the view layer-backed up front so that AppKit doesn't do it
187 // itself and break the association with its context.
188 native_view.setWantsLayer(YES);
189
190 native_view.layer().setBackgroundColor_(
191 msg_send![class!(NSColor), colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0],
192 );
193
194 native_window.setContentView_(native_view.autorelease());
195 native_window.makeFirstResponder_(native_view);
196
197 native_window.center();
198 native_window.makeKeyAndOrderFront_(nil);
199
200 pool.drain();
201
202 Ok(window)
203 }
204 }
205
206 pub fn zoom(&self) {
207 unsafe {
208 self.0.native_window.performZoom_(nil);
209 }
210 }
211
212 pub fn on_event<F: 'static + FnMut(Event) -> bool>(&mut self, callback: F) {
213 *self.0.event_callback.borrow_mut() = Some(Box::new(callback));
214 }
215
216 pub fn on_resize<F: 'static + FnMut(Vector2F, f32)>(&mut self, callback: F) {
217 *self.0.resize_callback.borrow_mut() = Some(Box::new(callback));
218 }
219}
220
221impl Drop for Window {
222 fn drop(&mut self) {
223 unsafe {
224 self.0.native_window.close();
225 let _: () = msg_send![self.0.native_window.delegate(), release];
226 }
227 }
228}
229
230impl platform::Window for Window {
231 fn size(&self) -> Vector2F {
232 self.0.size()
233 }
234
235 fn scale_factor(&self) -> f32 {
236 self.0.scale_factor()
237 }
238
239 fn render_scene(&self, scene: Scene) {
240 log::info!("render scene");
241 }
242}
243
244impl WindowState {
245 fn size(&self) -> Vector2F {
246 let NSSize { width, height, .. } =
247 unsafe { NSView::frame(self.native_window.contentView()) }.size;
248 vec2f(width as f32, height as f32)
249 }
250
251 fn scale_factor(&self) -> f32 {
252 unsafe {
253 let screen: id = msg_send![self.native_window, screen];
254 NSScreen::backingScaleFactor(screen) as f32
255 }
256 }
257
258 fn next_synthetic_drag_id(&self) -> usize {
259 let next_id = self.synthetic_drag_counter.get() + 1;
260 self.synthetic_drag_counter.set(next_id);
261 next_id
262 }
263}
264
265unsafe fn window_state(object: &Object) -> Rc<WindowState> {
266 let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
267 let rc1 = Rc::from_raw(raw as *mut WindowState);
268 let rc2 = rc1.clone();
269 mem::forget(rc1);
270 rc2
271}
272
273unsafe fn drop_window_state(object: &Object) {
274 let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR);
275 Rc::from_raw(raw as *mut WindowState);
276}
277
278extern "C" fn yes(_: &Object, _: Sel) -> BOOL {
279 YES
280}
281
282extern "C" fn dealloc_window(this: &Object, _: Sel) {
283 unsafe {
284 drop_window_state(this);
285 let () = msg_send![super(this, class!(NSWindow)), dealloc];
286 }
287}
288
289extern "C" fn dealloc_view(this: &Object, _: Sel) {
290 unsafe {
291 drop_window_state(this);
292 let () = msg_send![super(this, class!(NSView)), dealloc];
293 }
294}
295
296extern "C" fn dealloc_delegate(this: &Object, _: Sel) {
297 unsafe {
298 let raw: *mut c_void = *this.get_ivar(WINDOW_STATE_IVAR);
299 Rc::from_raw(raw as *mut WindowState);
300 let () = msg_send![super(this, class!(NSObject)), dealloc];
301 }
302}
303
304extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) {
305 let window = unsafe { window_state(this) };
306
307 let event = unsafe { Event::from_native(native_event, Some(window.size().y())) };
308
309 if let Some(event) = event {
310 match event {
311 Event::LeftMouseDragged { position } => schedule_synthetic_drag(&window, position),
312 Event::LeftMouseUp { .. } => {
313 window.next_synthetic_drag_id();
314 }
315 _ => {}
316 }
317
318 if let Some(callback) = window.event_callback.borrow_mut().as_mut() {
319 if callback(event) {
320 return;
321 }
322 }
323 }
324}
325
326extern "C" fn send_event(this: &Object, _: Sel, native_event: id) {
327 unsafe {
328 let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event];
329 }
330}
331
332extern "C" fn window_did_resize(this: &Object, _: Sel, _: id) {
333 let window = unsafe { window_state(this) };
334 let size = window.size();
335 let scale_factor = window.scale_factor();
336 if let Some(callback) = window.resize_callback.borrow_mut().as_mut() {
337 callback(size, scale_factor);
338 }
339 drop(window);
340}
341
342fn schedule_synthetic_drag(window_state: &Rc<WindowState>, position: Vector2F) {
343 let drag_id = window_state.next_synthetic_drag_id();
344 let weak_window_state = Rc::downgrade(window_state);
345 let instant = Instant::now() + Duration::from_millis(16);
346 window_state
347 .executor
348 .spawn(async move {
349 Timer::at(instant).await;
350 if let Some(window_state) = weak_window_state.upgrade() {
351 if window_state.synthetic_drag_counter.get() == drag_id {
352 if let Some(callback) = window_state.event_callback.borrow_mut().as_mut() {
353 schedule_synthetic_drag(&window_state, position);
354 callback(Event::LeftMouseDragged { position });
355 }
356 }
357 }
358 })
359 .detach();
360}