From 78a85d6002d508cc7a355ed676cf448d1a4f1470 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Wed, 12 Jul 2017 00:14:06 -0500 Subject: [PATCH 01/24] Refactor Cocoa event handling --- src/platform/macos/events_loop.rs | 707 +++++++++++++++++------------- src/platform/macos/mod.rs | 2 + src/platform/macos/send_event.rs | 24 + 3 files changed, 432 insertions(+), 301 deletions(-) create mode 100644 src/platform/macos/send_event.rs diff --git a/src/platform/macos/events_loop.rs b/src/platform/macos/events_loop.rs index 8973ab5e25..e9dd72589e 100644 --- a/src/platform/macos/events_loop.rs +++ b/src/platform/macos/events_loop.rs @@ -1,17 +1,19 @@ use {ControlFlow, EventsLoopClosed}; use cocoa::{self, appkit, foundation}; use cocoa::appkit::{NSApplication, NSEvent, NSView, NSWindow}; +use core_foundation::base::{CFRetain, CFRelease, CFTypeRef}; use events::{self, ElementState, Event, MouseButton, TouchPhase, WindowEvent, DeviceEvent, ModifiersState, KeyboardInput}; use std::collections::VecDeque; use std::sync::{Arc, Mutex, Weak}; use super::window::Window; use std; use super::DeviceId; - +use super::send_event::SendEvent; pub struct EventsLoop { modifiers: Modifiers, pub shared: Arc, + current_event: Option, } // State shared between the `EventsLoop` and its registered windows. @@ -158,13 +160,13 @@ impl UserCallback { } - impl EventsLoop { pub fn new() -> Self { EventsLoop { shared: Arc::new(Shared::new()), modifiers: Modifiers::new(), + current_event: None, } } @@ -181,28 +183,34 @@ impl EventsLoop { // Loop as long as we have pending events to return. loop { - unsafe { - // First, yield all pending events. - self.shared.call_user_callback_with_pending_events(); + // First, yield all pending events. + self.shared.call_user_callback_with_pending_events(); - let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); - - // Poll for the next event, returning `nil` if there are none. - let ns_event = appkit::NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( - appkit::NSAnyEventMask.bits() | appkit::NSEventMaskPressure.bits(), - foundation::NSDate::distantPast(cocoa::base::nil), - foundation::NSDefaultRunLoopMode, - cocoa::base::YES); + // Are we already working on something? + if self.current_event.is_none() { + // Attempt to receive a single message with an immediate timeout + self.current_event = CocoaEvent::receive(true); + } - let event = self.ns_event_to_event(ns_event); + // Do we have something to process? + if let Some(mut current_event) = self.current_event.take() { + // Process it until it's done + while !current_event.work(self) { + // Repeat + } - let _: () = msg_send![pool, release]; + // Did we get an event? + if let CocoaEvent::Complete(Some(winit_event)) = current_event { + // Call the user's callback + unsafe { self.shared.user_callback.call_with_event(winit_event); } - match event { - // Call the user's callback. - Some(event) => self.shared.user_callback.call_with_event(event), - None => break, + // Repeat + continue } + } else { + // The event loop returned no message + // Finish + break } } @@ -237,25 +245,28 @@ impl EventsLoop { break; } - let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); - - // Wait for the next event. Note that this function blocks during resize. - let ns_event = appkit::NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( - appkit::NSAnyEventMask.bits() | appkit::NSEventMaskPressure.bits(), - foundation::NSDate::distantFuture(cocoa::base::nil), - foundation::NSDefaultRunLoopMode, - cocoa::base::YES); + // Are we already working on something? + if self.current_event.is_none() { + // Attempt to receive a single message with a forever timeout + self.current_event = CocoaEvent::receive(false); + } - let maybe_event = self.ns_event_to_event(ns_event); + // Do we have something to process? + if let Some(mut current_event) = self.current_event.take() { + // Process it until it's done + while !current_event.work(self) { + // Repeat + } - // Release the pool before calling the top callback in case the user calls either - // `run_forever` or `poll_events` within the callback. - let _: () = msg_send![pool, release]; + // Did we get an event? + if let CocoaEvent::Complete(Some(winit_event)) = current_event { + // Call the callback + self.shared.user_callback.call_with_event(winit_event); - if let Some(event) = maybe_event { - self.shared.user_callback.call_with_event(event); - if let ControlFlow::Break = control_flow.get() { - break; + // Exit as directed + if let ControlFlow::Break = control_flow.get() { + break; + } } } } @@ -264,8 +275,88 @@ impl EventsLoop { self.shared.user_callback.drop(); } - // Convert some given `NSEvent` into a winit `Event`. - unsafe fn ns_event_to_event(&mut self, ns_event: cocoa::base::id) -> Option { + pub fn create_proxy(&self) -> Proxy { + Proxy {} + } +} + +impl Proxy { + pub fn wakeup(&self) -> Result<(), EventsLoopClosed> { + // Awaken the event loop by triggering `NSApplicationActivatedEventType`. + unsafe { + let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); + let event = + NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( + cocoa::base::nil, + appkit::NSApplicationDefined, + foundation::NSPoint::new(0.0, 0.0), + appkit::NSEventModifierFlags::empty(), + 0.0, + 0, + cocoa::base::nil, + appkit::NSEventSubtype::NSApplicationActivatedEventType, + 0, + 0); + appkit::NSApp().postEvent_atStart_(event, cocoa::base::NO); + foundation::NSAutoreleasePool::drain(pool); + } + Ok(()) + } +} + +struct RetainedEvent(cocoa::base::id); +impl RetainedEvent { + fn new(event: cocoa::base::id) -> RetainedEvent { + unsafe { CFRetain(event as CFTypeRef); } + RetainedEvent(event) + } + fn into_inner(self) -> cocoa::base::id { + self.0 + } +} +impl Drop for RetainedEvent { + fn drop(&mut self) { + unsafe { CFRelease(self.0 as CFTypeRef); } + } +} + +enum CocoaEvent { + Received(RetainedEvent), + Sending(SendEvent, RetainedEvent), + Complete(Option), +} + +impl CocoaEvent { + fn receive(immediate_timeout: bool) -> Option { + unsafe { + let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); + + // Pick a timeout + let timeout = if immediate_timeout { + foundation::NSDate::distantPast(cocoa::base::nil) + } else { + foundation::NSDate::distantFuture(cocoa::base::nil) + }; + + // Poll for the next event + let ns_event = appkit::NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( + appkit::NSAnyEventMask.bits() | appkit::NSEventMaskPressure.bits(), + timeout, + foundation::NSDefaultRunLoopMode, + cocoa::base::YES); + + // Wrap the result in a CocoaEvent + let event = Self::new(ns_event); + + let _: () = msg_send![pool, release]; + + return event + } + } + + fn new(ns_event: cocoa::base::id) -> Option { + // we (possibly) received `ns_event` from the windowing subsystem + // is this an event? if ns_event == cocoa::base::nil { return None; } @@ -276,301 +367,315 @@ impl EventsLoop { // enum as there is no variant associated with the value. Thus, we return early if this // sneaky event occurs. If someone does find some documentation on this, please fix this by // adding an appropriate variant to the `NSEventType` enum in the cocoa-rs crate. - if ns_event.eventType() as u64 == 21 { + if unsafe { ns_event.eventType() } as u64 == 21 { return None; } - let event_type = ns_event.eventType(); - let ns_window = ns_event.window(); - let window_id = super::window::get_window_id(ns_window); - - // FIXME: Document this. Why do we do this? Seems like it passes on events to window/app. - // If we don't do this, window does not become main for some reason. - match event_type { - appkit::NSKeyDown => (), - _ => appkit::NSApp().sendEvent_(ns_event), - } - - let windows = self.shared.windows.lock().unwrap(); - let maybe_window = windows.iter() - .filter_map(Weak::upgrade) - .find(|window| window_id == window.id()); - - let into_event = |window_event| Event::WindowEvent { - window_id: ::WindowId(window_id), - event: window_event, - }; - - // Returns `Some` window if one of our windows is the key window. - let maybe_key_window = || windows.iter() - .filter_map(Weak::upgrade) - .find(|window| { - let is_key_window: cocoa::base::BOOL = msg_send![*window.window, isKeyWindow]; - is_key_window == cocoa::base::YES - }); + // good enough + Some(CocoaEvent::Received(RetainedEvent::new(ns_event))) + } - match event_type { - - appkit::NSKeyDown => { - let mut events = std::collections::VecDeque::new(); - let received_c_str = foundation::NSString::UTF8String(ns_event.characters()); - let received_str = std::ffi::CStr::from_ptr(received_c_str); - - let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); - let state = ElementState::Pressed; - let code = NSEvent::keyCode(ns_event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: vkey, - modifiers: event_mods(ns_event), - }, + // Attempt to push a CocoaEvent towards a winit Event + // Returns true on completion + fn work(&mut self, events_loop: &mut EventsLoop) -> bool { + // take ourselves and match on it + let (new_event, is_complete) = match std::mem::replace(self, CocoaEvent::Complete(None)) { + CocoaEvent::Received(retained_event) => { + // Determine if we need to send this event + // FIXME: Document this. Why do we do this? Seems like it passes on events to window/app. + // If we don't do this, window does not become main for some reason. + let needs_send = match unsafe { retained_event.0.eventType() } { + appkit::NSKeyDown => false, + _ => true, }; - for received_char in std::str::from_utf8(received_str.to_bytes()).unwrap().chars() { - let window_event = WindowEvent::ReceivedCharacter(received_char); - events.push_back(into_event(window_event)); - } - self.shared.pending_events.lock().unwrap().extend(events.into_iter()); - Some(into_event(window_event)) - }, - - appkit::NSKeyUp => { - let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); - - let state = ElementState::Released; - let code = NSEvent::keyCode(ns_event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: vkey, - modifiers: event_mods(ns_event), - }, - }; - Some(into_event(window_event)) - }, - - appkit::NSFlagsChanged => { - unsafe fn modifier_event(event: cocoa::base::id, - keymask: appkit::NSEventModifierFlags, - key: events::VirtualKeyCode, - key_pressed: bool) -> Option - { - if !key_pressed && NSEvent::modifierFlags(event).contains(keymask) { - let state = ElementState::Pressed; - let code = NSEvent::keyCode(event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: Some(key), - modifiers: event_mods(event), - }, - }; - Some(window_event) - - } else if key_pressed && !NSEvent::modifierFlags(event).contains(keymask) { - let state = ElementState::Released; - let code = NSEvent::keyCode(event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: Some(key), - modifiers: event_mods(event), - }, - }; - Some(window_event) - } else { - None - } + if needs_send { + (CocoaEvent::Sending(SendEvent::new(retained_event.0), retained_event), false) + } else { + (CocoaEvent::Complete(Self::to_event(events_loop, retained_event.into_inner())), true) } + } - let mut events = std::collections::VecDeque::new(); - if let Some(window_event) = modifier_event(ns_event, - appkit::NSShiftKeyMask, - events::VirtualKeyCode::LShift, - self.modifiers.shift_pressed) - { - self.modifiers.shift_pressed = !self.modifiers.shift_pressed; - events.push_back(into_event(window_event)); + CocoaEvent::Sending(send_event, retained_event) => { + // Try to advance send event + if let Some(new_send_event) = send_event.work() { + // Needs more time + (CocoaEvent::Sending(new_send_event, retained_event), false) + } else { + // Done + (CocoaEvent::Complete(Self::to_event(events_loop, retained_event.into_inner())), true) } + } - if let Some(window_event) = modifier_event(ns_event, - appkit::NSControlKeyMask, - events::VirtualKeyCode::LControl, - self.modifiers.ctrl_pressed) - { - self.modifiers.ctrl_pressed = !self.modifiers.ctrl_pressed; - events.push_back(into_event(window_event)); - } + CocoaEvent::Complete(event) => { + // nothing to do + (CocoaEvent::Complete(event), true) + } + }; - if let Some(window_event) = modifier_event(ns_event, - appkit::NSCommandKeyMask, - events::VirtualKeyCode::LWin, - self.modifiers.win_pressed) - { - self.modifiers.win_pressed = !self.modifiers.win_pressed; - events.push_back(into_event(window_event)); - } + // replace ourselves with the result of the match + std::mem::replace(self, new_event); - if let Some(window_event) = modifier_event(ns_event, - appkit::NSAlternateKeyMask, - events::VirtualKeyCode::LAlt, - self.modifiers.alt_pressed) - { - self.modifiers.alt_pressed = !self.modifiers.alt_pressed; - events.push_back(into_event(window_event)); - } + // return the completion flag + is_complete + } - let event = events.pop_front(); - self.shared.pending_events.lock().unwrap().extend(events.into_iter()); - event - }, - - appkit::NSLeftMouseDown => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Left })) }, - appkit::NSLeftMouseUp => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Left })) }, - appkit::NSRightMouseDown => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Right })) }, - appkit::NSRightMouseUp => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Right })) }, - appkit::NSOtherMouseDown => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Middle })) }, - appkit::NSOtherMouseUp => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Middle })) }, - - appkit::NSMouseEntered => { Some(into_event(WindowEvent::MouseEntered { device_id: DEVICE_ID })) }, - appkit::NSMouseExited => { Some(into_event(WindowEvent::MouseLeft { device_id: DEVICE_ID })) }, - - appkit::NSMouseMoved | - appkit::NSLeftMouseDragged | - appkit::NSOtherMouseDragged | - appkit::NSRightMouseDragged => { - // If the mouse movement was on one of our windows, use it. - // Otherwise, if one of our windows is the key window (receiving input), use it. - // Otherwise, return `None`. - let window = match maybe_window.or_else(maybe_key_window) { - Some(window) => window, - None => return None, - }; + fn to_event(events_loop: &mut EventsLoop, ns_event: cocoa::base::id) -> Option { + unsafe { + let event_type = ns_event.eventType(); + let ns_window = ns_event.window(); + let window_id = super::window::get_window_id(ns_window); + + let windows = events_loop.shared.windows.lock().unwrap(); + let maybe_window = windows.iter() + .filter_map(Weak::upgrade) + .find(|window| window_id == window.id()); + + let into_event = |window_event| Event::WindowEvent { + window_id: ::WindowId(window_id), + event: window_event, + }; - let window_point = ns_event.locationInWindow(); - let view_point = if ns_window == cocoa::base::nil { - let ns_size = foundation::NSSize::new(0.0, 0.0); - let ns_rect = foundation::NSRect::new(window_point, ns_size); - let window_rect = window.window.convertRectFromScreen_(ns_rect); - window.view.convertPoint_fromView_(window_rect.origin, cocoa::base::nil) - } else { - window.view.convertPoint_fromView_(window_point, cocoa::base::nil) - }; - let view_rect = NSView::frame(*window.view); - let scale_factor = window.hidpi_factor(); + // Returns `Some` window if one of our windows is the key window. + let maybe_key_window = || windows.iter() + .filter_map(Weak::upgrade) + .find(|window| { + let is_key_window: cocoa::base::BOOL = msg_send![*window.window, isKeyWindow]; + is_key_window == cocoa::base::YES + }); + + match event_type { + appkit::NSKeyDown => { + let mut events = std::collections::VecDeque::new(); + let received_c_str = foundation::NSString::UTF8String(ns_event.characters()); + let received_str = std::ffi::CStr::from_ptr(received_c_str); + + let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); + let state = ElementState::Pressed; + let code = NSEvent::keyCode(ns_event) as u32; + let window_event = WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: state, + scancode: code, + virtual_keycode: vkey, + modifiers: event_mods(ns_event), + }, + }; + for received_char in std::str::from_utf8(received_str.to_bytes()).unwrap().chars() { + let window_event = WindowEvent::ReceivedCharacter(received_char); + events.push_back(into_event(window_event)); + } + events_loop.shared.pending_events.lock().unwrap().extend(events.into_iter()); + Some(into_event(window_event)) + }, - let mut events = std::collections::VecDeque::new(); + appkit::NSKeyUp => { + let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); + + let state = ElementState::Released; + let code = NSEvent::keyCode(ns_event) as u32; + let window_event = WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: state, + scancode: code, + virtual_keycode: vkey, + modifiers: event_mods(ns_event), + }, + }; + Some(into_event(window_event)) + }, - { - let x = (scale_factor * view_point.x as f32) as f64; - let y = (scale_factor * (view_rect.size.height - view_point.y) as f32) as f64; - let window_event = WindowEvent::MouseMoved { device_id: DEVICE_ID, position: (x, y) }; - let event = Event::WindowEvent { window_id: ::WindowId(window.id()), event: window_event }; - events.push_back(event); - } + appkit::NSFlagsChanged => { + unsafe fn modifier_event(event: cocoa::base::id, + keymask: appkit::NSEventModifierFlags, + key: events::VirtualKeyCode, + key_pressed: bool) -> Option + { + if !key_pressed && NSEvent::modifierFlags(event).contains(keymask) { + let state = ElementState::Pressed; + let code = NSEvent::keyCode(event) as u32; + let window_event = WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: state, + scancode: code, + virtual_keycode: Some(key), + modifiers: event_mods(event), + }, + }; + Some(window_event) + } else if key_pressed && !NSEvent::modifierFlags(event).contains(keymask) { + let state = ElementState::Released; + let code = NSEvent::keyCode(event) as u32; + let window_event = WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: state, + scancode: code, + virtual_keycode: Some(key), + modifiers: event_mods(event), + }, + }; + Some(window_event) + } else { + None + } + } - let delta_x = (scale_factor * ns_event.deltaX() as f32) as f64; - if delta_x != 0.0 { - let motion_event = DeviceEvent::Motion { axis: 0, value: delta_x }; - let event = Event::DeviceEvent{ device_id: DEVICE_ID, event: motion_event }; - events.push_back(event); - } + let mut events = std::collections::VecDeque::new(); + if let Some(window_event) = modifier_event(ns_event, + appkit::NSShiftKeyMask, + events::VirtualKeyCode::LShift, + events_loop.modifiers.shift_pressed) + { + events_loop.modifiers.shift_pressed = !events_loop.modifiers.shift_pressed; + events.push_back(into_event(window_event)); + } + + if let Some(window_event) = modifier_event(ns_event, + appkit::NSControlKeyMask, + events::VirtualKeyCode::LControl, + events_loop.modifiers.ctrl_pressed) + { + events_loop.modifiers.ctrl_pressed = !events_loop.modifiers.ctrl_pressed; + events.push_back(into_event(window_event)); + } + + if let Some(window_event) = modifier_event(ns_event, + appkit::NSCommandKeyMask, + events::VirtualKeyCode::LWin, + events_loop.modifiers.win_pressed) + { + events_loop.modifiers.win_pressed = !events_loop.modifiers.win_pressed; + events.push_back(into_event(window_event)); + } + + if let Some(window_event) = modifier_event(ns_event, + appkit::NSAlternateKeyMask, + events::VirtualKeyCode::LAlt, + events_loop.modifiers.alt_pressed) + { + events_loop.modifiers.alt_pressed = !events_loop.modifiers.alt_pressed; + events.push_back(into_event(window_event)); + } + + let event = events.pop_front(); + events_loop.shared.pending_events.lock().unwrap().extend(events.into_iter()); + event + }, - let delta_y = (scale_factor * ns_event.deltaY() as f32) as f64; - if delta_y != 0.0 { - let motion_event = DeviceEvent::Motion { axis: 1, value: delta_y }; - let event = Event::DeviceEvent{ device_id: DEVICE_ID, event: motion_event }; - events.push_back(event); - } + appkit::NSLeftMouseDown => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Left })) }, + appkit::NSLeftMouseUp => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Left })) }, + appkit::NSRightMouseDown => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Right })) }, + appkit::NSRightMouseUp => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Right })) }, + appkit::NSOtherMouseDown => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Middle })) }, + appkit::NSOtherMouseUp => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Middle })) }, + + appkit::NSMouseEntered => { Some(into_event(WindowEvent::MouseEntered { device_id: DEVICE_ID })) }, + appkit::NSMouseExited => { Some(into_event(WindowEvent::MouseLeft { device_id: DEVICE_ID })) }, + + appkit::NSMouseMoved | + appkit::NSLeftMouseDragged | + appkit::NSOtherMouseDragged | + appkit::NSRightMouseDragged => { + // If the mouse movement was on one of our windows, use it. + // Otherwise, if one of our windows is the key window (receiving input), use it. + // Otherwise, return `None`. + let window = match maybe_window.or_else(maybe_key_window) { + Some(window) => window, + None => return None, + }; + + let window_point = ns_event.locationInWindow(); + let view_point = if ns_window == cocoa::base::nil { + let ns_size = foundation::NSSize::new(0.0, 0.0); + let ns_rect = foundation::NSRect::new(window_point, ns_size); + let window_rect = window.window.convertRectFromScreen_(ns_rect); + window.view.convertPoint_fromView_(window_rect.origin, cocoa::base::nil) + } else { + window.view.convertPoint_fromView_(window_point, cocoa::base::nil) + }; + let view_rect = NSView::frame(*window.view); + let scale_factor = window.hidpi_factor(); + + let mut events = std::collections::VecDeque::new(); + + { + let x = (scale_factor * view_point.x as f32) as f64; + let y = (scale_factor * (view_rect.size.height - view_point.y) as f32) as f64; + let window_event = WindowEvent::MouseMoved { device_id: DEVICE_ID, position: (x, y) }; + let event = Event::WindowEvent { window_id: ::WindowId(window.id()), event: window_event }; + events.push_back(event); + } - let event = events.pop_front(); - self.shared.pending_events.lock().unwrap().extend(events.into_iter()); - event - }, + let delta_x = (scale_factor * ns_event.deltaX() as f32) as f64; + if delta_x != 0.0 { + let motion_event = DeviceEvent::Motion { axis: 0, value: delta_x }; + let event = Event::DeviceEvent{ device_id: DEVICE_ID, event: motion_event }; + events.push_back(event); + } - appkit::NSScrollWheel => { - // If none of the windows received the scroll, return `None`. - let window = match maybe_window { - Some(window) => window, - None => return None, - }; + let delta_y = (scale_factor * ns_event.deltaY() as f32) as f64; + if delta_y != 0.0 { + let motion_event = DeviceEvent::Motion { axis: 1, value: delta_y }; + let event = Event::DeviceEvent{ device_id: DEVICE_ID, event: motion_event }; + events.push_back(event); + } - use events::MouseScrollDelta::{LineDelta, PixelDelta}; - let scale_factor = window.hidpi_factor(); - let delta = if ns_event.hasPreciseScrollingDeltas() == cocoa::base::YES { - PixelDelta(scale_factor * ns_event.scrollingDeltaX() as f32, - scale_factor * ns_event.scrollingDeltaY() as f32) - } else { - LineDelta(scale_factor * ns_event.scrollingDeltaX() as f32, - scale_factor * ns_event.scrollingDeltaY() as f32) - }; - let phase = match ns_event.phase() { - appkit::NSEventPhaseMayBegin | appkit::NSEventPhaseBegan => TouchPhase::Started, - appkit::NSEventPhaseEnded => TouchPhase::Ended, - _ => TouchPhase::Moved, - }; - let window_event = WindowEvent::MouseWheel { device_id: DEVICE_ID, delta: delta, phase: phase }; - Some(into_event(window_event)) - }, - - appkit::NSEventTypePressure => { - let pressure = ns_event.pressure(); - let stage = ns_event.stage(); - let window_event = WindowEvent::TouchpadPressure { device_id: DEVICE_ID, pressure: pressure, stage: stage }; - Some(into_event(window_event)) - }, - - appkit::NSApplicationDefined => match ns_event.subtype() { - appkit::NSEventSubtype::NSApplicationActivatedEventType => { - Some(Event::Awakened) + let event = events.pop_front(); + events.shared.pending_events.lock().unwrap().extend(events.into_iter()); + event }, - _ => None, - }, - _ => None, - } - } + appkit::NSScrollWheel => { + // If none of the windows received the scroll, return `None`. + let window = match maybe_window { + Some(window) => window, + None => return None, + }; + + use events::MouseScrollDelta::{LineDelta, PixelDelta}; + let scale_factor = window.hidpi_factor(); + let delta = if ns_event.hasPreciseScrollingDeltas() == cocoa::base::YES { + PixelDelta(scale_factor * ns_event.scrollingDeltaX() as f32, + scale_factor * ns_event.scrollingDeltaY() as f32) + } else { + LineDelta(scale_factor * ns_event.scrollingDeltaX() as f32, + scale_factor * ns_event.scrollingDeltaY() as f32) + }; + let phase = match ns_event.phase() { + appkit::NSEventPhaseMayBegin | appkit::NSEventPhaseBegan => TouchPhase::Started, + appkit::NSEventPhaseEnded => TouchPhase::Ended, + _ => TouchPhase::Moved, + }; + let window_event = WindowEvent::MouseWheel { device_id: DEVICE_ID, delta: delta, phase: phase }; + Some(into_event(window_event)) + }, - pub fn create_proxy(&self) -> Proxy { - Proxy {} - } + appkit::NSEventTypePressure => { + let pressure = ns_event.pressure(); + let stage = ns_event.stage(); + let window_event = WindowEvent::TouchpadPressure { device_id: DEVICE_ID, pressure: pressure, stage: stage }; + Some(into_event(window_event)) + }, -} + appkit::NSApplicationDefined => match ns_event.subtype() { + appkit::NSEventSubtype::NSApplicationActivatedEventType => { + Some(Event::Awakened) + }, + _ => None, + }, -impl Proxy { - pub fn wakeup(&self) -> Result<(), EventsLoopClosed> { - // Awaken the event loop by triggering `NSApplicationActivatedEventType`. - unsafe { - let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); - let event = - NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( - cocoa::base::nil, - appkit::NSApplicationDefined, - foundation::NSPoint::new(0.0, 0.0), - appkit::NSEventModifierFlags::empty(), - 0.0, - 0, - cocoa::base::nil, - appkit::NSEventSubtype::NSApplicationActivatedEventType, - 0, - 0); - appkit::NSApp().postEvent_atStart_(event, cocoa::base::NO); - foundation::NSAutoreleasePool::drain(pool); + _ => None, + } } - Ok(()) } } + fn to_virtual_key_code(code: u16) -> Option { Some(match code { 0x00 => events::VirtualKeyCode::A, diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index dbde5ee94a..36232918b6 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -40,3 +40,5 @@ impl Window2 { mod events_loop; mod monitor; mod window; + +mod send_event; \ No newline at end of file diff --git a/src/platform/macos/send_event.rs b/src/platform/macos/send_event.rs new file mode 100644 index 0000000000..d59969d0b2 --- /dev/null +++ b/src/platform/macos/send_event.rs @@ -0,0 +1,24 @@ +use cocoa; +use cocoa::appkit::{NSApp,NSApplication}; + +// The `SendEvent` struct encapsulates the idea of calling [NSApp sendEvent:event]. +// This is a separate struct because, in the case of resize events, dispatching an event can enter +// an internal runloop, and we don't want to get stuck there. +pub struct SendEvent { + event: cocoa::base::id, +} + +impl SendEvent { + pub fn new(event: cocoa::base::id) -> SendEvent { + SendEvent{event: event} + } + + // Attempt to work the send, which either a) consumes the SendEvent, indicating completion, or + // b) returns a SendEvent, indicating there is more work yet to perform. + pub fn work(self) -> Option { + unsafe { + NSApp().sendEvent_(self.event); + } + None + } +} \ No newline at end of file From bf48df1b3da43d883fdfd8708e1eb082cddd3e26 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sun, 16 Jul 2017 15:42:25 -0500 Subject: [PATCH 02/24] Add context::Context-based Cocoa event dispatching --- Cargo.toml | 1 + src/lib.rs | 3 + src/platform/macos/mod.rs | 8 +- src/platform/macos/send_event_context.rs | 105 +++++++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/platform/macos/send_event_context.rs diff --git a/Cargo.toml b/Cargo.toml index fcebe2eff9..3ec635c192 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ objc = "0.2" cocoa = "0.9" core-foundation = "0.4" core-graphics = "0.8" +context = { version = "2.0", optional = true } [target.'cfg(target_os = "windows")'.dependencies] winapi = "0.2" diff --git a/src/lib.rs b/src/lib.rs index 85394f499c..924890c038 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,9 @@ extern crate x11_dl; #[macro_use] extern crate wayland_client; +#[cfg(feature="context")] +extern crate context; + pub use events::*; pub use window::{AvailableMonitorsIter, MonitorId, get_available_monitors, get_primary_monitor}; pub use native_monitor::NativeMonitorId; diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 36232918b6..91f9f5ab3a 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -41,4 +41,10 @@ mod events_loop; mod monitor; mod window; -mod send_event; \ No newline at end of file +#[cfg(not(feature="context"))] +mod send_event; + +#[cfg(feature="context")] +mod send_event_context; +#[cfg(feature="context")] +use self::send_event_context as send_event; diff --git a/src/platform/macos/send_event_context.rs b/src/platform/macos/send_event_context.rs new file mode 100644 index 0000000000..73b8968676 --- /dev/null +++ b/src/platform/macos/send_event_context.rs @@ -0,0 +1,105 @@ +use cocoa; +use cocoa::appkit::{NSApp,NSApplication}; +use context; +use std::mem; + +// Size of the coroutine's stack +const STACK_SIZE: usize = 512 * 1024; + +// The `SendEvent` struct encapsulates the idea of calling [NSApp sendEvent:event]. +// This is a separate struct because, in the case of resize events, dispatching an event can enter +// an internal runloop, and we don't want to get stuck there. +pub struct SendEvent { + stack: context::stack::ProtectedFixedSizeStack, + ctx: context::Context +} + +// An instance of this struct is passed from `SendEvent::new()` to `send_event_fn()`. +// Any data that needs to flow that direction should be included here. +struct SendEventInvocation { + event: cocoa::base::id, +} + +impl SendEventInvocation { + // `run()` is called from the SendEvent coroutine. + // + // It should resume t.context with 1 when there is more work to do, or 0 if it is complete. + fn run(self, t: context::Transfer) { + // boring + unsafe { + NSApp().sendEvent_(self.event); + } + + // signal completion + unsafe { t.context.resume(0); } + } +} + +impl SendEvent { + pub fn new(event: cocoa::base::id) -> SendEvent { + // Set up the invocation struct + let invocation = SendEventInvocation { + event: event, + }; + + // Pack the invocation into an Option<> of itself + let mut invocation: Option = Some(invocation); + + // Make a callback to run from inside the coroutine + extern fn send_event_fn(mut t: context::Transfer) -> ! { + // t.data is a pointer to the caller's `invocation` Option + let invocation: *mut Option = t.data as _; + + // Turn this into a mutable borrow, then move the invocation into the coroutine's stack + let invocation: SendEventInvocation = + unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(invocation) } + .take() + .unwrap(); + + + // Yield back to `SendEvent::new()` + t = unsafe { t.context.resume(1) }; + + // Run the SendEvent process + invocation.run(t); + + unreachable!(); + } + + // Set up a stack + let stack = context::stack::ProtectedFixedSizeStack::new(STACK_SIZE) + .expect("SendEvent stack allocation"); + + // Set up a new context + let result = unsafe { + // Start by calling send_event_fn above + let ctx = context::Context::new(&stack, send_event_fn); + + // Yield to the coroutine, giving it a pointer to the invocation, and wait for it come back + ctx.resume(&mut invocation as *mut Option as usize) + }; + + SendEvent{ + stack: stack, + ctx: result.context, + } + } + + // Attempt to work the send, which either a) consumes the SendEvent, indicating completion, or + // b) returns a SendEvent, indicating there is more work yet to perform. + pub fn work(self) -> Option { + // resume the coroutine + let result = unsafe { self.ctx.resume(0) }; + + if result.data == 0 { + // done + None + } else { + // more work to do + Some(SendEvent{ + stack: self.stack, + ctx: result.context, + }) + } + } +} From 278f13dee280a0203d8ecaa1437e367f9ed3bd2a Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sun, 16 Jul 2017 16:59:47 -0500 Subject: [PATCH 03/24] Add a runloop observer --- src/platform/macos/events_loop.rs | 8 +- src/platform/macos/send_event_context.rs | 128 +++++++++++++++++++++-- 2 files changed, 128 insertions(+), 8 deletions(-) diff --git a/src/platform/macos/events_loop.rs b/src/platform/macos/events_loop.rs index e9dd72589e..ef60fc04f0 100644 --- a/src/platform/macos/events_loop.rs +++ b/src/platform/macos/events_loop.rs @@ -320,9 +320,15 @@ impl Drop for RetainedEvent { } } +// Encapsulates the lifecycle of a Cocoa event enum CocoaEvent { + // We just received this event, and haven't processed it yet Received(RetainedEvent), + + // We're trying to sending to the windowing system and haven't completed it yet Sending(SendEvent, RetainedEvent), + + // We delivered the message to the windowing system and possibly got back an events::Event Complete(Option), } @@ -371,7 +377,7 @@ impl CocoaEvent { return None; } - // good enough + // good enough, let's dispatch Some(CocoaEvent::Received(RetainedEvent::new(ns_event))) } diff --git a/src/platform/macos/send_event_context.rs b/src/platform/macos/send_event_context.rs index 73b8968676..4e0bc41aac 100644 --- a/src/platform/macos/send_event_context.rs +++ b/src/platform/macos/send_event_context.rs @@ -1,7 +1,12 @@ use cocoa; use cocoa::appkit::{NSApp,NSApplication}; +use core_foundation::base::*; +use core_foundation::runloop::*; use context; use std::mem; +use std::cell::{Cell,RefCell}; +use std::rc::Rc; +use libc::c_void; // Size of the coroutine's stack const STACK_SIZE: usize = 512 * 1024; @@ -14,6 +19,104 @@ pub struct SendEvent { ctx: context::Context } +struct Resumable(Cell>); + +impl Resumable { + fn new(ctx: context::Context) -> Self { + Resumable(Cell::new(Some(ctx))) + } + + unsafe fn resume(&self, value: usize) { + // fish out the current context + let mut context = self.0.take().expect("Resumable should always have a context"); + + // resume it, getting a new context + let result = context.resume(value); + + // store the new context and return + self.0.set(Some(result.context)); + } +} + +// A RunLoopObserver corresponds to a CFRunLoopObserver. +struct RunLoopObserver { + id: CFRunLoopObserverRef, +} + +extern "C" fn runloop_observer_callback(observer: CFRunLoopObserverRef, activity: CFRunLoopActivity, info: *mut c_void) { + // convert the raw pointer into an Rc + let mut resumable: Rc = unsafe { Rc::from_raw(info as _) }; + + // we're either about to wait or just finished waiting + // in either case, yield back to the caller, signaling the operation is still in progress + unsafe { + resumable.resume(1); + } + + // convert the Rc back into a raw pointer to retain the refcount + Rc::into_raw(resumable); +} + +extern "C" fn retain_resumable(info: *const c_void) { + // convert the raw pointer into an Rc + let mut resumable: Rc = unsafe { Rc::from_raw(info as _) }; + + // clone it and conver to a raw pointer to increment the refcount + Rc::into_raw(resumable.clone()); + + // convert the Rc back into a raw pointer to retain the refcount + Rc::into_raw(resumable); +} + +extern "C" fn release_resumable(info: *const c_void) { + // convert the raw pointer into an Rc + let mut resumable: Rc = unsafe { Rc::from_raw(info as _) }; + + // let it drop to decrement the refcount +} + +impl RunLoopObserver { + fn new(resumable: Rc) -> RunLoopObserver { + // CFRunLoopObserverCreate copies this struct, so we can give it a pointer to this local + let mut context: CFRunLoopObserverContext = unsafe { mem::zeroed() }; + context.info = Rc::into_raw(resumable) as _; + context.release = release_resumable; + + // Make the runloop observer itself + let id = unsafe { + CFRunLoopObserverCreate( + kCFAllocatorDefault, + kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting, + 1, // repeats + 0, // order + runloop_observer_callback, + &mut context as *mut CFRunLoopObserverContext, + ) + }; + + // Add to event loop + unsafe { + CFRunLoopAddObserver(CFRunLoopGetMain(), id, kCFRunLoopCommonModes); + } + + // Decrement the refcount now that the platform owns it + release_resumable(context.info); + + RunLoopObserver{ + id, + } + } +} + +impl Drop for RunLoopObserver { + fn drop(&mut self) { + unsafe { + CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.id, kCFRunLoopCommonModes); + CFRelease(self.id as _); + } + } +} + // An instance of this struct is passed from `SendEvent::new()` to `send_event_fn()`. // Any data that needs to flow that direction should be included here. struct SendEventInvocation { @@ -24,14 +127,27 @@ impl SendEventInvocation { // `run()` is called from the SendEvent coroutine. // // It should resume t.context with 1 when there is more work to do, or 0 if it is complete. - fn run(self, t: context::Transfer) { - // boring - unsafe { - NSApp().sendEvent_(self.event); + fn run(self, t: context::Transfer) -> ! { + // save our current context + let resumable: Rc = Rc::new(Resumable::new(t.context)); + + { + // make a runloop observer for its side effects + let _observer = RunLoopObserver::new(resumable.clone()); + + // send the message + unsafe { + NSApp().sendEvent_(self.event); + } + + // drop the runloop observer } // signal completion - unsafe { t.context.resume(0); } + unsafe { resumable.resume(0); } + + // we should never be resumed after completion + unreachable!(); } } @@ -62,8 +178,6 @@ impl SendEvent { // Run the SendEvent process invocation.run(t); - - unreachable!(); } // Set up a stack From f3d09586771b59117886bf8a58f1d2ab67ed50ee Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sun, 16 Jul 2017 19:54:31 -0500 Subject: [PATCH 04/24] Refactor the MacOS event loop poll_events() and run_forever() now delegate event production to get_event(), a new function whose job it is to either a) produce Some(Event) by a Timeout or b) return None. This dramatically simplifies the entire event loop. MacOS in particular has multiple ways that Events can be produced: wakeup(), AppDelegate callbacks, and a Cocoa event receive -> send -> translate cycle. The first two are handled by an event queue; wakeup() now posts an Event::Awakened to the queue, just like the callbacks. The CocoaEvent cycle is handled either via the usual blocking [NSApp sendEvent:] call or via a coroutine-based event dispatcher. The coroutine yields back to get_event() on a regular basis, even while it's in the middle of sending events. This allows get_event() to return an event to the calling application even if it is still in the middle of a sendEvent: call. --- src/platform/macos/events_loop.rs | 284 ++++++++++-------------------- src/platform/macos/window.rs | 2 +- 2 files changed, 97 insertions(+), 189 deletions(-) diff --git a/src/platform/macos/events_loop.rs b/src/platform/macos/events_loop.rs index ef60fc04f0..2ab15a0bc1 100644 --- a/src/platform/macos/events_loop.rs +++ b/src/platform/macos/events_loop.rs @@ -2,6 +2,7 @@ use {ControlFlow, EventsLoopClosed}; use cocoa::{self, appkit, foundation}; use cocoa::appkit::{NSApplication, NSEvent, NSView, NSWindow}; use core_foundation::base::{CFRetain, CFRelease, CFTypeRef}; +use core_foundation::runloop; use events::{self, ElementState, Event, MouseButton, TouchPhase, WindowEvent, DeviceEvent, ModifiersState, KeyboardInput}; use std::collections::VecDeque; use std::sync::{Arc, Mutex, Weak}; @@ -13,25 +14,20 @@ use super::send_event::SendEvent; pub struct EventsLoop { modifiers: Modifiers, pub shared: Arc, - current_event: Option, + current_cocoa_event: Option, } // State shared between the `EventsLoop` and its registered windows. pub struct Shared { pub windows: Mutex>>, + + // A queue of events that are pending delivery to the library user. pub pending_events: Mutex>, - // The user event callback given via either of the `poll_events` or `run_forever` methods. - // - // We store the user's callback here so that it may be accessed by each of the window delegate - // callbacks (e.g. resize, close, etc) for the duration of a call to either of the - // `poll_events` or `run_forever` methods. - // - // This is *only* `Some` for the duration of a call to either of these methods and will be - // `None` otherwise. - user_callback: UserCallback, } -pub struct Proxy {} +pub struct Proxy { + shared: Weak, +} struct Modifiers { shift_pressed: bool, @@ -56,36 +52,22 @@ impl Shared { Shared { windows: Mutex::new(Vec::new()), pending_events: Mutex::new(VecDeque::new()), - user_callback: UserCallback { mutex: Mutex::new(None) }, } } - fn call_user_callback_with_pending_events(&self) { - loop { - let event = match self.pending_events.lock().unwrap().pop_front() { - Some(event) => event, - None => return, - }; - unsafe { - self.user_callback.call_with_event(event); - } + // Enqueues the event for prompt delivery to the application. + pub fn enqueue_event(&self, event: Event) { + self.pending_events.lock().unwrap().push_back(event); + + // attempt to wake the runloop + unsafe { + runloop::CFRunLoopWakeUp(runloop::CFRunLoopGetMain()); } } - // Calls the user callback if one exists. - // - // Otherwise, stores the event in the `pending_events` queue. - // - // This is necessary for the case when `WindowDelegate` callbacks are triggered during a call - // to the user's callback. - pub fn call_user_callback_with_event_or_store_in_pending(&self, event: Event) { - if self.user_callback.mutex.lock().unwrap().is_some() { - unsafe { - self.user_callback.call_with_event(event); - } - } else { - self.pending_events.lock().unwrap().push_back(event); - } + // Dequeues the first event, if any, from the queue. + fn dequeue_event(&self) -> Option { + self.pending_events.lock().unwrap().pop_front() } // Removes the window with the given `Id` from the `windows` list. @@ -114,50 +96,19 @@ impl Modifiers { } } +#[derive(Debug,Clone,Copy,Eq,PartialEq)] +enum Timeout { + Now, + Forever, +} -impl UserCallback { - - // Here we store user's `callback` behind the mutex so that they may be safely shared between - // each of the window delegates. - // - // In order to make sure that the pointer is always valid, we must manually guarantee that it - // is dropped before the callback itself is dropped. Thus, this should *only* be called at the - // beginning of a call to `poll_events` and `run_forever`, both of which *must* drop the - // callback at the end of their scope using the `drop` method. - fn store(&self, callback: &mut F) - where F: FnMut(Event) - { - let trait_object = callback as &mut FnMut(Event); - let trait_object_ptr = trait_object as *const FnMut(Event) as *mut FnMut(Event); - *self.mutex.lock().unwrap() = Some(trait_object_ptr); - } - - // Emits the given event via the user-given callback. - // - // This is unsafe as it requires dereferencing the pointer to the user-given callback. We - // guarantee this is safe by ensuring the `UserCallback` never lives longer than the user-given - // callback. - // - // Note that the callback may not always be `Some`. This is because some `NSWindowDelegate` - // callbacks can be triggered by means other than `NSApp().sendEvent`. For example, if a window - // is destroyed or created during a call to the user's callback, the `WindowDelegate` methods - // may be called with `windowShouldClose` or `windowDidResignKey`. - unsafe fn call_with_event(&self, event: Event) { - let callback = match self.mutex.lock().unwrap().take() { - Some(callback) => callback, - None => return, - }; - (*callback)(event); - *self.mutex.lock().unwrap() = Some(callback); - } - - // Used to drop the user callback pointer at the end of the `poll_events` and `run_forever` - // methods. This is done to enforce our guarantee that the top callback will never live longer - // than the call to either `poll_events` or `run_forever` to which it was given. - fn drop(&self) { - self.mutex.lock().unwrap().take(); +impl Timeout { + fn is_elapsed(&self) -> bool { + match self { + &Timeout::Now => true, + &Timeout::Forever => false, + } } - } impl EventsLoop { @@ -166,141 +117,106 @@ impl EventsLoop { EventsLoop { shared: Arc::new(Shared::new()), modifiers: Modifiers::new(), - current_event: None, + current_cocoa_event: None, } } - pub fn poll_events(&mut self, mut callback: F) - where F: FnMut(Event), - { + // Attempt to get an Event by a specified timeout. + fn get_event(&mut self, timeout: Timeout) -> Option { unsafe { if !msg_send![cocoa::base::class("NSThread"), isMainThread] { panic!("Events can only be polled from the main thread on macOS"); } } - self.shared.user_callback.store(&mut callback); - - // Loop as long as we have pending events to return. loop { - // First, yield all pending events. - self.shared.call_user_callback_with_pending_events(); + // Pop any queued events + // This is immediate, so no need to consider a timeout + if let Some(event) = self.shared.dequeue_event() { + return Some(event); + } - // Are we already working on something? - if self.current_event.is_none() { - // Attempt to receive a single message with an immediate timeout - self.current_event = CocoaEvent::receive(true); + // If we have no CocoaEvent, attempt to receive one + // CocoaEvent::receive() respects the timeout + if self.current_cocoa_event.is_none() { + self.current_cocoa_event = CocoaEvent::receive(timeout); } - // Do we have something to process? - if let Some(mut current_event) = self.current_event.take() { - // Process it until it's done - while !current_event.work(self) { - // Repeat + // If we have a CocoaEvent, attempt to process it + // TODO: plumb timeouts down to CocoaEvent::work() + if let Some(mut current_event) = self.current_cocoa_event.take() { + if current_event.work(self) == false { + // Event is not complete + // We must either process it further or store it again for later + if let Some(event) = self.shared.dequeue_event() { + // Another event landed while we were working this + // Store the CocoaEvent and return the Event from the queue + self.current_cocoa_event = Some(current_event); + return Some(event); + + } else if timeout.is_elapsed() { + // Timeout is elapsed; we must return empty-handed + // Store the CocoaEvent and return nothing + self.current_cocoa_event = Some(current_event); + return None; + + } else { + // We can repeat + continue; + } } - // Did we get an event? + // CocoaEvent processing is complete + // Is it an event? if let CocoaEvent::Complete(Some(winit_event)) = current_event { - // Call the user's callback - unsafe { self.shared.user_callback.call_with_event(winit_event); } - - // Repeat - continue + // Return it + return Some(winit_event); + } else { + // CocoaEvent did not translate into an events::Event + // Loop around again } - } else { - // The event loop returned no message - // Finish - break } } + } - self.shared.user_callback.drop(); + pub fn poll_events(&mut self, mut callback: F) + where F: FnMut(Event), + { + // Return as many events as we can without blocking + while let Some(event) = self.get_event(Timeout::Now) { + callback(event); + } } pub fn run_forever(&mut self, mut callback: F) where F: FnMut(Event) -> ControlFlow { - unsafe { - if !msg_send![cocoa::base::class("NSThread"), isMainThread] { - panic!("Events can only be polled from the main thread on macOS"); + // Get events until we're told to stop + while let Some(event) = self.get_event(Timeout::Forever) { + // Send to the app + let control_flow = callback(event); + + // Do what it says + match control_flow { + ControlFlow::Break => break, + ControlFlow::Continue => (), } } - - // Track whether or not control flow has changed. - let control_flow = std::cell::Cell::new(ControlFlow::Continue); - - let mut callback = |event| { - if let ControlFlow::Break = callback(event) { - control_flow.set(ControlFlow::Break); - } - }; - - self.shared.user_callback.store(&mut callback); - - loop { - unsafe { - // First, yield all pending events. - self.shared.call_user_callback_with_pending_events(); - if let ControlFlow::Break = control_flow.get() { - break; - } - - // Are we already working on something? - if self.current_event.is_none() { - // Attempt to receive a single message with a forever timeout - self.current_event = CocoaEvent::receive(false); - } - - // Do we have something to process? - if let Some(mut current_event) = self.current_event.take() { - // Process it until it's done - while !current_event.work(self) { - // Repeat - } - - // Did we get an event? - if let CocoaEvent::Complete(Some(winit_event)) = current_event { - // Call the callback - self.shared.user_callback.call_with_event(winit_event); - - // Exit as directed - if let ControlFlow::Break = control_flow.get() { - break; - } - } - } - } - } - - self.shared.user_callback.drop(); } pub fn create_proxy(&self) -> Proxy { - Proxy {} + Proxy { shared: Arc::downgrade(&self.shared) } } } impl Proxy { pub fn wakeup(&self) -> Result<(), EventsLoopClosed> { - // Awaken the event loop by triggering `NSApplicationActivatedEventType`. - unsafe { - let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); - let event = - NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( - cocoa::base::nil, - appkit::NSApplicationDefined, - foundation::NSPoint::new(0.0, 0.0), - appkit::NSEventModifierFlags::empty(), - 0.0, - 0, - cocoa::base::nil, - appkit::NSEventSubtype::NSApplicationActivatedEventType, - 0, - 0); - appkit::NSApp().postEvent_atStart_(event, cocoa::base::NO); - foundation::NSAutoreleasePool::drain(pool); + if let Some(shared) = self.shared.upgrade() { + shared.enqueue_event(Event::Awakened); + Ok(()) + } else { + Err(EventsLoopClosed) } - Ok(()) } } @@ -333,15 +249,14 @@ enum CocoaEvent { } impl CocoaEvent { - fn receive(immediate_timeout: bool) -> Option { + fn receive(timeout: Timeout) -> Option { unsafe { let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); // Pick a timeout - let timeout = if immediate_timeout { - foundation::NSDate::distantPast(cocoa::base::nil) - } else { - foundation::NSDate::distantFuture(cocoa::base::nil) + let timeout = match timeout { + Timeout::Now => foundation::NSDate::distantPast(cocoa::base::nil), + Timeout::Forever => foundation::NSDate::distantFuture(cocoa::base::nil), }; // Poll for the next event @@ -667,13 +582,6 @@ impl CocoaEvent { Some(into_event(window_event)) }, - appkit::NSApplicationDefined => match ns_event.subtype() { - appkit::NSEventSubtype::NSApplicationActivatedEventType => { - Some(Event::Awakened) - }, - _ => None, - }, - _ => None, } } diff --git a/src/platform/macos/window.rs b/src/platform/macos/window.rs index 62f0720a05..b306d885b0 100644 --- a/src/platform/macos/window.rs +++ b/src/platform/macos/window.rs @@ -54,7 +54,7 @@ impl WindowDelegate { }; if let Some(shared) = state.shared.upgrade() { - shared.call_user_callback_with_event_or_store_in_pending(event); + shared.enqueue_event(event); } } From 0f95f20886902e7ad8115b627ba38ad2f9b17d41 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sun, 16 Jul 2017 20:35:42 -0500 Subject: [PATCH 05/24] Switch to thread-local context storage --- src/platform/macos/send_event_context.rs | 87 ++++++++---------------- 1 file changed, 29 insertions(+), 58 deletions(-) diff --git a/src/platform/macos/send_event_context.rs b/src/platform/macos/send_event_context.rs index 4e0bc41aac..0b0d5b5c4c 100644 --- a/src/platform/macos/send_event_context.rs +++ b/src/platform/macos/send_event_context.rs @@ -4,8 +4,7 @@ use core_foundation::base::*; use core_foundation::runloop::*; use context; use std::mem; -use std::cell::{Cell,RefCell}; -use std::rc::Rc; +use std::cell::Cell; use libc::c_void; // Size of the coroutine's stack @@ -19,23 +18,23 @@ pub struct SendEvent { ctx: context::Context } -struct Resumable(Cell>); - -impl Resumable { - fn new(ctx: context::Context) -> Self { - Resumable(Cell::new(Some(ctx))) - } +thread_local!{ + static INNER_CONTEXT: Cell> = Cell::new(None); +} - unsafe fn resume(&self, value: usize) { - // fish out the current context - let mut context = self.0.take().expect("Resumable should always have a context"); +unsafe fn resume(value: usize) { + // get the context we're resuming + let context = INNER_CONTEXT.with(|c| { + c.take() + }).expect("resume context"); - // resume it, getting a new context - let result = context.resume(value); + // resume it, getting a new context + let result = context.resume(value); - // store the new context and return - self.0.set(Some(result.context)); - } + // store the new context and return + INNER_CONTEXT.with(move |c| { + c.set(Some(result.context)); + }); } // A RunLoopObserver corresponds to a CFRunLoopObserver. @@ -43,44 +42,18 @@ struct RunLoopObserver { id: CFRunLoopObserverRef, } -extern "C" fn runloop_observer_callback(observer: CFRunLoopObserverRef, activity: CFRunLoopActivity, info: *mut c_void) { - // convert the raw pointer into an Rc - let mut resumable: Rc = unsafe { Rc::from_raw(info as _) }; - +extern "C" fn runloop_observer_callback(_observer: CFRunLoopObserverRef, _activity: CFRunLoopActivity, _info: *mut c_void) { // we're either about to wait or just finished waiting // in either case, yield back to the caller, signaling the operation is still in progress unsafe { - resumable.resume(1); + resume(1); } - - // convert the Rc back into a raw pointer to retain the refcount - Rc::into_raw(resumable); -} - -extern "C" fn retain_resumable(info: *const c_void) { - // convert the raw pointer into an Rc - let mut resumable: Rc = unsafe { Rc::from_raw(info as _) }; - - // clone it and conver to a raw pointer to increment the refcount - Rc::into_raw(resumable.clone()); - - // convert the Rc back into a raw pointer to retain the refcount - Rc::into_raw(resumable); -} - -extern "C" fn release_resumable(info: *const c_void) { - // convert the raw pointer into an Rc - let mut resumable: Rc = unsafe { Rc::from_raw(info as _) }; - - // let it drop to decrement the refcount } impl RunLoopObserver { - fn new(resumable: Rc) -> RunLoopObserver { + fn new() -> RunLoopObserver { // CFRunLoopObserverCreate copies this struct, so we can give it a pointer to this local let mut context: CFRunLoopObserverContext = unsafe { mem::zeroed() }; - context.info = Rc::into_raw(resumable) as _; - context.release = release_resumable; // Make the runloop observer itself let id = unsafe { @@ -99,9 +72,6 @@ impl RunLoopObserver { CFRunLoopAddObserver(CFRunLoopGetMain(), id, kCFRunLoopCommonModes); } - // Decrement the refcount now that the platform owns it - release_resumable(context.info); - RunLoopObserver{ id, } @@ -127,13 +97,10 @@ impl SendEventInvocation { // `run()` is called from the SendEvent coroutine. // // It should resume t.context with 1 when there is more work to do, or 0 if it is complete. - fn run(self, t: context::Transfer) -> ! { - // save our current context - let resumable: Rc = Rc::new(Resumable::new(t.context)); - + fn run(self) -> ! { { // make a runloop observer for its side effects - let _observer = RunLoopObserver::new(resumable.clone()); + let _observer = RunLoopObserver::new(); // send the message unsafe { @@ -144,7 +111,7 @@ impl SendEventInvocation { } // signal completion - unsafe { resumable.resume(0); } + unsafe { resume(0); } // we should never be resumed after completion unreachable!(); @@ -162,22 +129,26 @@ impl SendEvent { let mut invocation: Option = Some(invocation); // Make a callback to run from inside the coroutine - extern fn send_event_fn(mut t: context::Transfer) -> ! { + extern fn send_event_fn(t: context::Transfer) -> ! { // t.data is a pointer to the caller's `invocation` Option let invocation: *mut Option = t.data as _; + // Move the coroutine context to thread-local storage + INNER_CONTEXT.with(move |c| { + c.set(Some(t.context)); + }); + // Turn this into a mutable borrow, then move the invocation into the coroutine's stack let invocation: SendEventInvocation = unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(invocation) } .take() .unwrap(); - // Yield back to `SendEvent::new()` - t = unsafe { t.context.resume(1) }; + unsafe { resume(0); } // Run the SendEvent process - invocation.run(t); + invocation.run(); } // Set up a stack From 87b7e70f70c0749c950e69cd7c5d47ee555a8494 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sun, 16 Jul 2017 20:36:04 -0500 Subject: [PATCH 06/24] Remove unnecessary UserCallback struct --- src/platform/macos/events_loop.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/platform/macos/events_loop.rs b/src/platform/macos/events_loop.rs index 2ab15a0bc1..b573da7b5e 100644 --- a/src/platform/macos/events_loop.rs +++ b/src/platform/macos/events_loop.rs @@ -36,16 +36,6 @@ struct Modifiers { alt_pressed: bool, } -// Wrapping the user callback in a type allows us to: -// -// - ensure the callback pointer is never accidentally cloned -// - ensure that only the `EventsLoop` can `store` and `drop` the callback pointer -// - Share access to the user callback with the NSWindow callbacks. -pub struct UserCallback { - mutex: Mutex>, -} - - impl Shared { pub fn new() -> Self { From b464e5fa030adf1ead3c22e5fcf22e1fac14bb9d Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sun, 16 Jul 2017 21:11:23 -0500 Subject: [PATCH 07/24] Add a timer to assist with waking the runloop --- src/platform/macos/events_loop.rs | 5 ++ src/platform/macos/mod.rs | 2 + src/platform/macos/send_event_context.rs | 35 +++++++---- src/platform/macos/timer.rs | 78 ++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 13 deletions(-) create mode 100644 src/platform/macos/timer.rs diff --git a/src/platform/macos/events_loop.rs b/src/platform/macos/events_loop.rs index b573da7b5e..b68b3ee863 100644 --- a/src/platform/macos/events_loop.rs +++ b/src/platform/macos/events_loop.rs @@ -10,6 +10,7 @@ use super::window::Window; use std; use super::DeviceId; use super::send_event::SendEvent; +use super::timer::Timer; pub struct EventsLoop { modifiers: Modifiers, @@ -23,6 +24,8 @@ pub struct Shared { // A queue of events that are pending delivery to the library user. pub pending_events: Mutex>, + + timer: Timer, } pub struct Proxy { @@ -42,6 +45,7 @@ impl Shared { Shared { windows: Mutex::new(Vec::new()), pending_events: Mutex::new(VecDeque::new()), + timer: Timer::new(), } } @@ -50,6 +54,7 @@ impl Shared { self.pending_events.lock().unwrap().push_back(event); // attempt to wake the runloop + self.timer.trigger(); unsafe { runloop::CFRunLoopWakeUp(runloop::CFRunLoopGetMain()); } diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 91f9f5ab3a..702d1cc215 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -41,6 +41,8 @@ mod events_loop; mod monitor; mod window; +mod timer; + #[cfg(not(feature="context"))] mod send_event; diff --git a/src/platform/macos/send_event_context.rs b/src/platform/macos/send_event_context.rs index 0b0d5b5c4c..5063c57548 100644 --- a/src/platform/macos/send_event_context.rs +++ b/src/platform/macos/send_event_context.rs @@ -23,18 +23,26 @@ thread_local!{ } unsafe fn resume(value: usize) { - // get the context we're resuming - let context = INNER_CONTEXT.with(|c| { - c.take() - }).expect("resume context"); - - // resume it, getting a new context - let result = context.resume(value); - - // store the new context and return - INNER_CONTEXT.with(move |c| { - c.set(Some(result.context)); - }); + if try_resume(value) == false { + panic!("no coroutine context to resume"); + } +} + +pub unsafe fn try_resume(value: usize) -> bool { + if let Some(context) = INNER_CONTEXT.with(|c| { c.take() }) { + // resume it, getting a new context + let result = context.resume(value); + + // store the new context and return + INNER_CONTEXT.with(move |c| { + c.set(Some(result.context)); + }); + + true + } else { + // no context + false + } } // A RunLoopObserver corresponds to a CFRunLoopObserver. @@ -45,8 +53,9 @@ struct RunLoopObserver { extern "C" fn runloop_observer_callback(_observer: CFRunLoopObserverRef, _activity: CFRunLoopActivity, _info: *mut c_void) { // we're either about to wait or just finished waiting // in either case, yield back to the caller, signaling the operation is still in progress + // this is strictly advisory, so don't worry about it if there's nothing o resume unsafe { - resume(1); + try_resume(1); } } diff --git a/src/platform/macos/timer.rs b/src/platform/macos/timer.rs new file mode 100644 index 0000000000..d416d468d6 --- /dev/null +++ b/src/platform/macos/timer.rs @@ -0,0 +1,78 @@ +use std::mem; +use libc::c_void; +use core_foundation::base::*; +use core_foundation::runloop::*; +use core_foundation::date::*; + +// Encapsulates a CFRunLoopTimer that has a far-future time to fire, but which can be triggered +// across threads for the purpose of waking up an event loop. +pub struct Timer { + timer: CFRunLoopTimerRef, +} + +#[cfg(feature="context")] +extern "C" fn timer_callback(timer: CFRunLoopTimerRef, info: *mut c_void) { + // attempt to yield back to the caller + use super::send_event_context::try_resume; + unsafe { + try_resume(1); + } +} + +#[cfg(not(feature="context"))] +extern "C" fn timer_callback(timer: CFRunLoopTimerRef, info: *mut c_void) { + // nothing to do +} + +impl Timer { + pub fn new() -> Timer { + // default to firing every year, starting one year in the future + let one_year: CFTimeInterval = 86400f64 * 365f64; + let now = unsafe { CFAbsoluteTimeGetCurrent() }; + let one_year_from_now = now + one_year; + + let mut context: CFRunLoopTimerContext = unsafe { mem::zeroed() }; + + // create a timer + let timer = unsafe { + CFRunLoopTimerCreate( + kCFAllocatorDefault, + one_year_from_now, // fireDate + one_year, // interval + 0, // flags + 0, // order + timer_callback, + &mut context as *mut CFRunLoopTimerContext, + ) + }; + + // add it to the runloop + unsafe { + CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes); + } + + Timer{ + timer + } + } + + // Cause the timer to fire ASAP. Can be called across threads. + pub fn trigger(&self) { + unsafe { + CFRunLoopTimerSetNextFireDate(self.timer, CFAbsoluteTimeGetCurrent()); + } + } +} + +impl Drop for Timer { + fn drop(&mut self) { + unsafe { + CFRunLoopRemoveTimer(CFRunLoopGetMain(), self.timer, kCFRunLoopCommonModes); + CFRelease(self.timer as _); + } + } +} + +// Rust doesn't know that __CFRunLoopTimer is thread safe, but the docs say it is +unsafe impl Send for Timer {} +unsafe impl Sync for Timer {} From ed6a22545b10cd0f0ccc69777af24b2ab9c96e67 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sun, 16 Jul 2017 21:51:15 -0500 Subject: [PATCH 08/24] Small fixes --- src/platform/macos/events_loop.rs | 3 +-- src/platform/macos/timer.rs | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/platform/macos/events_loop.rs b/src/platform/macos/events_loop.rs index b68b3ee863..b9782440d8 100644 --- a/src/platform/macos/events_loop.rs +++ b/src/platform/macos/events_loop.rs @@ -140,7 +140,7 @@ impl EventsLoop { // If we have a CocoaEvent, attempt to process it // TODO: plumb timeouts down to CocoaEvent::work() if let Some(mut current_event) = self.current_cocoa_event.take() { - if current_event.work(self) == false { + while current_event.work(self) == false { // Event is not complete // We must either process it further or store it again for later if let Some(event) = self.shared.dequeue_event() { @@ -157,7 +157,6 @@ impl EventsLoop { } else { // We can repeat - continue; } } diff --git a/src/platform/macos/timer.rs b/src/platform/macos/timer.rs index d416d468d6..6d031ad449 100644 --- a/src/platform/macos/timer.rs +++ b/src/platform/macos/timer.rs @@ -11,7 +11,7 @@ pub struct Timer { } #[cfg(feature="context")] -extern "C" fn timer_callback(timer: CFRunLoopTimerRef, info: *mut c_void) { +extern "C" fn timer_callback(_timer: CFRunLoopTimerRef, _info: *mut c_void) { // attempt to yield back to the caller use super::send_event_context::try_resume; unsafe { From bf4dcb331b992c0f82bfcc3ee25af47b49cf0e09 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Mon, 17 Jul 2017 10:05:49 -0500 Subject: [PATCH 09/24] Context switch from enqueue_event() to get_event() directly --- src/platform/macos/events_loop.rs | 13 +++++++++---- src/platform/macos/send_event.rs | 4 ++++ src/platform/macos/send_event_context.rs | 2 ++ src/platform/macos/timer.rs | 8 +------- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/platform/macos/events_loop.rs b/src/platform/macos/events_loop.rs index b9782440d8..785cd327f4 100644 --- a/src/platform/macos/events_loop.rs +++ b/src/platform/macos/events_loop.rs @@ -53,10 +53,15 @@ impl Shared { pub fn enqueue_event(&self, event: Event) { self.pending_events.lock().unwrap().push_back(event); - // attempt to wake the runloop - self.timer.trigger(); - unsafe { - runloop::CFRunLoopWakeUp(runloop::CFRunLoopGetMain()); + // attempt to hop back + if unsafe { super::send_event::try_resume(1) } { + // success! + } else { + // attempt to wake the runloop + self.timer.trigger(); + unsafe { + runloop::CFRunLoopWakeUp(runloop::CFRunLoopGetMain()); + } } } diff --git a/src/platform/macos/send_event.rs b/src/platform/macos/send_event.rs index d59969d0b2..9ffa7f06c2 100644 --- a/src/platform/macos/send_event.rs +++ b/src/platform/macos/send_event.rs @@ -1,6 +1,10 @@ use cocoa; use cocoa::appkit::{NSApp,NSApplication}; +pub unsafe fn try_resume(value: usize) -> bool { + false +} + // The `SendEvent` struct encapsulates the idea of calling [NSApp sendEvent:event]. // This is a separate struct because, in the case of resize events, dispatching an event can enter // an internal runloop, and we don't want to get stuck there. diff --git a/src/platform/macos/send_event_context.rs b/src/platform/macos/send_event_context.rs index 5063c57548..bc9caf59e5 100644 --- a/src/platform/macos/send_event_context.rs +++ b/src/platform/macos/send_event_context.rs @@ -28,6 +28,8 @@ unsafe fn resume(value: usize) { } } +// Attempt to hop back to the "normal" stack frame, yielding `value`. Returns false if we're not +// inside a coroutine. pub unsafe fn try_resume(value: usize) -> bool { if let Some(context) = INNER_CONTEXT.with(|c| { c.take() }) { // resume it, getting a new context diff --git a/src/platform/macos/timer.rs b/src/platform/macos/timer.rs index 6d031ad449..530fb7cffc 100644 --- a/src/platform/macos/timer.rs +++ b/src/platform/macos/timer.rs @@ -10,19 +10,13 @@ pub struct Timer { timer: CFRunLoopTimerRef, } -#[cfg(feature="context")] extern "C" fn timer_callback(_timer: CFRunLoopTimerRef, _info: *mut c_void) { // attempt to yield back to the caller - use super::send_event_context::try_resume; unsafe { - try_resume(1); + super::send_event::try_resume(1); } } -#[cfg(not(feature="context"))] -extern "C" fn timer_callback(timer: CFRunLoopTimerRef, info: *mut c_void) { - // nothing to do -} impl Timer { pub fn new() -> Timer { From 230e3f7268232e3b1a3d07bacfeebaa55711c812 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Mon, 17 Jul 2017 20:04:33 -0500 Subject: [PATCH 10/24] Spike: runloop refactor events_loop::runloop is now responsible for the entire Cocoa runloop cycle. It's a simple blocking implementation (which fails during a resize), but it's an API that maps cleanly to a coroutine-based implementation. NSEvent concerns are now separated into events_loop::nsevent. --- src/platform/macos/events_loop.rs | 740 ---------------------- src/platform/macos/events_loop/mod.rs | 165 +++++ src/platform/macos/events_loop/nsevent.rs | 498 +++++++++++++++ src/platform/macos/events_loop/runloop.rs | 120 ++++ src/platform/macos/mod.rs | 8 - src/platform/macos/send_event.rs | 28 - src/platform/macos/send_event_context.rs | 201 ------ 7 files changed, 783 insertions(+), 977 deletions(-) delete mode 100644 src/platform/macos/events_loop.rs create mode 100644 src/platform/macos/events_loop/mod.rs create mode 100644 src/platform/macos/events_loop/nsevent.rs create mode 100644 src/platform/macos/events_loop/runloop.rs delete mode 100644 src/platform/macos/send_event.rs delete mode 100644 src/platform/macos/send_event_context.rs diff --git a/src/platform/macos/events_loop.rs b/src/platform/macos/events_loop.rs deleted file mode 100644 index 785cd327f4..0000000000 --- a/src/platform/macos/events_loop.rs +++ /dev/null @@ -1,740 +0,0 @@ -use {ControlFlow, EventsLoopClosed}; -use cocoa::{self, appkit, foundation}; -use cocoa::appkit::{NSApplication, NSEvent, NSView, NSWindow}; -use core_foundation::base::{CFRetain, CFRelease, CFTypeRef}; -use core_foundation::runloop; -use events::{self, ElementState, Event, MouseButton, TouchPhase, WindowEvent, DeviceEvent, ModifiersState, KeyboardInput}; -use std::collections::VecDeque; -use std::sync::{Arc, Mutex, Weak}; -use super::window::Window; -use std; -use super::DeviceId; -use super::send_event::SendEvent; -use super::timer::Timer; - -pub struct EventsLoop { - modifiers: Modifiers, - pub shared: Arc, - current_cocoa_event: Option, -} - -// State shared between the `EventsLoop` and its registered windows. -pub struct Shared { - pub windows: Mutex>>, - - // A queue of events that are pending delivery to the library user. - pub pending_events: Mutex>, - - timer: Timer, -} - -pub struct Proxy { - shared: Weak, -} - -struct Modifiers { - shift_pressed: bool, - ctrl_pressed: bool, - win_pressed: bool, - alt_pressed: bool, -} - -impl Shared { - - pub fn new() -> Self { - Shared { - windows: Mutex::new(Vec::new()), - pending_events: Mutex::new(VecDeque::new()), - timer: Timer::new(), - } - } - - // Enqueues the event for prompt delivery to the application. - pub fn enqueue_event(&self, event: Event) { - self.pending_events.lock().unwrap().push_back(event); - - // attempt to hop back - if unsafe { super::send_event::try_resume(1) } { - // success! - } else { - // attempt to wake the runloop - self.timer.trigger(); - unsafe { - runloop::CFRunLoopWakeUp(runloop::CFRunLoopGetMain()); - } - } - } - - // Dequeues the first event, if any, from the queue. - fn dequeue_event(&self) -> Option { - self.pending_events.lock().unwrap().pop_front() - } - - // Removes the window with the given `Id` from the `windows` list. - // - // This is called when a window is either `Closed` or `Drop`ped. - pub fn find_and_remove_window(&self, id: super::window::Id) { - if let Ok(mut windows) = self.windows.lock() { - windows.retain(|w| match w.upgrade() { - Some(w) => w.id() != id, - None => true, - }); - } - } - -} - - -impl Modifiers { - pub fn new() -> Self { - Modifiers { - shift_pressed: false, - ctrl_pressed: false, - win_pressed: false, - alt_pressed: false, - } - } -} - -#[derive(Debug,Clone,Copy,Eq,PartialEq)] -enum Timeout { - Now, - Forever, -} - -impl Timeout { - fn is_elapsed(&self) -> bool { - match self { - &Timeout::Now => true, - &Timeout::Forever => false, - } - } -} - -impl EventsLoop { - - pub fn new() -> Self { - EventsLoop { - shared: Arc::new(Shared::new()), - modifiers: Modifiers::new(), - current_cocoa_event: None, - } - } - - // Attempt to get an Event by a specified timeout. - fn get_event(&mut self, timeout: Timeout) -> Option { - unsafe { - if !msg_send![cocoa::base::class("NSThread"), isMainThread] { - panic!("Events can only be polled from the main thread on macOS"); - } - } - - loop { - // Pop any queued events - // This is immediate, so no need to consider a timeout - if let Some(event) = self.shared.dequeue_event() { - return Some(event); - } - - // If we have no CocoaEvent, attempt to receive one - // CocoaEvent::receive() respects the timeout - if self.current_cocoa_event.is_none() { - self.current_cocoa_event = CocoaEvent::receive(timeout); - } - - // If we have a CocoaEvent, attempt to process it - // TODO: plumb timeouts down to CocoaEvent::work() - if let Some(mut current_event) = self.current_cocoa_event.take() { - while current_event.work(self) == false { - // Event is not complete - // We must either process it further or store it again for later - if let Some(event) = self.shared.dequeue_event() { - // Another event landed while we were working this - // Store the CocoaEvent and return the Event from the queue - self.current_cocoa_event = Some(current_event); - return Some(event); - - } else if timeout.is_elapsed() { - // Timeout is elapsed; we must return empty-handed - // Store the CocoaEvent and return nothing - self.current_cocoa_event = Some(current_event); - return None; - - } else { - // We can repeat - } - } - - // CocoaEvent processing is complete - // Is it an event? - if let CocoaEvent::Complete(Some(winit_event)) = current_event { - // Return it - return Some(winit_event); - } else { - // CocoaEvent did not translate into an events::Event - // Loop around again - } - } - } - } - - pub fn poll_events(&mut self, mut callback: F) - where F: FnMut(Event), - { - // Return as many events as we can without blocking - while let Some(event) = self.get_event(Timeout::Now) { - callback(event); - } - } - - pub fn run_forever(&mut self, mut callback: F) - where F: FnMut(Event) -> ControlFlow - { - // Get events until we're told to stop - while let Some(event) = self.get_event(Timeout::Forever) { - // Send to the app - let control_flow = callback(event); - - // Do what it says - match control_flow { - ControlFlow::Break => break, - ControlFlow::Continue => (), - } - } - } - - pub fn create_proxy(&self) -> Proxy { - Proxy { shared: Arc::downgrade(&self.shared) } - } -} - -impl Proxy { - pub fn wakeup(&self) -> Result<(), EventsLoopClosed> { - if let Some(shared) = self.shared.upgrade() { - shared.enqueue_event(Event::Awakened); - Ok(()) - } else { - Err(EventsLoopClosed) - } - } -} - -struct RetainedEvent(cocoa::base::id); -impl RetainedEvent { - fn new(event: cocoa::base::id) -> RetainedEvent { - unsafe { CFRetain(event as CFTypeRef); } - RetainedEvent(event) - } - fn into_inner(self) -> cocoa::base::id { - self.0 - } -} -impl Drop for RetainedEvent { - fn drop(&mut self) { - unsafe { CFRelease(self.0 as CFTypeRef); } - } -} - -// Encapsulates the lifecycle of a Cocoa event -enum CocoaEvent { - // We just received this event, and haven't processed it yet - Received(RetainedEvent), - - // We're trying to sending to the windowing system and haven't completed it yet - Sending(SendEvent, RetainedEvent), - - // We delivered the message to the windowing system and possibly got back an events::Event - Complete(Option), -} - -impl CocoaEvent { - fn receive(timeout: Timeout) -> Option { - unsafe { - let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); - - // Pick a timeout - let timeout = match timeout { - Timeout::Now => foundation::NSDate::distantPast(cocoa::base::nil), - Timeout::Forever => foundation::NSDate::distantFuture(cocoa::base::nil), - }; - - // Poll for the next event - let ns_event = appkit::NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( - appkit::NSAnyEventMask.bits() | appkit::NSEventMaskPressure.bits(), - timeout, - foundation::NSDefaultRunLoopMode, - cocoa::base::YES); - - // Wrap the result in a CocoaEvent - let event = Self::new(ns_event); - - let _: () = msg_send![pool, release]; - - return event - } - } - - fn new(ns_event: cocoa::base::id) -> Option { - // we (possibly) received `ns_event` from the windowing subsystem - // is this an event? - if ns_event == cocoa::base::nil { - return None; - } - - // FIXME: Despite not being documented anywhere, an `NSEvent` is produced when a user opens - // Spotlight while the NSApplication is in focus. This `NSEvent` produces a `NSEventType` - // with value `21`. This causes a SEGFAULT as soon as we try to match on the `NSEventType` - // enum as there is no variant associated with the value. Thus, we return early if this - // sneaky event occurs. If someone does find some documentation on this, please fix this by - // adding an appropriate variant to the `NSEventType` enum in the cocoa-rs crate. - if unsafe { ns_event.eventType() } as u64 == 21 { - return None; - } - - // good enough, let's dispatch - Some(CocoaEvent::Received(RetainedEvent::new(ns_event))) - } - - // Attempt to push a CocoaEvent towards a winit Event - // Returns true on completion - fn work(&mut self, events_loop: &mut EventsLoop) -> bool { - // take ourselves and match on it - let (new_event, is_complete) = match std::mem::replace(self, CocoaEvent::Complete(None)) { - CocoaEvent::Received(retained_event) => { - // Determine if we need to send this event - // FIXME: Document this. Why do we do this? Seems like it passes on events to window/app. - // If we don't do this, window does not become main for some reason. - let needs_send = match unsafe { retained_event.0.eventType() } { - appkit::NSKeyDown => false, - _ => true, - }; - - if needs_send { - (CocoaEvent::Sending(SendEvent::new(retained_event.0), retained_event), false) - } else { - (CocoaEvent::Complete(Self::to_event(events_loop, retained_event.into_inner())), true) - } - } - - CocoaEvent::Sending(send_event, retained_event) => { - // Try to advance send event - if let Some(new_send_event) = send_event.work() { - // Needs more time - (CocoaEvent::Sending(new_send_event, retained_event), false) - } else { - // Done - (CocoaEvent::Complete(Self::to_event(events_loop, retained_event.into_inner())), true) - } - } - - CocoaEvent::Complete(event) => { - // nothing to do - (CocoaEvent::Complete(event), true) - } - }; - - // replace ourselves with the result of the match - std::mem::replace(self, new_event); - - // return the completion flag - is_complete - } - - fn to_event(events_loop: &mut EventsLoop, ns_event: cocoa::base::id) -> Option { - unsafe { - let event_type = ns_event.eventType(); - let ns_window = ns_event.window(); - let window_id = super::window::get_window_id(ns_window); - - let windows = events_loop.shared.windows.lock().unwrap(); - let maybe_window = windows.iter() - .filter_map(Weak::upgrade) - .find(|window| window_id == window.id()); - - let into_event = |window_event| Event::WindowEvent { - window_id: ::WindowId(window_id), - event: window_event, - }; - - // Returns `Some` window if one of our windows is the key window. - let maybe_key_window = || windows.iter() - .filter_map(Weak::upgrade) - .find(|window| { - let is_key_window: cocoa::base::BOOL = msg_send![*window.window, isKeyWindow]; - is_key_window == cocoa::base::YES - }); - - match event_type { - appkit::NSKeyDown => { - let mut events = std::collections::VecDeque::new(); - let received_c_str = foundation::NSString::UTF8String(ns_event.characters()); - let received_str = std::ffi::CStr::from_ptr(received_c_str); - - let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); - let state = ElementState::Pressed; - let code = NSEvent::keyCode(ns_event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: vkey, - modifiers: event_mods(ns_event), - }, - }; - for received_char in std::str::from_utf8(received_str.to_bytes()).unwrap().chars() { - let window_event = WindowEvent::ReceivedCharacter(received_char); - events.push_back(into_event(window_event)); - } - events_loop.shared.pending_events.lock().unwrap().extend(events.into_iter()); - Some(into_event(window_event)) - }, - - appkit::NSKeyUp => { - let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); - - let state = ElementState::Released; - let code = NSEvent::keyCode(ns_event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: vkey, - modifiers: event_mods(ns_event), - }, - }; - Some(into_event(window_event)) - }, - - appkit::NSFlagsChanged => { - unsafe fn modifier_event(event: cocoa::base::id, - keymask: appkit::NSEventModifierFlags, - key: events::VirtualKeyCode, - key_pressed: bool) -> Option - { - if !key_pressed && NSEvent::modifierFlags(event).contains(keymask) { - let state = ElementState::Pressed; - let code = NSEvent::keyCode(event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: Some(key), - modifiers: event_mods(event), - }, - }; - Some(window_event) - } else if key_pressed && !NSEvent::modifierFlags(event).contains(keymask) { - let state = ElementState::Released; - let code = NSEvent::keyCode(event) as u32; - let window_event = WindowEvent::KeyboardInput { - device_id: DEVICE_ID, - input: KeyboardInput { - state: state, - scancode: code, - virtual_keycode: Some(key), - modifiers: event_mods(event), - }, - }; - Some(window_event) - } else { - None - } - } - - let mut events = std::collections::VecDeque::new(); - if let Some(window_event) = modifier_event(ns_event, - appkit::NSShiftKeyMask, - events::VirtualKeyCode::LShift, - events_loop.modifiers.shift_pressed) - { - events_loop.modifiers.shift_pressed = !events_loop.modifiers.shift_pressed; - events.push_back(into_event(window_event)); - } - - if let Some(window_event) = modifier_event(ns_event, - appkit::NSControlKeyMask, - events::VirtualKeyCode::LControl, - events_loop.modifiers.ctrl_pressed) - { - events_loop.modifiers.ctrl_pressed = !events_loop.modifiers.ctrl_pressed; - events.push_back(into_event(window_event)); - } - - if let Some(window_event) = modifier_event(ns_event, - appkit::NSCommandKeyMask, - events::VirtualKeyCode::LWin, - events_loop.modifiers.win_pressed) - { - events_loop.modifiers.win_pressed = !events_loop.modifiers.win_pressed; - events.push_back(into_event(window_event)); - } - - if let Some(window_event) = modifier_event(ns_event, - appkit::NSAlternateKeyMask, - events::VirtualKeyCode::LAlt, - events_loop.modifiers.alt_pressed) - { - events_loop.modifiers.alt_pressed = !events_loop.modifiers.alt_pressed; - events.push_back(into_event(window_event)); - } - - let event = events.pop_front(); - events_loop.shared.pending_events.lock().unwrap().extend(events.into_iter()); - event - }, - - appkit::NSLeftMouseDown => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Left })) }, - appkit::NSLeftMouseUp => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Left })) }, - appkit::NSRightMouseDown => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Right })) }, - appkit::NSRightMouseUp => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Right })) }, - appkit::NSOtherMouseDown => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Middle })) }, - appkit::NSOtherMouseUp => { Some(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Middle })) }, - - appkit::NSMouseEntered => { Some(into_event(WindowEvent::MouseEntered { device_id: DEVICE_ID })) }, - appkit::NSMouseExited => { Some(into_event(WindowEvent::MouseLeft { device_id: DEVICE_ID })) }, - - appkit::NSMouseMoved | - appkit::NSLeftMouseDragged | - appkit::NSOtherMouseDragged | - appkit::NSRightMouseDragged => { - // If the mouse movement was on one of our windows, use it. - // Otherwise, if one of our windows is the key window (receiving input), use it. - // Otherwise, return `None`. - let window = match maybe_window.or_else(maybe_key_window) { - Some(window) => window, - None => return None, - }; - - let window_point = ns_event.locationInWindow(); - let view_point = if ns_window == cocoa::base::nil { - let ns_size = foundation::NSSize::new(0.0, 0.0); - let ns_rect = foundation::NSRect::new(window_point, ns_size); - let window_rect = window.window.convertRectFromScreen_(ns_rect); - window.view.convertPoint_fromView_(window_rect.origin, cocoa::base::nil) - } else { - window.view.convertPoint_fromView_(window_point, cocoa::base::nil) - }; - let view_rect = NSView::frame(*window.view); - let scale_factor = window.hidpi_factor(); - - let mut events = std::collections::VecDeque::new(); - - { - let x = (scale_factor * view_point.x as f32) as f64; - let y = (scale_factor * (view_rect.size.height - view_point.y) as f32) as f64; - let window_event = WindowEvent::MouseMoved { device_id: DEVICE_ID, position: (x, y) }; - let event = Event::WindowEvent { window_id: ::WindowId(window.id()), event: window_event }; - events.push_back(event); - } - - let delta_x = (scale_factor * ns_event.deltaX() as f32) as f64; - if delta_x != 0.0 { - let motion_event = DeviceEvent::Motion { axis: 0, value: delta_x }; - let event = Event::DeviceEvent{ device_id: DEVICE_ID, event: motion_event }; - events.push_back(event); - } - - let delta_y = (scale_factor * ns_event.deltaY() as f32) as f64; - if delta_y != 0.0 { - let motion_event = DeviceEvent::Motion { axis: 1, value: delta_y }; - let event = Event::DeviceEvent{ device_id: DEVICE_ID, event: motion_event }; - events.push_back(event); - } - - let event = events.pop_front(); - events.shared.pending_events.lock().unwrap().extend(events.into_iter()); - event - }, - - appkit::NSScrollWheel => { - // If none of the windows received the scroll, return `None`. - let window = match maybe_window { - Some(window) => window, - None => return None, - }; - - use events::MouseScrollDelta::{LineDelta, PixelDelta}; - let scale_factor = window.hidpi_factor(); - let delta = if ns_event.hasPreciseScrollingDeltas() == cocoa::base::YES { - PixelDelta(scale_factor * ns_event.scrollingDeltaX() as f32, - scale_factor * ns_event.scrollingDeltaY() as f32) - } else { - LineDelta(scale_factor * ns_event.scrollingDeltaX() as f32, - scale_factor * ns_event.scrollingDeltaY() as f32) - }; - let phase = match ns_event.phase() { - appkit::NSEventPhaseMayBegin | appkit::NSEventPhaseBegan => TouchPhase::Started, - appkit::NSEventPhaseEnded => TouchPhase::Ended, - _ => TouchPhase::Moved, - }; - let window_event = WindowEvent::MouseWheel { device_id: DEVICE_ID, delta: delta, phase: phase }; - Some(into_event(window_event)) - }, - - appkit::NSEventTypePressure => { - let pressure = ns_event.pressure(); - let stage = ns_event.stage(); - let window_event = WindowEvent::TouchpadPressure { device_id: DEVICE_ID, pressure: pressure, stage: stage }; - Some(into_event(window_event)) - }, - - _ => None, - } - } - } -} - - - -fn to_virtual_key_code(code: u16) -> Option { - Some(match code { - 0x00 => events::VirtualKeyCode::A, - 0x01 => events::VirtualKeyCode::S, - 0x02 => events::VirtualKeyCode::D, - 0x03 => events::VirtualKeyCode::F, - 0x04 => events::VirtualKeyCode::H, - 0x05 => events::VirtualKeyCode::G, - 0x06 => events::VirtualKeyCode::Z, - 0x07 => events::VirtualKeyCode::X, - 0x08 => events::VirtualKeyCode::C, - 0x09 => events::VirtualKeyCode::V, - //0x0a => World 1, - 0x0b => events::VirtualKeyCode::B, - 0x0c => events::VirtualKeyCode::Q, - 0x0d => events::VirtualKeyCode::W, - 0x0e => events::VirtualKeyCode::E, - 0x0f => events::VirtualKeyCode::R, - 0x10 => events::VirtualKeyCode::Y, - 0x11 => events::VirtualKeyCode::T, - 0x12 => events::VirtualKeyCode::Key1, - 0x13 => events::VirtualKeyCode::Key2, - 0x14 => events::VirtualKeyCode::Key3, - 0x15 => events::VirtualKeyCode::Key4, - 0x16 => events::VirtualKeyCode::Key6, - 0x17 => events::VirtualKeyCode::Key5, - 0x18 => events::VirtualKeyCode::Equals, - 0x19 => events::VirtualKeyCode::Key9, - 0x1a => events::VirtualKeyCode::Key7, - 0x1b => events::VirtualKeyCode::Minus, - 0x1c => events::VirtualKeyCode::Key8, - 0x1d => events::VirtualKeyCode::Key0, - 0x1e => events::VirtualKeyCode::RBracket, - 0x1f => events::VirtualKeyCode::O, - 0x20 => events::VirtualKeyCode::U, - 0x21 => events::VirtualKeyCode::LBracket, - 0x22 => events::VirtualKeyCode::I, - 0x23 => events::VirtualKeyCode::P, - 0x24 => events::VirtualKeyCode::Return, - 0x25 => events::VirtualKeyCode::L, - 0x26 => events::VirtualKeyCode::J, - 0x27 => events::VirtualKeyCode::Apostrophe, - 0x28 => events::VirtualKeyCode::K, - 0x29 => events::VirtualKeyCode::Semicolon, - 0x2a => events::VirtualKeyCode::Backslash, - 0x2b => events::VirtualKeyCode::Comma, - 0x2c => events::VirtualKeyCode::Slash, - 0x2d => events::VirtualKeyCode::N, - 0x2e => events::VirtualKeyCode::M, - 0x2f => events::VirtualKeyCode::Period, - 0x30 => events::VirtualKeyCode::Tab, - 0x31 => events::VirtualKeyCode::Space, - 0x32 => events::VirtualKeyCode::Grave, - 0x33 => events::VirtualKeyCode::Back, - //0x34 => unkown, - 0x35 => events::VirtualKeyCode::Escape, - 0x36 => events::VirtualKeyCode::RWin, - 0x37 => events::VirtualKeyCode::LWin, - 0x38 => events::VirtualKeyCode::LShift, - //0x39 => Caps lock, - //0x3a => Left alt, - 0x3b => events::VirtualKeyCode::LControl, - 0x3c => events::VirtualKeyCode::RShift, - //0x3d => Right alt, - 0x3e => events::VirtualKeyCode::RControl, - //0x3f => Fn key, - //0x40 => F17 Key, - 0x41 => events::VirtualKeyCode::Decimal, - //0x42 -> unkown, - 0x43 => events::VirtualKeyCode::Multiply, - //0x44 => unkown, - 0x45 => events::VirtualKeyCode::Add, - //0x46 => unkown, - 0x47 => events::VirtualKeyCode::Numlock, - //0x48 => KeypadClear, - 0x49 => events::VirtualKeyCode::VolumeUp, - 0x4a => events::VirtualKeyCode::VolumeDown, - 0x4b => events::VirtualKeyCode::Divide, - 0x4c => events::VirtualKeyCode::NumpadEnter, - //0x4d => unkown, - 0x4e => events::VirtualKeyCode::Subtract, - //0x4f => F18 key, - //0x50 => F19 Key, - 0x51 => events::VirtualKeyCode::NumpadEquals, - 0x52 => events::VirtualKeyCode::Numpad0, - 0x53 => events::VirtualKeyCode::Numpad1, - 0x54 => events::VirtualKeyCode::Numpad2, - 0x55 => events::VirtualKeyCode::Numpad3, - 0x56 => events::VirtualKeyCode::Numpad4, - 0x57 => events::VirtualKeyCode::Numpad5, - 0x58 => events::VirtualKeyCode::Numpad6, - 0x59 => events::VirtualKeyCode::Numpad7, - //0x5a => F20 Key, - 0x5b => events::VirtualKeyCode::Numpad8, - 0x5c => events::VirtualKeyCode::Numpad9, - //0x5d => unkown, - //0x5e => unkown, - //0x5f => unkown, - 0x60 => events::VirtualKeyCode::F5, - 0x61 => events::VirtualKeyCode::F6, - 0x62 => events::VirtualKeyCode::F7, - 0x63 => events::VirtualKeyCode::F3, - 0x64 => events::VirtualKeyCode::F8, - 0x65 => events::VirtualKeyCode::F9, - //0x66 => unkown, - 0x67 => events::VirtualKeyCode::F11, - //0x68 => unkown, - 0x69 => events::VirtualKeyCode::F13, - //0x6a => F16 Key, - 0x6b => events::VirtualKeyCode::F14, - //0x6c => unkown, - 0x6d => events::VirtualKeyCode::F10, - //0x6e => unkown, - 0x6f => events::VirtualKeyCode::F12, - //0x70 => unkown, - 0x71 => events::VirtualKeyCode::F15, - 0x72 => events::VirtualKeyCode::Insert, - 0x73 => events::VirtualKeyCode::Home, - 0x74 => events::VirtualKeyCode::PageUp, - 0x75 => events::VirtualKeyCode::Delete, - 0x76 => events::VirtualKeyCode::F4, - 0x77 => events::VirtualKeyCode::End, - 0x78 => events::VirtualKeyCode::F2, - 0x79 => events::VirtualKeyCode::PageDown, - 0x7a => events::VirtualKeyCode::F1, - 0x7b => events::VirtualKeyCode::Left, - 0x7c => events::VirtualKeyCode::Right, - 0x7d => events::VirtualKeyCode::Down, - 0x7e => events::VirtualKeyCode::Up, - //0x7f => unkown, - - _ => return None, - }) -} - -fn event_mods(event: cocoa::base::id) -> ModifiersState { - let flags = unsafe { - NSEvent::modifierFlags(event) - }; - ModifiersState { - shift: flags.contains(appkit::NSShiftKeyMask), - ctrl: flags.contains(appkit::NSControlKeyMask), - alt: flags.contains(appkit::NSAlternateKeyMask), - logo: flags.contains(appkit::NSCommandKeyMask), - } -} - -// Constant device ID, to be removed when this backend is updated to report real device IDs. -const DEVICE_ID: ::DeviceId = ::DeviceId(DeviceId); diff --git a/src/platform/macos/events_loop/mod.rs b/src/platform/macos/events_loop/mod.rs new file mode 100644 index 0000000000..1ce4ab621d --- /dev/null +++ b/src/platform/macos/events_loop/mod.rs @@ -0,0 +1,165 @@ +use {ControlFlow, EventsLoopClosed}; +use events::Event; +use std::collections::VecDeque; +use std::sync::{Arc, Mutex, Weak}; +use super::window::{self, Window}; + +mod nsevent; + +mod runloop; +use self::runloop::Runloop; + +pub struct EventsLoop { + pub shared: Arc, + runloop: Runloop, +} + +// State shared between the `EventsLoop` and its registered windows. +pub struct Shared { + pub windows: Mutex>>, + + // A queue of events that are pending delivery to the library user. + pub pending_events: Mutex>, +} + +impl nsevent::WindowFinder for Shared { + fn find_window_by_id(&self, id: window::Id) -> Option> { + for window in self.windows.lock().unwrap().iter() { + if let Some(window) = window.upgrade() { + if window.id() == id { + return Some(window); + } + } + } + + None + } +} + +pub struct Proxy { + shared: Weak, +} + +impl Shared { + + pub fn new() -> Self { + Shared { + windows: Mutex::new(Vec::new()), + pending_events: Mutex::new(VecDeque::new()), + } + } + + // Enqueues the event for prompt delivery to the application. + pub fn enqueue_event(&self, event: Event) { + self.pending_events.lock().unwrap().push_back(event); + + // TODO: wake the runloop + } + + // Dequeues the first event, if any, from the queue. + fn dequeue_event(&self) -> Option { + self.pending_events.lock().unwrap().pop_front() + } + + // Removes the window with the given `Id` from the `windows` list. + // + // This is called when a window is either `Closed` or `Drop`ped. + pub fn find_and_remove_window(&self, id: super::window::Id) { + if let Ok(mut windows) = self.windows.lock() { + windows.retain(|w| match w.upgrade() { + Some(w) => w.id() != id, + None => true, + }); + } + } + +} + + +#[derive(Debug,Clone,Copy,Eq,PartialEq)] +enum Timeout { + Now, + Forever, +} + +impl Timeout { + fn is_elapsed(&self) -> bool { + match self { + &Timeout::Now => true, + &Timeout::Forever => false, + } + } +} + +impl EventsLoop { + + pub fn new() -> Self { + let shared = Arc::new(Shared::new()); + EventsLoop { + runloop: Runloop::new(Arc::downgrade(&shared)), + shared: shared, + } + } + + // Attempt to get an Event by a specified timeout. + fn get_event(&mut self, timeout: Timeout) -> Option { + loop { + // Pop any queued events + // This is immediate, so no need to consider a timeout + if let Some(event) = self.shared.dequeue_event() { + return Some(event); + } + + // Attempt to get more events from the runloop + self.runloop.work(timeout); + + // Is our time up? + if timeout.is_elapsed() { + // Check the queue again before returning, just in case + return self.shared.dequeue_event(); + } + + // Loop around again + } + } + + pub fn poll_events(&mut self, mut callback: F) + where F: FnMut(Event), + { + // Return as many events as we can without blocking + while let Some(event) = self.get_event(Timeout::Now) { + callback(event); + } + } + + pub fn run_forever(&mut self, mut callback: F) + where F: FnMut(Event) -> ControlFlow + { + // Get events until we're told to stop + while let Some(event) = self.get_event(Timeout::Forever) { + // Send to the app + let control_flow = callback(event); + + // Do what it says + match control_flow { + ControlFlow::Break => break, + ControlFlow::Continue => (), + } + } + } + + pub fn create_proxy(&self) -> Proxy { + Proxy { shared: Arc::downgrade(&self.shared) } + } +} + +impl Proxy { + pub fn wakeup(&self) -> Result<(), EventsLoopClosed> { + if let Some(shared) = self.shared.upgrade() { + shared.enqueue_event(Event::Awakened); + Ok(()) + } else { + Err(EventsLoopClosed) + } + } +} diff --git a/src/platform/macos/events_loop/nsevent.rs b/src/platform/macos/events_loop/nsevent.rs new file mode 100644 index 0000000000..1c94e3e0e5 --- /dev/null +++ b/src/platform/macos/events_loop/nsevent.rs @@ -0,0 +1,498 @@ +use std; +use std::sync::{Arc,Weak}; +use cocoa; +use cocoa::appkit::{self, NSApplication, NSApp, NSEvent, NSView, NSWindow}; +use cocoa::foundation; +use core_foundation::base::{CFRetain,CFRelease,CFTypeRef}; + +use super::EventsLoop; +use super::super::DeviceId; +use super::super::window::{self, Window}; +use events::{self, ElementState, Event, MouseButton, TouchPhase, DeviceEvent, WindowEvent, ModifiersState, KeyboardInput}; + +// RetainedEvent wraps an `NSEvent`, incrementing its refcount on `new` and decrementing on `drop`. +pub struct RetainedEvent(cocoa::base::id); + +impl RetainedEvent { + pub fn new(event: cocoa::base::id) -> RetainedEvent { + unsafe { CFRetain(event as CFTypeRef); } + RetainedEvent(event) + } + pub fn into_inner(self) -> cocoa::base::id { + self.0 + } + pub fn id(&self) -> cocoa::base::id { + self.0 + } +} + +impl Drop for RetainedEvent { + fn drop(&mut self) { + unsafe { CFRelease(self.0 as CFTypeRef); } + } +} + +/// Should this event be discarded immediately after receipt? +pub fn should_discard_event_early(event: &RetainedEvent) -> bool { + // is this even an event? + if event.0 == cocoa::base::nil { + // discard + return true; + } + + // FIXME: Despite not being documented anywhere, an `NSEvent` is produced when a user opens + // Spotlight while the NSApplication is in focus. This `NSEvent` produces a `NSEventType` + // with value `21`. This causes a SEGFAULT as soon as we try to match on the `NSEventType` + // enum as there is no variant associated with the value. Thus, we return early if this + // sneaky event occurs. If someone does find some documentation on this, please fix this by + // adding an appropriate variant to the `NSEventType` enum in the cocoa-rs crate. + if unsafe { event.0.eventType() } as u64 == 21 { + // discard + return true; + } + + return false; +} + +/// Should this event be forwarded back to the windowing system? +pub fn should_forward_event(event: &RetainedEvent) -> bool { + // Determine if we need to send this event + // FIXME: Document this. Why do we do this? Seems like it passes on events to window/app. + // If we don't do this, window does not become main for some reason. + match unsafe { event.0.eventType() } { + appkit::NSKeyDown => false, + _ => true, + } +} + +/// Processing of events sometimes requires persisting state from one event to another, e.g. +/// "shift key down", "A key down" => "shift A". The state required for that is stored here. +pub struct PersistentState { + modifiers: Modifiers, +} + +impl PersistentState { + pub fn new() -> PersistentState { + PersistentState { + modifiers: Modifiers::new(), + } + } +} + +pub trait WindowFinder { + fn find_window_by_id(&self, id: window::Id) -> Option>; + + fn find_key_window(&self) -> Option> { + unsafe { + let cocoa_id = msg_send![NSApp(), keyWindow]; + if cocoa_id == cocoa::base::nil { + None + } else { + self.find_window_by_id(window::get_window_id(cocoa_id)) + } + } + } +} + +struct Modifiers { + shift_pressed: bool, + ctrl_pressed: bool, + win_pressed: bool, + alt_pressed: bool, +} + +impl Modifiers { + pub fn new() -> Self { + Modifiers { + shift_pressed: false, + ctrl_pressed: false, + win_pressed: false, + alt_pressed: false, + } + } +} + +// Attempt to translate an `NSEvent` into zero or more `Event`s. +pub fn to_events(event: &RetainedEvent, state: &mut PersistentState, window_finder: &WF) -> Vec + where WF: WindowFinder +{ + let ns_event = event.0; + let mut events: Vec = Vec::new(); + + unsafe { + let event_type = ns_event.eventType(); + let ns_window = ns_event.window(); + let window_id = window::get_window_id(ns_window); + + let maybe_window = window_finder.find_window_by_id(window_id); + + let into_event = |window_event| Event::WindowEvent { + window_id: ::WindowId(window_id), + event: window_event, + }; + + match event_type { + appkit::NSKeyDown => { + let received_c_str = foundation::NSString::UTF8String(ns_event.characters()); + let received_str = std::ffi::CStr::from_ptr(received_c_str); + + let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); + let state = ElementState::Pressed; + let code = NSEvent::keyCode(ns_event) as u32; + let window_event = WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: state, + scancode: code, + virtual_keycode: vkey, + modifiers: event_mods(ns_event), + }, + }; + + events.push(into_event(window_event)); + + for received_char in std::str::from_utf8(received_str.to_bytes()).unwrap().chars() { + let window_event = WindowEvent::ReceivedCharacter(received_char); + events.push(into_event(window_event)); + } + }, + + appkit::NSKeyUp => { + let vkey = to_virtual_key_code(NSEvent::keyCode(ns_event)); + + let state = ElementState::Released; + let code = NSEvent::keyCode(ns_event) as u32; + let window_event = WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: state, + scancode: code, + virtual_keycode: vkey, + modifiers: event_mods(ns_event), + }, + }; + + events.push(into_event(window_event)); + }, + + appkit::NSFlagsChanged => { + unsafe fn modifier_event(event: cocoa::base::id, + keymask: appkit::NSEventModifierFlags, + key: events::VirtualKeyCode, + key_pressed: bool) -> Option + { + if !key_pressed && NSEvent::modifierFlags(event).contains(keymask) { + let state = ElementState::Pressed; + let code = NSEvent::keyCode(event) as u32; + let window_event = WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: state, + scancode: code, + virtual_keycode: Some(key), + modifiers: event_mods(event), + }, + }; + Some(window_event) + + } else if key_pressed && !NSEvent::modifierFlags(event).contains(keymask) { + let state = ElementState::Released; + let code = NSEvent::keyCode(event) as u32; + let window_event = WindowEvent::KeyboardInput { + device_id: DEVICE_ID, + input: KeyboardInput { + state: state, + scancode: code, + virtual_keycode: Some(key), + modifiers: event_mods(event), + }, + }; + Some(window_event) + + } else { + None + } + } + + if let Some(window_event) = modifier_event(ns_event, + appkit::NSShiftKeyMask, + events::VirtualKeyCode::LShift, + state.modifiers.shift_pressed) + { + state.modifiers.shift_pressed = !state.modifiers.shift_pressed; + events.push(into_event(window_event)); + } + + if let Some(window_event) = modifier_event(ns_event, + appkit::NSControlKeyMask, + events::VirtualKeyCode::LControl, + state.modifiers.ctrl_pressed) + { + state.modifiers.ctrl_pressed = !state.modifiers.ctrl_pressed; + events.push(into_event(window_event)); + } + + if let Some(window_event) = modifier_event(ns_event, + appkit::NSCommandKeyMask, + events::VirtualKeyCode::LWin, + state.modifiers.win_pressed) + { + state.modifiers.win_pressed = !state.modifiers.win_pressed; + events.push(into_event(window_event)); + } + + if let Some(window_event) = modifier_event(ns_event, + appkit::NSAlternateKeyMask, + events::VirtualKeyCode::LAlt, + state.modifiers.alt_pressed) + { + state.modifiers.alt_pressed = !state.modifiers.alt_pressed; + events.push(into_event(window_event)); + } + }, + + appkit::NSLeftMouseDown => { events.push(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Left })); }, + appkit::NSLeftMouseUp => { events.push(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Left })); }, + appkit::NSRightMouseDown => { events.push(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Right })); }, + appkit::NSRightMouseUp => { events.push(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Right })); }, + appkit::NSOtherMouseDown => { events.push(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Pressed, button: MouseButton::Middle })); }, + appkit::NSOtherMouseUp => { events.push(into_event(WindowEvent::MouseInput { device_id: DEVICE_ID, state: ElementState::Released, button: MouseButton::Middle })); }, + + appkit::NSMouseEntered => { events.push(into_event(WindowEvent::MouseEntered { device_id: DEVICE_ID })); }, + appkit::NSMouseExited => { events.push(into_event(WindowEvent::MouseLeft { device_id: DEVICE_ID })); }, + + appkit::NSMouseMoved | + appkit::NSLeftMouseDragged | + appkit::NSOtherMouseDragged | + appkit::NSRightMouseDragged => { + // If the mouse movement was on one of our windows, use it. + // Otherwise, if one of our windows is the key window (receiving input), use it. + // Otherwise, exit early. + let window = match maybe_window.or_else(|| window_finder.find_key_window()) { + Some(window) => window, + None => return events, + }; + + let window_point = ns_event.locationInWindow(); + let view_point = if ns_window == cocoa::base::nil { + let ns_size = foundation::NSSize::new(0.0, 0.0); + let ns_rect = foundation::NSRect::new(window_point, ns_size); + let window_rect = window.window.convertRectFromScreen_(ns_rect); + window.view.convertPoint_fromView_(window_rect.origin, cocoa::base::nil) + } else { + window.view.convertPoint_fromView_(window_point, cocoa::base::nil) + }; + let view_rect = NSView::frame(*window.view); + let scale_factor = window.hidpi_factor(); + + { + let x = (scale_factor * view_point.x as f32) as f64; + let y = (scale_factor * (view_rect.size.height - view_point.y) as f32) as f64; + let window_event = WindowEvent::MouseMoved { device_id: DEVICE_ID, position: (x, y) }; + let event = Event::WindowEvent { window_id: ::WindowId(window.id()), event: window_event }; + events.push(event); + } + + let delta_x = (scale_factor * ns_event.deltaX() as f32) as f64; + if delta_x != 0.0 { + let motion_event = DeviceEvent::Motion { axis: 0, value: delta_x }; + let event = Event::DeviceEvent{ device_id: DEVICE_ID, event: motion_event }; + events.push(event); + } + + let delta_y = (scale_factor * ns_event.deltaY() as f32) as f64; + if delta_y != 0.0 { + let motion_event = DeviceEvent::Motion { axis: 1, value: delta_y }; + let event = Event::DeviceEvent{ device_id: DEVICE_ID, event: motion_event }; + events.push(event); + } + }, + + appkit::NSScrollWheel => { + // If none of the windows received the scroll, return early. + let window = match maybe_window { + Some(window) => window, + None => return events, + }; + + use events::MouseScrollDelta::{LineDelta, PixelDelta}; + let scale_factor = window.hidpi_factor(); + let delta = if ns_event.hasPreciseScrollingDeltas() == cocoa::base::YES { + PixelDelta(scale_factor * ns_event.scrollingDeltaX() as f32, + scale_factor * ns_event.scrollingDeltaY() as f32) + } else { + LineDelta(scale_factor * ns_event.scrollingDeltaX() as f32, + scale_factor * ns_event.scrollingDeltaY() as f32) + }; + let phase = match ns_event.phase() { + appkit::NSEventPhaseMayBegin | appkit::NSEventPhaseBegan => TouchPhase::Started, + appkit::NSEventPhaseEnded => TouchPhase::Ended, + _ => TouchPhase::Moved, + }; + let window_event = WindowEvent::MouseWheel { device_id: DEVICE_ID, delta: delta, phase: phase }; + events.push(into_event(window_event)); + }, + + appkit::NSEventTypePressure => { + let pressure = ns_event.pressure(); + let stage = ns_event.stage(); + let window_event = WindowEvent::TouchpadPressure { device_id: DEVICE_ID, pressure: pressure, stage: stage }; + events.push(into_event(window_event)); + }, + + _ => (), + } + } + + events +} + +fn to_virtual_key_code(code: u16) -> Option { + Some(match code { + 0x00 => events::VirtualKeyCode::A, + 0x01 => events::VirtualKeyCode::S, + 0x02 => events::VirtualKeyCode::D, + 0x03 => events::VirtualKeyCode::F, + 0x04 => events::VirtualKeyCode::H, + 0x05 => events::VirtualKeyCode::G, + 0x06 => events::VirtualKeyCode::Z, + 0x07 => events::VirtualKeyCode::X, + 0x08 => events::VirtualKeyCode::C, + 0x09 => events::VirtualKeyCode::V, + //0x0a => World 1, + 0x0b => events::VirtualKeyCode::B, + 0x0c => events::VirtualKeyCode::Q, + 0x0d => events::VirtualKeyCode::W, + 0x0e => events::VirtualKeyCode::E, + 0x0f => events::VirtualKeyCode::R, + 0x10 => events::VirtualKeyCode::Y, + 0x11 => events::VirtualKeyCode::T, + 0x12 => events::VirtualKeyCode::Key1, + 0x13 => events::VirtualKeyCode::Key2, + 0x14 => events::VirtualKeyCode::Key3, + 0x15 => events::VirtualKeyCode::Key4, + 0x16 => events::VirtualKeyCode::Key6, + 0x17 => events::VirtualKeyCode::Key5, + 0x18 => events::VirtualKeyCode::Equals, + 0x19 => events::VirtualKeyCode::Key9, + 0x1a => events::VirtualKeyCode::Key7, + 0x1b => events::VirtualKeyCode::Minus, + 0x1c => events::VirtualKeyCode::Key8, + 0x1d => events::VirtualKeyCode::Key0, + 0x1e => events::VirtualKeyCode::RBracket, + 0x1f => events::VirtualKeyCode::O, + 0x20 => events::VirtualKeyCode::U, + 0x21 => events::VirtualKeyCode::LBracket, + 0x22 => events::VirtualKeyCode::I, + 0x23 => events::VirtualKeyCode::P, + 0x24 => events::VirtualKeyCode::Return, + 0x25 => events::VirtualKeyCode::L, + 0x26 => events::VirtualKeyCode::J, + 0x27 => events::VirtualKeyCode::Apostrophe, + 0x28 => events::VirtualKeyCode::K, + 0x29 => events::VirtualKeyCode::Semicolon, + 0x2a => events::VirtualKeyCode::Backslash, + 0x2b => events::VirtualKeyCode::Comma, + 0x2c => events::VirtualKeyCode::Slash, + 0x2d => events::VirtualKeyCode::N, + 0x2e => events::VirtualKeyCode::M, + 0x2f => events::VirtualKeyCode::Period, + 0x30 => events::VirtualKeyCode::Tab, + 0x31 => events::VirtualKeyCode::Space, + 0x32 => events::VirtualKeyCode::Grave, + 0x33 => events::VirtualKeyCode::Back, + //0x34 => unkown, + 0x35 => events::VirtualKeyCode::Escape, + 0x36 => events::VirtualKeyCode::RWin, + 0x37 => events::VirtualKeyCode::LWin, + 0x38 => events::VirtualKeyCode::LShift, + //0x39 => Caps lock, + //0x3a => Left alt, + 0x3b => events::VirtualKeyCode::LControl, + 0x3c => events::VirtualKeyCode::RShift, + //0x3d => Right alt, + 0x3e => events::VirtualKeyCode::RControl, + //0x3f => Fn key, + //0x40 => F17 Key, + 0x41 => events::VirtualKeyCode::Decimal, + //0x42 -> unkown, + 0x43 => events::VirtualKeyCode::Multiply, + //0x44 => unkown, + 0x45 => events::VirtualKeyCode::Add, + //0x46 => unkown, + 0x47 => events::VirtualKeyCode::Numlock, + //0x48 => KeypadClear, + 0x49 => events::VirtualKeyCode::VolumeUp, + 0x4a => events::VirtualKeyCode::VolumeDown, + 0x4b => events::VirtualKeyCode::Divide, + 0x4c => events::VirtualKeyCode::NumpadEnter, + //0x4d => unkown, + 0x4e => events::VirtualKeyCode::Subtract, + //0x4f => F18 key, + //0x50 => F19 Key, + 0x51 => events::VirtualKeyCode::NumpadEquals, + 0x52 => events::VirtualKeyCode::Numpad0, + 0x53 => events::VirtualKeyCode::Numpad1, + 0x54 => events::VirtualKeyCode::Numpad2, + 0x55 => events::VirtualKeyCode::Numpad3, + 0x56 => events::VirtualKeyCode::Numpad4, + 0x57 => events::VirtualKeyCode::Numpad5, + 0x58 => events::VirtualKeyCode::Numpad6, + 0x59 => events::VirtualKeyCode::Numpad7, + //0x5a => F20 Key, + 0x5b => events::VirtualKeyCode::Numpad8, + 0x5c => events::VirtualKeyCode::Numpad9, + //0x5d => unkown, + //0x5e => unkown, + //0x5f => unkown, + 0x60 => events::VirtualKeyCode::F5, + 0x61 => events::VirtualKeyCode::F6, + 0x62 => events::VirtualKeyCode::F7, + 0x63 => events::VirtualKeyCode::F3, + 0x64 => events::VirtualKeyCode::F8, + 0x65 => events::VirtualKeyCode::F9, + //0x66 => unkown, + 0x67 => events::VirtualKeyCode::F11, + //0x68 => unkown, + 0x69 => events::VirtualKeyCode::F13, + //0x6a => F16 Key, + 0x6b => events::VirtualKeyCode::F14, + //0x6c => unkown, + 0x6d => events::VirtualKeyCode::F10, + //0x6e => unkown, + 0x6f => events::VirtualKeyCode::F12, + //0x70 => unkown, + 0x71 => events::VirtualKeyCode::F15, + 0x72 => events::VirtualKeyCode::Insert, + 0x73 => events::VirtualKeyCode::Home, + 0x74 => events::VirtualKeyCode::PageUp, + 0x75 => events::VirtualKeyCode::Delete, + 0x76 => events::VirtualKeyCode::F4, + 0x77 => events::VirtualKeyCode::End, + 0x78 => events::VirtualKeyCode::F2, + 0x79 => events::VirtualKeyCode::PageDown, + 0x7a => events::VirtualKeyCode::F1, + 0x7b => events::VirtualKeyCode::Left, + 0x7c => events::VirtualKeyCode::Right, + 0x7d => events::VirtualKeyCode::Down, + 0x7e => events::VirtualKeyCode::Up, + //0x7f => unkown, + + _ => return None, + }) +} + +fn event_mods(event: cocoa::base::id) -> ModifiersState { + let flags = unsafe { + NSEvent::modifierFlags(event) + }; + ModifiersState { + shift: flags.contains(appkit::NSShiftKeyMask), + ctrl: flags.contains(appkit::NSControlKeyMask), + alt: flags.contains(appkit::NSAlternateKeyMask), + logo: flags.contains(appkit::NSCommandKeyMask), + } +} + +// Constant device ID, to be removed when this backend is updated to report real device IDs. +const DEVICE_ID: ::DeviceId = ::DeviceId(DeviceId); diff --git a/src/platform/macos/events_loop/runloop.rs b/src/platform/macos/events_loop/runloop.rs new file mode 100644 index 0000000000..69ed80d259 --- /dev/null +++ b/src/platform/macos/events_loop/runloop.rs @@ -0,0 +1,120 @@ +use std::sync::Weak; +use core_foundation; +use cocoa::{self, foundation}; +use cocoa::appkit::{self, NSApplication, NSApp}; + +use super::{Shared,Timeout}; +use super::nsevent; + +// The Runloop is responsible for: +// - receiving NSEvents from Cocoa +// - forwarding NSEvents back to Cocoa +// - posting Events to the queue +pub struct Runloop { + shared: Weak, + event_state: nsevent::PersistentState, +} + +impl Runloop { + // Create a runloop + pub fn new(shared: Weak) -> Runloop { + Runloop { + shared, + event_state: nsevent::PersistentState::new() + } + } + + // Work the runloop, attempting to respect the timeout + pub fn work(&mut self, timeout: Timeout) { + unsafe { + if !msg_send![cocoa::base::class("NSThread"), isMainThread] { + panic!("Events can only be polled from the main thread on macOS"); + } + } + + let shared = match self.shared.upgrade() { + None => return, // event loop went away + Some(shared) => shared + }; + + loop { + let event = match self.receive_event_from_cocoa(timeout) { + None => { + // Our timeout expired + // Bail out + return; + }, + Some(event) => { + event + } + }; + + // Is this a message type that doesn't need further processing? + if nsevent::should_discard_event_early(&event) { + continue; + } + + // Is this a message type that we should forward back to Cocoa? + if nsevent::should_forward_event(&event) { + self.forward_event_to_cocoa(&event); + } + + // Can we turn it into one or more events? + let events = nsevent::to_events(&event, &mut self.event_state, shared.as_ref()); + let events_len = events.len(); + + // Post them + for event in events { + shared.enqueue_event(event); + } + + // Return if we've accomplished something or if we're out of time + if events_len > 0 || timeout.is_elapsed() { + break; + } + } + } + + // Attempt to wake the Runloop. Must be thread safe. + pub fn wake(&self) { + unsafe { + core_foundation::runloop::CFRunLoopWakeUp(core_foundation::runloop::CFRunLoopGetMain()); + } + } + + fn receive_event_from_cocoa(&mut self, timeout: Timeout) -> Option { + unsafe { + let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); + + // Pick a timeout + let timeout = match timeout { + Timeout::Now => foundation::NSDate::distantPast(cocoa::base::nil), + Timeout::Forever => foundation::NSDate::distantFuture(cocoa::base::nil), + }; + + // Poll for the next event + let ns_event = appkit::NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( + appkit::NSAnyEventMask.bits() | appkit::NSEventMaskPressure.bits(), + timeout, + foundation::NSDefaultRunLoopMode, + cocoa::base::YES); + + // Wrap the event, if any, in a RetainedEvent + let event = if ns_event == cocoa::base::nil { + None + } else { + Some(nsevent::RetainedEvent::new(ns_event)) + }; + + let _: () = msg_send![pool, release]; + + return event + } + } + + fn forward_event_to_cocoa(&mut self, event: &nsevent::RetainedEvent) { + unsafe { + NSApp().sendEvent_(event.id()); + } + } +} \ No newline at end of file diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index 702d1cc215..a383710af1 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -42,11 +42,3 @@ mod monitor; mod window; mod timer; - -#[cfg(not(feature="context"))] -mod send_event; - -#[cfg(feature="context")] -mod send_event_context; -#[cfg(feature="context")] -use self::send_event_context as send_event; diff --git a/src/platform/macos/send_event.rs b/src/platform/macos/send_event.rs deleted file mode 100644 index 9ffa7f06c2..0000000000 --- a/src/platform/macos/send_event.rs +++ /dev/null @@ -1,28 +0,0 @@ -use cocoa; -use cocoa::appkit::{NSApp,NSApplication}; - -pub unsafe fn try_resume(value: usize) -> bool { - false -} - -// The `SendEvent` struct encapsulates the idea of calling [NSApp sendEvent:event]. -// This is a separate struct because, in the case of resize events, dispatching an event can enter -// an internal runloop, and we don't want to get stuck there. -pub struct SendEvent { - event: cocoa::base::id, -} - -impl SendEvent { - pub fn new(event: cocoa::base::id) -> SendEvent { - SendEvent{event: event} - } - - // Attempt to work the send, which either a) consumes the SendEvent, indicating completion, or - // b) returns a SendEvent, indicating there is more work yet to perform. - pub fn work(self) -> Option { - unsafe { - NSApp().sendEvent_(self.event); - } - None - } -} \ No newline at end of file diff --git a/src/platform/macos/send_event_context.rs b/src/platform/macos/send_event_context.rs deleted file mode 100644 index bc9caf59e5..0000000000 --- a/src/platform/macos/send_event_context.rs +++ /dev/null @@ -1,201 +0,0 @@ -use cocoa; -use cocoa::appkit::{NSApp,NSApplication}; -use core_foundation::base::*; -use core_foundation::runloop::*; -use context; -use std::mem; -use std::cell::Cell; -use libc::c_void; - -// Size of the coroutine's stack -const STACK_SIZE: usize = 512 * 1024; - -// The `SendEvent` struct encapsulates the idea of calling [NSApp sendEvent:event]. -// This is a separate struct because, in the case of resize events, dispatching an event can enter -// an internal runloop, and we don't want to get stuck there. -pub struct SendEvent { - stack: context::stack::ProtectedFixedSizeStack, - ctx: context::Context -} - -thread_local!{ - static INNER_CONTEXT: Cell> = Cell::new(None); -} - -unsafe fn resume(value: usize) { - if try_resume(value) == false { - panic!("no coroutine context to resume"); - } -} - -// Attempt to hop back to the "normal" stack frame, yielding `value`. Returns false if we're not -// inside a coroutine. -pub unsafe fn try_resume(value: usize) -> bool { - if let Some(context) = INNER_CONTEXT.with(|c| { c.take() }) { - // resume it, getting a new context - let result = context.resume(value); - - // store the new context and return - INNER_CONTEXT.with(move |c| { - c.set(Some(result.context)); - }); - - true - } else { - // no context - false - } -} - -// A RunLoopObserver corresponds to a CFRunLoopObserver. -struct RunLoopObserver { - id: CFRunLoopObserverRef, -} - -extern "C" fn runloop_observer_callback(_observer: CFRunLoopObserverRef, _activity: CFRunLoopActivity, _info: *mut c_void) { - // we're either about to wait or just finished waiting - // in either case, yield back to the caller, signaling the operation is still in progress - // this is strictly advisory, so don't worry about it if there's nothing o resume - unsafe { - try_resume(1); - } -} - -impl RunLoopObserver { - fn new() -> RunLoopObserver { - // CFRunLoopObserverCreate copies this struct, so we can give it a pointer to this local - let mut context: CFRunLoopObserverContext = unsafe { mem::zeroed() }; - - // Make the runloop observer itself - let id = unsafe { - CFRunLoopObserverCreate( - kCFAllocatorDefault, - kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting, - 1, // repeats - 0, // order - runloop_observer_callback, - &mut context as *mut CFRunLoopObserverContext, - ) - }; - - // Add to event loop - unsafe { - CFRunLoopAddObserver(CFRunLoopGetMain(), id, kCFRunLoopCommonModes); - } - - RunLoopObserver{ - id, - } - } -} - -impl Drop for RunLoopObserver { - fn drop(&mut self) { - unsafe { - CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.id, kCFRunLoopCommonModes); - CFRelease(self.id as _); - } - } -} - -// An instance of this struct is passed from `SendEvent::new()` to `send_event_fn()`. -// Any data that needs to flow that direction should be included here. -struct SendEventInvocation { - event: cocoa::base::id, -} - -impl SendEventInvocation { - // `run()` is called from the SendEvent coroutine. - // - // It should resume t.context with 1 when there is more work to do, or 0 if it is complete. - fn run(self) -> ! { - { - // make a runloop observer for its side effects - let _observer = RunLoopObserver::new(); - - // send the message - unsafe { - NSApp().sendEvent_(self.event); - } - - // drop the runloop observer - } - - // signal completion - unsafe { resume(0); } - - // we should never be resumed after completion - unreachable!(); - } -} - -impl SendEvent { - pub fn new(event: cocoa::base::id) -> SendEvent { - // Set up the invocation struct - let invocation = SendEventInvocation { - event: event, - }; - - // Pack the invocation into an Option<> of itself - let mut invocation: Option = Some(invocation); - - // Make a callback to run from inside the coroutine - extern fn send_event_fn(t: context::Transfer) -> ! { - // t.data is a pointer to the caller's `invocation` Option - let invocation: *mut Option = t.data as _; - - // Move the coroutine context to thread-local storage - INNER_CONTEXT.with(move |c| { - c.set(Some(t.context)); - }); - - // Turn this into a mutable borrow, then move the invocation into the coroutine's stack - let invocation: SendEventInvocation = - unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(invocation) } - .take() - .unwrap(); - - // Yield back to `SendEvent::new()` - unsafe { resume(0); } - - // Run the SendEvent process - invocation.run(); - } - - // Set up a stack - let stack = context::stack::ProtectedFixedSizeStack::new(STACK_SIZE) - .expect("SendEvent stack allocation"); - - // Set up a new context - let result = unsafe { - // Start by calling send_event_fn above - let ctx = context::Context::new(&stack, send_event_fn); - - // Yield to the coroutine, giving it a pointer to the invocation, and wait for it come back - ctx.resume(&mut invocation as *mut Option as usize) - }; - - SendEvent{ - stack: stack, - ctx: result.context, - } - } - - // Attempt to work the send, which either a) consumes the SendEvent, indicating completion, or - // b) returns a SendEvent, indicating there is more work yet to perform. - pub fn work(self) -> Option { - // resume the coroutine - let result = unsafe { self.ctx.resume(0) }; - - if result.data == 0 { - // done - None - } else { - // more work to do - Some(SendEvent{ - stack: self.stack, - ctx: result.context, - }) - } - } -} From b245574b6f2da5ea9f1995ee8423351d5d2d0167 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Mon, 17 Jul 2017 20:40:48 -0500 Subject: [PATCH 11/24] Add a Runloop that runs in a coroutine --- src/platform/macos/events_loop/mod.rs | 52 ++-- .../macos/events_loop/runloop_context.rs | 249 ++++++++++++++++++ src/platform/macos/{ => events_loop}/timer.rs | 0 src/platform/macos/mod.rs | 2 - 4 files changed, 280 insertions(+), 23 deletions(-) create mode 100644 src/platform/macos/events_loop/runloop_context.rs rename src/platform/macos/{ => events_loop}/timer.rs (100%) diff --git a/src/platform/macos/events_loop/mod.rs b/src/platform/macos/events_loop/mod.rs index 1ce4ab621d..018d034a0b 100644 --- a/src/platform/macos/events_loop/mod.rs +++ b/src/platform/macos/events_loop/mod.rs @@ -6,7 +6,15 @@ use super::window::{self, Window}; mod nsevent; +// Simple blocking runloop +#[cfg(not(feature="context"))] mod runloop; + +// Coroutine-based nonblocking runloop +#[cfg(feature="context")] +#[path="runloop_context.rs"] +mod runloop; + use self::runloop::Runloop; pub struct EventsLoop { @@ -22,26 +30,7 @@ pub struct Shared { pub pending_events: Mutex>, } -impl nsevent::WindowFinder for Shared { - fn find_window_by_id(&self, id: window::Id) -> Option> { - for window in self.windows.lock().unwrap().iter() { - if let Some(window) = window.upgrade() { - if window.id() == id { - return Some(window); - } - } - } - - None - } -} - -pub struct Proxy { - shared: Weak, -} - impl Shared { - pub fn new() -> Self { Shared { windows: Mutex::new(Vec::new()), @@ -61,6 +50,11 @@ impl Shared { self.pending_events.lock().unwrap().pop_front() } + // Are there any events pending delivery? + fn has_queued_events(&self) -> bool { + !self.pending_events.lock().unwrap().is_empty() + } + // Removes the window with the given `Id` from the `windows` list. // // This is called when a window is either `Closed` or `Drop`ped. @@ -72,12 +66,24 @@ impl Shared { }); } } - } +impl nsevent::WindowFinder for Shared { + fn find_window_by_id(&self, id: window::Id) -> Option> { + for window in self.windows.lock().unwrap().iter() { + if let Some(window) = window.upgrade() { + if window.id() == id { + return Some(window); + } + } + } + + None + } +} #[derive(Debug,Clone,Copy,Eq,PartialEq)] -enum Timeout { +pub enum Timeout { Now, Forever, } @@ -153,6 +159,10 @@ impl EventsLoop { } } +pub struct Proxy { + shared: Weak, +} + impl Proxy { pub fn wakeup(&self) -> Result<(), EventsLoopClosed> { if let Some(shared) = self.shared.upgrade() { diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs new file mode 100644 index 0000000000..c042dfb4f4 --- /dev/null +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -0,0 +1,249 @@ +use std::sync::Weak; +use std::mem; +use context; +use core_foundation; +use cocoa::{self, foundation}; +use cocoa::appkit::{self, NSApplication, NSApp}; + +use super::{Shared,Timeout}; +use super::nsevent; +use events::Event; + +const STACK_SIZE: usize = 512 * 1024; + +// The Runloop is responsible for: +// - receiving NSEvents from Cocoa +// - forwarding NSEvents back to Cocoa +// - posting Events to the queue +pub struct Runloop { + stack: context::stack::ProtectedFixedSizeStack, + ctx: Option, +} + +impl Runloop { + // Create a runloop + pub fn new(shared: Weak) -> Runloop { + // Create an inner runloop + let mut inner: Option = Some(InnerRunloop::new(shared)); + + // Create a stack for it + let stack = context::stack::ProtectedFixedSizeStack::new(STACK_SIZE) + .expect("Runloop coroutine stack allocation"); + + // Make a callback to run from inside the coroutine which delegates to the + extern fn inner_runloop_entrypoint(t: context::Transfer) -> ! { + // t.data is a pointer to the constructor's `inner` variable + let inner: *mut Option = t.data as _; + + // Turn this into a mutable borrow, then move the inner runloop into the coroutine's stack + let mut inner: InnerRunloop = + unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(inner) } + .take() + .unwrap(); + + // Store the caller's context + inner.caller = Some(t.context); + + // Yield back to `Runloop::new()` so it can return + inner.yield_to_caller(); + + // Run the inner runloop + inner.run_coroutine(); + } + + // Set up a new context + let result = unsafe { + // Start by calling inner_runloop_entrypoint + let ctx = context::Context::new(&stack, inner_runloop_entrypoint); + + // Yield to the coroutine, giving it a pointer to the inner runloop, and wait for it come back + ctx.resume(&mut inner as *mut Option as usize) + }; + + Runloop{ + stack, + ctx: Some(result.context) + } + } + + // Work the runloop, attempting to respect the timeout + pub fn work(&mut self, timeout: Timeout) { + unsafe { + if !msg_send![cocoa::base::class("NSThread"), isMainThread] { + panic!("Events can only be polled from the main thread on macOS"); + } + } + + // Make an Option that contains the timeout + // The coroutine will .take() it as soon as it returns (see InnerRunloop::yield_to_caller()) + let mut timeout: Option = Some(timeout); + + // Resume the coroutine, giving it a pointer to our local timeout + let context = self.ctx.take().expect("coroutine context"); + let result = unsafe { + context.resume(&mut timeout as *mut Option<_> as usize) + }; + + // Store the new coroutine context + self.ctx = Some(result.context); + + assert_eq!(result.data, 1, "expected coroutine runloop to be active"); + + // Return to caller + } + + // Attempt to wake the Runloop. Must be thread safe. + pub fn wake(&self) { + unsafe { + core_foundation::runloop::CFRunLoopWakeUp(core_foundation::runloop::CFRunLoopGetMain()); + } + } +} + +pub struct InnerRunloop { + shared: Weak, + event_state: nsevent::PersistentState, + timeout: Timeout, + shutdown: bool, // should the runloop shut down? + caller: Option, +} + +impl InnerRunloop { + fn new(shared: Weak) -> InnerRunloop { + InnerRunloop{ + shared, + event_state: nsevent::PersistentState::new(), + timeout: Timeout::Now, + shutdown: false, + caller: None, + } + } + + fn yield_to_caller(&mut self) { + if let Some(ctx) = self.caller.take() { + // yield + let t = unsafe { ctx.resume(1) }; + + // t.context is the caller's context + self.caller = Some(t.context); + + // t.data is a pointer to an Option + // take it + let timeout = t.data as *mut Option; + let timeout = + unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(timeout) } + .take() + .unwrap(); + + // store the new timeout + self.timeout = timeout; + } + } + + fn enqueue_event(&mut self, event: Event) { + if let Some(shared) = self.shared.upgrade() { + shared.enqueue_event(event); + self.yield_to_caller(); + } else { + // shared went away + self.shutdown = true; + } + } + + fn run_coroutine(mut self) -> ! { + // run the normal process + self.run(); + + // extract the context + let mut ctx = self.caller.take().expect("run_coroutine() context"); + + // drop the rest + drop(self); + + // keep yielding until they give up + loop { + let t = unsafe { ctx.resume(0) }; + println!("coroutine runloop is terminated but is still getting called"); + ctx = t.context; + } + } + + fn run(&mut self) { + while !self.shutdown { + // upgrade the shared pointer + let shared = match self.shared.upgrade() { + None => return, + Some(shared) => shared + }; + + // try to receive an event + let event = match self.receive_event_from_cocoa() { + None => { + // Our timeout expired + // Yield + self.yield_to_caller(); + + // Retry + continue; + }, + Some(event) => { + event + } + }; + + // Is this a message type that doesn't need further processing? + if nsevent::should_discard_event_early(&event) { + continue; + } + + // Is this a message type that we should forward back to Cocoa? + if nsevent::should_forward_event(&event) { + self.forward_event_to_cocoa(&event); + } + + // Can we turn it into one or more events? + let events = nsevent::to_events(&event, &mut self.event_state, shared.as_ref()); + + // Post them + for event in events { + self.enqueue_event(event); + } + } + } + + fn receive_event_from_cocoa(&mut self) -> Option { + unsafe { + let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); + + // Pick a timeout + let timeout = match self.timeout { + Timeout::Now => foundation::NSDate::distantPast(cocoa::base::nil), + Timeout::Forever => foundation::NSDate::distantFuture(cocoa::base::nil), + }; + + // Poll for the next event + let ns_event = appkit::NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( + appkit::NSAnyEventMask.bits() | appkit::NSEventMaskPressure.bits(), + timeout, + foundation::NSDefaultRunLoopMode, + cocoa::base::YES); + + // Wrap the event, if any, in a RetainedEvent + let event = if ns_event == cocoa::base::nil { + None + } else { + Some(nsevent::RetainedEvent::new(ns_event)) + }; + + let _: () = msg_send![pool, release]; + + return event + } + } + + fn forward_event_to_cocoa(&mut self, event: &nsevent::RetainedEvent) { + unsafe { + NSApp().sendEvent_(event.id()); + } + } +} \ No newline at end of file diff --git a/src/platform/macos/timer.rs b/src/platform/macos/events_loop/timer.rs similarity index 100% rename from src/platform/macos/timer.rs rename to src/platform/macos/events_loop/timer.rs diff --git a/src/platform/macos/mod.rs b/src/platform/macos/mod.rs index a383710af1..dbde5ee94a 100644 --- a/src/platform/macos/mod.rs +++ b/src/platform/macos/mod.rs @@ -40,5 +40,3 @@ impl Window2 { mod events_loop; mod monitor; mod window; - -mod timer; From 84e036fd72f4b967f2792268cb90f4eafdd73e87 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Mon, 17 Jul 2017 20:55:07 -0500 Subject: [PATCH 12/24] Link enqueue_event() to Runloop::wake() --- src/platform/macos/events_loop/mod.rs | 4 +- src/platform/macos/events_loop/runloop.rs | 2 +- .../macos/events_loop/runloop_context.rs | 55 ++++++++++++++----- 3 files changed, 44 insertions(+), 17 deletions(-) diff --git a/src/platform/macos/events_loop/mod.rs b/src/platform/macos/events_loop/mod.rs index 018d034a0b..24741d26d6 100644 --- a/src/platform/macos/events_loop/mod.rs +++ b/src/platform/macos/events_loop/mod.rs @@ -40,9 +40,11 @@ impl Shared { // Enqueues the event for prompt delivery to the application. pub fn enqueue_event(&self, event: Event) { + // Store the event self.pending_events.lock().unwrap().push_back(event); - // TODO: wake the runloop + // Attempt to wake the runloop + Runloop::wake(); } // Dequeues the first event, if any, from the queue. diff --git a/src/platform/macos/events_loop/runloop.rs b/src/platform/macos/events_loop/runloop.rs index 69ed80d259..9339af4fd6 100644 --- a/src/platform/macos/events_loop/runloop.rs +++ b/src/platform/macos/events_loop/runloop.rs @@ -76,7 +76,7 @@ impl Runloop { } // Attempt to wake the Runloop. Must be thread safe. - pub fn wake(&self) { + pub fn wake() { unsafe { core_foundation::runloop::CFRunLoopWakeUp(core_foundation::runloop::CFRunLoopGetMain()); } diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs index c042dfb4f4..9d7fc83651 100644 --- a/src/platform/macos/events_loop/runloop_context.rs +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -1,5 +1,6 @@ -use std::sync::Weak; +use std::cell::Cell; use std::mem; +use std::sync::Weak; use context; use core_foundation; use cocoa::{self, foundation}; @@ -93,13 +94,37 @@ impl Runloop { } // Attempt to wake the Runloop. Must be thread safe. - pub fn wake(&self) { - unsafe { - core_foundation::runloop::CFRunLoopWakeUp(core_foundation::runloop::CFRunLoopGetMain()); + pub fn wake() { + // Try to context switch back to the main thread + if yield_to_caller() { + // We did! + } else { + unsafe { + core_foundation::runloop::CFRunLoopWakeUp(core_foundation::runloop::CFRunLoopGetMain()); + } } } } +thread_local!{ + // A pointer to the InnerRunloop, if we are presently inside the InnerRunloop coroutine + static INSIDE_INNER_RUNLOOP: Cell> = Cell::new(None); +} + +// If we're inside the InnerRunloop, call InnerRunloop::yield_to_caller() and return true; +// if we're outside, do nothing and return false +fn yield_to_caller() -> bool { + INSIDE_INNER_RUNLOOP.with(|runloop| { + if let Some(runloop) = runloop.get() { + let runloop: &mut InnerRunloop = unsafe { mem::transmute(runloop) }; + runloop.yield_to_caller(); + true + } else { + false + } + }) +} + pub struct InnerRunloop { shared: Weak, event_state: nsevent::PersistentState, @@ -121,6 +146,11 @@ impl InnerRunloop { fn yield_to_caller(&mut self) { if let Some(ctx) = self.caller.take() { + // clear INSIDE_INNER_RUNLOOP, since we're leaving + INSIDE_INNER_RUNLOOP.with(|runloop| { + runloop.set(None); + }); + // yield let t = unsafe { ctx.resume(1) }; @@ -137,16 +167,11 @@ impl InnerRunloop { // store the new timeout self.timeout = timeout; - } - } - fn enqueue_event(&mut self, event: Event) { - if let Some(shared) = self.shared.upgrade() { - shared.enqueue_event(event); - self.yield_to_caller(); - } else { - // shared went away - self.shutdown = true; + // set INSIDE_INNER_RUNLOOP, since we're entering + INSIDE_INNER_RUNLOOP.with(|runloop| { + runloop.set(Some(self as *mut InnerRunloop)); + }); } } @@ -169,7 +194,7 @@ impl InnerRunloop { } fn run(&mut self) { - while !self.shutdown { + loop { // upgrade the shared pointer let shared = match self.shared.upgrade() { None => return, @@ -206,7 +231,7 @@ impl InnerRunloop { // Post them for event in events { - self.enqueue_event(event); + shared.enqueue_event(event); } } } From 9b975676291cb72fe5d4a356e1d5f76ef1a4480c Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Mon, 17 Jul 2017 21:11:55 -0500 Subject: [PATCH 13/24] Move receive_event_from_cocoa()/forward_event_to_cocoa() to nsevent --- src/platform/macos/events_loop/nsevent.rs | 38 +++++++++++++++++- src/platform/macos/events_loop/runloop.rs | 4 +- .../macos/events_loop/runloop_context.rs | 40 +------------------ 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/platform/macos/events_loop/nsevent.rs b/src/platform/macos/events_loop/nsevent.rs index 1c94e3e0e5..74f23b7e18 100644 --- a/src/platform/macos/events_loop/nsevent.rs +++ b/src/platform/macos/events_loop/nsevent.rs @@ -5,7 +5,7 @@ use cocoa::appkit::{self, NSApplication, NSApp, NSEvent, NSView, NSWindow}; use cocoa::foundation; use core_foundation::base::{CFRetain,CFRelease,CFTypeRef}; -use super::EventsLoop; +use super::{EventsLoop, Timeout}; use super::super::DeviceId; use super::super::window::{self, Window}; use events::{self, ElementState, Event, MouseButton, TouchPhase, DeviceEvent, WindowEvent, ModifiersState, KeyboardInput}; @@ -112,6 +112,42 @@ impl Modifiers { } } +pub fn receive_event_from_cocoa(timeout: Timeout) -> Option { + unsafe { + let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); + + // Pick a timeout + let timeout = match timeout { + Timeout::Now => foundation::NSDate::distantPast(cocoa::base::nil), + Timeout::Forever => foundation::NSDate::distantFuture(cocoa::base::nil), + }; + + // Poll for the next event + let ns_event = appkit::NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( + appkit::NSAnyEventMask.bits() | appkit::NSEventMaskPressure.bits(), + timeout, + foundation::NSDefaultRunLoopMode, + cocoa::base::YES); + + // Wrap the event, if any, in a RetainedEvent + let event = if ns_event == cocoa::base::nil { + None + } else { + Some(RetainedEvent::new(ns_event)) + }; + + let _: () = msg_send![pool, release]; + + return event + } +} + +pub fn forward_event_to_cocoa(event: &RetainedEvent) { + unsafe { + NSApp().sendEvent_(event.id()); + } +} + // Attempt to translate an `NSEvent` into zero or more `Event`s. pub fn to_events(event: &RetainedEvent, state: &mut PersistentState, window_finder: &WF) -> Vec where WF: WindowFinder diff --git a/src/platform/macos/events_loop/runloop.rs b/src/platform/macos/events_loop/runloop.rs index 9339af4fd6..1930bc1f40 100644 --- a/src/platform/macos/events_loop/runloop.rs +++ b/src/platform/macos/events_loop/runloop.rs @@ -38,7 +38,7 @@ impl Runloop { }; loop { - let event = match self.receive_event_from_cocoa(timeout) { + let event = match nsevent::receive_event_from_cocoa(timeout) { None => { // Our timeout expired // Bail out @@ -56,7 +56,7 @@ impl Runloop { // Is this a message type that we should forward back to Cocoa? if nsevent::should_forward_event(&event) { - self.forward_event_to_cocoa(&event); + nsevent::forward_event_to_cocoa(&event); } // Can we turn it into one or more events? diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs index 9d7fc83651..f7f7f94fd0 100644 --- a/src/platform/macos/events_loop/runloop_context.rs +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -202,7 +202,7 @@ impl InnerRunloop { }; // try to receive an event - let event = match self.receive_event_from_cocoa() { + let event = match nsevent::receive_event_from_cocoa(self.timeout) { None => { // Our timeout expired // Yield @@ -223,7 +223,7 @@ impl InnerRunloop { // Is this a message type that we should forward back to Cocoa? if nsevent::should_forward_event(&event) { - self.forward_event_to_cocoa(&event); + nsevent::forward_event_to_cocoa(&event); } // Can we turn it into one or more events? @@ -235,40 +235,4 @@ impl InnerRunloop { } } } - - fn receive_event_from_cocoa(&mut self) -> Option { - unsafe { - let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); - - // Pick a timeout - let timeout = match self.timeout { - Timeout::Now => foundation::NSDate::distantPast(cocoa::base::nil), - Timeout::Forever => foundation::NSDate::distantFuture(cocoa::base::nil), - }; - - // Poll for the next event - let ns_event = appkit::NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( - appkit::NSAnyEventMask.bits() | appkit::NSEventMaskPressure.bits(), - timeout, - foundation::NSDefaultRunLoopMode, - cocoa::base::YES); - - // Wrap the event, if any, in a RetainedEvent - let event = if ns_event == cocoa::base::nil { - None - } else { - Some(nsevent::RetainedEvent::new(ns_event)) - }; - - let _: () = msg_send![pool, release]; - - return event - } - } - - fn forward_event_to_cocoa(&mut self, event: &nsevent::RetainedEvent) { - unsafe { - NSApp().sendEvent_(event.id()); - } - } } \ No newline at end of file From f777fa3af6e1bb4bc2d5cbbc438518759b5809ea Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Mon, 17 Jul 2017 21:49:19 -0500 Subject: [PATCH 14/24] Hook up a runloop observer and a timer for the runloop coroutine This puts an upper bound on how long the coroutine can execute without yielding back to the main thread. The runloop observer fires before going to sleep, which is useful if `forward_event_to_cocoa()` has triggered an inner event loop. The timer could be used to wake the event loop from a different thread, but in practice that has a few milliseconds of latency, and it also requires finding a reference to the timer from `Runloop::wake()`. Instead, the timer is configured to fire every few milliseconds all the time, again in the pursuit of lower latency. --- src/platform/macos/events_loop/mod.rs | 1 + .../macos/events_loop/runloop_context.rs | 63 ++++++++++++++++++- src/platform/macos/events_loop/timer.rs | 27 ++++---- 3 files changed, 78 insertions(+), 13 deletions(-) diff --git a/src/platform/macos/events_loop/mod.rs b/src/platform/macos/events_loop/mod.rs index 24741d26d6..b934d94b46 100644 --- a/src/platform/macos/events_loop/mod.rs +++ b/src/platform/macos/events_loop/mod.rs @@ -5,6 +5,7 @@ use std::sync::{Arc, Mutex, Weak}; use super::window::{self, Window}; mod nsevent; +mod timer; // Simple blocking runloop #[cfg(not(feature="context"))] diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs index f7f7f94fd0..f9c10fc9de 100644 --- a/src/platform/macos/events_loop/runloop_context.rs +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -3,11 +3,15 @@ use std::mem; use std::sync::Weak; use context; use core_foundation; +use core_foundation::base::*; +use core_foundation::runloop::*; use cocoa::{self, foundation}; use cocoa::appkit::{self, NSApplication, NSApp}; +use libc::c_void; use super::{Shared,Timeout}; use super::nsevent; +use super::timer::Timer; use events::Event; const STACK_SIZE: usize = 512 * 1024; @@ -19,6 +23,12 @@ const STACK_SIZE: usize = 512 * 1024; pub struct Runloop { stack: context::stack::ProtectedFixedSizeStack, ctx: Option, + + // Hang onto a timer that goes off every few milliseconds + _timer: Timer, + + // Hang onto a runloop observer + _observer: RunloopObserver, } impl Runloop { @@ -63,7 +73,9 @@ impl Runloop { Runloop{ stack, - ctx: Some(result.context) + ctx: Some(result.context), + _timer: Timer::new(0.005), + _observer: RunloopObserver::new(), } } @@ -99,6 +111,7 @@ impl Runloop { if yield_to_caller() { // We did! } else { + // Wake the runloop, because... we can, I guess? unsafe { core_foundation::runloop::CFRunLoopWakeUp(core_foundation::runloop::CFRunLoopGetMain()); } @@ -235,4 +248,50 @@ impl InnerRunloop { } } } -} \ No newline at end of file +} + +// A RunloopObserver corresponds to a CFRunLoopObserver. +struct RunloopObserver { + id: CFRunLoopObserverRef, +} + +extern "C" fn runloop_observer_callback(_observer: CFRunLoopObserverRef, _activity: CFRunLoopActivity, _info: *mut c_void) { + yield_to_caller(); +} + +impl RunloopObserver { + fn new() -> RunloopObserver { + // CFRunLoopObserverCreate copies this struct, so we can give it a pointer to this local + let mut context: CFRunLoopObserverContext = unsafe { mem::zeroed() }; + + // Make the runloop observer itself + let id = unsafe { + CFRunLoopObserverCreate( + kCFAllocatorDefault, + kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting, + 1, // repeats + 0, // order + runloop_observer_callback, + &mut context as *mut CFRunLoopObserverContext, + ) + }; + + // Add to event loop + unsafe { + CFRunLoopAddObserver(CFRunLoopGetMain(), id, kCFRunLoopCommonModes); + } + + RunloopObserver { + id, + } + } +} + +impl Drop for RunloopObserver { + fn drop(&mut self) { + unsafe { + CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.id, kCFRunLoopCommonModes); + CFRelease(self.id as _); + } + } +} diff --git a/src/platform/macos/events_loop/timer.rs b/src/platform/macos/events_loop/timer.rs index 530fb7cffc..feacf636c5 100644 --- a/src/platform/macos/events_loop/timer.rs +++ b/src/platform/macos/events_loop/timer.rs @@ -4,26 +4,31 @@ use core_foundation::base::*; use core_foundation::runloop::*; use core_foundation::date::*; +use super::runloop::Runloop; + // Encapsulates a CFRunLoopTimer that has a far-future time to fire, but which can be triggered // across threads for the purpose of waking up an event loop. pub struct Timer { timer: CFRunLoopTimerRef, } +#[cfg(feature="context")] extern "C" fn timer_callback(_timer: CFRunLoopTimerRef, _info: *mut c_void) { - // attempt to yield back to the caller - unsafe { - super::send_event::try_resume(1); - } + // wake the runloop, which here means "try to context switch out of the event loop coroutine" + Runloop::wake(); } +#[cfg(not(feature="context"))] +extern "C" fn timer_callback(_timer: CFRunLoopTimerRef, _info: *mut c_void) { + // can't really accomplish anything +} impl Timer { - pub fn new() -> Timer { + pub fn new(interval_seconds: f64) -> Timer { // default to firing every year, starting one year in the future - let one_year: CFTimeInterval = 86400f64 * 365f64; + let interval: CFTimeInterval = interval_seconds; let now = unsafe { CFAbsoluteTimeGetCurrent() }; - let one_year_from_now = now + one_year; + let next_interval = now + interval; let mut context: CFRunLoopTimerContext = unsafe { mem::zeroed() }; @@ -31,10 +36,10 @@ impl Timer { let timer = unsafe { CFRunLoopTimerCreate( kCFAllocatorDefault, - one_year_from_now, // fireDate - one_year, // interval - 0, // flags - 0, // order + now + interval, // fireDate + interval, // interval + 0, // flags + 0, // order timer_callback, &mut context as *mut CFRunLoopTimerContext, ) From 7d02e3d781d4e4e5bd9154cb94579ef200401b8b Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Tue, 18 Jul 2017 09:15:30 -0500 Subject: [PATCH 15/24] Minor refactorings to better separate concerns --- src/platform/macos/events_loop/nsevent.rs | 4 +- .../macos/events_loop/runloop_context.rs | 170 ++++++++---------- 2 files changed, 77 insertions(+), 97 deletions(-) diff --git a/src/platform/macos/events_loop/nsevent.rs b/src/platform/macos/events_loop/nsevent.rs index 74f23b7e18..9e26530a28 100644 --- a/src/platform/macos/events_loop/nsevent.rs +++ b/src/platform/macos/events_loop/nsevent.rs @@ -1,11 +1,11 @@ use std; -use std::sync::{Arc,Weak}; +use std::sync::Arc; use cocoa; use cocoa::appkit::{self, NSApplication, NSApp, NSEvent, NSView, NSWindow}; use cocoa::foundation; use core_foundation::base::{CFRetain,CFRelease,CFTypeRef}; -use super::{EventsLoop, Timeout}; +use super::Timeout; use super::super::DeviceId; use super::super::window::{self, Window}; use events::{self, ElementState, Event, MouseButton, TouchPhase, DeviceEvent, WindowEvent, ModifiersState, KeyboardInput}; diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs index f9c10fc9de..20213f3431 100644 --- a/src/platform/macos/events_loop/runloop_context.rs +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -12,7 +12,6 @@ use libc::c_void; use super::{Shared,Timeout}; use super::nsevent; use super::timer::Timer; -use events::Event; const STACK_SIZE: usize = 512 * 1024; @@ -21,7 +20,7 @@ const STACK_SIZE: usize = 512 * 1024; // - forwarding NSEvents back to Cocoa // - posting Events to the queue pub struct Runloop { - stack: context::stack::ProtectedFixedSizeStack, + _stack: context::stack::ProtectedFixedSizeStack, ctx: Option, // Hang onto a timer that goes off every few milliseconds @@ -41,27 +40,6 @@ impl Runloop { let stack = context::stack::ProtectedFixedSizeStack::new(STACK_SIZE) .expect("Runloop coroutine stack allocation"); - // Make a callback to run from inside the coroutine which delegates to the - extern fn inner_runloop_entrypoint(t: context::Transfer) -> ! { - // t.data is a pointer to the constructor's `inner` variable - let inner: *mut Option = t.data as _; - - // Turn this into a mutable borrow, then move the inner runloop into the coroutine's stack - let mut inner: InnerRunloop = - unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(inner) } - .take() - .unwrap(); - - // Store the caller's context - inner.caller = Some(t.context); - - // Yield back to `Runloop::new()` so it can return - inner.yield_to_caller(); - - // Run the inner runloop - inner.run_coroutine(); - } - // Set up a new context let result = unsafe { // Start by calling inner_runloop_entrypoint @@ -72,7 +50,7 @@ impl Runloop { }; Runloop{ - stack, + _stack: stack, ctx: Some(result.context), _timer: Timer::new(0.005), _observer: RunloopObserver::new(), @@ -100,8 +78,6 @@ impl Runloop { // Store the new coroutine context self.ctx = Some(result.context); - assert_eq!(result.data, 1, "expected coroutine runloop to be active"); - // Return to caller } @@ -120,30 +96,86 @@ impl Runloop { } thread_local!{ - // A pointer to the InnerRunloop, if we are presently inside the InnerRunloop coroutine - static INSIDE_INNER_RUNLOOP: Cell> = Cell::new(None); + // If we are inside the inner runloop, this contains the caller's context and their Timeout + static INSIDE_INNER_RUNLOOP_CONTEXT: Cell> = Cell::new(None); + static INSIDE_INNER_RUNLOOP_TIMEOUT: Cell> = Cell::new(None); +} + +// This is the first function called from inside the coroutine. It must not return. +// Contract: t.data is a *mut Option. +extern fn inner_runloop_entrypoint(t: context::Transfer) -> ! { + let inner: *mut Option = t.data as _; + + // Turn this into a mutable borrow, then move the inner runloop into the coroutine's stack + let mut inner: InnerRunloop = + unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(inner) } + .take() + .unwrap(); + + // Store the caller's context in the usual place + let context = Some(t.context); + INSIDE_INNER_RUNLOOP_CONTEXT.with(move |ctx| { ctx.set(context) }); + + // Yield back to `Runloop::new()` so it can return + // Our next execution -- and all subsequent executions -- will happen inside `Runloop::work()`. + yield_to_caller(); + + // Run the inner runloop + inner.run(); + + // Drop it + drop(inner); + + // Yield forever + loop { + yield_to_caller(); + } } -// If we're inside the InnerRunloop, call InnerRunloop::yield_to_caller() and return true; + +// If we're inside the InnerRunloop, return the current Timeout. +fn current_timeout() -> Option { + INSIDE_INNER_RUNLOOP_TIMEOUT.with(|timeout| { + timeout.get() + }) +} + +// If we're inside the InnerRunloop, context switch and return true; // if we're outside, do nothing and return false fn yield_to_caller() -> bool { - INSIDE_INNER_RUNLOOP.with(|runloop| { - if let Some(runloop) = runloop.get() { - let runloop: &mut InnerRunloop = unsafe { mem::transmute(runloop) }; - runloop.yield_to_caller(); - true - } else { - false - } - }) + // See if we we're inside the inner runloop + // If we are in the inner runloop, take the context since we're leaving + if let Some(context) = INSIDE_INNER_RUNLOOP_CONTEXT.with(|context_cell| { context_cell.take() }) { + // Yield + let t = unsafe { context.resume(0) }; + // We're returned + + // t.context is the caller's context + let context = Some(t.context); + // t.data is a pointer to an Option + // take() it + let timeout: *mut Option = t.data as *mut Option<_>; + let timeout: Option = + unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(timeout) } + .take(); + + // Store the new values in the thread local cells until we yield back + INSIDE_INNER_RUNLOOP_CONTEXT.with(move |context_cell| { + context_cell.set(context); + }); + INSIDE_INNER_RUNLOOP_TIMEOUT.with(move |timeout_cell| { + timeout_cell.set(timeout); + }); + + true + } else { + false + } } pub struct InnerRunloop { shared: Weak, event_state: nsevent::PersistentState, - timeout: Timeout, - shutdown: bool, // should the runloop shut down? - caller: Option, } impl InnerRunloop { @@ -151,58 +183,6 @@ impl InnerRunloop { InnerRunloop{ shared, event_state: nsevent::PersistentState::new(), - timeout: Timeout::Now, - shutdown: false, - caller: None, - } - } - - fn yield_to_caller(&mut self) { - if let Some(ctx) = self.caller.take() { - // clear INSIDE_INNER_RUNLOOP, since we're leaving - INSIDE_INNER_RUNLOOP.with(|runloop| { - runloop.set(None); - }); - - // yield - let t = unsafe { ctx.resume(1) }; - - // t.context is the caller's context - self.caller = Some(t.context); - - // t.data is a pointer to an Option - // take it - let timeout = t.data as *mut Option; - let timeout = - unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(timeout) } - .take() - .unwrap(); - - // store the new timeout - self.timeout = timeout; - - // set INSIDE_INNER_RUNLOOP, since we're entering - INSIDE_INNER_RUNLOOP.with(|runloop| { - runloop.set(Some(self as *mut InnerRunloop)); - }); - } - } - - fn run_coroutine(mut self) -> ! { - // run the normal process - self.run(); - - // extract the context - let mut ctx = self.caller.take().expect("run_coroutine() context"); - - // drop the rest - drop(self); - - // keep yielding until they give up - loop { - let t = unsafe { ctx.resume(0) }; - println!("coroutine runloop is terminated but is still getting called"); - ctx = t.context; } } @@ -215,11 +195,11 @@ impl InnerRunloop { }; // try to receive an event - let event = match nsevent::receive_event_from_cocoa(self.timeout) { + let event = match nsevent::receive_event_from_cocoa(current_timeout().unwrap_or(Timeout::Now)) { None => { // Our timeout expired // Yield - self.yield_to_caller(); + yield_to_caller(); // Retry continue; From 62533689ddfa8e424a02d1df512e2d0a247852f6 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Tue, 18 Jul 2017 16:56:53 -0500 Subject: [PATCH 16/24] Ask Travis to test --features context too --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 9fe675317d..71b71d2875 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,7 @@ script: - cargo build --verbose - if [ $TRAVIS_OS_NAME = osx ]; then cargo build --target x86_64-apple-ios --verbose; fi - cargo test --verbose + - if [ $TRAVIS_OS_NAME = osx ]; then cargo test --features context --verbose; fi os: - linux From cf4c6d6903ed36b437eef2156ab3e15d7d1b879b Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sat, 22 Jul 2017 22:06:24 -0500 Subject: [PATCH 17/24] The blocking runloop should wake itself by posting an event Posting an event causes receive_event_from_cocoa() to return, which gives the runloop an opportunity to check Shared::has_queued_events(). This ensures that wakeup events can be delivered promptly in the absence of other event traffic. --- src/platform/macos/events_loop/nsevent.rs | 24 +++++++++++++++++++++++ src/platform/macos/events_loop/runloop.rs | 7 +++++++ 2 files changed, 31 insertions(+) diff --git a/src/platform/macos/events_loop/nsevent.rs b/src/platform/macos/events_loop/nsevent.rs index 9e26530a28..80264d82a3 100644 --- a/src/platform/macos/events_loop/nsevent.rs +++ b/src/platform/macos/events_loop/nsevent.rs @@ -112,6 +112,30 @@ impl Modifiers { } } +// Send an application-defined event to ourselves. May be called from any thread. +// +// The event itself corresponds to no `Event`, but it does cause `receive_event_from_cocoa()` to +// return to its caller. +pub fn post_event_to_self() { + unsafe { + let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); + let event = + NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2_( + cocoa::base::nil, + appkit::NSApplicationDefined, + foundation::NSPoint::new(0.0, 0.0), + appkit::NSEventModifierFlags::empty(), + 0.0, + 0, + cocoa::base::nil, + appkit::NSEventSubtype::NSApplicationActivatedEventType, + 0, + 0); + appkit::NSApp().postEvent_atStart_(event, cocoa::base::NO); + foundation::NSAutoreleasePool::drain(pool); + } +} + pub fn receive_event_from_cocoa(timeout: Timeout) -> Option { unsafe { let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); diff --git a/src/platform/macos/events_loop/runloop.rs b/src/platform/macos/events_loop/runloop.rs index 1930bc1f40..0a20e530a8 100644 --- a/src/platform/macos/events_loop/runloop.rs +++ b/src/platform/macos/events_loop/runloop.rs @@ -38,6 +38,11 @@ impl Runloop { }; loop { + // Return if there's already an event waiting + if shared.has_queued_events() { + return; + } + let event = match nsevent::receive_event_from_cocoa(timeout) { None => { // Our timeout expired @@ -77,6 +82,8 @@ impl Runloop { // Attempt to wake the Runloop. Must be thread safe. pub fn wake() { + nsevent::post_event_to_self(); + unsafe { core_foundation::runloop::CFRunLoopWakeUp(core_foundation::runloop::CFRunLoopGetMain()); } From 9c4b2f884a978f46ff8685ab67142e2c94450cae Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sat, 22 Jul 2017 22:10:43 -0500 Subject: [PATCH 18/24] Clean up unused functions --- src/platform/macos/events_loop/runloop.rs | 36 ----------------------- 1 file changed, 36 deletions(-) diff --git a/src/platform/macos/events_loop/runloop.rs b/src/platform/macos/events_loop/runloop.rs index 0a20e530a8..2fd9e66f50 100644 --- a/src/platform/macos/events_loop/runloop.rs +++ b/src/platform/macos/events_loop/runloop.rs @@ -88,40 +88,4 @@ impl Runloop { core_foundation::runloop::CFRunLoopWakeUp(core_foundation::runloop::CFRunLoopGetMain()); } } - - fn receive_event_from_cocoa(&mut self, timeout: Timeout) -> Option { - unsafe { - let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); - - // Pick a timeout - let timeout = match timeout { - Timeout::Now => foundation::NSDate::distantPast(cocoa::base::nil), - Timeout::Forever => foundation::NSDate::distantFuture(cocoa::base::nil), - }; - - // Poll for the next event - let ns_event = appkit::NSApp().nextEventMatchingMask_untilDate_inMode_dequeue_( - appkit::NSAnyEventMask.bits() | appkit::NSEventMaskPressure.bits(), - timeout, - foundation::NSDefaultRunLoopMode, - cocoa::base::YES); - - // Wrap the event, if any, in a RetainedEvent - let event = if ns_event == cocoa::base::nil { - None - } else { - Some(nsevent::RetainedEvent::new(ns_event)) - }; - - let _: () = msg_send![pool, release]; - - return event - } - } - - fn forward_event_to_cocoa(&mut self, event: &nsevent::RetainedEvent) { - unsafe { - NSApp().sendEvent_(event.id()); - } - } } \ No newline at end of file From 90559d60f0b9e59636878f9d306b29bee0072294 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sat, 22 Jul 2017 22:42:56 -0500 Subject: [PATCH 19/24] Add wakeup latency tool --- examples/wakeup_latency.rs | 102 +++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 examples/wakeup_latency.rs diff --git a/examples/wakeup_latency.rs b/examples/wakeup_latency.rs new file mode 100644 index 0000000000..f8131cce4f --- /dev/null +++ b/examples/wakeup_latency.rs @@ -0,0 +1,102 @@ +extern crate winit; + +use std::thread; +use std::time::{Duration,Instant}; +use std::sync::mpsc; +use std::collections::VecDeque; + +enum Action { + WakeupSent(Instant), + AwakenedReceived(Instant), +} + +fn calculate_latency(rx: mpsc::Receiver) { + thread::spawn(move || { + let mut wakeups_sent: VecDeque = VecDeque::new(); + let mut awakeneds_received: VecDeque = VecDeque::new(); + + let mut latency_history: Vec = Vec::with_capacity(1000); + + println!("wakeup() -> Event::Awakened latency (all times in µs)"); + println!("mean\tmax\t99%\t95%\t50%\t5%\t1%\tmin"); + + while let Ok(action) = rx.recv() { + match action { + Action::WakeupSent(instant) => wakeups_sent.push_back(instant), + Action::AwakenedReceived(instant) => awakeneds_received.push_back(instant), + } + + while wakeups_sent.len() > 0 && awakeneds_received.len() > 0 { + let sent = wakeups_sent.pop_front().unwrap(); + let recvd = awakeneds_received.pop_front().unwrap(); + if recvd > sent { + let latency = recvd.duration_since(sent); + let latency_us = latency.as_secs() * 1_000_000 + + (latency.subsec_nanos() / 1_000) as u64; + latency_history.push(latency_us); + } + } + + if latency_history.len() > 300 { + latency_history.sort(); + + { + let mean = latency_history.iter() + .fold(0u64, |acc,&u| acc + u) / latency_history.len() as u64; + let max = latency_history.last().unwrap(); + let pct99 = latency_history.get(latency_history.len() * 99 / 100).unwrap(); + let pct95 = latency_history.get(latency_history.len() * 95 / 100).unwrap(); + let pct50 = latency_history.get(latency_history.len() * 50 / 100).unwrap(); + let pct5 = latency_history.get(latency_history.len() * 5 / 100).unwrap(); + let pct1 = latency_history.get(latency_history.len() * 1 / 100).unwrap(); + let min = latency_history.first().unwrap(); + println!("{}\t{}\t{}\t{}\t{}\t{}\t{}\t{}", mean, max, pct99, pct95, pct50, pct5, pct1, min); + } + + latency_history.clear(); + } + } + }); +} + +fn send_wakeups(tx: mpsc::Sender, proxy: winit::EventsLoopProxy) { + thread::spawn(move || { + loop { + let sent_at = Instant::now(); + proxy.wakeup().expect("wakeup"); + tx.send(Action::WakeupSent(sent_at)).unwrap(); + + thread::sleep(Duration::from_secs(1) / 60); + } + }); +} + +fn main() { + let mut events_loop = winit::EventsLoop::new(); + + let _window = winit::WindowBuilder::new() + .with_title("A fantastic window!") + .build(&events_loop) + .unwrap(); + + let (tx,rx) = mpsc::channel::(); + + calculate_latency(rx); + send_wakeups(tx.clone(), events_loop.create_proxy()); + + events_loop.run_forever(|event| { + match event { + winit::Event::Awakened { .. } => { + // got awakened + tx.send(Action::AwakenedReceived(Instant::now())).unwrap(); + + winit::ControlFlow::Continue + } + + winit::Event::WindowEvent { event: winit::WindowEvent::Closed, .. } => { + winit::ControlFlow::Break + }, + _ => winit::ControlFlow::Continue, + } + }); +} From 6b0cd654bbdba8d5f196c0a6d95a71f1f789fd87 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Sun, 23 Jul 2017 11:25:03 -0500 Subject: [PATCH 20/24] Remove dead code and mark conditionally-dead code --- src/platform/macos/events_loop/mod.rs | 6 +++++- src/platform/macos/events_loop/nsevent.rs | 4 +--- src/platform/macos/events_loop/runloop.rs | 3 +-- src/platform/macos/events_loop/runloop_context.rs | 3 +-- src/platform/macos/events_loop/timer.rs | 8 -------- 5 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/platform/macos/events_loop/mod.rs b/src/platform/macos/events_loop/mod.rs index b934d94b46..64c9dbea77 100644 --- a/src/platform/macos/events_loop/mod.rs +++ b/src/platform/macos/events_loop/mod.rs @@ -5,7 +5,6 @@ use std::sync::{Arc, Mutex, Weak}; use super::window::{self, Window}; mod nsevent; -mod timer; // Simple blocking runloop #[cfg(not(feature="context"))] @@ -16,6 +15,10 @@ mod runloop; #[path="runloop_context.rs"] mod runloop; +// NSTimer wrapper, needed only for coroutine-baesd runloop +#[cfg(feature="context")] +mod timer; + use self::runloop::Runloop; pub struct EventsLoop { @@ -54,6 +57,7 @@ impl Shared { } // Are there any events pending delivery? + #[allow(dead_code)] fn has_queued_events(&self) -> bool { !self.pending_events.lock().unwrap().is_empty() } diff --git a/src/platform/macos/events_loop/nsevent.rs b/src/platform/macos/events_loop/nsevent.rs index 80264d82a3..5bf68dc781 100644 --- a/src/platform/macos/events_loop/nsevent.rs +++ b/src/platform/macos/events_loop/nsevent.rs @@ -18,9 +18,6 @@ impl RetainedEvent { unsafe { CFRetain(event as CFTypeRef); } RetainedEvent(event) } - pub fn into_inner(self) -> cocoa::base::id { - self.0 - } pub fn id(&self) -> cocoa::base::id { self.0 } @@ -116,6 +113,7 @@ impl Modifiers { // // The event itself corresponds to no `Event`, but it does cause `receive_event_from_cocoa()` to // return to its caller. +#[allow(dead_code)] pub fn post_event_to_self() { unsafe { let pool = foundation::NSAutoreleasePool::new(cocoa::base::nil); diff --git a/src/platform/macos/events_loop/runloop.rs b/src/platform/macos/events_loop/runloop.rs index 2fd9e66f50..a746cbd632 100644 --- a/src/platform/macos/events_loop/runloop.rs +++ b/src/platform/macos/events_loop/runloop.rs @@ -1,7 +1,6 @@ use std::sync::Weak; use core_foundation; -use cocoa::{self, foundation}; -use cocoa::appkit::{self, NSApplication, NSApp}; +use cocoa; use super::{Shared,Timeout}; use super::nsevent; diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs index 20213f3431..9a9265ab36 100644 --- a/src/platform/macos/events_loop/runloop_context.rs +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -5,8 +5,7 @@ use context; use core_foundation; use core_foundation::base::*; use core_foundation::runloop::*; -use cocoa::{self, foundation}; -use cocoa::appkit::{self, NSApplication, NSApp}; +use cocoa; use libc::c_void; use super::{Shared,Timeout}; diff --git a/src/platform/macos/events_loop/timer.rs b/src/platform/macos/events_loop/timer.rs index feacf636c5..c46c6232e9 100644 --- a/src/platform/macos/events_loop/timer.rs +++ b/src/platform/macos/events_loop/timer.rs @@ -28,7 +28,6 @@ impl Timer { // default to firing every year, starting one year in the future let interval: CFTimeInterval = interval_seconds; let now = unsafe { CFAbsoluteTimeGetCurrent() }; - let next_interval = now + interval; let mut context: CFRunLoopTimerContext = unsafe { mem::zeroed() }; @@ -54,13 +53,6 @@ impl Timer { timer } } - - // Cause the timer to fire ASAP. Can be called across threads. - pub fn trigger(&self) { - unsafe { - CFRunLoopTimerSetNextFireDate(self.timer, CFAbsoluteTimeGetCurrent()); - } - } } impl Drop for Timer { From cbe2be926d578f8dd5dd149883822d6cff3b7efc Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Wed, 2 Aug 2017 19:44:39 -0500 Subject: [PATCH 21/24] Replace runloop observer with dispatch --- Cargo.toml | 1 + src/lib.rs | 2 + .../macos/events_loop/runloop_context.rs | 61 +++---------------- 3 files changed, 10 insertions(+), 54 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3ec635c192..a0c1486063 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,6 +24,7 @@ objc = "0.2" [target.'cfg(target_os = "macos")'.dependencies] objc = "0.2" cocoa = "0.9" +dispatch = "0.1" core-foundation = "0.4" core-graphics = "0.8" context = { version = "2.0", optional = true } diff --git a/src/lib.rs b/src/lib.rs index 924890c038..b2c59c9fb2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -109,6 +109,8 @@ extern crate cocoa; extern crate core_foundation; #[cfg(target_os = "macos")] extern crate core_graphics; +#[cfg(target_os = "macos")] +extern crate dispatch; #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd"))] extern crate x11_dl; #[cfg(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "openbsd"))] diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs index 9a9265ab36..0c55a000a1 100644 --- a/src/platform/macos/events_loop/runloop_context.rs +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -3,10 +3,8 @@ use std::mem; use std::sync::Weak; use context; use core_foundation; -use core_foundation::base::*; -use core_foundation::runloop::*; use cocoa; -use libc::c_void; +use dispatch; use super::{Shared,Timeout}; use super::nsevent; @@ -24,9 +22,6 @@ pub struct Runloop { // Hang onto a timer that goes off every few milliseconds _timer: Timer, - - // Hang onto a runloop observer - _observer: RunloopObserver, } impl Runloop { @@ -52,7 +47,6 @@ impl Runloop { _stack: stack, ctx: Some(result.context), _timer: Timer::new(0.005), - _observer: RunloopObserver::new(), } } @@ -86,7 +80,12 @@ impl Runloop { if yield_to_caller() { // We did! } else { - // Wake the runloop, because... we can, I guess? + // Queue a block that will yield back to the caller + dispatch::Queue::main().async(|| { + yield_to_caller(); + }); + + // Wake the runloop so it notices the new block unsafe { core_foundation::runloop::CFRunLoopWakeUp(core_foundation::runloop::CFRunLoopGetMain()); } @@ -228,49 +227,3 @@ impl InnerRunloop { } } } - -// A RunloopObserver corresponds to a CFRunLoopObserver. -struct RunloopObserver { - id: CFRunLoopObserverRef, -} - -extern "C" fn runloop_observer_callback(_observer: CFRunLoopObserverRef, _activity: CFRunLoopActivity, _info: *mut c_void) { - yield_to_caller(); -} - -impl RunloopObserver { - fn new() -> RunloopObserver { - // CFRunLoopObserverCreate copies this struct, so we can give it a pointer to this local - let mut context: CFRunLoopObserverContext = unsafe { mem::zeroed() }; - - // Make the runloop observer itself - let id = unsafe { - CFRunLoopObserverCreate( - kCFAllocatorDefault, - kCFRunLoopBeforeWaiting | kCFRunLoopAfterWaiting, - 1, // repeats - 0, // order - runloop_observer_callback, - &mut context as *mut CFRunLoopObserverContext, - ) - }; - - // Add to event loop - unsafe { - CFRunLoopAddObserver(CFRunLoopGetMain(), id, kCFRunLoopCommonModes); - } - - RunloopObserver { - id, - } - } -} - -impl Drop for RunloopObserver { - fn drop(&mut self) { - unsafe { - CFRunLoopRemoveObserver(CFRunLoopGetMain(), self.id, kCFRunLoopCommonModes); - CFRelease(self.id as _); - } - } -} From 51c64f65be83e4fb67fc2183eb821b7f282f99ef Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Wed, 2 Aug 2017 20:12:21 -0500 Subject: [PATCH 22/24] Replace the timer with dispatch after_ms() --- src/platform/macos/events_loop/mod.rs | 4 -- .../macos/events_loop/runloop_context.rs | 38 ++++++++-- src/platform/macos/events_loop/timer.rs | 69 ------------------- 3 files changed, 33 insertions(+), 78 deletions(-) delete mode 100644 src/platform/macos/events_loop/timer.rs diff --git a/src/platform/macos/events_loop/mod.rs b/src/platform/macos/events_loop/mod.rs index 64c9dbea77..4ba458af94 100644 --- a/src/platform/macos/events_loop/mod.rs +++ b/src/platform/macos/events_loop/mod.rs @@ -15,10 +15,6 @@ mod runloop; #[path="runloop_context.rs"] mod runloop; -// NSTimer wrapper, needed only for coroutine-baesd runloop -#[cfg(feature="context")] -mod timer; - use self::runloop::Runloop; pub struct EventsLoop { diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs index 0c55a000a1..dbb94b5376 100644 --- a/src/platform/macos/events_loop/runloop_context.rs +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -1,6 +1,7 @@ use std::cell::Cell; use std::mem; use std::sync::Weak; +use std::sync::atomic::{AtomicUsize,ATOMIC_USIZE_INIT,Ordering}; use context; use core_foundation; use cocoa; @@ -8,7 +9,6 @@ use dispatch; use super::{Shared,Timeout}; use super::nsevent; -use super::timer::Timer; const STACK_SIZE: usize = 512 * 1024; @@ -19,9 +19,6 @@ const STACK_SIZE: usize = 512 * 1024; pub struct Runloop { _stack: context::stack::ProtectedFixedSizeStack, ctx: Option, - - // Hang onto a timer that goes off every few milliseconds - _timer: Timer, } impl Runloop { @@ -46,7 +43,6 @@ impl Runloop { Runloop{ _stack: stack, ctx: Some(result.context), - _timer: Timer::new(0.005), } } @@ -157,6 +153,12 @@ fn yield_to_caller() -> bool { unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(timeout) } .take(); + // Does the caller want their thread back soon? + if timeout == Some(Timeout::Now) { + // Try to ensure we'll yield again soon, regardless of what happens inside Cocoa + guard_against_lengthy_operations(); + } + // Store the new values in the thread local cells until we yield back INSIDE_INNER_RUNLOOP_CONTEXT.with(move |context_cell| { context_cell.set(context); @@ -171,6 +173,32 @@ fn yield_to_caller() -> bool { } } + +fn guard_against_lengthy_operations() { + // Schedule a block to run in the near future, just in case + // We can get called repeatedly, and we only want the most recent call to matter, so keep track + // using an atomic counter + static INVOCATIONS: AtomicUsize = ATOMIC_USIZE_INIT; + + // Get the current value of the counter, and increment it + let this_invocation = INVOCATIONS.fetch_add(1, Ordering::SeqCst); + + // Queue a block in two milliseconds + dispatch::Queue::main().after_ms(2, move || { + // Get the most recent invocation, which is one before the current value of the counter + let current_counter = INVOCATIONS.load(Ordering::Acquire); + let (most_recent_invocation, _) = current_counter.overflowing_sub(1); + + // Are we the most recent call? + if most_recent_invocation == this_invocation { + yield_to_caller(); + } else { + // We have already yielded and returned + // Do nothing + } + }); +} + pub struct InnerRunloop { shared: Weak, event_state: nsevent::PersistentState, diff --git a/src/platform/macos/events_loop/timer.rs b/src/platform/macos/events_loop/timer.rs deleted file mode 100644 index c46c6232e9..0000000000 --- a/src/platform/macos/events_loop/timer.rs +++ /dev/null @@ -1,69 +0,0 @@ -use std::mem; -use libc::c_void; -use core_foundation::base::*; -use core_foundation::runloop::*; -use core_foundation::date::*; - -use super::runloop::Runloop; - -// Encapsulates a CFRunLoopTimer that has a far-future time to fire, but which can be triggered -// across threads for the purpose of waking up an event loop. -pub struct Timer { - timer: CFRunLoopTimerRef, -} - -#[cfg(feature="context")] -extern "C" fn timer_callback(_timer: CFRunLoopTimerRef, _info: *mut c_void) { - // wake the runloop, which here means "try to context switch out of the event loop coroutine" - Runloop::wake(); -} - -#[cfg(not(feature="context"))] -extern "C" fn timer_callback(_timer: CFRunLoopTimerRef, _info: *mut c_void) { - // can't really accomplish anything -} - -impl Timer { - pub fn new(interval_seconds: f64) -> Timer { - // default to firing every year, starting one year in the future - let interval: CFTimeInterval = interval_seconds; - let now = unsafe { CFAbsoluteTimeGetCurrent() }; - - let mut context: CFRunLoopTimerContext = unsafe { mem::zeroed() }; - - // create a timer - let timer = unsafe { - CFRunLoopTimerCreate( - kCFAllocatorDefault, - now + interval, // fireDate - interval, // interval - 0, // flags - 0, // order - timer_callback, - &mut context as *mut CFRunLoopTimerContext, - ) - }; - - // add it to the runloop - unsafe { - CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes); - } - - Timer{ - timer - } - } -} - -impl Drop for Timer { - fn drop(&mut self) { - unsafe { - CFRunLoopRemoveTimer(CFRunLoopGetMain(), self.timer, kCFRunLoopCommonModes); - CFRelease(self.timer as _); - } - } -} - -// Rust doesn't know that __CFRunLoopTimer is thread safe, but the docs say it is -unsafe impl Send for Timer {} -unsafe impl Sync for Timer {} From 0bb781113756a13a3e49bfc321245e8a2ff650a3 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Wed, 2 Aug 2017 20:48:24 -0500 Subject: [PATCH 23/24] Add doc comments --- src/platform/macos/events_loop/mod.rs | 23 ++++ .../macos/events_loop/runloop_context.rs | 102 ++++++++++++++++++ 2 files changed, 125 insertions(+) diff --git a/src/platform/macos/events_loop/mod.rs b/src/platform/macos/events_loop/mod.rs index 4ba458af94..2251f5ca2c 100644 --- a/src/platform/macos/events_loop/mod.rs +++ b/src/platform/macos/events_loop/mod.rs @@ -1,3 +1,26 @@ +//! The MacOS event loop has a few components: +//! +//! `EventsLoop` is the user-facing object that encapsulates everything events related. It contains +//! a `Shared` object which keeps track of windows and contains an internal event queue. It also +//! contains a `Runloop` whose job is to interface with MacOS X, use the `nsevent` module to +//! translate Cocoa events into Winit events, and to deliver events to the `Shared` event queue. +//! +//! `Runloop` exposes three functions: +//! +//! * `Runloop::new(shared: Weak) -> Runloop` to create a new `Runloop` +//! * `Runloop::work(&mut self, Timeout)` to drive the runloop either for one cycle or for the +//! specified `Timeout`, whichever comes first +//! * `Runloop::wake()` to wake up the runloop as quickly as possible +//! +//! There are two `Runloop` implementations: one which operates in a straighforward blocking manner, +//! and a second which runs in a coroutine. +//! +//! The coroutine-based runloop is necessary because Cocoa event processing can trigger internal +//! runloops, for example during a resize operation. The blocking `Runloop` cannot return from an +//! inner runloop even if it knows its timeout has expired, whereas the coroutine `Runloop` can +//! suspend itself from any configuration and return to the caller. For additional discussion, see +//! [tomaka/winit#219](https://github.com/tomaka/winit/issues/219#issuecomment-315830359). + use {ControlFlow, EventsLoopClosed}; use events::Event; use std::collections::VecDeque; diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs index dbb94b5376..eecef49bbe 100644 --- a/src/platform/macos/events_loop/runloop_context.rs +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -1,3 +1,105 @@ +//! This is the coroutine-based `Runloop` implementation. See `runloop.rs` for the simple blocking +//! `Runloop` implementation. +//! +//! ## Structure +//! +//! The basic `Runloop` does everything in `Runloop`. The `context`-enabled version moves those +//! functions to `InnerRunloop`, and it adds a `Runloop` with the same public interface whose +//! purpose is to start an `InnerRunloop` coroutine and context switch into it as needed. +//! +//! ## Entering the coroutine +//! +//! After initialization in `Runloop::new()`, `Runloop::work()` is the only place where the main +//! thread context switches into the `InnerRunloop` coroutine. +//! +//! `Runloop::work()` is called only by `EventLoop::get_event()`. Whatever invariants about the main +//! thread are true at that point remain true for the entire duration of the inner runloop. For +//! example, we know that the main thread is not holding locks inside `Shared`, so the coroutine can +//! acquire and release locks on `Shared` without deadlocking on the main thread. +//! +//! `Runloop::work()` checks the `NSThread`'s identity to ensure that the coroutine can only be +//! resumed from the main thread. +//! +//! ## Moving data into the coroutine +//! +//! The initial call into the coroutine entrypoint needs to bring an `InnerRunloop`, and all +//! subsequent calls into the coroutine bring a `Timeout`. The strategy here is to accomplish this +//! by combining three properties: +//! +//! * `context` can carry a `data: usize` along during a context switch. +//! * When execution is transferred into a coroutine, the caller stops running until execution +//! transfers back. +//! * `Option::take()` moves a value out of the `Option`. +//! +//! Put together, this means the caller can declare a local `Option`, pass a `&mut` of it into the +//! coroutine, and the coroutine can safely `.take()` its value. Again, there's no concurrency at +//! work -- everything executes sequentially -- so we can guarantee that there's only one mutable +//! borrow to the caller's `Option`. +//! +//! Actually doing this via a `usize` uses `&mut` as `*mut` as `usize` on the way down, and then +//! `usize` as `*mut` transmute `&mut` on the way up. One could transmute straight to and from +//! `usize`, but casting all the way through preserves symmetry. +//! +//! Calls into the coroutine look like: +//! +//! ```ignore +//! // caller +//! let mut input: Option = Some(Foo); +//! context.resume(&mut input as *mut Option as usize); +//! +//! // coroutine's resume() returns, holding the &mut Option +//! let t: context::Transfer = context.resume( /* … */ ); +//! let input: *mut Option = t.data as *mut Option; +//! let input: &mut Option = mem::transmute(input); +//! let input: Foo = input.take().unwrap(); +//! // input is now moved to the coroutine +//! // coroutine eventually returns to the caller +//! t.context.resume( /* … */ ); +//! +//! // caller's context.resume() returns +//! // input = None, since the value was taken by the coroutine +//! ``` +//! +//! ## Inside the coroutine +//! +//! `yield_to_caller()` is the place where the coroutine context switches back to `Runloop::work()`, +//! and it is therefore also the place where the coroutine resumes. +//! +//! `yield_to_caller()` sets a thread local cell containing the caller's context when execution +//! switched into the coroutine, and it moves the context out of that cell before it switches back. +//! If that cell is full, then we are currently inside the coroutine; if that cell is empty, then we +//! are not. +//! +//! `yield_to_caller()` also sets a thread local cell containing the caller's `Timeout`, which can +//! be retrieved by `fn current_timeout() -> Option`. The inner runloop uses this when +//! asking Cocoa to receive an event. +//! +//! The coroutine's `InnerRunloop` looks very much like the normal blocking `Runloop`. It tries to +//! receive an event from Cocoa, forwards it back to Cocoa, translates it into zero or more +//! `Event`s, and posts them to the queue. +//! +//! ## Exiting the coroutine +//! +//! `Shared::enqueue_event()` enqueues the event and then tries to wake the runloop, and the +//! coroutine version of `Runloop:::wake()` calls `yield_to_caller()`. This means that if we enqueue +//! an event from inside the coroutine -- for example, from the normal inner runloop or because a +//! Cocoa callback posted an event -- then execution immediately returns to +//! `EventLoop::get_event()`, which checks `Shared`'s event queue, finds an event, and returns it to +//! its caller. +//! +//! If `Runloop::wake()` finds that its caller is _not_ inside the coroutine -- for example, because +//! it's on a different thread calling `Proxy::wakeup()` -- it uses `libdispatch` to enqueues a +//! block on the main thread that calls `yield_to_caller()`, then uses `CFRunLoopWakeUp()` to wake +//! the thread in case it was sleeping. The system runloop will then check its dispatch queue, run +//! the block, and thus yield control of the main thread, even if we're stuck inside someone else's +//! runloop. +//! +//! Additionally, if the coroutine is invoked with `Timeout::Now`, it calls +//! `guard_against_lengthy_operations()` which enqueues a block for execution in the very near +//! future, i.e. a couple milliseconds. This puts an upper bound on how long the coroutine will run +//! after a caller has specified `Timeout::Now`. + + use std::cell::Cell; use std::mem; use std::sync::Weak; From cb5df718d5583e3ef459e79fefe5c37967f55564 Mon Sep 17 00:00:00 2001 From: Will Glynn Date: Thu, 17 Aug 2017 20:08:12 -0500 Subject: [PATCH 24/24] Avoid overlap in guard_against_lengthy_operations() --- .../macos/events_loop/runloop_context.rs | 64 ++++++++++++------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/platform/macos/events_loop/runloop_context.rs b/src/platform/macos/events_loop/runloop_context.rs index eecef49bbe..9275190717 100644 --- a/src/platform/macos/events_loop/runloop_context.rs +++ b/src/platform/macos/events_loop/runloop_context.rs @@ -103,7 +103,8 @@ use std::cell::Cell; use std::mem; use std::sync::Weak; -use std::sync::atomic::{AtomicUsize,ATOMIC_USIZE_INIT,Ordering}; +use std::sync::atomic::{AtomicBool,ATOMIC_BOOL_INIT,Ordering}; +use std::time::{Instant,Duration}; use context; use core_foundation; use cocoa; @@ -195,6 +196,7 @@ thread_local!{ // If we are inside the inner runloop, this contains the caller's context and their Timeout static INSIDE_INNER_RUNLOOP_CONTEXT: Cell> = Cell::new(None); static INSIDE_INNER_RUNLOOP_TIMEOUT: Cell> = Cell::new(None); + static INSIDE_INNER_RUNLOOP_ENTERED_AT: Cell = Cell::new(Instant::now()); } // This is the first function called from inside the coroutine. It must not return. @@ -255,12 +257,6 @@ fn yield_to_caller() -> bool { unsafe { mem::transmute::<*mut Option<_>, &mut Option<_>>(timeout) } .take(); - // Does the caller want their thread back soon? - if timeout == Some(Timeout::Now) { - // Try to ensure we'll yield again soon, regardless of what happens inside Cocoa - guard_against_lengthy_operations(); - } - // Store the new values in the thread local cells until we yield back INSIDE_INNER_RUNLOOP_CONTEXT.with(move |context_cell| { context_cell.set(context); @@ -268,6 +264,15 @@ fn yield_to_caller() -> bool { INSIDE_INNER_RUNLOOP_TIMEOUT.with(move |timeout_cell| { timeout_cell.set(timeout); }); + INSIDE_INNER_RUNLOOP_ENTERED_AT.with(move |entered_at_cell| { + entered_at_cell.set(Instant::now()); + }); + + // Does the caller want their thread back soon? + if timeout == Some(Timeout::Now) { + // Try to ensure we'll yield again soon, regardless of what happens inside Cocoa + guard_against_lengthy_operations(); + } true } else { @@ -275,28 +280,39 @@ fn yield_to_caller() -> bool { } } - fn guard_against_lengthy_operations() { - // Schedule a block to run in the near future, just in case - // We can get called repeatedly, and we only want the most recent call to matter, so keep track - // using an atomic counter - static INVOCATIONS: AtomicUsize = ATOMIC_USIZE_INIT; - - // Get the current value of the counter, and increment it - let this_invocation = INVOCATIONS.fetch_add(1, Ordering::SeqCst); + // We can get called repeatedly, and we only want a single block in the runloop's execution + // queue, so keep track of if there is currently one queued + static HAS_BLOCK_QUEUED: AtomicBool = ATOMIC_BOOL_INIT; + + // Is there currently a block queued? + if HAS_BLOCK_QUEUED.load(Ordering::Acquire) { + // Do nothing + return; + } // Queue a block in two milliseconds dispatch::Queue::main().after_ms(2, move || { - // Get the most recent invocation, which is one before the current value of the counter - let current_counter = INVOCATIONS.load(Ordering::Acquire); - let (most_recent_invocation, _) = current_counter.overflowing_sub(1); + // Indicate that there is not currently a block queued + HAS_BLOCK_QUEUED.store(false, Ordering::Release); + + // Are we in an invocation that's supposed to yield promptly? + if current_timeout() == Some(Timeout::Now) { + // Figure out when we entered the runloop as compared to now + let runloop_entered_at = INSIDE_INNER_RUNLOOP_ENTERED_AT.with(move |entered_at_cell| { + entered_at_cell.get() + }); + let duration_since_runloop_entry = runloop_entered_at.elapsed(); - // Are we the most recent call? - if most_recent_invocation == this_invocation { - yield_to_caller(); - } else { - // We have already yielded and returned - // Do nothing + // Did we enter more than one millisecond ago? + if duration_since_runloop_entry > Duration::from_millis(1) { + // Return, even if this is a bit early + yield_to_caller(); + } else { + // We haven't been in the runloop very long + // Instead of returning, queue another block for the near future + guard_against_lengthy_operations(); + } } }); }