window.rs

  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}