window.rs

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