use crate::{ executor, geometry::vector::Vector2F, platform::{self, Event}, Scene, }; use anyhow::{anyhow, Result}; use cocoa::{ appkit::{ NSBackingStoreBuffered, NSScreen, NSView, NSViewHeightSizable, NSViewWidthSizable, NSWindow, NSWindowStyleMask, }, base::{id, nil}, foundation::{NSAutoreleasePool, NSSize, NSString}, quartzcore::AutoresizingMask, }; use ctor::ctor; use foreign_types::ForeignType as _; use metal::{MTLClearColor, MTLLoadAction, MTLStoreAction}; use objc::{ class, declare::ClassDecl, msg_send, runtime::{Class, Object, Protocol, Sel, BOOL, NO, YES}, sel, sel_impl, }; use pathfinder_geometry::vector::vec2f; use smol::Timer; use std::{ cell::{Cell, RefCell}, ffi::c_void, mem, ptr, rc::Rc, time::{Duration, Instant}, }; use super::{geometry::RectFExt, renderer::Renderer}; const WINDOW_STATE_IVAR: &'static str = "windowState"; static mut WINDOW_CLASS: *const Class = ptr::null(); static mut VIEW_CLASS: *const Class = ptr::null(); #[ctor] unsafe fn build_classes() { WINDOW_CLASS = { let mut decl = ClassDecl::new("GPUIWindow", class!(NSWindow)).unwrap(); decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); decl.add_method(sel!(dealloc), dealloc_window as extern "C" fn(&Object, Sel)); decl.add_method( sel!(canBecomeMainWindow), yes as extern "C" fn(&Object, Sel) -> BOOL, ); decl.add_method( sel!(canBecomeKeyWindow), yes as extern "C" fn(&Object, Sel) -> BOOL, ); decl.add_method( sel!(sendEvent:), send_event as extern "C" fn(&Object, Sel, id), ); decl.register() }; VIEW_CLASS = { let mut decl = ClassDecl::new("GPUIView", class!(NSView)).unwrap(); decl.add_ivar::<*mut c_void>(WINDOW_STATE_IVAR); decl.add_method(sel!(dealloc), dealloc_view as extern "C" fn(&Object, Sel)); decl.add_method( sel!(keyDown:), handle_view_event as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(mouseDown:), handle_view_event as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(mouseUp:), handle_view_event as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(mouseDragged:), handle_view_event as extern "C" fn(&Object, Sel, id), ); decl.add_method( sel!(scrollWheel:), handle_view_event as extern "C" fn(&Object, Sel, id), ); decl.add_protocol(Protocol::get("CALayerDelegate").unwrap()); decl.add_method( sel!(makeBackingLayer), make_backing_layer as extern "C" fn(&Object, Sel) -> id, ); decl.add_method( sel!(viewDidChangeBackingProperties), view_did_change_backing_properties as extern "C" fn(&Object, Sel), ); decl.add_method( sel!(setFrameSize:), set_frame_size as extern "C" fn(&Object, Sel, NSSize), ); decl.add_method( sel!(displayLayer:), display_layer as extern "C" fn(&Object, Sel, id), ); decl.register() }; } pub struct Window(Rc); struct WindowState { native_window: id, event_callback: RefCell bool>>>, resize_callback: RefCell>>, synthetic_drag_counter: Cell, executor: Rc, scene_to_render: RefCell>, renderer: RefCell, command_queue: metal::CommandQueue, device: metal::Device, layer: id, } pub struct RenderContext<'a> { pub drawable_size: Vector2F, pub device: &'a metal::Device, pub command_encoder: &'a metal::RenderCommandEncoderRef, } impl Window { pub fn open( options: platform::WindowOptions, executor: Rc, ) -> Result { const PIXEL_FORMAT: metal::MTLPixelFormat = metal::MTLPixelFormat::BGRA8Unorm; unsafe { let pool = NSAutoreleasePool::new(nil); let frame = options.bounds.to_ns_rect(); let style_mask = NSWindowStyleMask::NSClosableWindowMask | NSWindowStyleMask::NSMiniaturizableWindowMask | NSWindowStyleMask::NSResizableWindowMask | NSWindowStyleMask::NSTitledWindowMask; let native_window: id = msg_send![WINDOW_CLASS, alloc]; let native_window = native_window.initWithContentRect_styleMask_backing_defer_( frame, style_mask, NSBackingStoreBuffered, NO, ); if native_window == nil { return Err(anyhow!("window returned nil from initializer")); } let device = metal::Device::system_default() .ok_or_else(|| anyhow!("could not find default metal device"))?; let layer: id = msg_send![class!(CAMetalLayer), layer]; let _: () = msg_send![layer, setDevice: device.as_ptr()]; let _: () = msg_send![layer, setPixelFormat: PIXEL_FORMAT]; let _: () = msg_send![layer, setAllowsNextDrawableTimeout: NO]; let _: () = msg_send![layer, setNeedsDisplayOnBoundsChange: YES]; let _: () = msg_send![layer, setPresentsWithTransaction: YES]; let _: () = msg_send![ layer, setAutoresizingMask: AutoresizingMask::WIDTH_SIZABLE | AutoresizingMask::HEIGHT_SIZABLE ]; let native_view: id = msg_send![VIEW_CLASS, alloc]; let native_view = NSView::init(native_view); if native_view == nil { return Err(anyhow!("view return nil from initializer")); } let window = Self(Rc::new(WindowState { native_window, event_callback: RefCell::new(None), resize_callback: RefCell::new(None), synthetic_drag_counter: Cell::new(0), executor, scene_to_render: Default::default(), renderer: RefCell::new(Renderer::new(&device, PIXEL_FORMAT)?), command_queue: device.new_command_queue(), device, layer, })); (*native_window).set_ivar( WINDOW_STATE_IVAR, Rc::into_raw(window.0.clone()) as *const c_void, ); (*native_view).set_ivar( WINDOW_STATE_IVAR, Rc::into_raw(window.0.clone()) as *const c_void, ); if let Some(title) = options.title.as_ref() { native_window.setTitle_(NSString::alloc(nil).init_str(title)); } native_window.setAcceptsMouseMovedEvents_(YES); native_view.setAutoresizingMask_(NSViewWidthSizable | NSViewHeightSizable); native_view.setWantsBestResolutionOpenGLSurface_(YES); // From winit crate: On Mojave, views automatically become layer-backed shortly after // being added to a native_window. Changing the layer-backedness of a view breaks the // association between the view and its associated OpenGL context. To work around this, // on we explicitly make the view layer-backed up front so that AppKit doesn't do it // itself and break the association with its context. native_view.setWantsLayer(YES); native_view.layer().setBackgroundColor_( msg_send![class!(NSColor), colorWithRed:1.0 green:0.0 blue:0.0 alpha:1.0], ); native_window.setContentView_(native_view.autorelease()); native_window.makeFirstResponder_(native_view); native_window.center(); native_window.makeKeyAndOrderFront_(nil); pool.drain(); Ok(window) } } pub fn zoom(&self) { unsafe { self.0.native_window.performZoom_(nil); } } pub fn on_event bool>(&mut self, callback: F) { *self.0.event_callback.borrow_mut() = Some(Box::new(callback)); } pub fn on_resize(&mut self, callback: F) { *self.0.resize_callback.borrow_mut() = Some(Box::new(callback)); } } impl Drop for Window { fn drop(&mut self) { unsafe { self.0.native_window.close(); let _: () = msg_send![self.0.native_window.delegate(), release]; } } } impl platform::Window for Window { fn size(&self) -> Vector2F { self.0.size() } fn scale_factor(&self) -> f32 { self.0.scale_factor() } fn render_scene(&self, scene: Scene) { *self.0.scene_to_render.borrow_mut() = Some(scene); unsafe { let _: () = msg_send![self.0.native_window.contentView(), setNeedsDisplay: YES]; } } } impl WindowState { fn size(&self) -> Vector2F { let NSSize { width, height, .. } = unsafe { NSView::frame(self.native_window.contentView()) }.size; vec2f(width as f32, height as f32) } fn scale_factor(&self) -> f32 { unsafe { let screen: id = msg_send![self.native_window, screen]; NSScreen::backingScaleFactor(screen) as f32 } } fn next_synthetic_drag_id(&self) -> usize { let next_id = self.synthetic_drag_counter.get() + 1; self.synthetic_drag_counter.set(next_id); next_id } } unsafe fn window_state(object: &Object) -> Rc { let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); let rc1 = Rc::from_raw(raw as *mut WindowState); let rc2 = rc1.clone(); mem::forget(rc1); rc2 } unsafe fn drop_window_state(object: &Object) { let raw: *mut c_void = *object.get_ivar(WINDOW_STATE_IVAR); Rc::from_raw(raw as *mut WindowState); } extern "C" fn yes(_: &Object, _: Sel) -> BOOL { YES } extern "C" fn dealloc_window(this: &Object, _: Sel) { unsafe { drop_window_state(this); let () = msg_send![super(this, class!(NSWindow)), dealloc]; } } extern "C" fn dealloc_view(this: &Object, _: Sel) { unsafe { drop_window_state(this); let () = msg_send![super(this, class!(NSView)), dealloc]; } } extern "C" fn handle_view_event(this: &Object, _: Sel, native_event: id) { let window = unsafe { window_state(this) }; let event = unsafe { Event::from_native(native_event, Some(window.size().y())) }; if let Some(event) = event { match event { Event::LeftMouseDragged { position } => schedule_synthetic_drag(&window, position), Event::LeftMouseUp { .. } => { window.next_synthetic_drag_id(); } _ => {} } if let Some(callback) = window.event_callback.borrow_mut().as_mut() { if callback(event) { return; } } } } extern "C" fn send_event(this: &Object, _: Sel, native_event: id) { unsafe { let () = msg_send![super(this, class!(NSWindow)), sendEvent: native_event]; } } extern "C" fn make_backing_layer(this: &Object, _: Sel) -> id { let window = unsafe { window_state(this) }; window.layer } extern "C" fn view_did_change_backing_properties(this: &Object, _: Sel) { let window; unsafe { window = window_state(this); let _: () = msg_send![window.layer, setContentsScale: window.scale_factor() as f64]; } if let Some(callback) = window.resize_callback.borrow_mut().as_mut() { let size = window.size(); let scale_factor = window.scale_factor(); callback(size, scale_factor); }; } extern "C" fn set_frame_size(this: &Object, _: Sel, size: NSSize) { let window; unsafe { window = window_state(this); if window.size() == vec2f(size.width as f32, size.height as f32) { return; } let _: () = msg_send![super(this, class!(NSView)), setFrameSize: size]; let scale_factor = window.scale_factor() as f64; let drawable_size: NSSize = NSSize { width: size.width * scale_factor, height: size.height * scale_factor, }; let _: () = msg_send![window.layer, setDrawableSize: drawable_size]; } if let Some(callback) = window.resize_callback.borrow_mut().as_mut() { let size = window.size(); let scale_factor = window.scale_factor(); callback(size, scale_factor); }; } extern "C" fn display_layer(this: &Object, _: Sel, _: id) { unsafe { let window = window_state(this); if let Some(scene) = window.scene_to_render.borrow_mut().take() { let drawable: &metal::MetalDrawableRef = msg_send![window.layer, nextDrawable]; let render_pass_descriptor = metal::RenderPassDescriptor::new(); let color_attachment = render_pass_descriptor .color_attachments() .object_at(0) .unwrap(); color_attachment.set_texture(Some(drawable.texture())); color_attachment.set_load_action(MTLLoadAction::Clear); color_attachment.set_store_action(MTLStoreAction::Store); color_attachment.set_clear_color(MTLClearColor::new(0., 0., 0., 1.)); let command_buffer = window.command_queue.new_command_buffer(); let command_encoder = command_buffer.new_render_command_encoder(render_pass_descriptor); window.renderer.borrow_mut().render( &scene, RenderContext { drawable_size: window.size() * window.scale_factor(), device: &window.device, command_encoder, }, ); command_encoder.end_encoding(); command_buffer.commit(); command_buffer.wait_until_completed(); drawable.present(); }; } } fn schedule_synthetic_drag(window_state: &Rc, position: Vector2F) { let drag_id = window_state.next_synthetic_drag_id(); let weak_window_state = Rc::downgrade(window_state); let instant = Instant::now() + Duration::from_millis(16); window_state .executor .spawn(async move { Timer::at(instant).await; if let Some(window_state) = weak_window_state.upgrade() { if window_state.synthetic_drag_counter.get() == drag_id { if let Some(callback) = window_state.event_callback.borrow_mut().as_mut() { schedule_synthetic_drag(&window_state, position); callback(Event::LeftMouseDragged { position }); } } } }) .detach(); }