diff --git a/CHANGELOG.md b/CHANGELOG.md index bf53744b65c..7f57c4d9263 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,41 @@ # Unreleased +- Changes below are considered **breaking**. +- Change all occurrences of `EventsLoop` to `EventLoop`. +- Previously flat API is now exposed through `event`, `event_loop`, `monitor`, and `window` modules. +- `os` module changes: + - Renamed to `platform`. + - All traits now have platform-specific suffixes. + - Exposes new `desktop` module on Windows, Mac, and Linux. +- Changes to event loop types: + - `EventLoopProxy::wakeup` has been removed in favor of `send_event`. + - **Major:** New `run` method drives winit event loop. + - Returns `!` to ensure API behaves identically across all supported platforms. + - This allows `emscripten` implementation to work without lying about the API. + - `ControlFlow`'s variants have been replaced with `Wait`, `WaitUntil(Instant)`, `Poll`, and `Exit`. + - Is read after `EventsCleared` is processed. + - `Wait` waits until new events are available. + - `WaitUntil` waits until either new events are available or the provided time has been reached. + - `Poll` instantly resumes the event loop. + - `Exit` aborts the event loop. + - Takes a closure that implements `'static + FnMut(Event, &EventLoop, &mut ControlFlow)`. + - `&EventLoop` is provided to allow new `Window`s to be created. + - **Major:** `platform::desktop` module exposes `EventLoopExtDesktop` trait with `run_return` method. + - Behaves identically to `run`, but returns control flow to the calling context can take non-`'static` closures. + - `EventLoop`'s `poll_events` and `run_forever` methods have been removed in favor of `run` and `run_return`. +- Changes to events: + - Remove `Event::Awakened` in favor of `Event::UserEvent(T)`. + - Can be sent with `EventLoopProxy::send_event`. + - Rename `WindowEvent::Refresh` to `WindowEvent::RedrawRequested`. + - `RedrawRequested` can be sent by the user with the `Window::request_redraw` method. + - `EventLoop`, `EventLoopProxy`, and `Event` are now generic over `T`, for use in `UserEvent`. + - **Major:** Add `NewEvents(StartCause)`, `EventsCleared`, and `LoopDestroyed` variants to `Event`. + - `NewEvents` is emitted when new events are ready to be processed by event loop. + - `StartCause` describes why new events are available, with `ResumeTimeReached`, `Poll`, `WaitCancelled`, and `Init` (sent once at start of loop). + - `EventsCleared` is emitted when all available events have been processed. + - Can be used to perform logic that depends on all events being processed (e.g. an iteration of a game loop). + - `LoopDestroyed` is emitted when the `run` or `run_return` method is about to exit. +- Rename `MonitorId` to `MonitorHandle`. +- Removed `serde` implementations from `ControlFlow`. - Changes below are considered **breaking**. - Change all occurrences of `EventsLoop` to `EventLoop`. diff --git a/Cargo.toml b/Cargo.toml index 370b8ff6a5b..3ed03fd6b17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,7 @@ serde = { version = "1", optional = true, features = ["serde_derive"] } [dev-dependencies] image = "0.21" +env_logger = "0.5" [target.'cfg(target_os = "android")'.dependencies.android_glue] version = "0.2" @@ -29,10 +30,11 @@ version = "0.2" objc = "0.2.3" [target.'cfg(target_os = "macos")'.dependencies] -objc = "0.2.3" cocoa = "0.18.4" core-foundation = "0.6" core-graphics = "0.17.3" +dispatch = "0.1.4" +objc = "0.2.3" [target.'cfg(target_os = "windows")'.dependencies] backtrace = "0.3" diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs new file mode 100644 index 00000000000..f5907ca3efd --- /dev/null +++ b/examples/multithreaded.rs @@ -0,0 +1,115 @@ +extern crate env_logger; +extern crate winit; + +use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; + +use winit::{ + event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, window::{MouseCursor, WindowBuilder}, +}; + +const WINDOW_COUNT: usize = 3; +const WINDOW_SIZE: (u32, u32) = (600, 400); + +fn main() { + env_logger::init(); + let event_loop = EventLoop::new(); + let mut window_senders = HashMap::with_capacity(WINDOW_COUNT); + for _ in 0..WINDOW_COUNT { + let window = WindowBuilder::new() + .with_dimensions(WINDOW_SIZE.into()) + .build(&event_loop) + .unwrap(); + let (tx, rx) = mpsc::channel(); + window_senders.insert(window.id(), tx); + thread::spawn(move || { + while let Ok(event) = rx.recv() { + match event { + WindowEvent::KeyboardInput { input: KeyboardInput { + state: ElementState::Released, + virtual_keycode: Some(key), + modifiers, + .. + }, .. } => { + window.set_title(&format!("{:?}", key)); + let state = !modifiers.shift; + use self::VirtualKeyCode::*; + match key { + A => window.set_always_on_top(state), + C => window.set_cursor(match state { + true => MouseCursor::Progress, + false => MouseCursor::Default, + }), + D => window.set_decorations(!state), + F => window.set_fullscreen(match state { + true => Some(window.get_current_monitor()), + false => None, + }), + G => window.grab_cursor(state).unwrap(), + H => window.hide_cursor(state), + I => { + println!("Info:"); + println!("-> position : {:?}", window.get_position()); + println!("-> inner_position : {:?}", window.get_inner_position()); + println!("-> outer_size : {:?}", window.get_outer_size()); + println!("-> inner_size : {:?}", window.get_inner_size()); + }, + L => window.set_min_dimensions(match state { + true => Some(WINDOW_SIZE.into()), + false => None, + }), + M => window.set_maximized(state), + P => window.set_position({ + let mut position = window.get_position().unwrap(); + let sign = if state { 1.0 } else { -1.0 }; + position.x += 10.0 * sign; + position.y += 10.0 * sign; + position + }), + Q => window.request_redraw(), + R => window.set_resizable(state), + S => window.set_inner_size(match state { + true => (WINDOW_SIZE.0 + 100, WINDOW_SIZE.1 + 100), + false => WINDOW_SIZE, + }.into()), + W => window.set_cursor_position(( + WINDOW_SIZE.0 as i32 / 2, + WINDOW_SIZE.1 as i32 / 2, + ).into()).unwrap(), + Z => { + window.hide(); + thread::sleep(Duration::from_secs(1)); + window.show(); + }, + _ => (), + } + }, + _ => (), + } + } + }); + } + event_loop.run(move |event, _event_loop, control_flow| { + *control_flow = match !window_senders.is_empty() { + true => ControlFlow::Wait, + false => ControlFlow::Exit, + }; + match event { + Event::WindowEvent { event, window_id } => { + match event { + WindowEvent::CloseRequested + | WindowEvent::Destroyed + | WindowEvent::KeyboardInput { input: KeyboardInput { + virtual_keycode: Some(VirtualKeyCode::Escape), + .. }, .. } => { + window_senders.remove(&window_id); + }, + _ => if let Some(tx) = window_senders.get(&window_id) { + tx.send(event).unwrap(); + }, + } + } + _ => (), + } + }) +} diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 8997e3b2657..cca09f91491 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -1,7 +1,10 @@ #![cfg(target_os = "macos")] use std::os::raw::c_void; -use {LogicalSize, MonitorHandle, Window, WindowBuilder}; + +use crate::dpi::LogicalSize; +use crate::monitor::MonitorHandle; +use crate::window::{Window, WindowBuilder}; /// Additional methods on `Window` that are specific to MacOS. pub trait WindowExtMacOS { diff --git a/src/platform_impl/macos/app.rs b/src/platform_impl/macos/app.rs new file mode 100644 index 00000000000..2699eedc2b0 --- /dev/null +++ b/src/platform_impl/macos/app.rs @@ -0,0 +1,88 @@ +use std::collections::VecDeque; + +use cocoa::{appkit::{self, NSEvent}, base::id}; +use objc::{declare::ClassDecl, runtime::{Class, Object, Sel}}; + +use event::{DeviceEvent, Event}; +use platform_impl::platform::{app_state::AppState, DEVICE_ID, util}; + +pub struct AppClass(pub *const Class); +unsafe impl Send for AppClass {} +unsafe impl Sync for AppClass {} + +lazy_static! { + pub static ref APP_CLASS: AppClass = unsafe { + let superclass = class!(NSApplication); + let mut decl = ClassDecl::new("WinitApp", superclass).unwrap(); + + decl.add_method( + sel!(sendEvent:), + send_event as extern fn(&Object, Sel, id), + ); + + AppClass(decl.register()) + }; +} + +// Normally, holding Cmd + any key never sends us a `keyUp` event for that key. +// Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196) +// Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553) +extern fn send_event(this: &Object, _sel: Sel, event: id) { + unsafe { + // For posterity, there are some undocumented event types + // (https://github.com/servo/cocoa-rs/issues/155) + // but that doesn't really matter here. + let event_type = event.eventType(); + let modifier_flags = event.modifierFlags(); + if event_type == appkit::NSKeyUp && util::has_flag( + modifier_flags, + appkit::NSEventModifierFlags::NSCommandKeyMask, + ) { + let key_window: id = msg_send![this, keyWindow]; + let _: () = msg_send![key_window, sendEvent:event]; + } else { + maybe_dispatch_device_event(event); + let superclass = util::superclass(this); + let _: () = msg_send![super(this, superclass), sendEvent:event]; + } + } +} + +unsafe fn maybe_dispatch_device_event(event: id) { + let event_type = event.eventType(); + match event_type { + appkit::NSMouseMoved | + appkit::NSLeftMouseDragged | + appkit::NSOtherMouseDragged | + appkit::NSRightMouseDragged => { + let mut events = VecDeque::with_capacity(3); + + let delta_x = event.deltaX() as f64; + let delta_y = event.deltaY() as f64; + + if delta_x != 0.0 { + events.push_back(Event::DeviceEvent { + device_id: DEVICE_ID, + event: DeviceEvent::Motion { axis: 0, value: delta_x }, + }); + } + + if delta_y != 0.0 { + events.push_back(Event::DeviceEvent { + device_id: DEVICE_ID, + event: DeviceEvent::Motion { axis: 1, value: delta_y }, + }); + } + + if delta_x != 0.0 || delta_y != 0.0 { + events.push_back(Event::DeviceEvent { + device_id: DEVICE_ID, + event: DeviceEvent::MouseMotion { delta: (delta_x, delta_y) }, + }); + } + + AppState::queue_events(events); + }, + _ => (), + } +} diff --git a/src/platform_impl/macos/app_delegate.rs b/src/platform_impl/macos/app_delegate.rs new file mode 100644 index 00000000000..8e4ab19dcd2 --- /dev/null +++ b/src/platform_impl/macos/app_delegate.rs @@ -0,0 +1,101 @@ +use cocoa::base::id; +use objc::{runtime::{Class, Object, Sel, BOOL, YES}, declare::ClassDecl}; + +use platform_impl::platform::app_state::AppState; + +pub struct AppDelegateClass(pub *const Class); +unsafe impl Send for AppDelegateClass {} +unsafe impl Sync for AppDelegateClass {} + +lazy_static! { + pub static ref APP_DELEGATE_CLASS: AppDelegateClass = unsafe { + let superclass = class!(NSResponder); + let mut decl = ClassDecl::new("WinitAppDelegate", superclass).unwrap(); + + decl.add_method( + sel!(applicationDidFinishLaunching:), + did_finish_launching as extern fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(applicationDidBecomeActive:), + did_become_active as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(applicationWillResignActive:), + will_resign_active as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(applicationWillEnterForeground:), + will_enter_foreground as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(applicationDidEnterBackground:), + did_enter_background as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(applicationWillTerminate:), + will_terminate as extern fn(&Object, Sel, id), + ); + + AppDelegateClass(decl.register()) + }; +} + +extern fn did_finish_launching(_: &Object, _: Sel, _: id) -> BOOL { + trace!("Triggered `didFinishLaunching`"); + AppState::launched(); + trace!("Completed `didFinishLaunching`"); + YES +} + +extern fn did_become_active(_: &Object, _: Sel, _: id) { + trace!("Triggered `didBecomeActive`"); + /*unsafe { + HANDLER.lock().unwrap().handle_nonuser_event(Event::Suspended(false)) + }*/ + trace!("Completed `didBecomeActive`"); +} + +extern fn will_resign_active(_: &Object, _: Sel, _: id) { + trace!("Triggered `willResignActive`"); + /*unsafe { + HANDLER.lock().unwrap().handle_nonuser_event(Event::Suspended(true)) + }*/ + trace!("Completed `willResignActive`"); +} + +extern fn will_enter_foreground(_: &Object, _: Sel, _: id) { + trace!("Triggered `willEnterForeground`"); + trace!("Completed `willEnterForeground`"); +} + +extern fn did_enter_background(_: &Object, _: Sel, _: id) { + trace!("Triggered `didEnterBackground`"); + trace!("Completed `didEnterBackground`"); +} + +extern fn will_terminate(_: &Object, _: Sel, _: id) { + trace!("Triggered `willTerminate`"); + /*unsafe { + let app: id = msg_send![class!(UIApplication), sharedApplication]; + let windows: id = msg_send![app, windows]; + let windows_enum: id = msg_send![windows, objectEnumerator]; + let mut events = Vec::new(); + loop { + let window: id = msg_send![windows_enum, nextObject]; + if window == nil { + break + } + let is_winit_window: BOOL = msg_send![window, isKindOfClass:class!(WinitUIWindow)]; + if is_winit_window == YES { + events.push(Event::WindowEvent { + window_id: RootWindowId(window.into()), + event: WindowEvent::Destroyed, + }); + } + } + HANDLER.lock().unwrap().handle_nonuser_events(events); + HANDLER.lock().unwrap().terminated(); + }*/ + trace!("Completed `willTerminate`"); +} diff --git a/src/platform_impl/macos/app_state.rs b/src/platform_impl/macos/app_state.rs new file mode 100644 index 00000000000..593aa765f51 --- /dev/null +++ b/src/platform_impl/macos/app_state.rs @@ -0,0 +1,310 @@ +use std::{ + collections::VecDeque, fmt::{self, Debug, Formatter}, + hint::unreachable_unchecked, mem, + sync::{atomic::{AtomicBool, Ordering}, Mutex, MutexGuard}, time::Instant, +}; + +use cocoa::{appkit::NSApp, base::nil}; + +use { + event::{Event, StartCause, WindowEvent}, + event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget}, + window::WindowId, +}; +use platform_impl::platform::{observer::EventLoopWaker, util::Never}; + +lazy_static! { + static ref HANDLER: Handler = Default::default(); +} + +impl Event { + fn userify(self) -> Event { + self.map_nonuser_event() + // `Never` can't be constructed, so the `UserEvent` variant can't + // be present here. + .unwrap_or_else(|_| unsafe { unreachable_unchecked() }) + } +} + +pub trait EventHandler: Debug { + fn handle_nonuser_event(&mut self, event: Event, control_flow: &mut ControlFlow); + fn handle_user_events(&mut self, control_flow: &mut ControlFlow); +} + +struct EventLoopHandler { + callback: F, + will_exit: bool, + window_target: RootWindowTarget, +} + +impl Debug for EventLoopHandler { + fn fmt(&self, formatter: &mut Formatter) -> fmt::Result { + formatter.debug_struct("EventLoopHandler") + .field("window_target", &self.window_target) + .finish() + } +} + +impl EventHandler for EventLoopHandler +where + F: 'static + FnMut(Event, &RootWindowTarget, &mut ControlFlow), + T: 'static, +{ + fn handle_nonuser_event(&mut self, event: Event, control_flow: &mut ControlFlow) { + (self.callback)( + event.userify(), + &self.window_target, + control_flow, + ); + self.will_exit |= *control_flow == ControlFlow::Exit; + if self.will_exit { + *control_flow = ControlFlow::Exit; + } + } + + fn handle_user_events(&mut self, control_flow: &mut ControlFlow) { + let mut will_exit = self.will_exit; + for event in self.window_target.inner.receiver.try_iter() { + (self.callback)( + Event::UserEvent(event), + &self.window_target, + control_flow, + ); + will_exit |= *control_flow == ControlFlow::Exit; + if will_exit { + *control_flow = ControlFlow::Exit; + } + } + self.will_exit = will_exit; + } +} + +#[derive(Default)] +struct Handler { + ready: AtomicBool, + in_callback: AtomicBool, + control_flow: Mutex, + control_flow_prev: Mutex, + start_time: Mutex>, + callback: Mutex>>, + pending_events: Mutex>>, + deferred_events: Mutex>>, + pending_redraw: Mutex>, + waker: Mutex, +} + +unsafe impl Send for Handler {} +unsafe impl Sync for Handler {} + +impl Handler { + fn events<'a>(&'a self) -> MutexGuard<'a, VecDeque>> { + self.pending_events.lock().unwrap() + } + + fn deferred<'a>(&'a self) -> MutexGuard<'a, VecDeque>> { + self.deferred_events.lock().unwrap() + } + + fn redraw<'a>(&'a self) -> MutexGuard<'a, Vec> { + self.pending_redraw.lock().unwrap() + } + + fn waker<'a>(&'a self) -> MutexGuard<'a, EventLoopWaker> { + self.waker.lock().unwrap() + } + + fn is_ready(&self) -> bool { + self.ready.load(Ordering::Acquire) + } + + fn set_ready(&self) { + self.ready.store(true, Ordering::Release); + } + + fn should_exit(&self) -> bool { + *self.control_flow.lock().unwrap() == ControlFlow::Exit + } + + fn get_control_flow_and_update_prev(&self) -> ControlFlow { + let control_flow = self.control_flow.lock().unwrap(); + *self.control_flow_prev.lock().unwrap() = *control_flow; + *control_flow + } + + fn get_old_and_new_control_flow(&self) -> (ControlFlow, ControlFlow) { + let old = *self.control_flow_prev.lock().unwrap(); + let new = *self.control_flow.lock().unwrap(); + (old, new) + } + + fn get_start_time(&self) -> Option { + *self.start_time.lock().unwrap() + } + + fn update_start_time(&self) { + *self.start_time.lock().unwrap() = Some(Instant::now()); + } + + fn take_events(&self) -> VecDeque> { + mem::replace(&mut *self.events(), Default::default()) + } + + fn take_deferred(&self) -> VecDeque> { + mem::replace(&mut *self.deferred(), Default::default()) + } + + fn should_redraw(&self) -> Vec { + mem::replace(&mut *self.redraw(), Default::default()) + } + + fn get_in_callback(&self) -> bool { + self.in_callback.load(Ordering::Acquire) + } + + fn set_in_callback(&self, in_callback: bool) { + self.in_callback.store(in_callback, Ordering::Release); + } + + fn handle_nonuser_event(&self, event: Event) { + if let Some(ref mut callback) = *self.callback.lock().unwrap() { + callback.handle_nonuser_event( + event, + &mut *self.control_flow.lock().unwrap(), + ); + } + } + + fn handle_user_events(&self) { + if let Some(ref mut callback) = *self.callback.lock().unwrap() { + callback.handle_user_events( + &mut *self.control_flow.lock().unwrap(), + ); + } + } +} + +pub enum AppState {} + +impl AppState { + pub fn set_callback(callback: F, window_target: RootWindowTarget) + where + F: 'static + FnMut(Event, &RootWindowTarget, &mut ControlFlow), + T: 'static, + { + *HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler { + callback, + will_exit: false, + window_target, + })); + } + + pub fn exit() { + HANDLER.set_in_callback(true); + HANDLER.handle_nonuser_event(Event::LoopDestroyed); + HANDLER.set_in_callback(false); + } + + pub fn launched() { + HANDLER.set_ready(); + HANDLER.waker().start(); + HANDLER.set_in_callback(true); + HANDLER.handle_nonuser_event(Event::NewEvents(StartCause::Init)); + HANDLER.set_in_callback(false); + } + + pub fn wakeup() { + if !HANDLER.is_ready() { return } + let start = HANDLER.get_start_time().unwrap(); + let cause = match HANDLER.get_control_flow_and_update_prev() { + ControlFlow::Poll => StartCause::Poll, + ControlFlow::Wait => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + ControlFlow::WaitUntil(requested_resume) => { + if Instant::now() >= requested_resume { + StartCause::ResumeTimeReached { + start, + requested_resume, + } + } else { + StartCause::WaitCancelled { + start, + requested_resume: Some(requested_resume), + } + } + }, + ControlFlow::Exit => StartCause::Poll,//panic!("unexpected `ControlFlow::Exit`"), + }; + HANDLER.set_in_callback(true); + HANDLER.handle_nonuser_event(Event::NewEvents(cause)); + HANDLER.set_in_callback(false); + } + + // This is called from multiple threads at present + pub fn queue_redraw(window_id: WindowId) { + let mut pending_redraw = HANDLER.redraw(); + if !pending_redraw.contains(&window_id) { + pending_redraw.push(window_id); + } + } + + pub fn queue_event(event: Event) { + if !unsafe { msg_send![class!(NSThread), isMainThread] } { + panic!("Event queued from different thread: {:#?}", event); + } + HANDLER.events().push_back(event); + } + + pub fn queue_events(mut events: VecDeque>) { + if !unsafe { msg_send![class!(NSThread), isMainThread] } { + panic!("Events queued from different thread: {:#?}", events); + } + HANDLER.events().append(&mut events); + } + + pub fn send_event_immediately(event: Event) { + if !unsafe { msg_send![class!(NSThread), isMainThread] } { + panic!("Event sent from different thread: {:#?}", event); + } + HANDLER.deferred().push_back(event); + if !HANDLER.get_in_callback() { + HANDLER.set_in_callback(true); + for event in HANDLER.take_deferred() { + HANDLER.handle_nonuser_event(event); + } + HANDLER.set_in_callback(false); + } + } + + pub fn cleared() { + if !HANDLER.is_ready() { return } + if !HANDLER.get_in_callback() { + HANDLER.set_in_callback(true); + HANDLER.handle_user_events(); + for event in HANDLER.take_events() { + HANDLER.handle_nonuser_event(event); + } + for window_id in HANDLER.should_redraw() { + HANDLER.handle_nonuser_event(Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }); + } + HANDLER.handle_nonuser_event(Event::EventsCleared); + HANDLER.set_in_callback(false); + } + if HANDLER.should_exit() { + let _: () = unsafe { msg_send![NSApp(), stop:nil] }; + return + } + HANDLER.update_start_time(); + match HANDLER.get_old_and_new_control_flow() { + (ControlFlow::Exit, _) | (_, ControlFlow::Exit) => unreachable!(), + (old, new) if old == new => (), + (_, ControlFlow::Wait) => HANDLER.waker().stop(), + (_, ControlFlow::WaitUntil(instant)) => HANDLER.waker().start_at(instant), + (_, ControlFlow::Poll) => HANDLER.waker().start(), + } + } +} diff --git a/src/platform_impl/macos/event.rs b/src/platform_impl/macos/event.rs new file mode 100644 index 00000000000..10cd876f81d --- /dev/null +++ b/src/platform_impl/macos/event.rs @@ -0,0 +1,202 @@ +use std::os::raw::c_ushort; + +use cocoa::{appkit::{NSEvent, NSEventModifierFlags}, base::id}; + +use event::{ + ElementState, KeyboardInput, + ModifiersState, VirtualKeyCode, WindowEvent, +}; +use platform_impl::platform::DEVICE_ID; + +pub fn to_virtual_keycode(scancode: c_ushort) -> Option { + Some(match scancode { + 0x00 => VirtualKeyCode::A, + 0x01 => VirtualKeyCode::S, + 0x02 => VirtualKeyCode::D, + 0x03 => VirtualKeyCode::F, + 0x04 => VirtualKeyCode::H, + 0x05 => VirtualKeyCode::G, + 0x06 => VirtualKeyCode::Z, + 0x07 => VirtualKeyCode::X, + 0x08 => VirtualKeyCode::C, + 0x09 => VirtualKeyCode::V, + //0x0a => World 1, + 0x0b => VirtualKeyCode::B, + 0x0c => VirtualKeyCode::Q, + 0x0d => VirtualKeyCode::W, + 0x0e => VirtualKeyCode::E, + 0x0f => VirtualKeyCode::R, + 0x10 => VirtualKeyCode::Y, + 0x11 => VirtualKeyCode::T, + 0x12 => VirtualKeyCode::Key1, + 0x13 => VirtualKeyCode::Key2, + 0x14 => VirtualKeyCode::Key3, + 0x15 => VirtualKeyCode::Key4, + 0x16 => VirtualKeyCode::Key6, + 0x17 => VirtualKeyCode::Key5, + 0x18 => VirtualKeyCode::Equals, + 0x19 => VirtualKeyCode::Key9, + 0x1a => VirtualKeyCode::Key7, + 0x1b => VirtualKeyCode::Minus, + 0x1c => VirtualKeyCode::Key8, + 0x1d => VirtualKeyCode::Key0, + 0x1e => VirtualKeyCode::RBracket, + 0x1f => VirtualKeyCode::O, + 0x20 => VirtualKeyCode::U, + 0x21 => VirtualKeyCode::LBracket, + 0x22 => VirtualKeyCode::I, + 0x23 => VirtualKeyCode::P, + 0x24 => VirtualKeyCode::Return, + 0x25 => VirtualKeyCode::L, + 0x26 => VirtualKeyCode::J, + 0x27 => VirtualKeyCode::Apostrophe, + 0x28 => VirtualKeyCode::K, + 0x29 => VirtualKeyCode::Semicolon, + 0x2a => VirtualKeyCode::Backslash, + 0x2b => VirtualKeyCode::Comma, + 0x2c => VirtualKeyCode::Slash, + 0x2d => VirtualKeyCode::N, + 0x2e => VirtualKeyCode::M, + 0x2f => VirtualKeyCode::Period, + 0x30 => VirtualKeyCode::Tab, + 0x31 => VirtualKeyCode::Space, + 0x32 => VirtualKeyCode::Grave, + 0x33 => VirtualKeyCode::Back, + //0x34 => unkown, + 0x35 => VirtualKeyCode::Escape, + 0x36 => VirtualKeyCode::RWin, + 0x37 => VirtualKeyCode::LWin, + 0x38 => VirtualKeyCode::LShift, + //0x39 => Caps lock, + 0x3a => VirtualKeyCode::LAlt, + 0x3b => VirtualKeyCode::LControl, + 0x3c => VirtualKeyCode::RShift, + 0x3d => VirtualKeyCode::RAlt, + 0x3e => VirtualKeyCode::RControl, + //0x3f => Fn key, + 0x40 => VirtualKeyCode::F17, + 0x41 => VirtualKeyCode::Decimal, + //0x42 -> unkown, + 0x43 => VirtualKeyCode::Multiply, + //0x44 => unkown, + 0x45 => VirtualKeyCode::Add, + //0x46 => unkown, + 0x47 => VirtualKeyCode::Numlock, + //0x48 => KeypadClear, + 0x49 => VirtualKeyCode::VolumeUp, + 0x4a => VirtualKeyCode::VolumeDown, + 0x4b => VirtualKeyCode::Divide, + 0x4c => VirtualKeyCode::NumpadEnter, + //0x4d => unkown, + 0x4e => VirtualKeyCode::Subtract, + 0x4f => VirtualKeyCode::F18, + 0x50 => VirtualKeyCode::F19, + 0x51 => VirtualKeyCode::NumpadEquals, + 0x52 => VirtualKeyCode::Numpad0, + 0x53 => VirtualKeyCode::Numpad1, + 0x54 => VirtualKeyCode::Numpad2, + 0x55 => VirtualKeyCode::Numpad3, + 0x56 => VirtualKeyCode::Numpad4, + 0x57 => VirtualKeyCode::Numpad5, + 0x58 => VirtualKeyCode::Numpad6, + 0x59 => VirtualKeyCode::Numpad7, + 0x5a => VirtualKeyCode::F20, + 0x5b => VirtualKeyCode::Numpad8, + 0x5c => VirtualKeyCode::Numpad9, + //0x5d => unkown, + //0x5e => unkown, + //0x5f => unkown, + 0x60 => VirtualKeyCode::F5, + 0x61 => VirtualKeyCode::F6, + 0x62 => VirtualKeyCode::F7, + 0x63 => VirtualKeyCode::F3, + 0x64 => VirtualKeyCode::F8, + 0x65 => VirtualKeyCode::F9, + //0x66 => unkown, + 0x67 => VirtualKeyCode::F11, + //0x68 => unkown, + 0x69 => VirtualKeyCode::F13, + 0x6a => VirtualKeyCode::F16, + 0x6b => VirtualKeyCode::F14, + //0x6c => unkown, + 0x6d => VirtualKeyCode::F10, + //0x6e => unkown, + 0x6f => VirtualKeyCode::F12, + //0x70 => unkown, + 0x71 => VirtualKeyCode::F15, + 0x72 => VirtualKeyCode::Insert, + 0x73 => VirtualKeyCode::Home, + 0x74 => VirtualKeyCode::PageUp, + 0x75 => VirtualKeyCode::Delete, + 0x76 => VirtualKeyCode::F4, + 0x77 => VirtualKeyCode::End, + 0x78 => VirtualKeyCode::F2, + 0x79 => VirtualKeyCode::PageDown, + 0x7a => VirtualKeyCode::F1, + 0x7b => VirtualKeyCode::Left, + 0x7c => VirtualKeyCode::Right, + 0x7d => VirtualKeyCode::Down, + 0x7e => VirtualKeyCode::Up, + //0x7f => unkown, + + 0xa => VirtualKeyCode::Caret, + _ => return None, + }) +} + +// While F1-F20 have scancodes we can match on, we have to check against UTF-16 +// constants for the rest. +// https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?preferredLanguage=occ +pub fn check_function_keys(string: &Option) -> Option { + string + .as_ref() + .and_then(|string| string.encode_utf16().next()) + .and_then(|character| match character { + 0xf718 => Some(VirtualKeyCode::F21), + 0xf719 => Some(VirtualKeyCode::F22), + 0xf71a => Some(VirtualKeyCode::F23), + 0xf71b => Some(VirtualKeyCode::F24), + _ => None, + }) +} + +pub fn event_mods(event: id) -> ModifiersState { + let flags = unsafe { + NSEvent::modifierFlags(event) + }; + ModifiersState { + shift: flags.contains(NSEventModifierFlags::NSShiftKeyMask), + ctrl: flags.contains(NSEventModifierFlags::NSControlKeyMask), + alt: flags.contains(NSEventModifierFlags::NSAlternateKeyMask), + logo: flags.contains(NSEventModifierFlags::NSCommandKeyMask), + } +} + +pub unsafe fn modifier_event( + ns_event: id, + keymask: NSEventModifierFlags, + was_key_pressed: bool, +) -> Option { + if !was_key_pressed && NSEvent::modifierFlags(ns_event).contains(keymask) + || was_key_pressed && !NSEvent::modifierFlags(ns_event).contains(keymask) { + let state = if was_key_pressed { + ElementState::Released + } else { + ElementState::Pressed + }; + let keycode = NSEvent::keyCode(ns_event); + let scancode = keycode as u32; + let virtual_keycode = to_virtual_keycode(keycode); + Some(WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state, + scancode, + virtual_keycode, + modifiers: event_mods(ns_event), + }, + }) + } else { + None + } +} diff --git a/src/platform_impl/macos/ffi.rs b/src/platform_impl/macos/ffi.rs index 31c9ed149e7..d199ebb6a8d 100644 --- a/src/platform_impl/macos/ffi.rs +++ b/src/platform_impl/macos/ffi.rs @@ -95,6 +95,7 @@ pub const kCGDesktopIconWindowLevelKey: NSInteger = 18; pub const kCGCursorWindowLevelKey: NSInteger = 19; pub const kCGNumberOfWindowLevelKeys: NSInteger = 20; +#[derive(Debug, Clone, Copy)] pub enum NSWindowLevel { NSNormalWindowLevel = kCGBaseWindowLevelKey as _, NSFloatingWindowLevel = kCGFloatingWindowLevelKey as _, diff --git a/src/platform_impl/macos/monitor.rs b/src/platform_impl/macos/monitor.rs index 7b3e848832e..b9fb053672f 100644 --- a/src/platform_impl/macos/monitor.rs +++ b/src/platform_impl/macos/monitor.rs @@ -1,23 +1,20 @@ -use std::collections::VecDeque; -use std::fmt; +use std::{collections::VecDeque, fmt}; -use cocoa::appkit::NSScreen; -use cocoa::base::{id, nil}; -use cocoa::foundation::{NSString, NSUInteger}; +use cocoa::{appkit::NSScreen, base::{id, nil}, foundation::{NSString, NSUInteger}}; use core_graphics::display::{CGDirectDisplayID, CGDisplay, CGDisplayBounds}; -use {PhysicalPosition, PhysicalSize}; +use crate::dpi::{PhysicalPosition, PhysicalSize}; use super::EventLoop; use super::window::{IdRef, Window2}; #[derive(Clone, PartialEq)] pub struct MonitorHandle(CGDirectDisplayID); -fn get_available_monitors() -> VecDeque { +pub fn get_available_monitors() -> VecDeque { if let Ok(displays) = CGDisplay::active_displays() { let mut monitors = VecDeque::with_capacity(displays.len()); - for d in displays { - monitors.push_back(MonitorHandle(d)); + for display in displays { + monitors.push_back(MonitorHandle(display)); } monitors } else { @@ -61,6 +58,7 @@ impl Window2 { impl fmt::Debug for MonitorHandle { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // TODO: Do this using the proper fmt API #[derive(Debug)] struct MonitorHandle { name: Option, @@ -83,6 +81,10 @@ impl fmt::Debug for MonitorHandle { } impl MonitorHandle { + pub fn new(id: CGDirectDisplayID) -> Self { + MonitorHandle(id) + } + pub fn get_name(&self) -> Option { let MonitorHandle(display_id) = *self; let screen_num = CGDisplay::new(display_id).model_number(); diff --git a/src/platform_impl/macos/observer.rs b/src/platform_impl/macos/observer.rs new file mode 100644 index 00000000000..79b4c997b73 --- /dev/null +++ b/src/platform_impl/macos/observer.rs @@ -0,0 +1,259 @@ +use std::{self, ptr, os::raw::*, time::Instant}; + +use platform_impl::platform::app_state::AppState; + +#[link(name = "CoreFoundation", kind = "framework")] +extern { + pub static kCFRunLoopDefaultMode: CFRunLoopMode; + pub static kCFRunLoopCommonModes: CFRunLoopMode; + + pub fn CFRunLoopGetMain() -> CFRunLoopRef; + pub fn CFRunLoopWakeUp(rl: CFRunLoopRef); + + pub fn CFRunLoopObserverCreate( + allocator: CFAllocatorRef, + activities: CFOptionFlags, + repeats: Boolean, + order: CFIndex, + callout: CFRunLoopObserverCallBack, + context: *mut CFRunLoopObserverContext, + ) -> CFRunLoopObserverRef; + pub fn CFRunLoopAddObserver( + rl: CFRunLoopRef, + observer: CFRunLoopObserverRef, + mode: CFRunLoopMode, + ); + + pub fn CFRunLoopTimerCreate( + allocator: CFAllocatorRef, + fireDate: CFAbsoluteTime, + interval: CFTimeInterval, + flags: CFOptionFlags, + order: CFIndex, + callout: CFRunLoopTimerCallBack, + context: *mut CFRunLoopTimerContext, + ) -> CFRunLoopTimerRef; + pub fn CFRunLoopAddTimer( + rl: CFRunLoopRef, + timer: CFRunLoopTimerRef, + mode: CFRunLoopMode, + ); + pub fn CFRunLoopTimerSetNextFireDate( + timer: CFRunLoopTimerRef, + fireDate: CFAbsoluteTime, + ); + pub fn CFRunLoopTimerInvalidate(time: CFRunLoopTimerRef); + + pub fn CFRunLoopSourceCreate( + allocator: CFAllocatorRef, + order: CFIndex, + context: *mut CFRunLoopSourceContext, + ) -> CFRunLoopSourceRef; + pub fn CFRunLoopAddSource( + rl: CFRunLoopRef, + source: CFRunLoopSourceRef, + mode: CFRunLoopMode, + ); + pub fn CFRunLoopSourceInvalidate(source: CFRunLoopSourceRef); + pub fn CFRunLoopSourceSignal(source: CFRunLoopSourceRef); + + pub fn CFAbsoluteTimeGetCurrent() -> CFAbsoluteTime; + pub fn CFRelease(cftype: *const c_void); +} + +pub type Boolean = u8; +const FALSE: Boolean = 0; +const TRUE: Boolean = 1; + +pub enum CFAllocator {} +pub type CFAllocatorRef = *mut CFAllocator; +pub enum CFRunLoop {} +pub type CFRunLoopRef = *mut CFRunLoop; +pub type CFRunLoopMode = CFStringRef; +pub enum CFRunLoopObserver {} +pub type CFRunLoopObserverRef = *mut CFRunLoopObserver; +pub enum CFRunLoopTimer {} +pub type CFRunLoopTimerRef = *mut CFRunLoopTimer; +pub enum CFRunLoopSource {} +pub type CFRunLoopSourceRef = *mut CFRunLoopSource; +pub enum CFString {} +pub type CFStringRef = *const CFString; + +pub type CFHashCode = c_ulong; +pub type CFIndex = c_long; +pub type CFOptionFlags = c_ulong; +pub type CFRunLoopActivity = CFOptionFlags; + +pub type CFAbsoluteTime = CFTimeInterval; +pub type CFTimeInterval = f64; + +pub const kCFRunLoopEntry: CFRunLoopActivity = 0; +pub const kCFRunLoopBeforeWaiting: CFRunLoopActivity = 1 << 5; +pub const kCFRunLoopAfterWaiting: CFRunLoopActivity = 1 << 6; +pub const kCFRunLoopExit: CFRunLoopActivity = 1 << 7; + +pub type CFRunLoopObserverCallBack = extern "C" fn( + observer: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + info: *mut c_void, +); +pub type CFRunLoopTimerCallBack = extern "C" fn( + timer: CFRunLoopTimerRef, + info: *mut c_void +); + +pub enum CFRunLoopObserverContext {} +pub enum CFRunLoopTimerContext {} + +#[repr(C)] +pub struct CFRunLoopSourceContext { + pub version: CFIndex, + pub info: *mut c_void, + pub retain: extern "C" fn(*const c_void) -> *const c_void, + pub release: extern "C" fn(*const c_void), + pub copyDescription: extern "C" fn(*const c_void) -> CFStringRef, + pub equal: extern "C" fn(*const c_void, *const c_void) -> Boolean, + pub hash: extern "C" fn(*const c_void) -> CFHashCode, + pub schedule: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), + pub cancel: extern "C" fn(*mut c_void, CFRunLoopRef, CFRunLoopMode), + pub perform: extern "C" fn(*mut c_void), +} + +// begin is queued with the highest priority to ensure it is processed before other observers +extern fn control_flow_begin_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, +) { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopAfterWaiting => { + //trace!("Triggered `CFRunLoopAfterWaiting`"); + AppState::wakeup(); + //trace!("Completed `CFRunLoopAfterWaiting`"); + }, + kCFRunLoopEntry => unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } +} + +// end is queued with the lowest priority to ensure it is processed after other observers +// without that, LoopDestroyed would get sent after EventsCleared +extern fn control_flow_end_handler( + _: CFRunLoopObserverRef, + activity: CFRunLoopActivity, + _: *mut c_void, +) { + #[allow(non_upper_case_globals)] + match activity { + kCFRunLoopBeforeWaiting => { + //trace!("Triggered `CFRunLoopBeforeWaiting`"); + AppState::cleared(); + //trace!("Completed `CFRunLoopBeforeWaiting`"); + }, + kCFRunLoopExit => (),//unimplemented!(), // not expected to ever happen + _ => unreachable!(), + } +} + +struct RunLoop(CFRunLoopRef); + +impl RunLoop { + unsafe fn get() -> Self { + RunLoop(CFRunLoopGetMain()) + } + + unsafe fn add_observer( + &self, + flags: CFOptionFlags, + priority: CFIndex, + handler: CFRunLoopObserverCallBack, + ) { + let observer = CFRunLoopObserverCreate( + ptr::null_mut(), + flags, + TRUE, // Indicates we want this to run repeatedly + priority, // The lower the value, the sooner this will run + handler, + ptr::null_mut(), + ); + CFRunLoopAddObserver(self.0, observer, kCFRunLoopDefaultMode); + } +} + +pub fn setup_control_flow_observers() { + unsafe { + let run_loop = RunLoop::get(); + run_loop.add_observer( + kCFRunLoopEntry | kCFRunLoopAfterWaiting, + CFIndex::min_value(), + control_flow_begin_handler, + ); + run_loop.add_observer( + kCFRunLoopExit | kCFRunLoopBeforeWaiting, + CFIndex::max_value(), + control_flow_end_handler, + ); + } +} + + +pub struct EventLoopWaker { + timer: CFRunLoopTimerRef, +} + +impl Drop for EventLoopWaker { + fn drop(&mut self) { + unsafe { + CFRunLoopTimerInvalidate(self.timer); + CFRelease(self.timer as _); + } + } +} + +impl Default for EventLoopWaker { + fn default() -> EventLoopWaker { + extern fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {} + unsafe { + // create a timer with a 1µs interval (1ns does not work) to mimic polling. + // it is initially setup with a first fire time really far into the + // future, but that gets changed to fire immediatley in did_finish_launching + let timer = CFRunLoopTimerCreate( + ptr::null_mut(), + std::f64::MAX, + 0.000_000_1, + 0, + 0, + wakeup_main_loop, + ptr::null_mut(), + ); + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes); + EventLoopWaker { timer } + } + } +} + +impl EventLoopWaker { + pub fn stop(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MAX) } + } + + pub fn start(&mut self) { + unsafe { CFRunLoopTimerSetNextFireDate(self.timer, std::f64::MIN) } + } + + pub fn start_at(&mut self, instant: Instant) { + let now = Instant::now(); + if now >= instant { + self.start(); + } else { + unsafe { + let current = CFAbsoluteTimeGetCurrent(); + let duration = instant - now; + let fsecs = duration.subsec_nanos() as f64 / 1_000_000_000.0 + + duration.as_secs() as f64; + CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs) + } + } + } +} diff --git a/src/platform_impl/macos/util/async.rs b/src/platform_impl/macos/util/async.rs new file mode 100644 index 00000000000..4ececc0396a --- /dev/null +++ b/src/platform_impl/macos/util/async.rs @@ -0,0 +1,327 @@ +use std::{os::raw::c_void, sync::{Mutex, Weak}}; + +use cocoa::{ + appkit::{CGFloat, NSWindow, NSWindowStyleMask}, + base::{id, nil}, + foundation::{NSAutoreleasePool, NSPoint, NSSize}, +}; +use dispatch::ffi::{dispatch_async_f, dispatch_get_main_queue, dispatch_sync_f}; + +use dpi::LogicalSize; +use platform_impl::platform::{ffi, window::SharedState}; + +unsafe fn set_style_mask(nswindow: id, nsview: id, mask: NSWindowStyleMask) { + nswindow.setStyleMask_(mask); + // If we don't do this, key handling will break + // (at least until the window is clicked again/etc.) + nswindow.makeFirstResponder_(nsview); +} + +struct SetStyleMaskData { + nswindow: id, + nsview: id, + mask: NSWindowStyleMask, +} +impl SetStyleMaskData { + fn new_ptr( + nswindow: id, + nsview: id, + mask: NSWindowStyleMask, + ) -> *mut Self { + Box::into_raw(Box::new(SetStyleMaskData { nswindow, nsview, mask })) + } +} +extern fn set_style_mask_callback(context: *mut c_void) { + unsafe { + let context_ptr = context as *mut SetStyleMaskData; + { + let context = &*context_ptr; + set_style_mask(context.nswindow, context.nsview, context.mask); + } + Box::from_raw(context_ptr); + } +} +// Always use this function instead of trying to modify `styleMask` directly! +// `setStyleMask:` isn't thread-safe, so we have to use Grand Central Dispatch. +// Otherwise, this would vomit out errors about not being on the main thread +// and fail to do anything. +pub unsafe fn set_style_mask_async(nswindow: id, nsview: id, mask: NSWindowStyleMask) { + let context = SetStyleMaskData::new_ptr(nswindow, nsview, mask); + dispatch_async_f( + dispatch_get_main_queue(), + context as *mut _, + set_style_mask_callback, + ); +} +pub unsafe fn set_style_mask_sync(nswindow: id, nsview: id, mask: NSWindowStyleMask) { + let context = SetStyleMaskData::new_ptr(nswindow, nsview, mask); + dispatch_sync_f( + dispatch_get_main_queue(), + context as *mut _, + set_style_mask_callback, + ); +} + +struct SetContentSizeData { + nswindow: id, + size: LogicalSize, +} +impl SetContentSizeData { + fn new_ptr( + nswindow: id, + size: LogicalSize, + ) -> *mut Self { + Box::into_raw(Box::new(SetContentSizeData { nswindow, size })) + } +} +extern fn set_content_size_callback(context: *mut c_void) { + unsafe { + let context_ptr = context as *mut SetContentSizeData; + { + let context = &*context_ptr; + NSWindow::setContentSize_( + context.nswindow, + NSSize::new( + context.size.width as CGFloat, + context.size.height as CGFloat, + ), + ); + } + Box::from_raw(context_ptr); + } +} +// `setContentSize:` isn't thread-safe either, though it doesn't log any errors +// and just fails silently. Anyway, GCD to the rescue! +pub unsafe fn set_content_size_async(nswindow: id, size: LogicalSize) { + let context = SetContentSizeData::new_ptr(nswindow, size); + dispatch_async_f( + dispatch_get_main_queue(), + context as *mut _, + set_content_size_callback, + ); +} + +struct SetFrameTopLeftPointData { + nswindow: id, + point: NSPoint, +} +impl SetFrameTopLeftPointData { + fn new_ptr( + nswindow: id, + point: NSPoint, + ) -> *mut Self { + Box::into_raw(Box::new(SetFrameTopLeftPointData { nswindow, point })) + } +} +extern fn set_frame_top_left_point_callback(context: *mut c_void) { + unsafe { + let context_ptr = context as *mut SetFrameTopLeftPointData; + { + let context = &*context_ptr; + NSWindow::setFrameTopLeftPoint_(context.nswindow, context.point); + } + Box::from_raw(context_ptr); + } +} +// `setFrameTopLeftPoint:` isn't thread-safe, but fortunately has the courtesy +// to log errors. +pub unsafe fn set_frame_top_left_point_async(nswindow: id, point: NSPoint) { + let context = SetFrameTopLeftPointData::new_ptr(nswindow, point); + dispatch_async_f( + dispatch_get_main_queue(), + context as *mut _, + set_frame_top_left_point_callback, + ); +} + +struct SetLevelData { + nswindow: id, + level: ffi::NSWindowLevel, +} +impl SetLevelData { + fn new_ptr( + nswindow: id, + level: ffi::NSWindowLevel, + ) -> *mut Self { + Box::into_raw(Box::new(SetLevelData { nswindow, level })) + } +} +extern fn set_level_callback(context: *mut c_void) { + unsafe { + let context_ptr = context as *mut SetLevelData; + { + let context = &*context_ptr; + context.nswindow.setLevel_(context.level as _); + } + Box::from_raw(context_ptr); + } +} +// `setFrameTopLeftPoint:` isn't thread-safe, and fails silently. +pub unsafe fn set_level_async(nswindow: id, level: ffi::NSWindowLevel) { + let context = SetLevelData::new_ptr(nswindow, level); + dispatch_async_f( + dispatch_get_main_queue(), + context as *mut _, + set_level_callback, + ); +} + +struct ToggleFullScreenData { + nswindow: id, + nsview: id, + not_fullscreen: bool, + shared_state: Weak>, +} +impl ToggleFullScreenData { + fn new_ptr( + nswindow: id, + nsview: id, + not_fullscreen: bool, + shared_state: Weak>, + ) -> *mut Self { + Box::into_raw(Box::new(ToggleFullScreenData { + nswindow, + nsview, + not_fullscreen, + shared_state, + })) + } +} +extern fn toggle_full_screen_callback(context: *mut c_void) { + unsafe { + let context_ptr = context as *mut ToggleFullScreenData; + { + let context = &*context_ptr; + + // `toggleFullScreen` doesn't work if the `StyleMask` is none, so we + // set a normal style temporarily. The previous state will be + // restored in `WindowDelegate::window_did_exit_fullscreen`. + if context.not_fullscreen { + let curr_mask = context.nswindow.styleMask(); + let required = NSWindowStyleMask::NSTitledWindowMask + | NSWindowStyleMask::NSResizableWindowMask; + if !curr_mask.contains(required) { + set_style_mask(context.nswindow, context.nsview, required); + if let Some(shared_state) = context.shared_state.upgrade() { + trace!("Locked shared state in `toggle_full_screen_callback`"); + let mut shared_state_lock = shared_state.lock().unwrap(); + (*shared_state_lock).saved_style = Some(curr_mask); + trace!("Unlocked shared state in `toggle_full_screen_callback`"); + } + } + } + + context.nswindow.toggleFullScreen_(nil); + } + Box::from_raw(context_ptr); + } +} +// `toggleFullScreen` is thread-safe, but our additional logic to account for +// window styles isn't. +pub unsafe fn toggle_full_screen_async( + nswindow: id, + nsview: id, + not_fullscreen: bool, + shared_state: Weak>, +) { + let context = ToggleFullScreenData::new_ptr( + nswindow, + nsview, + not_fullscreen, + shared_state, + ); + dispatch_async_f( + dispatch_get_main_queue(), + context as *mut _, + toggle_full_screen_callback, + ); +} + +struct OrderOutData { + nswindow: id, +} +impl OrderOutData { + fn new_ptr(nswindow: id) -> *mut Self { + Box::into_raw(Box::new(OrderOutData { nswindow })) + } +} +extern fn order_out_callback(context: *mut c_void) { + unsafe { + let context_ptr = context as *mut OrderOutData; + { + let context = &*context_ptr; + context.nswindow.orderOut_(nil); + } + Box::from_raw(context_ptr); + } +} +// `orderOut:` isn't thread-safe. Calling it from another thread actually works, +// but with an odd delay. +pub unsafe fn order_out_async(nswindow: id) { + let context = OrderOutData::new_ptr(nswindow); + dispatch_async_f( + dispatch_get_main_queue(), + context as *mut _, + order_out_callback, + ); +} + +struct MakeKeyAndOrderFrontData { + nswindow: id, +} +impl MakeKeyAndOrderFrontData { + fn new_ptr(nswindow: id) -> *mut Self { + Box::into_raw(Box::new(MakeKeyAndOrderFrontData { nswindow })) + } +} +extern fn make_key_and_order_front_callback(context: *mut c_void) { + unsafe { + let context_ptr = context as *mut MakeKeyAndOrderFrontData; + { + let context = &*context_ptr; + context.nswindow.makeKeyAndOrderFront_(nil); + } + Box::from_raw(context_ptr); + } +} +// `makeKeyAndOrderFront:` isn't thread-safe. Calling it from another thread +// actually works, but with an odd delay. +pub unsafe fn make_key_and_order_front_async(nswindow: id) { + let context = MakeKeyAndOrderFrontData::new_ptr(nswindow); + dispatch_async_f( + dispatch_get_main_queue(), + context as *mut _, + make_key_and_order_front_callback, + ); +} + +struct CloseData { + nswindow: id, +} +impl CloseData { + fn new_ptr(nswindow: id) -> *mut Self { + Box::into_raw(Box::new(CloseData { nswindow })) + } +} +extern fn close_callback(context: *mut c_void) { + unsafe { + let context_ptr = context as *mut CloseData; + { + let context = &*context_ptr; + let pool = NSAutoreleasePool::new(nil); + context.nswindow.close(); + pool.drain(); + } + Box::from_raw(context_ptr); + } +} +// `close:` is thread-safe, but we want the event to be triggered from the main +// thread. Though, it's a good idea to look into that more... +pub unsafe fn close_async(nswindow: id) { + let context = CloseData::new_ptr(nswindow); + dispatch_async_f( + dispatch_get_main_queue(), + context as *mut _, + close_callback, + ); +} diff --git a/src/platform_impl/macos/util/cursor.rs b/src/platform_impl/macos/util/cursor.rs index e7815d786b2..e741188af04 100644 --- a/src/platform_impl/macos/util/cursor.rs +++ b/src/platform_impl/macos/util/cursor.rs @@ -5,7 +5,7 @@ use cocoa::{ use objc::runtime::Sel; use super::IntoOption; -use MouseCursor; +use window::MouseCursor; pub enum Cursor { Native(&'static str), diff --git a/src/platform_impl/macos/window_delegate.rs b/src/platform_impl/macos/window_delegate.rs new file mode 100644 index 00000000000..83defb3f2bf --- /dev/null +++ b/src/platform_impl/macos/window_delegate.rs @@ -0,0 +1,460 @@ +use std::{f64, os::raw::c_void, sync::{Arc, Weak}}; + +use cocoa::{ + appkit::{self, NSView, NSWindow}, base::{id, nil}, + foundation::NSAutoreleasePool, +}; +use objc::{runtime::{Class, Object, Sel, BOOL, YES, NO}, declare::ClassDecl}; + +use {dpi::LogicalSize, event::{Event, WindowEvent}, window::WindowId}; +use platform_impl::platform::{ + app_state::AppState, util::{self, IdRef}, + window::{get_window_id, UnownedWindow}, +}; + +pub struct WindowDelegateState { + nswindow: IdRef, // never changes + nsview: IdRef, // never changes + + window: Weak, + + // TODO: It's possible for delegate methods to be called asynchronously, + // causing data races / `RefCell` panics. + + // This is set when WindowBuilder::with_fullscreen was set, + // see comments of `window_did_fail_to_enter_fullscreen` + initial_fullscreen: bool, + + // During `windowDidResize`, we use this to only send Moved if the position changed. + previous_position: Option<(f64, f64)>, + + // Used to prevent redundant events. + previous_dpi_factor: f64, +} + +impl WindowDelegateState { + pub fn new( + window: &Arc, + initial_fullscreen: bool, + ) -> Self { + let dpi_factor = window.get_hidpi_factor(); + + let mut delegate_state = WindowDelegateState { + nswindow: window.nswindow.clone(), + nsview: window.nsview.clone(), + window: Arc::downgrade(&window), + initial_fullscreen, + previous_position: None, + previous_dpi_factor: dpi_factor, + }; + + if dpi_factor != 1.0 { + delegate_state.emit_event(WindowEvent::HiDpiFactorChanged(dpi_factor)); + delegate_state.emit_resize_event(); + } + + delegate_state + } + + fn with_window(&mut self, callback: F) -> Option + where F: FnOnce(&UnownedWindow) -> T + { + self.window + .upgrade() + .map(|ref window| callback(window)) + } + + pub fn emit_event(&mut self, event: WindowEvent) { + let event = Event::WindowEvent { + window_id: WindowId(get_window_id(*self.nswindow)), + event, + }; + AppState::queue_event(event); + } + + pub fn emit_resize_event(&mut self) { + let rect = unsafe { NSView::frame(*self.nsview) }; + let size = LogicalSize::new(rect.size.width as f64, rect.size.height as f64); + let event = Event::WindowEvent { + window_id: WindowId(get_window_id(*self.nswindow)), + event: WindowEvent::Resized(size), + }; + AppState::send_event_immediately(event); + } + + fn emit_move_event(&mut self) { + let rect = unsafe { NSWindow::frame(*self.nswindow) }; + let x = rect.origin.x as f64; + let y = util::bottom_left_to_top_left(rect); + let moved = self.previous_position != Some((x, y)); + if moved { + self.previous_position = Some((x, y)); + self.emit_event(WindowEvent::Moved((x, y).into())); + } + } +} + +pub fn new_delegate(window: &Arc, initial_fullscreen: bool) -> IdRef { + let state = WindowDelegateState::new(window, initial_fullscreen); + unsafe { + // This is free'd in `dealloc` + let state_ptr = Box::into_raw(Box::new(state)) as *mut c_void; + let delegate: id = msg_send![WINDOW_DELEGATE_CLASS.0, alloc]; + IdRef::new(msg_send![delegate, initWithWinit:state_ptr]) + } +} + +struct WindowDelegateClass(*const Class); +unsafe impl Send for WindowDelegateClass {} +unsafe impl Sync for WindowDelegateClass {} + +lazy_static! { + static ref WINDOW_DELEGATE_CLASS: WindowDelegateClass = unsafe { + let superclass = class!(NSResponder); + let mut decl = ClassDecl::new("WinitWindowDelegate", superclass).unwrap(); + + decl.add_method( + sel!(dealloc), + dealloc as extern fn(&Object, Sel), + ); + decl.add_method( + sel!(initWithWinit:), + init_with_winit as extern fn(&Object, Sel, *mut c_void) -> id, + ); + + decl.add_method( + sel!(windowShouldClose:), + window_should_close as extern fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(windowWillClose:), + window_will_close as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidResize:), + window_did_resize as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidMove:), + window_did_move as extern fn(&Object, Sel, id)); + decl.add_method( + sel!(windowDidChangeScreen:), + window_did_change_screen as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidChangeBackingProperties:), + window_did_change_backing_properties as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidBecomeKey:), + window_did_become_key as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidResignKey:), + window_did_resign_key as extern fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(draggingEntered:), + dragging_entered as extern fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(prepareForDragOperation:), + prepare_for_drag_operation as extern fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(performDragOperation:), + perform_drag_operation as extern fn(&Object, Sel, id) -> BOOL, + ); + decl.add_method( + sel!(concludeDragOperation:), + conclude_drag_operation as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(draggingExited:), + dragging_exited as extern fn(&Object, Sel, id), + ); + + decl.add_method( + sel!(windowDidEnterFullScreen:), + window_did_enter_fullscreen as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowWillEnterFullScreen:), + window_will_enter_fullscreen as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidExitFullScreen:), + window_did_exit_fullscreen as extern fn(&Object, Sel, id), + ); + decl.add_method( + sel!(windowDidFailToEnterFullScreen:), + window_did_fail_to_enter_fullscreen as extern fn(&Object, Sel, id), + ); + + decl.add_ivar::<*mut c_void>("winitState"); + WindowDelegateClass(decl.register()) + }; +} + +// This function is definitely unsafe, but labeling that would increase +// boilerplate and wouldn't really clarify anything... +fn with_state T, T>(this: &Object, callback: F) { + let state_ptr = unsafe { + let state_ptr: *mut c_void = *this.get_ivar("winitState"); + &mut *(state_ptr as *mut WindowDelegateState) + }; + callback(state_ptr); +} + +extern fn dealloc(this: &Object, _sel: Sel) { + with_state(this, |state| unsafe { + Box::from_raw(state as *mut WindowDelegateState); + }); +} + +extern fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> id { + unsafe { + let this: id = msg_send![this, init]; + if this != nil { + (*this).set_ivar("winitState", state); + with_state(&*this, |state| { + let () = msg_send![*state.nswindow, setDelegate:this]; + }); + } + this + } +} + +extern fn window_should_close(this: &Object, _: Sel, _: id) -> BOOL { + trace!("Triggered `windowShouldClose:`"); + with_state(this, |state| state.emit_event(WindowEvent::CloseRequested)); + trace!("Completed `windowShouldClose:`"); + NO +} + +extern fn window_will_close(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowWillClose:`"); + with_state(this, |state| unsafe { + // `setDelegate:` retains the previous value and then autoreleases it + let pool = NSAutoreleasePool::new(nil); + // Since El Capitan, we need to be careful that delegate methods can't + // be called after the window closes. + let () = msg_send![*state.nswindow, setDelegate:nil]; + pool.drain(); + state.emit_event(WindowEvent::Destroyed); + }); + trace!("Completed `windowWillClose:`"); +} + +extern fn window_did_resize(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidResize:`"); + with_state(this, |state| { + state.emit_resize_event(); + state.emit_move_event(); + }); + trace!("Completed `windowDidResize:`"); +} + +// This won't be triggered if the move was part of a resize. +extern fn window_did_move(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidMove:`"); + with_state(this, |state| { + state.emit_move_event(); + }); + trace!("Completed `windowDidMove:`"); +} + +extern fn window_did_change_screen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidChangeScreen:`"); + with_state(this, |state| { + let dpi_factor = unsafe { + NSWindow::backingScaleFactor(*state.nswindow) + } as f64; + if state.previous_dpi_factor != dpi_factor { + state.previous_dpi_factor = dpi_factor; + state.emit_event(WindowEvent::HiDpiFactorChanged(dpi_factor)); + state.emit_resize_event(); + } + }); + trace!("Completed `windowDidChangeScreen:`"); +} + +// This will always be called before `window_did_change_screen`. +extern fn window_did_change_backing_properties(this: &Object, _:Sel, _:id) { + trace!("Triggered `windowDidChangeBackingProperties:`"); + with_state(this, |state| { + let dpi_factor = unsafe { + NSWindow::backingScaleFactor(*state.nswindow) + } as f64; + if state.previous_dpi_factor != dpi_factor { + state.previous_dpi_factor = dpi_factor; + state.emit_event(WindowEvent::HiDpiFactorChanged(dpi_factor)); + state.emit_resize_event(); + } + }); + trace!("Completed `windowDidChangeBackingProperties:`"); +} + +extern fn window_did_become_key(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidBecomeKey:`"); + with_state(this, |state| { + // TODO: center the cursor if the window had mouse grab when it + // lost focus + state.emit_event(WindowEvent::Focused(true)); + }); + trace!("Completed `windowDidBecomeKey:`"); +} + +extern fn window_did_resign_key(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidResignKey:`"); + with_state(this, |state| { + state.emit_event(WindowEvent::Focused(false)); + }); + trace!("Completed `windowDidResignKey:`"); +} + +/// Invoked when the dragged image enters destination bounds or frame +extern fn dragging_entered(this: &Object, _: Sel, sender: id) -> BOOL { + trace!("Triggered `draggingEntered:`"); + + use cocoa::appkit::NSPasteboard; + use cocoa::foundation::NSFastEnumeration; + use std::path::PathBuf; + + let pb: id = unsafe { msg_send![sender, draggingPasteboard] }; + let filenames = unsafe { NSPasteboard::propertyListForType(pb, appkit::NSFilenamesPboardType) }; + + for file in unsafe { filenames.iter() } { + use cocoa::foundation::NSString; + use std::ffi::CStr; + + unsafe { + let f = NSString::UTF8String(file); + let path = CStr::from_ptr(f).to_string_lossy().into_owned(); + + with_state(this, |state| { + state.emit_event(WindowEvent::HoveredFile(PathBuf::from(path))); + }); + } + }; + + trace!("Completed `draggingEntered:`"); + YES +} + +/// Invoked when the image is released +extern fn prepare_for_drag_operation(_: &Object, _: Sel, _: id) -> BOOL { + trace!("Triggered `prepareForDragOperation:`"); + trace!("Completed `prepareForDragOperation:`"); + YES +} + +/// Invoked after the released image has been removed from the screen +extern fn perform_drag_operation(this: &Object, _: Sel, sender: id) -> BOOL { + trace!("Triggered `performDragOperation:`"); + + use cocoa::appkit::NSPasteboard; + use cocoa::foundation::NSFastEnumeration; + use std::path::PathBuf; + + let pb: id = unsafe { msg_send![sender, draggingPasteboard] }; + let filenames = unsafe { NSPasteboard::propertyListForType(pb, appkit::NSFilenamesPboardType) }; + + for file in unsafe { filenames.iter() } { + use cocoa::foundation::NSString; + use std::ffi::CStr; + + unsafe { + let f = NSString::UTF8String(file); + let path = CStr::from_ptr(f).to_string_lossy().into_owned(); + + with_state(this, |state| { + state.emit_event(WindowEvent::DroppedFile(PathBuf::from(path))); + }); + } + }; + + trace!("Completed `performDragOperation:`"); + YES +} + +/// Invoked when the dragging operation is complete +extern fn conclude_drag_operation(_: &Object, _: Sel, _: id) { + trace!("Triggered `concludeDragOperation:`"); + trace!("Completed `concludeDragOperation:`"); +} + +/// Invoked when the dragging operation is cancelled +extern fn dragging_exited(this: &Object, _: Sel, _: id) { + trace!("Triggered `draggingExited:`"); + with_state(this, |state| state.emit_event(WindowEvent::HoveredFileCancelled)); + trace!("Completed `draggingExited:`"); +} + +/// Invoked when before enter fullscreen +extern fn window_will_enter_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowWillEnterFullscreen:`"); + with_state(this, |state| state.with_window(|window| { + trace!("Locked shared state in `window_will_enter_fullscreen`"); + window.shared_state.lock().unwrap().maximized = window.is_zoomed(); + trace!("Unlocked shared state in `window_will_enter_fullscreen`"); + })); + trace!("Completed `windowWillEnterFullscreen:`"); +} + +/// Invoked when entered fullscreen +extern fn window_did_enter_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidEnterFullscreen:`"); + with_state(this, |state| { + state.with_window(|window| { + let monitor = window.get_current_monitor(); + trace!("Locked shared state in `window_did_enter_fullscreen`"); + window.shared_state.lock().unwrap().fullscreen = Some(monitor); + trace!("Unlocked shared state in `window_will_enter_fullscreen`"); + }); + state.initial_fullscreen = false; + }); + trace!("Completed `windowDidEnterFullscreen:`"); +} + +/// Invoked when exited fullscreen +extern fn window_did_exit_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidExitFullscreen:`"); + with_state(this, |state| state.with_window(|window| { + window.restore_state_from_fullscreen(); + })); + trace!("Completed `windowDidExitFullscreen:`"); +} + +/// Invoked when fail to enter fullscreen +/// +/// When this window launch from a fullscreen app (e.g. launch from VS Code +/// terminal), it creates a new virtual destkop and a transition animation. +/// This animation takes one second and cannot be disable without +/// elevated privileges. In this animation time, all toggleFullscreen events +/// will be failed. In this implementation, we will try again by using +/// performSelector:withObject:afterDelay: until window_did_enter_fullscreen. +/// It should be fine as we only do this at initialzation (i.e with_fullscreen +/// was set). +/// +/// From Apple doc: +/// In some cases, the transition to enter full-screen mode can fail, +/// due to being in the midst of handling some other animation or user gesture. +/// This method indicates that there was an error, and you should clean up any +/// work you may have done to prepare to enter full-screen mode. +extern fn window_did_fail_to_enter_fullscreen(this: &Object, _: Sel, _: id) { + trace!("Triggered `windowDidFailToEnterFullscreen:`"); + with_state(this, |state| { + if state.initial_fullscreen { + let _: () = unsafe { msg_send![*state.nswindow, + performSelector:sel!(toggleFullScreen:) + withObject:nil + afterDelay: 0.5 + ] }; + } else { + state.with_window(|window| window.restore_state_from_fullscreen()); + } + }); + trace!("Completed `windowDidFailToEnterFullscreen:`"); +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 00000000000..6d50fb13885 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,12 @@ +use std::ops::BitAnd; + +// Replace with `!` once stable +#[derive(Debug)] +pub enum Never {} + +pub fn has_flag(bitset: T, flag: T) -> bool +where T: + Copy + PartialEq + BitAnd +{ + bitset & flag == flag +}