Files
winit/src/platform_impl/macos/app_state.rs
Kirill Chibisov 281077a0d8 Remove lifetime from the Event
Lifetimes don't work nicely when dealing with multithreaded environments
in the current design of the existing winit's event handling model, so
remove it in favor of `InnerSizeWriter` fences passed to client, so they
could try to update the size.

Fixes #1387.
2023-08-15 13:10:02 +04:00

677 lines
24 KiB
Rust

use std::{
cell::{RefCell, RefMut},
collections::VecDeque,
fmt::{self, Debug},
mem,
rc::{Rc, Weak},
sync::{
atomic::{AtomicBool, Ordering},
Arc, Mutex, MutexGuard,
},
time::Instant,
};
use core_foundation::runloop::{CFRunLoopGetMain, CFRunLoopWakeUp};
use icrate::Foundation::{is_main_thread, NSSize};
use objc2::rc::autoreleasepool;
use once_cell::sync::Lazy;
use super::appkit::{NSApp, NSApplication, NSApplicationActivationPolicy, NSEvent};
use crate::{
dpi::LogicalSize,
event::{Event, InnerSizeWriter, StartCause, WindowEvent},
event_loop::{ControlFlow, EventLoopWindowTarget as RootWindowTarget},
platform_impl::platform::{
event::{EventProxy, EventWrapper},
event_loop::PanicInfo,
menu,
observer::EventLoopWaker,
util::Never,
window::WinitWindow,
},
window::WindowId,
};
static HANDLER: Lazy<Handler> = Lazy::new(Default::default);
impl<Never> Event<Never> {
fn userify<T: 'static>(self) -> Event<T> {
self.map_nonuser_event()
// `Never` can't be constructed, so the `UserEvent` variant can't
// be present here.
.unwrap_or_else(|_| unreachable!())
}
}
pub trait EventHandler: Debug {
// Not sure probably it should accept Event<'static, Never>
fn handle_nonuser_event(&mut self, event: Event<Never>, control_flow: &mut ControlFlow);
fn handle_user_events(&mut self, control_flow: &mut ControlFlow);
}
pub(crate) type Callback<T> = RefCell<dyn FnMut(Event<T>, &RootWindowTarget<T>, &mut ControlFlow)>;
struct EventLoopHandler<T: 'static> {
callback: Weak<Callback<T>>,
window_target: Rc<RootWindowTarget<T>>,
}
impl<T> EventLoopHandler<T> {
fn with_callback<F>(&mut self, f: F)
where
F: FnOnce(
&mut EventLoopHandler<T>,
RefMut<'_, dyn FnMut(Event<T>, &RootWindowTarget<T>, &mut ControlFlow)>,
),
{
// The `NSApp` and our `HANDLER` are global state and so it's possible that
// we could get a delegate callback after the application has exit an
// `EventLoop`. If the loop has been exit then our weak `self.callback`
// will fail to upgrade.
//
// We don't want to panic or output any verbose logging if we fail to
// upgrade the weak reference since it might be valid that the application
// re-starts the `NSApp` after exiting a Winit `EventLoop`
if let Some(callback) = self.callback.upgrade() {
let callback = callback.borrow_mut();
(f)(self, callback);
}
}
}
impl<T> Debug for EventLoopHandler<T> {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter
.debug_struct("EventLoopHandler")
.field("window_target", &self.window_target)
.finish()
}
}
impl<T> EventHandler for EventLoopHandler<T> {
fn handle_nonuser_event(&mut self, event: Event<Never>, control_flow: &mut ControlFlow) {
self.with_callback(|this, mut callback| {
if let ControlFlow::ExitWithCode(code) = *control_flow {
// XXX: why isn't event dispatching simply skipped after control_flow = ExitWithCode?
let dummy = &mut ControlFlow::ExitWithCode(code);
(callback)(event.userify(), &this.window_target, dummy);
} else {
(callback)(event.userify(), &this.window_target, control_flow);
}
});
}
fn handle_user_events(&mut self, control_flow: &mut ControlFlow) {
self.with_callback(|this, mut callback| {
for event in this.window_target.p.receiver.try_iter() {
if let ControlFlow::ExitWithCode(code) = *control_flow {
// XXX: why isn't event dispatching simply skipped after control_flow = ExitWithCode?
let dummy = &mut ControlFlow::ExitWithCode(code);
(callback)(Event::UserEvent(event), &this.window_target, dummy);
} else {
(callback)(Event::UserEvent(event), &this.window_target, control_flow);
}
}
});
}
}
#[derive(Default)]
struct Handler {
stop_app_on_launch: AtomicBool,
stop_app_before_wait: AtomicBool,
stop_app_after_wait: AtomicBool,
stop_app_on_redraw: AtomicBool,
launched: AtomicBool,
running: AtomicBool,
in_callback: AtomicBool,
control_flow: Mutex<ControlFlow>,
start_time: Mutex<Option<Instant>>,
callback: Mutex<Option<Box<dyn EventHandler>>>,
pending_events: Mutex<VecDeque<EventWrapper>>,
pending_redraw: Mutex<Vec<WindowId>>,
wait_timeout: Mutex<Option<Instant>>,
waker: Mutex<EventLoopWaker>,
}
unsafe impl Send for Handler {}
unsafe impl Sync for Handler {}
impl Handler {
fn events(&self) -> MutexGuard<'_, VecDeque<EventWrapper>> {
self.pending_events.lock().unwrap()
}
fn redraw(&self) -> MutexGuard<'_, Vec<WindowId>> {
self.pending_redraw.lock().unwrap()
}
fn waker(&self) -> MutexGuard<'_, EventLoopWaker> {
self.waker.lock().unwrap()
}
/// `true` after `ApplicationDelegate::applicationDidFinishLaunching` called
///
/// NB: This is global / `NSApp` state and since the app will only be launched
/// once but an `EventLoop` may be run more than once then only the first
/// `EventLoop` will observe the `NSApp` before it is launched.
fn is_launched(&self) -> bool {
self.launched.load(Ordering::Acquire)
}
/// Set via `ApplicationDelegate::applicationDidFinishLaunching`
fn set_launched(&self) {
self.launched.store(true, Ordering::Release);
}
/// `true` if an `EventLoop` is currently running
///
/// NB: This is global / `NSApp` state and may persist beyond the lifetime of
/// a running `EventLoop`.
///
/// # Caveat
/// This is only intended to be called from the main thread
fn is_running(&self) -> bool {
self.running.load(Ordering::Relaxed)
}
/// Set when an `EventLoop` starts running, after the `NSApp` is launched
///
/// # Caveat
/// This is only intended to be called from the main thread
fn set_running(&self) {
self.running.store(true, Ordering::Relaxed);
}
fn should_exit(&self) -> bool {
matches!(
*self.control_flow.lock().unwrap(),
ControlFlow::ExitWithCode(_)
)
}
/// Clears the `running` state and resets the `control_flow` state when an `EventLoop` exits
///
/// Since an `EventLoop` may be run more than once we need make sure to reset the
/// `control_flow` state back to `Poll` each time the loop exits.
///
/// Note: that if the `NSApp` has been launched then that state is preserved, and we won't
/// need to re-launch the app if subsequent EventLoops are run.
///
/// # Caveat
/// This is only intended to be called from the main thread
fn exit(&self) {
// Relaxed ordering because we don't actually have multiple threads involved, we just want
// interiour mutability
//
// XXX: As an aside; having each individual bit of state for `Handler` be atomic or wrapped in a
// `Mutex` for the sake of interior mutability seems a bit odd, and also a potential foot
// gun in case the state is unwittingly accessed across threads because the fine-grained locking
// wouldn't ensure that there's interior consistency.
//
// Maybe the whole thing should just be put in a static `Mutex<>` to make it clear
// the we can mutate more than one peice of state while maintaining consistency. (though it
// looks like there have been recuring re-entrancy issues with callback handling that might
// make that awkward)
self.running.store(false, Ordering::Relaxed);
*self.control_flow.lock().unwrap() = ControlFlow::default();
self.set_stop_app_on_redraw_requested(false);
self.set_stop_app_before_wait(false);
self.set_stop_app_after_wait(false);
self.set_wait_timeout(None);
}
pub fn request_stop_app_on_launch(&self) {
// Relaxed ordering because we don't actually have multiple threads involved, we just want
// interior mutability
self.stop_app_on_launch.store(true, Ordering::Relaxed);
}
pub fn should_stop_app_on_launch(&self) -> bool {
// Relaxed ordering because we don't actually have multiple threads involved, we just want
// interior mutability
self.stop_app_on_launch.load(Ordering::Relaxed)
}
pub fn set_stop_app_before_wait(&self, stop_before_wait: bool) {
// Relaxed ordering because we don't actually have multiple threads involved, we just want
// interior mutability
self.stop_app_before_wait
.store(stop_before_wait, Ordering::Relaxed);
}
pub fn should_stop_app_before_wait(&self) -> bool {
// Relaxed ordering because we don't actually have multiple threads involved, we just want
// interior mutability
self.stop_app_before_wait.load(Ordering::Relaxed)
}
pub fn set_stop_app_after_wait(&self, stop_after_wait: bool) {
// Relaxed ordering because we don't actually have multiple threads involved, we just want
// interior mutability
self.stop_app_after_wait
.store(stop_after_wait, Ordering::Relaxed);
}
pub fn set_wait_timeout(&self, new_timeout: Option<Instant>) {
let mut timeout = self.wait_timeout.lock().unwrap();
*timeout = new_timeout;
}
pub fn wait_timeout(&self) -> Option<Instant> {
*self.wait_timeout.lock().unwrap()
}
pub fn should_stop_app_after_wait(&self) -> bool {
// Relaxed ordering because we don't actually have multiple threads involved, we just want
// interior mutability
self.stop_app_after_wait.load(Ordering::Relaxed)
}
pub fn set_stop_app_on_redraw_requested(&self, stop_on_redraw: bool) {
// Relaxed ordering because we don't actually have multiple threads involved, we just want
// interior mutability
self.stop_app_on_redraw
.store(stop_on_redraw, Ordering::Relaxed);
}
pub fn should_stop_app_on_redraw_requested(&self) -> bool {
// Relaxed ordering because we don't actually have multiple threads involved, we just want
// interior mutability
self.stop_app_on_redraw.load(Ordering::Relaxed)
}
fn control_flow(&self) -> ControlFlow {
*self.control_flow.lock().unwrap()
}
fn get_start_time(&self) -> Option<Instant> {
*self.start_time.lock().unwrap()
}
fn update_start_time(&self) {
*self.start_time.lock().unwrap() = Some(Instant::now());
}
fn take_events(&self) -> VecDeque<EventWrapper> {
mem::take(&mut *self.events())
}
fn should_redraw(&self) -> Vec<WindowId> {
mem::take(&mut *self.redraw())
}
fn get_in_callback(&self) -> bool {
self.in_callback.load(Ordering::Acquire)
}
fn set_in_callback(&self, in_callback: bool) {
self.in_callback.store(in_callback, Ordering::Release);
}
fn have_callback(&self) -> bool {
self.callback.lock().unwrap().is_some()
}
fn handle_nonuser_event(&self, wrapper: EventWrapper) {
if let Some(ref mut callback) = *self.callback.lock().unwrap() {
match wrapper {
EventWrapper::StaticEvent(event) => {
callback.handle_nonuser_event(event, &mut self.control_flow.lock().unwrap())
}
EventWrapper::EventProxy(proxy) => self.handle_proxy(proxy, callback),
}
}
}
fn handle_user_events(&self) {
if let Some(ref mut callback) = *self.callback.lock().unwrap() {
callback.handle_user_events(&mut self.control_flow.lock().unwrap());
}
}
fn handle_scale_factor_changed_event(
&self,
callback: &mut Box<dyn EventHandler + 'static>,
window: &WinitWindow,
suggested_size: LogicalSize<f64>,
scale_factor: f64,
) {
let new_inner_size = Arc::new(Mutex::new(suggested_size.to_physical(scale_factor)));
let event = Event::WindowEvent {
window_id: WindowId(window.id()),
event: WindowEvent::ScaleFactorChanged {
scale_factor,
inner_size_writer: InnerSizeWriter::new(Arc::downgrade(&new_inner_size)),
},
};
callback.handle_nonuser_event(event, &mut self.control_flow.lock().unwrap());
let physical_size = *new_inner_size.lock().unwrap();
drop(new_inner_size);
let logical_size = physical_size.to_logical(scale_factor);
let size = NSSize::new(logical_size.width, logical_size.height);
window.setContentSize(size);
}
fn handle_proxy(&self, proxy: EventProxy, callback: &mut Box<dyn EventHandler + 'static>) {
match proxy {
EventProxy::DpiChangedProxy {
window,
suggested_size,
scale_factor,
} => self.handle_scale_factor_changed_event(
callback,
&window,
suggested_size,
scale_factor,
),
}
}
}
pub(crate) enum AppState {}
impl AppState {
/// Associate the application's event callback with the (global static) Handler state
///
/// # Safety
/// This is ignoring the lifetime of the application callback (which may not be 'static)
/// and can lead to undefined behaviour if the callback is not cleared before the end of
/// its real lifetime.
///
/// All public APIs that take an event callback (`run`, `run_ondemand`,
/// `pump_events`) _must_ pair a call to `set_callback` with
/// a call to `clear_callback` before returning to avoid undefined behaviour.
pub unsafe fn set_callback<T>(
callback: Weak<Callback<T>>,
window_target: Rc<RootWindowTarget<T>>,
) {
*HANDLER.callback.lock().unwrap() = Some(Box::new(EventLoopHandler {
callback,
window_target,
}));
}
pub fn clear_callback() {
HANDLER.callback.lock().unwrap().take();
}
pub fn is_launched() -> bool {
HANDLER.is_launched()
}
pub fn is_running() -> bool {
HANDLER.is_running()
}
// If `pump_events` is called to progress the event loop then we bootstrap the event
// loop via `[NSApp run]` but will use `CFRunLoopRunInMode` for subsequent calls to
// `pump_events`
pub fn request_stop_on_launch() {
HANDLER.request_stop_app_on_launch();
}
pub fn set_stop_app_before_wait(stop_before_wait: bool) {
HANDLER.set_stop_app_before_wait(stop_before_wait);
}
pub fn set_stop_app_after_wait(stop_after_wait: bool) {
HANDLER.set_stop_app_after_wait(stop_after_wait);
}
pub fn set_wait_timeout(timeout: Option<Instant>) {
HANDLER.set_wait_timeout(timeout);
}
pub fn set_stop_app_on_redraw_requested(stop_on_redraw: bool) {
HANDLER.set_stop_app_on_redraw_requested(stop_on_redraw);
}
pub fn control_flow() -> ControlFlow {
HANDLER.control_flow()
}
pub fn exit() -> i32 {
HANDLER.set_in_callback(true);
HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::LoopExiting));
HANDLER.set_in_callback(false);
HANDLER.exit();
Self::clear_callback();
if let ControlFlow::ExitWithCode(code) = HANDLER.control_flow() {
code
} else {
0
}
}
pub fn dispatch_init_events() {
HANDLER.set_in_callback(true);
HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents(
StartCause::Init,
)));
// NB: For consistency all platforms must emit a 'resumed' event even though macOS
// applications don't themselves have a formal suspend/resume lifecycle.
HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::Resumed));
HANDLER.set_in_callback(false);
}
pub fn start_running() {
debug_assert!(HANDLER.is_launched());
HANDLER.set_running();
Self::dispatch_init_events()
}
pub fn launched(
activation_policy: NSApplicationActivationPolicy,
create_default_menu: bool,
activate_ignoring_other_apps: bool,
) {
let app = NSApp();
// We need to delay setting the activation policy and activating the app
// until `applicationDidFinishLaunching` has been called. Otherwise the
// menu bar is initially unresponsive on macOS 10.15.
app.setActivationPolicy(activation_policy);
window_activation_hack(&app);
app.activateIgnoringOtherApps(activate_ignoring_other_apps);
HANDLER.set_launched();
HANDLER.waker().start();
if create_default_menu {
// The menubar initialization should be before the `NewEvents` event, to allow
// overriding of the default menu even if it's created
menu::initialize();
}
Self::start_running();
// If the `NSApp` is being launched via `EventLoop::pump_events()` then we'll
// want to stop the app once it is launched (and return to the external loop)
//
// In this case we still want to consider Winit's `EventLoop` to be "running",
// so we call `start_running()` above.
if HANDLER.should_stop_app_on_launch() {
// Note: the original idea had been to only stop the underlying `RunLoop`
// for the app but that didn't work as expected (`[NSApp run]` effectively
// ignored the attempt to stop the RunLoop and re-started it.). So we
// return from `pump_events` by stopping the `NSApp`
Self::stop();
}
}
// Called by RunLoopObserver after finishing waiting for new events
pub fn wakeup(panic_info: Weak<PanicInfo>) {
let panic_info = panic_info
.upgrade()
.expect("The panic info must exist here. This failure indicates a developer error.");
// Return when in callback due to https://github.com/rust-windowing/winit/issues/1779
if panic_info.is_panicking()
|| !HANDLER.have_callback()
|| !HANDLER.is_running()
|| HANDLER.get_in_callback()
{
return;
}
if HANDLER.should_stop_app_after_wait() {
Self::stop();
}
let start = HANDLER.get_start_time().unwrap();
let cause = match HANDLER.control_flow() {
ControlFlow::Poll => StartCause::Poll,
ControlFlow::Wait => StartCause::WaitCancelled {
start,
requested_resume: None,
},
ControlFlow::WaitUntil(requested_resume) => {
if Instant::now() >= requested_resume {
StartCause::ResumeTimeReached {
start,
requested_resume,
}
} else {
StartCause::WaitCancelled {
start,
requested_resume: Some(requested_resume),
}
}
}
ControlFlow::ExitWithCode(_) => StartCause::Poll, //panic!("unexpected `ControlFlow::Exit`"),
};
HANDLER.set_in_callback(true);
HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::NewEvents(cause)));
HANDLER.set_in_callback(false);
}
// This is called from multiple threads at present
pub fn queue_redraw(window_id: WindowId) {
let mut pending_redraw = HANDLER.redraw();
if !pending_redraw.contains(&window_id) {
pending_redraw.push(window_id);
}
unsafe {
let rl = CFRunLoopGetMain();
CFRunLoopWakeUp(rl);
}
}
pub fn handle_redraw(window_id: WindowId) {
// Redraw request might come out of order from the OS.
// -> Don't go back into the callback when our callstack originates from there
if !HANDLER.in_callback.swap(true, Ordering::AcqRel) {
HANDLER
.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested(window_id)));
HANDLER.set_in_callback(false);
// `pump_events` will request to stop immediately _after_ dispatching RedrawRequested events
// as a way to ensure that `pump_events` can't block an external loop indefinitely
if HANDLER.should_stop_app_on_redraw_requested() {
AppState::stop();
}
}
}
pub fn queue_event(wrapper: EventWrapper) {
if !is_main_thread() {
panic!("Event queued from different thread: {wrapper:#?}");
}
HANDLER.events().push_back(wrapper);
}
pub fn stop() {
let app = NSApp();
autoreleasepool(|_| {
app.stop(None);
// To stop event loop immediately, we need to post some event here.
app.postEvent_atStart(&NSEvent::dummy(), true);
});
}
// Called by RunLoopObserver before waiting for new events
pub fn cleared(panic_info: Weak<PanicInfo>) {
let panic_info = panic_info
.upgrade()
.expect("The panic info must exist here. This failure indicates a developer error.");
// Return when in callback due to https://github.com/rust-windowing/winit/issues/1779
// XXX: how does it make sense that `get_in_callback()` can ever return `true` here if we're
// about to return to the `CFRunLoop` to poll for new events?
if panic_info.is_panicking()
|| !HANDLER.have_callback()
|| !HANDLER.is_running()
|| HANDLER.get_in_callback()
{
return;
}
HANDLER.set_in_callback(true);
HANDLER.handle_user_events();
for event in HANDLER.take_events() {
HANDLER.handle_nonuser_event(event);
}
for window_id in HANDLER.should_redraw() {
HANDLER
.handle_nonuser_event(EventWrapper::StaticEvent(Event::RedrawRequested(window_id)));
}
HANDLER.handle_nonuser_event(EventWrapper::StaticEvent(Event::AboutToWait));
HANDLER.set_in_callback(false);
if HANDLER.should_exit() {
Self::stop();
}
if HANDLER.should_stop_app_before_wait() {
Self::stop();
}
HANDLER.update_start_time();
let wait_timeout = HANDLER.wait_timeout(); // configured by pump_events
let app_timeout = match HANDLER.control_flow() {
ControlFlow::Wait => None,
ControlFlow::Poll | ControlFlow::ExitWithCode(_) => Some(Instant::now()),
ControlFlow::WaitUntil(instant) => Some(instant),
};
HANDLER
.waker()
.start_at(min_timeout(wait_timeout, app_timeout));
}
}
/// Returns the minimum `Option<Instant>`, taking into account that `None`
/// equates to an infinite timeout, not a zero timeout (so can't just use
/// `Option::min`)
fn min_timeout(a: Option<Instant>, b: Option<Instant>) -> Option<Instant> {
a.map_or(b, |a_timeout| {
b.map_or(Some(a_timeout), |b_timeout| Some(a_timeout.min(b_timeout)))
})
}
/// A hack to make activation of multiple windows work when creating them before
/// `applicationDidFinishLaunching:` / `Event::Event::NewEvents(StartCause::Init)`.
///
/// Alternative to this would be the user calling `window.set_visible(true)` in
/// `StartCause::Init`.
///
/// If this becomes too bothersome to maintain, it can probably be removed
/// without too much damage.
fn window_activation_hack(app: &NSApplication) {
// TODO: Proper ordering of the windows
app.windows().into_iter().for_each(|window| {
// Call `makeKeyAndOrderFront` if it was called on the window in `WinitWindow::new`
// This way we preserve the user's desired initial visiblity status
// TODO: Also filter on the type/"level" of the window, and maybe other things?
if window.isVisible() {
trace!("Activating visible window");
window.makeKeyAndOrderFront(None);
} else {
trace!("Skipping activating invisible window");
}
})
}