Move iOS and macOS implementations into new apple module (#3756)
Move iOS and macOS implementations to a shared folder called `apple`, to allow us to reduce the code-duplication between these platforms in the future. The folder structure is now: - `src/platform_impl/apple/` - `appkit/` - `uikit/` - `example_shared_file.rs` - `mod.rs` * Add preliminary support for tvOS, watchOS and visionOS * Reduce duplication in Cargo.toml when specifying dependencies
This commit is contained in:
parent
ecb887e5c3
commit
9d5412ffe1
28 changed files with 52 additions and 59 deletions
106
src/platform_impl/apple/appkit/app.rs
Normal file
106
src/platform_impl/apple/appkit/app.rs
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
#![allow(clippy::unnecessary_cast)]
|
||||
|
||||
use objc2::{declare_class, msg_send, mutability, ClassType, DeclaredClass};
|
||||
use objc2_app_kit::{NSApplication, NSEvent, NSEventModifierFlags, NSEventType, NSResponder};
|
||||
use objc2_foundation::{MainThreadMarker, NSObject};
|
||||
|
||||
use super::app_state::ApplicationDelegate;
|
||||
use super::DEVICE_ID;
|
||||
use crate::event::{DeviceEvent, ElementState};
|
||||
|
||||
declare_class!(
|
||||
pub(super) struct WinitApplication;
|
||||
|
||||
unsafe impl ClassType for WinitApplication {
|
||||
#[inherits(NSResponder, NSObject)]
|
||||
type Super = NSApplication;
|
||||
type Mutability = mutability::MainThreadOnly;
|
||||
const NAME: &'static str = "WinitApplication";
|
||||
}
|
||||
|
||||
impl DeclaredClass for WinitApplication {}
|
||||
|
||||
unsafe impl WinitApplication {
|
||||
// Normally, holding Cmd + any key never sends us a `keyUp` event for that key.
|
||||
// Overriding `sendEvent:` like this fixes that. (https://stackoverflow.com/a/15294196)
|
||||
// Fun fact: Firefox still has this bug! (https://bugzilla.mozilla.org/show_bug.cgi?id=1299553)
|
||||
#[method(sendEvent:)]
|
||||
fn send_event(&self, event: &NSEvent) {
|
||||
// For posterity, there are some undocumented event types
|
||||
// (https://github.com/servo/cocoa-rs/issues/155)
|
||||
// but that doesn't really matter here.
|
||||
let event_type = unsafe { event.r#type() };
|
||||
let modifier_flags = unsafe { event.modifierFlags() };
|
||||
if event_type == NSEventType::KeyUp
|
||||
&& modifier_flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand)
|
||||
{
|
||||
if let Some(key_window) = self.keyWindow() {
|
||||
key_window.sendEvent(event);
|
||||
}
|
||||
} else {
|
||||
let delegate = ApplicationDelegate::get(MainThreadMarker::from(self));
|
||||
maybe_dispatch_device_event(&delegate, event);
|
||||
unsafe { msg_send![super(self), sendEvent: event] }
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
fn maybe_dispatch_device_event(delegate: &ApplicationDelegate, event: &NSEvent) {
|
||||
let event_type = unsafe { event.r#type() };
|
||||
#[allow(non_upper_case_globals)]
|
||||
match event_type {
|
||||
NSEventType::MouseMoved
|
||||
| NSEventType::LeftMouseDragged
|
||||
| NSEventType::OtherMouseDragged
|
||||
| NSEventType::RightMouseDragged => {
|
||||
let delta_x = unsafe { event.deltaX() } as f64;
|
||||
let delta_y = unsafe { event.deltaY() } as f64;
|
||||
|
||||
if delta_x != 0.0 {
|
||||
delegate.maybe_queue_with_handler(move |app, event_loop| {
|
||||
app.device_event(event_loop, DEVICE_ID, DeviceEvent::Motion {
|
||||
axis: 0,
|
||||
value: delta_x,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if delta_y != 0.0 {
|
||||
delegate.maybe_queue_with_handler(move |app, event_loop| {
|
||||
app.device_event(event_loop, DEVICE_ID, DeviceEvent::Motion {
|
||||
axis: 1,
|
||||
value: delta_y,
|
||||
});
|
||||
})
|
||||
}
|
||||
|
||||
if delta_x != 0.0 || delta_y != 0.0 {
|
||||
delegate.maybe_queue_with_handler(move |app, event_loop| {
|
||||
app.device_event(event_loop, DEVICE_ID, DeviceEvent::MouseMotion {
|
||||
delta: (delta_x, delta_y),
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
NSEventType::LeftMouseDown | NSEventType::RightMouseDown | NSEventType::OtherMouseDown => {
|
||||
let button = unsafe { event.buttonNumber() } as u32;
|
||||
delegate.maybe_queue_with_handler(move |app, event_loop| {
|
||||
app.device_event(event_loop, DEVICE_ID, DeviceEvent::Button {
|
||||
button,
|
||||
state: ElementState::Pressed,
|
||||
});
|
||||
});
|
||||
},
|
||||
NSEventType::LeftMouseUp | NSEventType::RightMouseUp | NSEventType::OtherMouseUp => {
|
||||
let button = unsafe { event.buttonNumber() } as u32;
|
||||
delegate.maybe_queue_with_handler(move |app, event_loop| {
|
||||
app.device_event(event_loop, DEVICE_ID, DeviceEvent::Button {
|
||||
button,
|
||||
state: ElementState::Released,
|
||||
});
|
||||
});
|
||||
},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
428
src/platform_impl/apple/appkit/app_state.rs
Normal file
428
src/platform_impl/apple/appkit/app_state.rs
Normal file
|
|
@ -0,0 +1,428 @@
|
|||
use std::cell::{Cell, RefCell};
|
||||
use std::mem;
|
||||
use std::rc::Weak;
|
||||
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use objc2::rc::Retained;
|
||||
use objc2::{declare_class, msg_send_id, mutability, ClassType, DeclaredClass};
|
||||
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSApplicationDelegate};
|
||||
use objc2_foundation::{MainThreadMarker, NSNotification, NSObject, NSObjectProtocol};
|
||||
|
||||
use crate::application::ApplicationHandler;
|
||||
use crate::event::{StartCause, WindowEvent};
|
||||
use crate::event_loop::{ActiveEventLoop as RootActiveEventLoop, ControlFlow};
|
||||
use crate::window::WindowId as RootWindowId;
|
||||
|
||||
use super::event_handler::EventHandler;
|
||||
use super::event_loop::{stop_app_immediately, ActiveEventLoop, PanicInfo};
|
||||
use super::observer::{EventLoopWaker, RunLoop};
|
||||
use super::{menu, WindowId};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct AppState {
|
||||
activation_policy: NSApplicationActivationPolicy,
|
||||
default_menu: bool,
|
||||
activate_ignoring_other_apps: bool,
|
||||
run_loop: RunLoop,
|
||||
proxy_wake_up: Arc<AtomicBool>,
|
||||
event_handler: EventHandler,
|
||||
stop_on_launch: Cell<bool>,
|
||||
stop_before_wait: Cell<bool>,
|
||||
stop_after_wait: Cell<bool>,
|
||||
stop_on_redraw: Cell<bool>,
|
||||
/// Whether `applicationDidFinishLaunching:` has been run or not.
|
||||
is_launched: Cell<bool>,
|
||||
/// Whether an `EventLoop` is currently running.
|
||||
is_running: Cell<bool>,
|
||||
/// Whether the user has requested the event loop to exit.
|
||||
exit: Cell<bool>,
|
||||
control_flow: Cell<ControlFlow>,
|
||||
waker: RefCell<EventLoopWaker>,
|
||||
start_time: Cell<Option<Instant>>,
|
||||
wait_timeout: Cell<Option<Instant>>,
|
||||
pending_redraw: RefCell<Vec<WindowId>>,
|
||||
// NOTE: This is strongly referenced by our `NSWindowDelegate` and our `NSView` subclass, and
|
||||
// as such should be careful to not add fields that, in turn, strongly reference those.
|
||||
}
|
||||
|
||||
declare_class!(
|
||||
#[derive(Debug)]
|
||||
pub(super) struct ApplicationDelegate;
|
||||
|
||||
unsafe impl ClassType for ApplicationDelegate {
|
||||
type Super = NSObject;
|
||||
type Mutability = mutability::MainThreadOnly;
|
||||
const NAME: &'static str = "WinitApplicationDelegate";
|
||||
}
|
||||
|
||||
impl DeclaredClass for ApplicationDelegate {
|
||||
type Ivars = AppState;
|
||||
}
|
||||
|
||||
unsafe impl NSObjectProtocol for ApplicationDelegate {}
|
||||
|
||||
unsafe impl NSApplicationDelegate for ApplicationDelegate {
|
||||
#[method(applicationDidFinishLaunching:)]
|
||||
fn app_did_finish_launching(&self, notification: &NSNotification) {
|
||||
self.did_finish_launching(notification)
|
||||
}
|
||||
|
||||
#[method(applicationWillTerminate:)]
|
||||
fn app_will_terminate(&self, notification: &NSNotification) {
|
||||
self.will_terminate(notification)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl ApplicationDelegate {
|
||||
pub(super) fn new(
|
||||
mtm: MainThreadMarker,
|
||||
activation_policy: NSApplicationActivationPolicy,
|
||||
proxy_wake_up: Arc<AtomicBool>,
|
||||
default_menu: bool,
|
||||
activate_ignoring_other_apps: bool,
|
||||
) -> Retained<Self> {
|
||||
let this = mtm.alloc().set_ivars(AppState {
|
||||
activation_policy,
|
||||
proxy_wake_up,
|
||||
default_menu,
|
||||
activate_ignoring_other_apps,
|
||||
run_loop: RunLoop::main(mtm),
|
||||
event_handler: EventHandler::new(),
|
||||
stop_on_launch: Cell::new(false),
|
||||
stop_before_wait: Cell::new(false),
|
||||
stop_after_wait: Cell::new(false),
|
||||
stop_on_redraw: Cell::new(false),
|
||||
is_launched: Cell::new(false),
|
||||
is_running: Cell::new(false),
|
||||
exit: Cell::new(false),
|
||||
control_flow: Cell::new(ControlFlow::default()),
|
||||
waker: RefCell::new(EventLoopWaker::new()),
|
||||
start_time: Cell::new(None),
|
||||
wait_timeout: Cell::new(None),
|
||||
pending_redraw: RefCell::new(vec![]),
|
||||
});
|
||||
unsafe { msg_send_id![super(this), init] }
|
||||
}
|
||||
|
||||
// NOTE: This will, globally, only be run once, no matter how many
|
||||
// `EventLoop`s the user creates.
|
||||
fn did_finish_launching(&self, _notification: &NSNotification) {
|
||||
trace_scope!("applicationDidFinishLaunching:");
|
||||
self.ivars().is_launched.set(true);
|
||||
|
||||
let mtm = MainThreadMarker::from(self);
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
// 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(self.ivars().activation_policy);
|
||||
|
||||
window_activation_hack(&app);
|
||||
#[allow(deprecated)]
|
||||
app.activateIgnoringOtherApps(self.ivars().activate_ignoring_other_apps);
|
||||
|
||||
if self.ivars().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(&app);
|
||||
}
|
||||
|
||||
self.ivars().waker.borrow_mut().start();
|
||||
|
||||
self.set_is_running(true);
|
||||
self.dispatch_init_events();
|
||||
|
||||
// If the application is being launched via `EventLoop::pump_app_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 self.ivars().stop_on_launch.get() {
|
||||
// NOTE: the original idea had been to only stop the underlying `RunLoop`
|
||||
// for the app but that didn't work as expected (`-[NSApplication run]`
|
||||
// effectively ignored the attempt to stop the RunLoop and re-started it).
|
||||
//
|
||||
// So we return from `pump_events` by stopping the application.
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
stop_app_immediately(&app);
|
||||
}
|
||||
}
|
||||
|
||||
fn will_terminate(&self, _notification: &NSNotification) {
|
||||
trace_scope!("applicationWillTerminate:");
|
||||
// TODO: Notify every window that it will be destroyed, like done in iOS?
|
||||
self.internal_exit();
|
||||
}
|
||||
|
||||
pub fn get(mtm: MainThreadMarker) -> Retained<Self> {
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
let delegate =
|
||||
unsafe { app.delegate() }.expect("a delegate was not configured on the application");
|
||||
if delegate.is_kind_of::<Self>() {
|
||||
// SAFETY: Just checked that the delegate is an instance of `ApplicationDelegate`
|
||||
unsafe { Retained::cast(delegate) }
|
||||
} else {
|
||||
panic!("tried to get a delegate that was not the one Winit has registered")
|
||||
}
|
||||
}
|
||||
|
||||
/// Place the event handler in the application delegate for the duration
|
||||
/// of the given closure.
|
||||
pub fn set_event_handler<R>(
|
||||
&self,
|
||||
handler: &mut dyn ApplicationHandler,
|
||||
closure: impl FnOnce() -> R,
|
||||
) -> R {
|
||||
self.ivars().event_handler.set(handler, closure)
|
||||
}
|
||||
|
||||
/// If `pump_events` is called to progress the event loop then we
|
||||
/// bootstrap the event loop via `-[NSApplication run]` but will use
|
||||
/// `CFRunLoopRunInMode` for subsequent calls to `pump_events`.
|
||||
pub fn set_stop_on_launch(&self) {
|
||||
self.ivars().stop_on_launch.set(true);
|
||||
}
|
||||
|
||||
pub fn set_stop_before_wait(&self, value: bool) {
|
||||
self.ivars().stop_before_wait.set(value)
|
||||
}
|
||||
|
||||
pub fn set_stop_after_wait(&self, value: bool) {
|
||||
self.ivars().stop_after_wait.set(value)
|
||||
}
|
||||
|
||||
pub fn set_stop_on_redraw(&self, value: bool) {
|
||||
self.ivars().stop_on_redraw.set(value)
|
||||
}
|
||||
|
||||
pub fn set_wait_timeout(&self, value: Option<Instant>) {
|
||||
self.ivars().wait_timeout.set(value)
|
||||
}
|
||||
|
||||
/// Clears the `running` state and resets the `control_flow` state when an `EventLoop` exits.
|
||||
///
|
||||
/// NOTE: that if the `NSApplication` has been launched then that state is preserved,
|
||||
/// and we won't need to re-launch the app if subsequent EventLoops are run.
|
||||
pub fn internal_exit(&self) {
|
||||
self.with_handler(|app, event_loop| {
|
||||
app.exiting(event_loop);
|
||||
});
|
||||
|
||||
self.set_is_running(false);
|
||||
self.set_stop_on_redraw(false);
|
||||
self.set_stop_before_wait(false);
|
||||
self.set_stop_after_wait(false);
|
||||
self.set_wait_timeout(None);
|
||||
}
|
||||
|
||||
pub fn is_launched(&self) -> bool {
|
||||
self.ivars().is_launched.get()
|
||||
}
|
||||
|
||||
pub fn set_is_running(&self, value: bool) {
|
||||
self.ivars().is_running.set(value)
|
||||
}
|
||||
|
||||
pub fn is_running(&self) -> bool {
|
||||
self.ivars().is_running.get()
|
||||
}
|
||||
|
||||
pub fn exit(&self) {
|
||||
self.ivars().exit.set(true)
|
||||
}
|
||||
|
||||
pub fn clear_exit(&self) {
|
||||
self.ivars().exit.set(false)
|
||||
}
|
||||
|
||||
pub fn exiting(&self) -> bool {
|
||||
self.ivars().exit.get()
|
||||
}
|
||||
|
||||
pub fn set_control_flow(&self, value: ControlFlow) {
|
||||
self.ivars().control_flow.set(value)
|
||||
}
|
||||
|
||||
pub fn control_flow(&self) -> ControlFlow {
|
||||
self.ivars().control_flow.get()
|
||||
}
|
||||
|
||||
pub fn handle_redraw(&self, window_id: WindowId) {
|
||||
let mtm = MainThreadMarker::from(self);
|
||||
// Redraw request might come out of order from the OS.
|
||||
// -> Don't go back into the event handler when our callstack originates from there
|
||||
if !self.ivars().event_handler.in_use() {
|
||||
self.with_handler(|app, event_loop| {
|
||||
app.window_event(event_loop, RootWindowId(window_id), WindowEvent::RedrawRequested);
|
||||
});
|
||||
|
||||
// `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 self.ivars().stop_on_redraw.get() {
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
stop_app_immediately(&app);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn queue_redraw(&self, window_id: WindowId) {
|
||||
let mut pending_redraw = self.ivars().pending_redraw.borrow_mut();
|
||||
if !pending_redraw.contains(&window_id) {
|
||||
pending_redraw.push(window_id);
|
||||
}
|
||||
self.ivars().run_loop.wakeup();
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
pub fn maybe_queue_with_handler(
|
||||
&self,
|
||||
callback: impl FnOnce(&mut dyn ApplicationHandler, &RootActiveEventLoop) + 'static,
|
||||
) {
|
||||
// Most programmer actions in AppKit (e.g. change window fullscreen, set focused, etc.)
|
||||
// result in an event being queued, and applied at a later point.
|
||||
//
|
||||
// However, it is not documented which actions do this, and which ones are done immediately,
|
||||
// so to make sure that we don't encounter re-entrancy issues, we first check if we're
|
||||
// currently handling another event, and if we are, we queue the event instead.
|
||||
if !self.ivars().event_handler.in_use() {
|
||||
self.with_handler(callback);
|
||||
} else {
|
||||
tracing::debug!("had to queue event since another is currently being handled");
|
||||
let this = self.retain();
|
||||
self.ivars().run_loop.queue_closure(move || {
|
||||
this.with_handler(callback);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn with_handler(
|
||||
&self,
|
||||
callback: impl FnOnce(&mut dyn ApplicationHandler, &RootActiveEventLoop),
|
||||
) {
|
||||
let event_loop = ActiveEventLoop::new_root(self.retain());
|
||||
self.ivars().event_handler.handle(callback, &event_loop);
|
||||
}
|
||||
|
||||
/// dispatch `NewEvents(Init)` + `Resumed`
|
||||
pub fn dispatch_init_events(&self) {
|
||||
self.with_handler(|app, event_loop| app.new_events(event_loop, 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.
|
||||
self.with_handler(|app, event_loop| app.resumed(event_loop));
|
||||
}
|
||||
|
||||
// Called by RunLoopObserver after finishing waiting for new events
|
||||
pub fn wakeup(&self, panic_info: Weak<PanicInfo>) {
|
||||
let mtm = MainThreadMarker::from(self);
|
||||
let panic_info = panic_info
|
||||
.upgrade()
|
||||
.expect("The panic info must exist here. This failure indicates a developer error.");
|
||||
|
||||
// Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779
|
||||
if panic_info.is_panicking() || !self.ivars().event_handler.ready() || !self.is_running() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.ivars().stop_after_wait.get() {
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
stop_app_immediately(&app);
|
||||
}
|
||||
|
||||
let start = self.ivars().start_time.get().unwrap();
|
||||
let cause = match self.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) }
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
self.with_handler(|app, event_loop| app.new_events(event_loop, cause));
|
||||
}
|
||||
|
||||
// Called by RunLoopObserver before waiting for new events
|
||||
pub fn cleared(&self, panic_info: Weak<PanicInfo>) {
|
||||
let mtm = MainThreadMarker::from(self);
|
||||
let panic_info = panic_info
|
||||
.upgrade()
|
||||
.expect("The panic info must exist here. This failure indicates a developer error.");
|
||||
|
||||
// Return when in event handler due to https://github.com/rust-windowing/winit/issues/1779
|
||||
// XXX: how does it make sense that `event_handler.ready()` can ever return `false` here if
|
||||
// we're about to return to the `CFRunLoop` to poll for new events?
|
||||
if panic_info.is_panicking() || !self.ivars().event_handler.ready() || !self.is_running() {
|
||||
return;
|
||||
}
|
||||
|
||||
if self.ivars().proxy_wake_up.swap(false, AtomicOrdering::Relaxed) {
|
||||
self.with_handler(|app, event_loop| app.proxy_wake_up(event_loop));
|
||||
}
|
||||
|
||||
let redraw = mem::take(&mut *self.ivars().pending_redraw.borrow_mut());
|
||||
for window_id in redraw {
|
||||
self.with_handler(|app, event_loop| {
|
||||
app.window_event(event_loop, RootWindowId(window_id), WindowEvent::RedrawRequested);
|
||||
});
|
||||
}
|
||||
self.with_handler(|app, event_loop| {
|
||||
app.about_to_wait(event_loop);
|
||||
});
|
||||
|
||||
if self.exiting() {
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
stop_app_immediately(&app);
|
||||
}
|
||||
|
||||
if self.ivars().stop_before_wait.get() {
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
stop_app_immediately(&app);
|
||||
}
|
||||
self.ivars().start_time.set(Some(Instant::now()));
|
||||
let wait_timeout = self.ivars().wait_timeout.get(); // configured by pump_events
|
||||
let app_timeout = match self.control_flow() {
|
||||
ControlFlow::Wait => None,
|
||||
ControlFlow::Poll => Some(Instant::now()),
|
||||
ControlFlow::WaitUntil(instant) => Some(instant),
|
||||
};
|
||||
self.ivars().waker.borrow_mut().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 visibility status
|
||||
// TODO: Also filter on the type/"level" of the window, and maybe other things?
|
||||
if window.isVisible() {
|
||||
tracing::trace!("Activating visible window");
|
||||
window.makeKeyAndOrderFront(None);
|
||||
} else {
|
||||
tracing::trace!("Skipping activating invisible window");
|
||||
}
|
||||
})
|
||||
}
|
||||
225
src/platform_impl/apple/appkit/cursor.rs
Normal file
225
src/platform_impl/apple/appkit/cursor.rs
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
use std::ffi::c_uchar;
|
||||
use std::slice;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::Sel;
|
||||
use objc2::{msg_send_id, sel, ClassType};
|
||||
use objc2_app_kit::{NSBitmapImageRep, NSCursor, NSDeviceRGBColorSpace, NSImage};
|
||||
use objc2_foundation::{
|
||||
ns_string, NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize,
|
||||
NSString,
|
||||
};
|
||||
|
||||
use crate::cursor::{CursorImage, OnlyCursorImageSource};
|
||||
use crate::window::CursorIcon;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
pub struct CustomCursor(pub(crate) Retained<NSCursor>);
|
||||
|
||||
// SAFETY: NSCursor is immutable and thread-safe
|
||||
// TODO(madsmtm): Put this logic in objc2-app-kit itself
|
||||
unsafe impl Send for CustomCursor {}
|
||||
unsafe impl Sync for CustomCursor {}
|
||||
|
||||
impl CustomCursor {
|
||||
pub(crate) fn new(cursor: OnlyCursorImageSource) -> CustomCursor {
|
||||
Self(cursor_from_image(&cursor.0))
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_from_image(cursor: &CursorImage) -> Retained<NSCursor> {
|
||||
let width = cursor.width;
|
||||
let height = cursor.height;
|
||||
|
||||
let bitmap = unsafe {
|
||||
NSBitmapImageRep::initWithBitmapDataPlanes_pixelsWide_pixelsHigh_bitsPerSample_samplesPerPixel_hasAlpha_isPlanar_colorSpaceName_bytesPerRow_bitsPerPixel(
|
||||
NSBitmapImageRep::alloc(),
|
||||
std::ptr::null_mut::<*mut c_uchar>(),
|
||||
width as isize,
|
||||
height as isize,
|
||||
8,
|
||||
4,
|
||||
true,
|
||||
false,
|
||||
NSDeviceRGBColorSpace,
|
||||
width as isize * 4,
|
||||
32,
|
||||
).unwrap()
|
||||
};
|
||||
let bitmap_data = unsafe { slice::from_raw_parts_mut(bitmap.bitmapData(), cursor.rgba.len()) };
|
||||
bitmap_data.copy_from_slice(&cursor.rgba);
|
||||
|
||||
let image = unsafe {
|
||||
NSImage::initWithSize(NSImage::alloc(), NSSize::new(width.into(), height.into()))
|
||||
};
|
||||
unsafe { image.addRepresentation(&bitmap) };
|
||||
|
||||
let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64);
|
||||
|
||||
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot)
|
||||
}
|
||||
|
||||
pub(crate) fn default_cursor() -> Retained<NSCursor> {
|
||||
NSCursor::arrowCursor()
|
||||
}
|
||||
|
||||
unsafe fn try_cursor_from_selector(sel: Sel) -> Option<Retained<NSCursor>> {
|
||||
let cls = NSCursor::class();
|
||||
if cls.responds_to(sel) {
|
||||
let cursor: Retained<NSCursor> = unsafe { msg_send_id![cls, performSelector: sel] };
|
||||
Some(cursor)
|
||||
} else {
|
||||
tracing::warn!("cursor `{sel}` appears to be invalid");
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! def_undocumented_cursor {
|
||||
{$(
|
||||
$(#[$($m:meta)*])*
|
||||
fn $name:ident();
|
||||
)*} => {$(
|
||||
$(#[$($m)*])*
|
||||
#[allow(non_snake_case)]
|
||||
fn $name() -> Retained<NSCursor> {
|
||||
unsafe { try_cursor_from_selector(sel!($name)).unwrap_or_else(|| default_cursor()) }
|
||||
}
|
||||
)*};
|
||||
}
|
||||
|
||||
def_undocumented_cursor!(
|
||||
// Undocumented cursors: https://stackoverflow.com/a/46635398/5435443
|
||||
fn _helpCursor();
|
||||
fn _zoomInCursor();
|
||||
fn _zoomOutCursor();
|
||||
fn _windowResizeNorthEastCursor();
|
||||
fn _windowResizeNorthWestCursor();
|
||||
fn _windowResizeSouthEastCursor();
|
||||
fn _windowResizeSouthWestCursor();
|
||||
fn _windowResizeNorthEastSouthWestCursor();
|
||||
fn _windowResizeNorthWestSouthEastCursor();
|
||||
|
||||
// While these two are available, the former just loads a white arrow,
|
||||
// and the latter loads an ugly deflated beachball!
|
||||
// pub fn _moveCursor();
|
||||
// pub fn _waitCursor();
|
||||
|
||||
// An even more undocumented cursor...
|
||||
// https://bugs.eclipse.org/bugs/show_bug.cgi?id=522349
|
||||
fn busyButClickableCursor();
|
||||
);
|
||||
|
||||
// Note that loading `busybutclickable` with this code won't animate
|
||||
// the frames; instead you'll just get them all in a column.
|
||||
unsafe fn load_webkit_cursor(name: &NSString) -> Retained<NSCursor> {
|
||||
// Snatch a cursor from WebKit; They fit the style of the native
|
||||
// cursors, and will seem completely standard to macOS users.
|
||||
//
|
||||
// https://stackoverflow.com/a/21786835/5435443
|
||||
let root = ns_string!(
|
||||
"/System/Library/Frameworks/ApplicationServices.framework/Versions/A/Frameworks/\
|
||||
HIServices.framework/Versions/A/Resources/cursors"
|
||||
);
|
||||
let cursor_path = root.stringByAppendingPathComponent(name);
|
||||
|
||||
let pdf_path = cursor_path.stringByAppendingPathComponent(ns_string!("cursor.pdf"));
|
||||
let image = NSImage::initByReferencingFile(NSImage::alloc(), &pdf_path).unwrap();
|
||||
|
||||
// TODO: Handle PLists better
|
||||
let info_path = cursor_path.stringByAppendingPathComponent(ns_string!("info.plist"));
|
||||
let info: Retained<NSDictionary<NSObject, NSObject>> = unsafe {
|
||||
msg_send_id![
|
||||
<NSDictionary<NSObject, NSObject>>::class(),
|
||||
dictionaryWithContentsOfFile: &*info_path,
|
||||
]
|
||||
};
|
||||
let mut x = 0.0;
|
||||
if let Some(n) = info.get(&*ns_string!("hotx")) {
|
||||
if n.is_kind_of::<NSNumber>() {
|
||||
let ptr: *const NSObject = n;
|
||||
let ptr: *const NSNumber = ptr.cast();
|
||||
x = unsafe { &*ptr }.as_cgfloat()
|
||||
}
|
||||
}
|
||||
let mut y = 0.0;
|
||||
if let Some(n) = info.get(&*ns_string!("hotx")) {
|
||||
if n.is_kind_of::<NSNumber>() {
|
||||
let ptr: *const NSObject = n;
|
||||
let ptr: *const NSNumber = ptr.cast();
|
||||
y = unsafe { &*ptr }.as_cgfloat()
|
||||
}
|
||||
}
|
||||
|
||||
let hotspot = NSPoint::new(x, y);
|
||||
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot)
|
||||
}
|
||||
|
||||
fn webkit_move() -> Retained<NSCursor> {
|
||||
unsafe { load_webkit_cursor(ns_string!("move")) }
|
||||
}
|
||||
|
||||
fn webkit_cell() -> Retained<NSCursor> {
|
||||
unsafe { load_webkit_cursor(ns_string!("cell")) }
|
||||
}
|
||||
|
||||
pub(crate) fn invisible_cursor() -> Retained<NSCursor> {
|
||||
// 16x16 GIF data for invisible cursor
|
||||
// You can reproduce this via ImageMagick.
|
||||
// $ convert -size 16x16 xc:none cursor.gif
|
||||
static CURSOR_BYTES: &[u8] = &[
|
||||
0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x10, 0x00, 0x10, 0x00, 0xf0, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x21, 0xf9, 0x04, 0x01, 0x00, 0x00, 0x00, 0x00, 0x2c, 0x00, 0x00,
|
||||
0x00, 0x00, 0x10, 0x00, 0x10, 0x00, 0x00, 0x02, 0x0e, 0x84, 0x8f, 0xa9, 0xcb, 0xed, 0x0f,
|
||||
0xa3, 0x9c, 0xb4, 0xda, 0x8b, 0xb3, 0x3e, 0x05, 0x00, 0x3b,
|
||||
];
|
||||
|
||||
fn new_invisible() -> Retained<NSCursor> {
|
||||
// TODO: Consider using `dataWithBytesNoCopy:`
|
||||
let data = NSData::with_bytes(CURSOR_BYTES);
|
||||
let image = NSImage::initWithData(NSImage::alloc(), &data).unwrap();
|
||||
let hotspot = NSPoint::new(0.0, 0.0);
|
||||
NSCursor::initWithImage_hotSpot(NSCursor::alloc(), &image, hotspot)
|
||||
}
|
||||
|
||||
// Cache this for efficiency
|
||||
static CURSOR: OnceLock<CustomCursor> = OnceLock::new();
|
||||
CURSOR.get_or_init(|| CustomCursor(new_invisible())).0.clone()
|
||||
}
|
||||
|
||||
pub(crate) fn cursor_from_icon(icon: CursorIcon) -> Retained<NSCursor> {
|
||||
match icon {
|
||||
CursorIcon::Default => default_cursor(),
|
||||
CursorIcon::Pointer => NSCursor::pointingHandCursor(),
|
||||
CursorIcon::Grab => NSCursor::openHandCursor(),
|
||||
CursorIcon::Grabbing => NSCursor::closedHandCursor(),
|
||||
CursorIcon::Text => NSCursor::IBeamCursor(),
|
||||
CursorIcon::VerticalText => NSCursor::IBeamCursorForVerticalLayout(),
|
||||
CursorIcon::Copy => NSCursor::dragCopyCursor(),
|
||||
CursorIcon::Alias => NSCursor::dragLinkCursor(),
|
||||
CursorIcon::NotAllowed | CursorIcon::NoDrop => NSCursor::operationNotAllowedCursor(),
|
||||
CursorIcon::ContextMenu => NSCursor::contextualMenuCursor(),
|
||||
CursorIcon::Crosshair => NSCursor::crosshairCursor(),
|
||||
CursorIcon::EResize => NSCursor::resizeRightCursor(),
|
||||
CursorIcon::NResize => NSCursor::resizeUpCursor(),
|
||||
CursorIcon::WResize => NSCursor::resizeLeftCursor(),
|
||||
CursorIcon::SResize => NSCursor::resizeDownCursor(),
|
||||
CursorIcon::EwResize | CursorIcon::ColResize => NSCursor::resizeLeftRightCursor(),
|
||||
CursorIcon::NsResize | CursorIcon::RowResize => NSCursor::resizeUpDownCursor(),
|
||||
CursorIcon::Help => _helpCursor(),
|
||||
CursorIcon::ZoomIn => _zoomInCursor(),
|
||||
CursorIcon::ZoomOut => _zoomOutCursor(),
|
||||
CursorIcon::NeResize => _windowResizeNorthEastCursor(),
|
||||
CursorIcon::NwResize => _windowResizeNorthWestCursor(),
|
||||
CursorIcon::SeResize => _windowResizeSouthEastCursor(),
|
||||
CursorIcon::SwResize => _windowResizeSouthWestCursor(),
|
||||
CursorIcon::NeswResize => _windowResizeNorthEastSouthWestCursor(),
|
||||
CursorIcon::NwseResize => _windowResizeNorthWestSouthEastCursor(),
|
||||
// This is the wrong semantics for `Wait`, but it's the same as
|
||||
// what's used in Safari and Chrome.
|
||||
CursorIcon::Wait | CursorIcon::Progress => busyButClickableCursor(),
|
||||
CursorIcon::Move | CursorIcon::AllScroll => webkit_move(),
|
||||
CursorIcon::Cell => webkit_cell(),
|
||||
_ => default_cursor(),
|
||||
}
|
||||
}
|
||||
626
src/platform_impl/apple/appkit/event.rs
Normal file
626
src/platform_impl/apple/appkit/event.rs
Normal file
|
|
@ -0,0 +1,626 @@
|
|||
use std::ffi::c_void;
|
||||
|
||||
use core_foundation::base::CFRelease;
|
||||
use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
|
||||
use objc2::rc::Retained;
|
||||
use objc2_app_kit::{NSEvent, NSEventModifierFlags, NSEventSubtype, NSEventType};
|
||||
use objc2_foundation::{run_on_main, NSPoint};
|
||||
use smol_str::SmolStr;
|
||||
|
||||
use super::ffi;
|
||||
use crate::event::{ElementState, KeyEvent, Modifiers};
|
||||
use crate::keyboard::{
|
||||
Key, KeyCode, KeyLocation, ModifiersKeys, ModifiersState, NamedKey, NativeKey, NativeKeyCode,
|
||||
PhysicalKey,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct KeyEventExtra {
|
||||
pub text_with_all_modifiers: Option<SmolStr>,
|
||||
pub key_without_modifiers: Key,
|
||||
}
|
||||
|
||||
/// Ignores ALL modifiers.
|
||||
pub fn get_modifierless_char(scancode: u16) -> Key {
|
||||
let mut string = [0; 16];
|
||||
let input_source;
|
||||
let layout;
|
||||
unsafe {
|
||||
input_source = ffi::TISCopyCurrentKeyboardLayoutInputSource();
|
||||
if input_source.is_null() {
|
||||
tracing::error!("`TISCopyCurrentKeyboardLayoutInputSource` returned null ptr");
|
||||
return Key::Unidentified(NativeKey::MacOS(scancode));
|
||||
}
|
||||
let layout_data =
|
||||
ffi::TISGetInputSourceProperty(input_source, ffi::kTISPropertyUnicodeKeyLayoutData);
|
||||
if layout_data.is_null() {
|
||||
CFRelease(input_source as *mut c_void);
|
||||
tracing::error!("`TISGetInputSourceProperty` returned null ptr");
|
||||
return Key::Unidentified(NativeKey::MacOS(scancode));
|
||||
}
|
||||
layout = CFDataGetBytePtr(layout_data as CFDataRef) as *const ffi::UCKeyboardLayout;
|
||||
}
|
||||
let keyboard_type = run_on_main(|_mtm| unsafe { ffi::LMGetKbdType() });
|
||||
|
||||
let mut result_len = 0;
|
||||
let mut dead_keys = 0;
|
||||
let modifiers = 0;
|
||||
let translate_result = unsafe {
|
||||
ffi::UCKeyTranslate(
|
||||
layout,
|
||||
scancode,
|
||||
ffi::kUCKeyActionDisplay,
|
||||
modifiers,
|
||||
keyboard_type as u32,
|
||||
ffi::kUCKeyTranslateNoDeadKeysMask,
|
||||
&mut dead_keys,
|
||||
string.len() as ffi::UniCharCount,
|
||||
&mut result_len,
|
||||
string.as_mut_ptr(),
|
||||
)
|
||||
};
|
||||
unsafe {
|
||||
CFRelease(input_source as *mut c_void);
|
||||
}
|
||||
if translate_result != 0 {
|
||||
tracing::error!("`UCKeyTranslate` returned with the non-zero value: {}", translate_result);
|
||||
return Key::Unidentified(NativeKey::MacOS(scancode));
|
||||
}
|
||||
if result_len == 0 {
|
||||
// This is fine - not all keys have text representation.
|
||||
// For instance, users that have mapped the `Fn` key to toggle
|
||||
// keyboard layouts will hit this code path.
|
||||
return Key::Unidentified(NativeKey::MacOS(scancode));
|
||||
}
|
||||
let chars = String::from_utf16_lossy(&string[0..result_len as usize]);
|
||||
Key::Character(SmolStr::new(chars))
|
||||
}
|
||||
|
||||
// Ignores all modifiers except for SHIFT (yes, even ALT is ignored).
|
||||
fn get_logical_key_char(ns_event: &NSEvent, modifierless_chars: &str) -> Key {
|
||||
let string = unsafe { ns_event.charactersIgnoringModifiers() }
|
||||
.map(|s| s.to_string())
|
||||
.unwrap_or_default();
|
||||
if string.is_empty() {
|
||||
// Probably a dead key
|
||||
let first_char = modifierless_chars.chars().next();
|
||||
return Key::Dead(first_char);
|
||||
}
|
||||
Key::Character(SmolStr::new(string))
|
||||
}
|
||||
|
||||
/// Create `KeyEvent` for the given `NSEvent`.
|
||||
///
|
||||
/// This function shouldn't be called when the IME input is in process.
|
||||
pub(crate) fn create_key_event(
|
||||
ns_event: &NSEvent,
|
||||
is_press: bool,
|
||||
is_repeat: bool,
|
||||
key_override: Option<PhysicalKey>,
|
||||
) -> KeyEvent {
|
||||
use ElementState::{Pressed, Released};
|
||||
let state = if is_press { Pressed } else { Released };
|
||||
|
||||
let scancode = unsafe { ns_event.keyCode() };
|
||||
let mut physical_key = key_override.unwrap_or_else(|| scancode_to_physicalkey(scancode as u32));
|
||||
|
||||
// NOTE: The logical key should heed both SHIFT and ALT if possible.
|
||||
// For instance:
|
||||
// * Pressing the A key: logical key should be "a"
|
||||
// * Pressing SHIFT A: logical key should be "A"
|
||||
// * Pressing CTRL SHIFT A: logical key should also be "A"
|
||||
// This is not easy to tease out of `NSEvent`, but we do our best.
|
||||
|
||||
let text_with_all_modifiers: Option<SmolStr> = if key_override.is_some() {
|
||||
None
|
||||
} else {
|
||||
let characters =
|
||||
unsafe { ns_event.characters() }.map(|s| s.to_string()).unwrap_or_default();
|
||||
if characters.is_empty() {
|
||||
None
|
||||
} else {
|
||||
if matches!(physical_key, PhysicalKey::Unidentified(_)) {
|
||||
// The key may be one of the funky function keys
|
||||
physical_key = extra_function_key_to_code(scancode, &characters);
|
||||
}
|
||||
Some(SmolStr::new(characters))
|
||||
}
|
||||
};
|
||||
|
||||
let key_from_code = code_to_key(physical_key, scancode);
|
||||
let (logical_key, key_without_modifiers) = if matches!(key_from_code, Key::Unidentified(_)) {
|
||||
// `get_modifierless_char/key_without_modifiers` ignores ALL modifiers.
|
||||
let key_without_modifiers = get_modifierless_char(scancode);
|
||||
|
||||
let modifiers = unsafe { ns_event.modifierFlags() };
|
||||
let has_ctrl = modifiers.contains(NSEventModifierFlags::NSEventModifierFlagControl);
|
||||
let has_cmd = modifiers.contains(NSEventModifierFlags::NSEventModifierFlagCommand);
|
||||
|
||||
let logical_key = match text_with_all_modifiers.as_ref() {
|
||||
// Only checking for ctrl and cmd here, not checking for alt because we DO want to
|
||||
// include its effect in the key. For example if -on the Germay layout- one
|
||||
// presses alt+8, the logical key should be "{"
|
||||
// Also not checking if this is a release event because then this issue would
|
||||
// still affect the key release.
|
||||
Some(text) if !has_ctrl && !has_cmd => {
|
||||
// Character heeding both SHIFT and ALT.
|
||||
Key::Character(text.clone())
|
||||
},
|
||||
|
||||
_ => match key_without_modifiers.as_ref() {
|
||||
// Character heeding just SHIFT, ignoring ALT.
|
||||
Key::Character(ch) => get_logical_key_char(ns_event, ch),
|
||||
|
||||
// Character ignoring ALL modifiers.
|
||||
_ => key_without_modifiers.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
(logical_key, key_without_modifiers)
|
||||
} else {
|
||||
(key_from_code.clone(), key_from_code)
|
||||
};
|
||||
|
||||
let text = if is_press { logical_key.to_text().map(SmolStr::new) } else { None };
|
||||
|
||||
let location = code_to_location(physical_key);
|
||||
|
||||
KeyEvent {
|
||||
location,
|
||||
logical_key,
|
||||
physical_key,
|
||||
repeat: is_repeat,
|
||||
state,
|
||||
text,
|
||||
platform_specific: KeyEventExtra { text_with_all_modifiers, key_without_modifiers },
|
||||
}
|
||||
}
|
||||
|
||||
pub fn code_to_key(key: PhysicalKey, scancode: u16) -> Key {
|
||||
let code = match key {
|
||||
PhysicalKey::Code(code) => code,
|
||||
PhysicalKey::Unidentified(code) => return Key::Unidentified(code.into()),
|
||||
};
|
||||
|
||||
Key::Named(match code {
|
||||
KeyCode::Enter => NamedKey::Enter,
|
||||
KeyCode::Tab => NamedKey::Tab,
|
||||
KeyCode::Space => NamedKey::Space,
|
||||
KeyCode::Backspace => NamedKey::Backspace,
|
||||
KeyCode::Escape => NamedKey::Escape,
|
||||
KeyCode::SuperRight => NamedKey::Super,
|
||||
KeyCode::SuperLeft => NamedKey::Super,
|
||||
KeyCode::ShiftLeft => NamedKey::Shift,
|
||||
KeyCode::AltLeft => NamedKey::Alt,
|
||||
KeyCode::ControlLeft => NamedKey::Control,
|
||||
KeyCode::ShiftRight => NamedKey::Shift,
|
||||
KeyCode::AltRight => NamedKey::Alt,
|
||||
KeyCode::ControlRight => NamedKey::Control,
|
||||
|
||||
KeyCode::NumLock => NamedKey::NumLock,
|
||||
KeyCode::AudioVolumeUp => NamedKey::AudioVolumeUp,
|
||||
KeyCode::AudioVolumeDown => NamedKey::AudioVolumeDown,
|
||||
|
||||
// Other numpad keys all generate text on macOS (if I understand correctly)
|
||||
KeyCode::NumpadEnter => NamedKey::Enter,
|
||||
|
||||
KeyCode::F1 => NamedKey::F1,
|
||||
KeyCode::F2 => NamedKey::F2,
|
||||
KeyCode::F3 => NamedKey::F3,
|
||||
KeyCode::F4 => NamedKey::F4,
|
||||
KeyCode::F5 => NamedKey::F5,
|
||||
KeyCode::F6 => NamedKey::F6,
|
||||
KeyCode::F7 => NamedKey::F7,
|
||||
KeyCode::F8 => NamedKey::F8,
|
||||
KeyCode::F9 => NamedKey::F9,
|
||||
KeyCode::F10 => NamedKey::F10,
|
||||
KeyCode::F11 => NamedKey::F11,
|
||||
KeyCode::F12 => NamedKey::F12,
|
||||
KeyCode::F13 => NamedKey::F13,
|
||||
KeyCode::F14 => NamedKey::F14,
|
||||
KeyCode::F15 => NamedKey::F15,
|
||||
KeyCode::F16 => NamedKey::F16,
|
||||
KeyCode::F17 => NamedKey::F17,
|
||||
KeyCode::F18 => NamedKey::F18,
|
||||
KeyCode::F19 => NamedKey::F19,
|
||||
KeyCode::F20 => NamedKey::F20,
|
||||
|
||||
KeyCode::Insert => NamedKey::Insert,
|
||||
KeyCode::Home => NamedKey::Home,
|
||||
KeyCode::PageUp => NamedKey::PageUp,
|
||||
KeyCode::Delete => NamedKey::Delete,
|
||||
KeyCode::End => NamedKey::End,
|
||||
KeyCode::PageDown => NamedKey::PageDown,
|
||||
KeyCode::ArrowLeft => NamedKey::ArrowLeft,
|
||||
KeyCode::ArrowRight => NamedKey::ArrowRight,
|
||||
KeyCode::ArrowDown => NamedKey::ArrowDown,
|
||||
KeyCode::ArrowUp => NamedKey::ArrowUp,
|
||||
_ => return Key::Unidentified(NativeKey::MacOS(scancode)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn code_to_location(key: PhysicalKey) -> KeyLocation {
|
||||
let code = match key {
|
||||
PhysicalKey::Code(code) => code,
|
||||
PhysicalKey::Unidentified(_) => return KeyLocation::Standard,
|
||||
};
|
||||
|
||||
match code {
|
||||
KeyCode::SuperRight => KeyLocation::Right,
|
||||
KeyCode::SuperLeft => KeyLocation::Left,
|
||||
KeyCode::ShiftLeft => KeyLocation::Left,
|
||||
KeyCode::AltLeft => KeyLocation::Left,
|
||||
KeyCode::ControlLeft => KeyLocation::Left,
|
||||
KeyCode::ShiftRight => KeyLocation::Right,
|
||||
KeyCode::AltRight => KeyLocation::Right,
|
||||
KeyCode::ControlRight => KeyLocation::Right,
|
||||
|
||||
KeyCode::NumLock => KeyLocation::Numpad,
|
||||
KeyCode::NumpadDecimal => KeyLocation::Numpad,
|
||||
KeyCode::NumpadMultiply => KeyLocation::Numpad,
|
||||
KeyCode::NumpadAdd => KeyLocation::Numpad,
|
||||
KeyCode::NumpadDivide => KeyLocation::Numpad,
|
||||
KeyCode::NumpadEnter => KeyLocation::Numpad,
|
||||
KeyCode::NumpadSubtract => KeyLocation::Numpad,
|
||||
KeyCode::NumpadEqual => KeyLocation::Numpad,
|
||||
KeyCode::Numpad0 => KeyLocation::Numpad,
|
||||
KeyCode::Numpad1 => KeyLocation::Numpad,
|
||||
KeyCode::Numpad2 => KeyLocation::Numpad,
|
||||
KeyCode::Numpad3 => KeyLocation::Numpad,
|
||||
KeyCode::Numpad4 => KeyLocation::Numpad,
|
||||
KeyCode::Numpad5 => KeyLocation::Numpad,
|
||||
KeyCode::Numpad6 => KeyLocation::Numpad,
|
||||
KeyCode::Numpad7 => KeyLocation::Numpad,
|
||||
KeyCode::Numpad8 => KeyLocation::Numpad,
|
||||
KeyCode::Numpad9 => KeyLocation::Numpad,
|
||||
|
||||
_ => KeyLocation::Standard,
|
||||
}
|
||||
}
|
||||
|
||||
// While F1-F20 have scancodes we can match on, we have to check against UTF-16
|
||||
// constants for the rest.
|
||||
// https://developer.apple.com/documentation/appkit/1535851-function-key_unicodes?preferredLanguage=occ
|
||||
pub fn extra_function_key_to_code(scancode: u16, string: &str) -> PhysicalKey {
|
||||
if let Some(ch) = string.encode_utf16().next() {
|
||||
match ch {
|
||||
0xf718 => PhysicalKey::Code(KeyCode::F21),
|
||||
0xf719 => PhysicalKey::Code(KeyCode::F22),
|
||||
0xf71a => PhysicalKey::Code(KeyCode::F23),
|
||||
0xf71b => PhysicalKey::Code(KeyCode::F24),
|
||||
_ => PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode)),
|
||||
}
|
||||
} else {
|
||||
PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode))
|
||||
}
|
||||
}
|
||||
|
||||
// The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259
|
||||
const NX_DEVICELCTLKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000001);
|
||||
const NX_DEVICELSHIFTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000002);
|
||||
const NX_DEVICERSHIFTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000004);
|
||||
const NX_DEVICELCMDKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000008);
|
||||
const NX_DEVICERCMDKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000010);
|
||||
const NX_DEVICELALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000020);
|
||||
const NX_DEVICERALTKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00000040);
|
||||
const NX_DEVICERCTLKEYMASK: NSEventModifierFlags = NSEventModifierFlags(0x00002000);
|
||||
|
||||
pub(super) fn lalt_pressed(event: &NSEvent) -> bool {
|
||||
unsafe { event.modifierFlags() }.contains(NX_DEVICELALTKEYMASK)
|
||||
}
|
||||
|
||||
pub(super) fn ralt_pressed(event: &NSEvent) -> bool {
|
||||
unsafe { event.modifierFlags() }.contains(NX_DEVICERALTKEYMASK)
|
||||
}
|
||||
|
||||
pub(super) fn event_mods(event: &NSEvent) -> Modifiers {
|
||||
let flags = unsafe { event.modifierFlags() };
|
||||
let mut state = ModifiersState::empty();
|
||||
let mut pressed_mods = ModifiersKeys::empty();
|
||||
|
||||
state
|
||||
.set(ModifiersState::SHIFT, flags.contains(NSEventModifierFlags::NSEventModifierFlagShift));
|
||||
pressed_mods.set(ModifiersKeys::LSHIFT, flags.contains(NX_DEVICELSHIFTKEYMASK));
|
||||
pressed_mods.set(ModifiersKeys::RSHIFT, flags.contains(NX_DEVICERSHIFTKEYMASK));
|
||||
|
||||
state.set(
|
||||
ModifiersState::CONTROL,
|
||||
flags.contains(NSEventModifierFlags::NSEventModifierFlagControl),
|
||||
);
|
||||
pressed_mods.set(ModifiersKeys::LCONTROL, flags.contains(NX_DEVICELCTLKEYMASK));
|
||||
pressed_mods.set(ModifiersKeys::RCONTROL, flags.contains(NX_DEVICERCTLKEYMASK));
|
||||
|
||||
state.set(ModifiersState::ALT, flags.contains(NSEventModifierFlags::NSEventModifierFlagOption));
|
||||
pressed_mods.set(ModifiersKeys::LALT, flags.contains(NX_DEVICELALTKEYMASK));
|
||||
pressed_mods.set(ModifiersKeys::RALT, flags.contains(NX_DEVICERALTKEYMASK));
|
||||
|
||||
state.set(
|
||||
ModifiersState::SUPER,
|
||||
flags.contains(NSEventModifierFlags::NSEventModifierFlagCommand),
|
||||
);
|
||||
pressed_mods.set(ModifiersKeys::LSUPER, flags.contains(NX_DEVICELCMDKEYMASK));
|
||||
pressed_mods.set(ModifiersKeys::RSUPER, flags.contains(NX_DEVICERCMDKEYMASK));
|
||||
|
||||
Modifiers { state, pressed_mods }
|
||||
}
|
||||
|
||||
pub(super) fn dummy_event() -> Option<Retained<NSEvent>> {
|
||||
unsafe {
|
||||
NSEvent::otherEventWithType_location_modifierFlags_timestamp_windowNumber_context_subtype_data1_data2(
|
||||
NSEventType::ApplicationDefined,
|
||||
NSPoint::new(0.0, 0.0),
|
||||
NSEventModifierFlags(0),
|
||||
0.0,
|
||||
0,
|
||||
None,
|
||||
NSEventSubtype::WindowExposed.0,
|
||||
0,
|
||||
0,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn physicalkey_to_scancode(physical_key: PhysicalKey) -> Option<u32> {
|
||||
let code = match physical_key {
|
||||
PhysicalKey::Code(code) => code,
|
||||
PhysicalKey::Unidentified(_) => return None,
|
||||
};
|
||||
|
||||
match code {
|
||||
KeyCode::KeyA => Some(0x00),
|
||||
KeyCode::KeyS => Some(0x01),
|
||||
KeyCode::KeyD => Some(0x02),
|
||||
KeyCode::KeyF => Some(0x03),
|
||||
KeyCode::KeyH => Some(0x04),
|
||||
KeyCode::KeyG => Some(0x05),
|
||||
KeyCode::KeyZ => Some(0x06),
|
||||
KeyCode::KeyX => Some(0x07),
|
||||
KeyCode::KeyC => Some(0x08),
|
||||
KeyCode::KeyV => Some(0x09),
|
||||
KeyCode::KeyB => Some(0x0b),
|
||||
KeyCode::KeyQ => Some(0x0c),
|
||||
KeyCode::KeyW => Some(0x0d),
|
||||
KeyCode::KeyE => Some(0x0e),
|
||||
KeyCode::KeyR => Some(0x0f),
|
||||
KeyCode::KeyY => Some(0x10),
|
||||
KeyCode::KeyT => Some(0x11),
|
||||
KeyCode::Digit1 => Some(0x12),
|
||||
KeyCode::Digit2 => Some(0x13),
|
||||
KeyCode::Digit3 => Some(0x14),
|
||||
KeyCode::Digit4 => Some(0x15),
|
||||
KeyCode::Digit6 => Some(0x16),
|
||||
KeyCode::Digit5 => Some(0x17),
|
||||
KeyCode::Equal => Some(0x18),
|
||||
KeyCode::Digit9 => Some(0x19),
|
||||
KeyCode::Digit7 => Some(0x1a),
|
||||
KeyCode::Minus => Some(0x1b),
|
||||
KeyCode::Digit8 => Some(0x1c),
|
||||
KeyCode::Digit0 => Some(0x1d),
|
||||
KeyCode::BracketRight => Some(0x1e),
|
||||
KeyCode::KeyO => Some(0x1f),
|
||||
KeyCode::KeyU => Some(0x20),
|
||||
KeyCode::BracketLeft => Some(0x21),
|
||||
KeyCode::KeyI => Some(0x22),
|
||||
KeyCode::KeyP => Some(0x23),
|
||||
KeyCode::Enter => Some(0x24),
|
||||
KeyCode::KeyL => Some(0x25),
|
||||
KeyCode::KeyJ => Some(0x26),
|
||||
KeyCode::Quote => Some(0x27),
|
||||
KeyCode::KeyK => Some(0x28),
|
||||
KeyCode::Semicolon => Some(0x29),
|
||||
KeyCode::Backslash => Some(0x2a),
|
||||
KeyCode::Comma => Some(0x2b),
|
||||
KeyCode::Slash => Some(0x2c),
|
||||
KeyCode::KeyN => Some(0x2d),
|
||||
KeyCode::KeyM => Some(0x2e),
|
||||
KeyCode::Period => Some(0x2f),
|
||||
KeyCode::Tab => Some(0x30),
|
||||
KeyCode::Space => Some(0x31),
|
||||
KeyCode::Backquote => Some(0x32),
|
||||
KeyCode::Backspace => Some(0x33),
|
||||
KeyCode::Escape => Some(0x35),
|
||||
KeyCode::SuperRight => Some(0x36),
|
||||
KeyCode::SuperLeft => Some(0x37),
|
||||
KeyCode::ShiftLeft => Some(0x38),
|
||||
KeyCode::AltLeft => Some(0x3a),
|
||||
KeyCode::ControlLeft => Some(0x3b),
|
||||
KeyCode::ShiftRight => Some(0x3c),
|
||||
KeyCode::AltRight => Some(0x3d),
|
||||
KeyCode::ControlRight => Some(0x3e),
|
||||
KeyCode::F17 => Some(0x40),
|
||||
KeyCode::NumpadDecimal => Some(0x41),
|
||||
KeyCode::NumpadMultiply => Some(0x43),
|
||||
KeyCode::NumpadAdd => Some(0x45),
|
||||
KeyCode::NumLock => Some(0x47),
|
||||
KeyCode::AudioVolumeUp => Some(0x49),
|
||||
KeyCode::AudioVolumeDown => Some(0x4a),
|
||||
KeyCode::NumpadDivide => Some(0x4b),
|
||||
KeyCode::NumpadEnter => Some(0x4c),
|
||||
KeyCode::NumpadSubtract => Some(0x4e),
|
||||
KeyCode::F18 => Some(0x4f),
|
||||
KeyCode::F19 => Some(0x50),
|
||||
KeyCode::NumpadEqual => Some(0x51),
|
||||
KeyCode::Numpad0 => Some(0x52),
|
||||
KeyCode::Numpad1 => Some(0x53),
|
||||
KeyCode::Numpad2 => Some(0x54),
|
||||
KeyCode::Numpad3 => Some(0x55),
|
||||
KeyCode::Numpad4 => Some(0x56),
|
||||
KeyCode::Numpad5 => Some(0x57),
|
||||
KeyCode::Numpad6 => Some(0x58),
|
||||
KeyCode::Numpad7 => Some(0x59),
|
||||
KeyCode::F20 => Some(0x5a),
|
||||
KeyCode::Numpad8 => Some(0x5b),
|
||||
KeyCode::Numpad9 => Some(0x5c),
|
||||
KeyCode::IntlYen => Some(0x5d),
|
||||
KeyCode::F5 => Some(0x60),
|
||||
KeyCode::F6 => Some(0x61),
|
||||
KeyCode::F7 => Some(0x62),
|
||||
KeyCode::F3 => Some(0x63),
|
||||
KeyCode::F8 => Some(0x64),
|
||||
KeyCode::F9 => Some(0x65),
|
||||
KeyCode::F11 => Some(0x67),
|
||||
KeyCode::F13 => Some(0x69),
|
||||
KeyCode::F16 => Some(0x6a),
|
||||
KeyCode::F14 => Some(0x6b),
|
||||
KeyCode::F10 => Some(0x6d),
|
||||
KeyCode::F12 => Some(0x6f),
|
||||
KeyCode::F15 => Some(0x71),
|
||||
KeyCode::Insert => Some(0x72),
|
||||
KeyCode::Home => Some(0x73),
|
||||
KeyCode::PageUp => Some(0x74),
|
||||
KeyCode::Delete => Some(0x75),
|
||||
KeyCode::F4 => Some(0x76),
|
||||
KeyCode::End => Some(0x77),
|
||||
KeyCode::F2 => Some(0x78),
|
||||
KeyCode::PageDown => Some(0x79),
|
||||
KeyCode::F1 => Some(0x7a),
|
||||
KeyCode::ArrowLeft => Some(0x7b),
|
||||
KeyCode::ArrowRight => Some(0x7c),
|
||||
KeyCode::ArrowDown => Some(0x7d),
|
||||
KeyCode::ArrowUp => Some(0x7e),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn scancode_to_physicalkey(scancode: u32) -> PhysicalKey {
|
||||
PhysicalKey::Code(match scancode {
|
||||
0x00 => KeyCode::KeyA,
|
||||
0x01 => KeyCode::KeyS,
|
||||
0x02 => KeyCode::KeyD,
|
||||
0x03 => KeyCode::KeyF,
|
||||
0x04 => KeyCode::KeyH,
|
||||
0x05 => KeyCode::KeyG,
|
||||
0x06 => KeyCode::KeyZ,
|
||||
0x07 => KeyCode::KeyX,
|
||||
0x08 => KeyCode::KeyC,
|
||||
0x09 => KeyCode::KeyV,
|
||||
// 0x0a => World 1,
|
||||
0x0b => KeyCode::KeyB,
|
||||
0x0c => KeyCode::KeyQ,
|
||||
0x0d => KeyCode::KeyW,
|
||||
0x0e => KeyCode::KeyE,
|
||||
0x0f => KeyCode::KeyR,
|
||||
0x10 => KeyCode::KeyY,
|
||||
0x11 => KeyCode::KeyT,
|
||||
0x12 => KeyCode::Digit1,
|
||||
0x13 => KeyCode::Digit2,
|
||||
0x14 => KeyCode::Digit3,
|
||||
0x15 => KeyCode::Digit4,
|
||||
0x16 => KeyCode::Digit6,
|
||||
0x17 => KeyCode::Digit5,
|
||||
0x18 => KeyCode::Equal,
|
||||
0x19 => KeyCode::Digit9,
|
||||
0x1a => KeyCode::Digit7,
|
||||
0x1b => KeyCode::Minus,
|
||||
0x1c => KeyCode::Digit8,
|
||||
0x1d => KeyCode::Digit0,
|
||||
0x1e => KeyCode::BracketRight,
|
||||
0x1f => KeyCode::KeyO,
|
||||
0x20 => KeyCode::KeyU,
|
||||
0x21 => KeyCode::BracketLeft,
|
||||
0x22 => KeyCode::KeyI,
|
||||
0x23 => KeyCode::KeyP,
|
||||
0x24 => KeyCode::Enter,
|
||||
0x25 => KeyCode::KeyL,
|
||||
0x26 => KeyCode::KeyJ,
|
||||
0x27 => KeyCode::Quote,
|
||||
0x28 => KeyCode::KeyK,
|
||||
0x29 => KeyCode::Semicolon,
|
||||
0x2a => KeyCode::Backslash,
|
||||
0x2b => KeyCode::Comma,
|
||||
0x2c => KeyCode::Slash,
|
||||
0x2d => KeyCode::KeyN,
|
||||
0x2e => KeyCode::KeyM,
|
||||
0x2f => KeyCode::Period,
|
||||
0x30 => KeyCode::Tab,
|
||||
0x31 => KeyCode::Space,
|
||||
0x32 => KeyCode::Backquote,
|
||||
0x33 => KeyCode::Backspace,
|
||||
// 0x34 => unknown,
|
||||
0x35 => KeyCode::Escape,
|
||||
0x36 => KeyCode::SuperRight,
|
||||
0x37 => KeyCode::SuperLeft,
|
||||
0x38 => KeyCode::ShiftLeft,
|
||||
0x39 => KeyCode::CapsLock,
|
||||
0x3a => KeyCode::AltLeft,
|
||||
0x3b => KeyCode::ControlLeft,
|
||||
0x3c => KeyCode::ShiftRight,
|
||||
0x3d => KeyCode::AltRight,
|
||||
0x3e => KeyCode::ControlRight,
|
||||
0x3f => KeyCode::Fn,
|
||||
0x40 => KeyCode::F17,
|
||||
0x41 => KeyCode::NumpadDecimal,
|
||||
// 0x42 -> unknown,
|
||||
0x43 => KeyCode::NumpadMultiply,
|
||||
// 0x44 => unknown,
|
||||
0x45 => KeyCode::NumpadAdd,
|
||||
// 0x46 => unknown,
|
||||
0x47 => KeyCode::NumLock,
|
||||
// 0x48 => KeyCode::NumpadClear,
|
||||
|
||||
// TODO: (Artur) for me, kVK_VolumeUp is 0x48
|
||||
// macOS 10.11
|
||||
// /System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/
|
||||
// Versions/A/Headers/Events.h
|
||||
0x49 => KeyCode::AudioVolumeUp,
|
||||
0x4a => KeyCode::AudioVolumeDown,
|
||||
0x4b => KeyCode::NumpadDivide,
|
||||
0x4c => KeyCode::NumpadEnter,
|
||||
// 0x4d => unknown,
|
||||
0x4e => KeyCode::NumpadSubtract,
|
||||
0x4f => KeyCode::F18,
|
||||
0x50 => KeyCode::F19,
|
||||
0x51 => KeyCode::NumpadEqual,
|
||||
0x52 => KeyCode::Numpad0,
|
||||
0x53 => KeyCode::Numpad1,
|
||||
0x54 => KeyCode::Numpad2,
|
||||
0x55 => KeyCode::Numpad3,
|
||||
0x56 => KeyCode::Numpad4,
|
||||
0x57 => KeyCode::Numpad5,
|
||||
0x58 => KeyCode::Numpad6,
|
||||
0x59 => KeyCode::Numpad7,
|
||||
0x5a => KeyCode::F20,
|
||||
0x5b => KeyCode::Numpad8,
|
||||
0x5c => KeyCode::Numpad9,
|
||||
0x5d => KeyCode::IntlYen,
|
||||
// 0x5e => JIS Ro,
|
||||
// 0x5f => unknown,
|
||||
0x60 => KeyCode::F5,
|
||||
0x61 => KeyCode::F6,
|
||||
0x62 => KeyCode::F7,
|
||||
0x63 => KeyCode::F3,
|
||||
0x64 => KeyCode::F8,
|
||||
0x65 => KeyCode::F9,
|
||||
// 0x66 => JIS Eisuu (macOS),
|
||||
0x67 => KeyCode::F11,
|
||||
// 0x68 => JIS Kanna (macOS),
|
||||
0x69 => KeyCode::F13,
|
||||
0x6a => KeyCode::F16,
|
||||
0x6b => KeyCode::F14,
|
||||
// 0x6c => unknown,
|
||||
0x6d => KeyCode::F10,
|
||||
// 0x6e => unknown,
|
||||
0x6f => KeyCode::F12,
|
||||
// 0x70 => unknown,
|
||||
0x71 => KeyCode::F15,
|
||||
0x72 => KeyCode::Insert,
|
||||
0x73 => KeyCode::Home,
|
||||
0x74 => KeyCode::PageUp,
|
||||
0x75 => KeyCode::Delete,
|
||||
0x76 => KeyCode::F4,
|
||||
0x77 => KeyCode::End,
|
||||
0x78 => KeyCode::F2,
|
||||
0x79 => KeyCode::PageDown,
|
||||
0x7a => KeyCode::F1,
|
||||
0x7b => KeyCode::ArrowLeft,
|
||||
0x7c => KeyCode::ArrowRight,
|
||||
0x7d => KeyCode::ArrowDown,
|
||||
0x7e => KeyCode::ArrowUp,
|
||||
// 0x7f => unknown,
|
||||
|
||||
// 0xA is the caret (^) an macOS's German QERTZ layout. This key is at the same location as
|
||||
// backquote (`) on Windows' US layout.
|
||||
0xa => KeyCode::Backquote,
|
||||
_ => return PhysicalKey::Unidentified(NativeKeyCode::MacOS(scancode as u16)),
|
||||
})
|
||||
}
|
||||
138
src/platform_impl/apple/appkit/event_handler.rs
Normal file
138
src/platform_impl/apple/appkit/event_handler.rs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
use std::cell::RefCell;
|
||||
use std::{fmt, mem};
|
||||
|
||||
use crate::application::ApplicationHandler;
|
||||
use crate::event_loop::ActiveEventLoop as RootActiveEventLoop;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct EventHandler {
|
||||
/// This can be in the following states:
|
||||
/// - Not registered by the event loop (None).
|
||||
/// - Present (Some(handler)).
|
||||
/// - Currently executing the handler / in use (RefCell borrowed).
|
||||
inner: RefCell<Option<&'static mut dyn ApplicationHandler>>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for EventHandler {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let state = match self.inner.try_borrow().as_deref() {
|
||||
Ok(Some(_)) => "<available>",
|
||||
Ok(None) => "<not set>",
|
||||
Err(_) => "<in use>",
|
||||
};
|
||||
f.debug_struct("EventHandler").field("state", &state).finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventHandler {
|
||||
pub(crate) fn new() -> Self {
|
||||
Self { inner: RefCell::new(None) }
|
||||
}
|
||||
|
||||
/// Set the event loop handler for the duration of the given closure.
|
||||
///
|
||||
/// This is similar to using the `scoped-tls` or `scoped-tls-hkt` crates
|
||||
/// to store the handler in a thread local, such that it can be accessed
|
||||
/// from within the closure.
|
||||
pub(crate) fn set<'handler, R>(
|
||||
&self,
|
||||
app: &'handler mut dyn ApplicationHandler,
|
||||
closure: impl FnOnce() -> R,
|
||||
) -> R {
|
||||
// SAFETY: We extend the lifetime of the handler here so that we can
|
||||
// store it in `EventHandler`'s `RefCell`.
|
||||
//
|
||||
// This is sound, since we make sure to unset the handler again at the
|
||||
// end of this function, and as such the lifetime isn't actually
|
||||
// extended beyond `'handler`.
|
||||
let handler = unsafe {
|
||||
mem::transmute::<
|
||||
&'handler mut dyn ApplicationHandler,
|
||||
&'static mut dyn ApplicationHandler,
|
||||
>(app)
|
||||
};
|
||||
|
||||
match self.inner.try_borrow_mut().as_deref_mut() {
|
||||
Ok(Some(_)) => {
|
||||
unreachable!("tried to set handler while another was already set");
|
||||
},
|
||||
Ok(data @ None) => {
|
||||
*data = Some(handler);
|
||||
},
|
||||
Err(_) => {
|
||||
unreachable!("tried to set handler that is currently in use");
|
||||
},
|
||||
}
|
||||
|
||||
struct ClearOnDrop<'a>(&'a EventHandler);
|
||||
|
||||
impl Drop for ClearOnDrop<'_> {
|
||||
fn drop(&mut self) {
|
||||
match self.0.inner.try_borrow_mut().as_deref_mut() {
|
||||
Ok(data @ Some(_)) => {
|
||||
*data = None;
|
||||
},
|
||||
Ok(None) => {
|
||||
tracing::error!("tried to clear handler, but no handler was set");
|
||||
},
|
||||
Err(_) => {
|
||||
// Note: This is not expected to ever happen, this
|
||||
// module generally controls the `RefCell`, and
|
||||
// prevents it from ever being borrowed outside of it.
|
||||
//
|
||||
// But if it _does_ happen, it is a serious error, and
|
||||
// we must abort the process, it'd be unsound if we
|
||||
// weren't able to unset the handler.
|
||||
eprintln!("tried to clear handler that is currently in use");
|
||||
std::process::abort();
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let _clear_on_drop = ClearOnDrop(self);
|
||||
|
||||
// Note: The RefCell should not be borrowed while executing the
|
||||
// closure, that'd defeat the whole point.
|
||||
closure()
|
||||
|
||||
// `_clear_on_drop` will be dropped here, or when unwinding, ensuring
|
||||
// soundness.
|
||||
}
|
||||
|
||||
pub(crate) fn in_use(&self) -> bool {
|
||||
self.inner.try_borrow().is_err()
|
||||
}
|
||||
|
||||
pub(crate) fn ready(&self) -> bool {
|
||||
matches!(self.inner.try_borrow().as_deref(), Ok(Some(_)))
|
||||
}
|
||||
|
||||
pub(crate) fn handle(
|
||||
&self,
|
||||
callback: impl FnOnce(&mut dyn ApplicationHandler, &RootActiveEventLoop),
|
||||
event_loop: &RootActiveEventLoop,
|
||||
) {
|
||||
match self.inner.try_borrow_mut().as_deref_mut() {
|
||||
Ok(Some(user_app)) => {
|
||||
// It is important that we keep the reference borrowed here,
|
||||
// so that `in_use` can properly detect that the handler is
|
||||
// still in use.
|
||||
//
|
||||
// If the handler unwinds, the `RefMut` will ensure that the
|
||||
// handler is no longer borrowed.
|
||||
callback(*user_app, event_loop);
|
||||
},
|
||||
Ok(None) => {
|
||||
// `NSApplication`, our app delegate and this handler are all
|
||||
// global state and so it's not impossible that we could get
|
||||
// an event after the application has exited the `EventLoop`.
|
||||
tracing::error!("tried to run event handler, but no handler was set");
|
||||
},
|
||||
Err(_) => {
|
||||
// Prevent re-entrancy.
|
||||
panic!("tried to handle event while another event is currently being handled");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
481
src/platform_impl/apple/appkit/event_loop.rs
Normal file
481
src/platform_impl/apple/appkit/event_loop.rs
Normal file
|
|
@ -0,0 +1,481 @@
|
|||
use std::any::Any;
|
||||
use std::cell::Cell;
|
||||
use std::collections::VecDeque;
|
||||
use std::marker::PhantomData;
|
||||
use std::os::raw::c_void;
|
||||
use std::panic::{catch_unwind, resume_unwind, RefUnwindSafe, UnwindSafe};
|
||||
use std::ptr;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::sync::atomic::{AtomicBool, Ordering as AtomicOrdering};
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use core_foundation::base::{CFIndex, CFRelease};
|
||||
use core_foundation::runloop::{
|
||||
kCFRunLoopCommonModes, CFRunLoopAddSource, CFRunLoopGetMain, CFRunLoopSourceContext,
|
||||
CFRunLoopSourceCreate, CFRunLoopSourceRef, CFRunLoopSourceSignal, CFRunLoopWakeUp,
|
||||
};
|
||||
use objc2::rc::{autoreleasepool, Retained};
|
||||
use objc2::runtime::ProtocolObject;
|
||||
use objc2::{msg_send_id, ClassType};
|
||||
use objc2_app_kit::{NSApplication, NSApplicationActivationPolicy, NSWindow};
|
||||
use objc2_foundation::{MainThreadMarker, NSObjectProtocol};
|
||||
|
||||
use super::app::WinitApplication;
|
||||
use super::app_state::ApplicationDelegate;
|
||||
use super::cursor::CustomCursor;
|
||||
use super::event::dummy_event;
|
||||
use super::monitor::{self, MonitorHandle};
|
||||
use super::observer::setup_control_flow_observers;
|
||||
use crate::application::ApplicationHandler;
|
||||
use crate::error::EventLoopError;
|
||||
use crate::event_loop::{ActiveEventLoop as RootWindowTarget, ControlFlow, DeviceEvents};
|
||||
use crate::platform::macos::ActivationPolicy;
|
||||
use crate::platform::pump_events::PumpStatus;
|
||||
use crate::window::{CustomCursor as RootCustomCursor, CustomCursorSource};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PanicInfo {
|
||||
inner: Cell<Option<Box<dyn Any + Send + 'static>>>,
|
||||
}
|
||||
|
||||
// WARNING:
|
||||
// As long as this struct is used through its `impl`, it is UnwindSafe.
|
||||
// (If `get_mut` is called on `inner`, unwind safety may get broken.)
|
||||
impl UnwindSafe for PanicInfo {}
|
||||
impl RefUnwindSafe for PanicInfo {}
|
||||
impl PanicInfo {
|
||||
pub fn is_panicking(&self) -> bool {
|
||||
let inner = self.inner.take();
|
||||
let result = inner.is_some();
|
||||
self.inner.set(inner);
|
||||
result
|
||||
}
|
||||
|
||||
/// Overwrites the current state if the current state is not panicking
|
||||
pub fn set_panic(&self, p: Box<dyn Any + Send + 'static>) {
|
||||
if !self.is_panicking() {
|
||||
self.inner.set(Some(p));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take(&self) -> Option<Box<dyn Any + Send + 'static>> {
|
||||
self.inner.take()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ActiveEventLoop {
|
||||
delegate: Retained<ApplicationDelegate>,
|
||||
pub(super) mtm: MainThreadMarker,
|
||||
}
|
||||
|
||||
impl ActiveEventLoop {
|
||||
pub(super) fn new_root(delegate: Retained<ApplicationDelegate>) -> RootWindowTarget {
|
||||
let mtm = MainThreadMarker::from(&*delegate);
|
||||
let p = Self { delegate, mtm };
|
||||
RootWindowTarget { p, _marker: PhantomData }
|
||||
}
|
||||
|
||||
pub(super) fn app_delegate(&self) -> &ApplicationDelegate {
|
||||
&self.delegate
|
||||
}
|
||||
|
||||
pub fn create_custom_cursor(&self, source: CustomCursorSource) -> RootCustomCursor {
|
||||
RootCustomCursor { inner: CustomCursor::new(source.inner) }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn available_monitors(&self) -> VecDeque<MonitorHandle> {
|
||||
monitor::available_monitors()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
|
||||
let monitor = monitor::primary_monitor();
|
||||
Some(monitor)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn listen_device_events(&self, _allowed: DeviceEvents) {}
|
||||
|
||||
#[cfg(feature = "rwh_05")]
|
||||
#[inline]
|
||||
pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle {
|
||||
rwh_05::RawDisplayHandle::AppKit(rwh_05::AppKitDisplayHandle::empty())
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_06")]
|
||||
#[inline]
|
||||
pub fn raw_display_handle_rwh_06(
|
||||
&self,
|
||||
) -> Result<rwh_06::RawDisplayHandle, rwh_06::HandleError> {
|
||||
Ok(rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new()))
|
||||
}
|
||||
|
||||
pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) {
|
||||
self.delegate.set_control_flow(control_flow)
|
||||
}
|
||||
|
||||
pub(crate) fn control_flow(&self) -> ControlFlow {
|
||||
self.delegate.control_flow()
|
||||
}
|
||||
|
||||
pub(crate) fn exit(&self) {
|
||||
self.delegate.exit()
|
||||
}
|
||||
|
||||
pub(crate) fn clear_exit(&self) {
|
||||
self.delegate.clear_exit()
|
||||
}
|
||||
|
||||
pub(crate) fn exiting(&self) -> bool {
|
||||
self.delegate.exiting()
|
||||
}
|
||||
|
||||
pub(crate) fn owned_display_handle(&self) -> OwnedDisplayHandle {
|
||||
OwnedDisplayHandle
|
||||
}
|
||||
|
||||
pub(crate) fn hide_application(&self) {
|
||||
NSApplication::sharedApplication(self.mtm).hide(None)
|
||||
}
|
||||
|
||||
pub(crate) fn hide_other_applications(&self) {
|
||||
NSApplication::sharedApplication(self.mtm).hideOtherApplications(None)
|
||||
}
|
||||
|
||||
pub(crate) fn set_allows_automatic_window_tabbing(&self, enabled: bool) {
|
||||
NSWindow::setAllowsAutomaticWindowTabbing(enabled, self.mtm)
|
||||
}
|
||||
|
||||
pub(crate) fn allows_automatic_window_tabbing(&self) -> bool {
|
||||
NSWindow::allowsAutomaticWindowTabbing(self.mtm)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventLoop {
|
||||
/// Store a reference to the application for convenience.
|
||||
///
|
||||
/// We intentionally don't store `WinitApplication` since we want to have
|
||||
/// the possibility of swapping that out at some point.
|
||||
app: Retained<NSApplication>,
|
||||
/// The application delegate that we've registered.
|
||||
///
|
||||
/// The delegate is only weakly referenced by NSApplication, so we must
|
||||
/// keep it around here as well.
|
||||
delegate: Retained<ApplicationDelegate>,
|
||||
|
||||
proxy_wake_up: Arc<AtomicBool>,
|
||||
|
||||
window_target: RootWindowTarget,
|
||||
panic_info: Rc<PanicInfo>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub(crate) struct PlatformSpecificEventLoopAttributes {
|
||||
pub(crate) activation_policy: ActivationPolicy,
|
||||
pub(crate) default_menu: bool,
|
||||
pub(crate) activate_ignoring_other_apps: bool,
|
||||
}
|
||||
|
||||
impl Default for PlatformSpecificEventLoopAttributes {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
activation_policy: Default::default(), // Regular
|
||||
default_menu: true,
|
||||
activate_ignoring_other_apps: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventLoop {
|
||||
pub(crate) fn new(
|
||||
attributes: &PlatformSpecificEventLoopAttributes,
|
||||
) -> Result<Self, EventLoopError> {
|
||||
let mtm = MainThreadMarker::new()
|
||||
.expect("on macOS, `EventLoop` must be created on the main thread!");
|
||||
|
||||
let app: Retained<NSApplication> =
|
||||
unsafe { msg_send_id![WinitApplication::class(), sharedApplication] };
|
||||
|
||||
if !app.is_kind_of::<WinitApplication>() {
|
||||
panic!(
|
||||
"`winit` requires control over the principal class. You must create the event \
|
||||
loop before other parts of your application initialize NSApplication"
|
||||
);
|
||||
}
|
||||
|
||||
let activation_policy = match attributes.activation_policy {
|
||||
ActivationPolicy::Regular => NSApplicationActivationPolicy::Regular,
|
||||
ActivationPolicy::Accessory => NSApplicationActivationPolicy::Accessory,
|
||||
ActivationPolicy::Prohibited => NSApplicationActivationPolicy::Prohibited,
|
||||
};
|
||||
|
||||
let proxy_wake_up = Arc::new(AtomicBool::new(false));
|
||||
|
||||
let delegate = ApplicationDelegate::new(
|
||||
mtm,
|
||||
activation_policy,
|
||||
proxy_wake_up.clone(),
|
||||
attributes.default_menu,
|
||||
attributes.activate_ignoring_other_apps,
|
||||
);
|
||||
|
||||
autoreleasepool(|_| {
|
||||
app.setDelegate(Some(ProtocolObject::from_ref(&*delegate)));
|
||||
});
|
||||
|
||||
let panic_info: Rc<PanicInfo> = Default::default();
|
||||
setup_control_flow_observers(mtm, Rc::downgrade(&panic_info));
|
||||
|
||||
Ok(EventLoop {
|
||||
app,
|
||||
delegate: delegate.clone(),
|
||||
window_target: RootWindowTarget {
|
||||
p: ActiveEventLoop { delegate, mtm },
|
||||
_marker: PhantomData,
|
||||
},
|
||||
proxy_wake_up,
|
||||
panic_info,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn window_target(&self) -> &RootWindowTarget {
|
||||
&self.window_target
|
||||
}
|
||||
|
||||
pub fn run_app<A: ApplicationHandler>(mut self, app: &mut A) -> Result<(), EventLoopError> {
|
||||
self.run_app_on_demand(app)
|
||||
}
|
||||
|
||||
// NB: we don't base this on `pump_events` because for `MacOs` we can't support
|
||||
// `pump_events` elegantly (we just ask to run the loop for a "short" amount of
|
||||
// time and so a layered implementation would end up using a lot of CPU due to
|
||||
// redundant wake ups.
|
||||
pub fn run_app_on_demand<A: ApplicationHandler>(
|
||||
&mut self,
|
||||
app: &mut A,
|
||||
) -> Result<(), EventLoopError> {
|
||||
self.delegate.set_event_handler(app, || {
|
||||
autoreleasepool(|_| {
|
||||
// clear / normalize pump_events state
|
||||
self.delegate.set_wait_timeout(None);
|
||||
self.delegate.set_stop_before_wait(false);
|
||||
self.delegate.set_stop_after_wait(false);
|
||||
self.delegate.set_stop_on_redraw(false);
|
||||
|
||||
if self.delegate.is_launched() {
|
||||
debug_assert!(!self.delegate.is_running());
|
||||
self.delegate.set_is_running(true);
|
||||
self.delegate.dispatch_init_events();
|
||||
}
|
||||
|
||||
// SAFETY: We do not run the application re-entrantly
|
||||
unsafe { self.app.run() };
|
||||
|
||||
// While the app is running it's possible that we catch a panic
|
||||
// to avoid unwinding across an objective-c ffi boundary, which
|
||||
// will lead to us stopping the `NSApplication` and saving the
|
||||
// `PanicInfo` so that we can resume the unwind at a controlled,
|
||||
// safe point in time.
|
||||
if let Some(panic) = self.panic_info.take() {
|
||||
resume_unwind(panic);
|
||||
}
|
||||
|
||||
self.delegate.internal_exit()
|
||||
})
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn pump_app_events<A: ApplicationHandler>(
|
||||
&mut self,
|
||||
timeout: Option<Duration>,
|
||||
app: &mut A,
|
||||
) -> PumpStatus {
|
||||
self.delegate.set_event_handler(app, || {
|
||||
autoreleasepool(|_| {
|
||||
// As a special case, if the application hasn't been launched yet then we at least
|
||||
// run the loop until it has fully launched.
|
||||
if !self.delegate.is_launched() {
|
||||
debug_assert!(!self.delegate.is_running());
|
||||
|
||||
self.delegate.set_stop_on_launch();
|
||||
// SAFETY: We do not run the application re-entrantly
|
||||
unsafe { self.app.run() };
|
||||
|
||||
// Note: we dispatch `NewEvents(Init)` + `Resumed` events after the application
|
||||
// has launched
|
||||
} else if !self.delegate.is_running() {
|
||||
// Even though the application may have been launched, it's possible we aren't
|
||||
// running if the `EventLoop` was run before and has since
|
||||
// exited. This indicates that we just starting to re-run
|
||||
// the same `EventLoop` again.
|
||||
self.delegate.set_is_running(true);
|
||||
self.delegate.dispatch_init_events();
|
||||
} else {
|
||||
// Only run for as long as the given `Duration` allows so we don't block the
|
||||
// external loop.
|
||||
match timeout {
|
||||
Some(Duration::ZERO) => {
|
||||
self.delegate.set_wait_timeout(None);
|
||||
self.delegate.set_stop_before_wait(true);
|
||||
},
|
||||
Some(duration) => {
|
||||
self.delegate.set_stop_before_wait(false);
|
||||
let timeout = Instant::now() + duration;
|
||||
self.delegate.set_wait_timeout(Some(timeout));
|
||||
self.delegate.set_stop_after_wait(true);
|
||||
},
|
||||
None => {
|
||||
self.delegate.set_wait_timeout(None);
|
||||
self.delegate.set_stop_before_wait(false);
|
||||
self.delegate.set_stop_after_wait(true);
|
||||
},
|
||||
}
|
||||
self.delegate.set_stop_on_redraw(true);
|
||||
// SAFETY: We do not run the application re-entrantly
|
||||
unsafe { self.app.run() };
|
||||
}
|
||||
|
||||
// While the app is running it's possible that we catch a panic
|
||||
// to avoid unwinding across an objective-c ffi boundary, which
|
||||
// will lead to us stopping the application and saving the
|
||||
// `PanicInfo` so that we can resume the unwind at a controlled,
|
||||
// safe point in time.
|
||||
if let Some(panic) = self.panic_info.take() {
|
||||
resume_unwind(panic);
|
||||
}
|
||||
|
||||
if self.delegate.exiting() {
|
||||
self.delegate.internal_exit();
|
||||
PumpStatus::Exit(0)
|
||||
} else {
|
||||
PumpStatus::Continue
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_proxy(&self) -> EventLoopProxy {
|
||||
EventLoopProxy::new(self.proxy_wake_up.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct OwnedDisplayHandle;
|
||||
|
||||
impl OwnedDisplayHandle {
|
||||
#[cfg(feature = "rwh_05")]
|
||||
#[inline]
|
||||
pub fn raw_display_handle_rwh_05(&self) -> rwh_05::RawDisplayHandle {
|
||||
rwh_05::AppKitDisplayHandle::empty().into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_06")]
|
||||
#[inline]
|
||||
pub fn raw_display_handle_rwh_06(
|
||||
&self,
|
||||
) -> Result<rwh_06::RawDisplayHandle, rwh_06::HandleError> {
|
||||
Ok(rwh_06::AppKitDisplayHandle::new().into())
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn stop_app_immediately(app: &NSApplication) {
|
||||
autoreleasepool(|_| {
|
||||
app.stop(None);
|
||||
// To stop event loop immediately, we need to post some event here.
|
||||
// See: https://stackoverflow.com/questions/48041279/stopping-the-nsapplication-main-event-loop/48064752#48064752
|
||||
app.postEvent_atStart(&dummy_event().unwrap(), true);
|
||||
});
|
||||
}
|
||||
|
||||
/// Catches panics that happen inside `f` and when a panic
|
||||
/// happens, stops the `sharedApplication`
|
||||
#[inline]
|
||||
pub fn stop_app_on_panic<F: FnOnce() -> R + UnwindSafe, R>(
|
||||
mtm: MainThreadMarker,
|
||||
panic_info: Weak<PanicInfo>,
|
||||
f: F,
|
||||
) -> Option<R> {
|
||||
match catch_unwind(f) {
|
||||
Ok(r) => Some(r),
|
||||
Err(e) => {
|
||||
// It's important that we set the panic before requesting a `stop`
|
||||
// because some callback are still called during the `stop` message
|
||||
// and we need to know in those callbacks if the application is currently
|
||||
// panicking
|
||||
{
|
||||
let panic_info = panic_info.upgrade().unwrap();
|
||||
panic_info.set_panic(e);
|
||||
}
|
||||
let app = NSApplication::sharedApplication(mtm);
|
||||
stop_app_immediately(&app);
|
||||
None
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub struct EventLoopProxy {
|
||||
proxy_wake_up: Arc<AtomicBool>,
|
||||
source: CFRunLoopSourceRef,
|
||||
}
|
||||
|
||||
unsafe impl Send for EventLoopProxy {}
|
||||
unsafe impl Sync for EventLoopProxy {}
|
||||
|
||||
impl Drop for EventLoopProxy {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
CFRelease(self.source as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for EventLoopProxy {
|
||||
fn clone(&self) -> Self {
|
||||
EventLoopProxy::new(self.proxy_wake_up.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl EventLoopProxy {
|
||||
fn new(proxy_wake_up: Arc<AtomicBool>) -> Self {
|
||||
unsafe {
|
||||
// just wake up the eventloop
|
||||
extern "C" fn event_loop_proxy_handler(_: *const c_void) {}
|
||||
|
||||
// adding a Source to the main CFRunLoop lets us wake it up and
|
||||
// process user events through the normal OS EventLoop mechanisms.
|
||||
let rl = CFRunLoopGetMain();
|
||||
let mut context = CFRunLoopSourceContext {
|
||||
version: 0,
|
||||
info: ptr::null_mut(),
|
||||
retain: None,
|
||||
release: None,
|
||||
copyDescription: None,
|
||||
equal: None,
|
||||
hash: None,
|
||||
schedule: None,
|
||||
cancel: None,
|
||||
perform: event_loop_proxy_handler,
|
||||
};
|
||||
let source = CFRunLoopSourceCreate(ptr::null_mut(), CFIndex::MAX - 1, &mut context);
|
||||
CFRunLoopAddSource(rl, source, kCFRunLoopCommonModes);
|
||||
CFRunLoopWakeUp(rl);
|
||||
|
||||
EventLoopProxy { proxy_wake_up, source }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wake_up(&self) {
|
||||
self.proxy_wake_up.store(true, AtomicOrdering::Relaxed);
|
||||
unsafe {
|
||||
// let the main thread know there's a new event
|
||||
CFRunLoopSourceSignal(self.source);
|
||||
let rl = CFRunLoopGetMain();
|
||||
CFRunLoopWakeUp(rl);
|
||||
}
|
||||
}
|
||||
}
|
||||
254
src/platform_impl/apple/appkit/ffi.rs
Normal file
254
src/platform_impl/apple/appkit/ffi.rs
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
// TODO: Upstream these
|
||||
|
||||
#![allow(dead_code, non_snake_case, non_upper_case_globals)]
|
||||
|
||||
use std::ffi::c_void;
|
||||
|
||||
use core_foundation::array::CFArrayRef;
|
||||
use core_foundation::dictionary::CFDictionaryRef;
|
||||
use core_foundation::string::CFStringRef;
|
||||
use core_foundation::uuid::CFUUIDRef;
|
||||
use core_graphics::base::CGError;
|
||||
use core_graphics::display::{CGDirectDisplayID, CGDisplayConfigRef};
|
||||
use objc2::ffi::NSInteger;
|
||||
use objc2::runtime::AnyObject;
|
||||
|
||||
pub type CGDisplayFadeInterval = f32;
|
||||
pub type CGDisplayReservationInterval = f32;
|
||||
pub type CGDisplayBlendFraction = f32;
|
||||
|
||||
pub const kCGDisplayBlendNormal: f32 = 0.0;
|
||||
pub const kCGDisplayBlendSolidColor: f32 = 1.0;
|
||||
|
||||
pub type CGDisplayFadeReservationToken = u32;
|
||||
pub const kCGDisplayFadeReservationInvalidToken: CGDisplayFadeReservationToken = 0;
|
||||
|
||||
pub type Boolean = u8;
|
||||
pub const FALSE: Boolean = 0;
|
||||
pub const TRUE: Boolean = 1;
|
||||
|
||||
pub const kCGErrorSuccess: i32 = 0;
|
||||
pub const kCGErrorFailure: i32 = 1000;
|
||||
pub const kCGErrorIllegalArgument: i32 = 1001;
|
||||
pub const kCGErrorInvalidConnection: i32 = 1002;
|
||||
pub const kCGErrorInvalidContext: i32 = 1003;
|
||||
pub const kCGErrorCannotComplete: i32 = 1004;
|
||||
pub const kCGErrorNotImplemented: i32 = 1006;
|
||||
pub const kCGErrorRangeCheck: i32 = 1007;
|
||||
pub const kCGErrorTypeCheck: i32 = 1008;
|
||||
pub const kCGErrorInvalidOperation: i32 = 1010;
|
||||
pub const kCGErrorNoneAvailable: i32 = 1011;
|
||||
|
||||
pub const IO1BitIndexedPixels: &str = "P";
|
||||
pub const IO2BitIndexedPixels: &str = "PP";
|
||||
pub const IO4BitIndexedPixels: &str = "PPPP";
|
||||
pub const IO8BitIndexedPixels: &str = "PPPPPPPP";
|
||||
pub const IO16BitDirectPixels: &str = "-RRRRRGGGGGBBBBB";
|
||||
pub const IO32BitDirectPixels: &str = "--------RRRRRRRRGGGGGGGGBBBBBBBB";
|
||||
|
||||
pub const kIO30BitDirectPixels: &str = "--RRRRRRRRRRGGGGGGGGGGBBBBBBBBBB";
|
||||
pub const kIO64BitDirectPixels: &str = "-16R16G16B16";
|
||||
|
||||
pub const kIO16BitFloatPixels: &str = "-16FR16FG16FB16";
|
||||
pub const kIO32BitFloatPixels: &str = "-32FR32FG32FB32";
|
||||
|
||||
pub const IOYUV422Pixels: &str = "Y4U2V2";
|
||||
pub const IO8BitOverlayPixels: &str = "O8";
|
||||
|
||||
pub type CGWindowLevel = i32;
|
||||
pub type CGDisplayModeRef = *mut c_void;
|
||||
|
||||
// `CGDisplayCreateUUIDFromDisplayID` comes from the `ColorSync` framework.
|
||||
// However, that framework was only introduced "publicly" in macOS 10.13.
|
||||
//
|
||||
// Since we want to support older versions, we can't link to `ColorSync`
|
||||
// directly. Fortunately, it has always been available as a subframework of
|
||||
// `ApplicationServices`, see:
|
||||
// https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/OSX_Technology_Overview/SystemFrameworks/SystemFrameworks.html#//apple_ref/doc/uid/TP40001067-CH210-BBCFFIEG
|
||||
#[link(name = "ApplicationServices", kind = "framework")]
|
||||
extern "C" {
|
||||
pub fn CGDisplayCreateUUIDFromDisplayID(display: CGDirectDisplayID) -> CFUUIDRef;
|
||||
}
|
||||
|
||||
#[link(name = "CoreGraphics", kind = "framework")]
|
||||
extern "C" {
|
||||
pub fn CGRestorePermanentDisplayConfiguration();
|
||||
pub fn CGDisplayCapture(display: CGDirectDisplayID) -> CGError;
|
||||
pub fn CGDisplayRelease(display: CGDirectDisplayID) -> CGError;
|
||||
pub fn CGConfigureDisplayFadeEffect(
|
||||
config: CGDisplayConfigRef,
|
||||
fadeOutSeconds: CGDisplayFadeInterval,
|
||||
fadeInSeconds: CGDisplayFadeInterval,
|
||||
fadeRed: f32,
|
||||
fadeGreen: f32,
|
||||
fadeBlue: f32,
|
||||
) -> CGError;
|
||||
pub fn CGAcquireDisplayFadeReservation(
|
||||
seconds: CGDisplayReservationInterval,
|
||||
token: *mut CGDisplayFadeReservationToken,
|
||||
) -> CGError;
|
||||
pub fn CGDisplayFade(
|
||||
token: CGDisplayFadeReservationToken,
|
||||
duration: CGDisplayFadeInterval,
|
||||
startBlend: CGDisplayBlendFraction,
|
||||
endBlend: CGDisplayBlendFraction,
|
||||
redBlend: f32,
|
||||
greenBlend: f32,
|
||||
blueBlend: f32,
|
||||
synchronous: Boolean,
|
||||
) -> CGError;
|
||||
pub fn CGReleaseDisplayFadeReservation(token: CGDisplayFadeReservationToken) -> CGError;
|
||||
pub fn CGShieldingWindowLevel() -> CGWindowLevel;
|
||||
pub fn CGDisplaySetDisplayMode(
|
||||
display: CGDirectDisplayID,
|
||||
mode: CGDisplayModeRef,
|
||||
options: CFDictionaryRef,
|
||||
) -> CGError;
|
||||
pub fn CGDisplayCopyAllDisplayModes(
|
||||
display: CGDirectDisplayID,
|
||||
options: CFDictionaryRef,
|
||||
) -> CFArrayRef;
|
||||
pub fn CGDisplayModeGetPixelWidth(mode: CGDisplayModeRef) -> usize;
|
||||
pub fn CGDisplayModeGetPixelHeight(mode: CGDisplayModeRef) -> usize;
|
||||
pub fn CGDisplayModeGetRefreshRate(mode: CGDisplayModeRef) -> f64;
|
||||
pub fn CGDisplayModeCopyPixelEncoding(mode: CGDisplayModeRef) -> CFStringRef;
|
||||
pub fn CGDisplayModeRetain(mode: CGDisplayModeRef);
|
||||
pub fn CGDisplayModeRelease(mode: CGDisplayModeRef);
|
||||
|
||||
// Wildly used private APIs; Apple uses them for their Terminal.app.
|
||||
pub fn CGSMainConnectionID() -> *mut AnyObject;
|
||||
pub fn CGSSetWindowBackgroundBlurRadius(
|
||||
connection_id: *mut AnyObject,
|
||||
window_id: NSInteger,
|
||||
radius: i64,
|
||||
) -> i32;
|
||||
}
|
||||
|
||||
mod core_video {
|
||||
use super::*;
|
||||
|
||||
#[link(name = "CoreVideo", kind = "framework")]
|
||||
extern "C" {}
|
||||
|
||||
// CVBase.h
|
||||
|
||||
pub type CVTimeFlags = i32; // int32_t
|
||||
pub const kCVTimeIsIndefinite: CVTimeFlags = 1 << 0;
|
||||
|
||||
#[repr(C)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CVTime {
|
||||
pub time_value: i64, // int64_t
|
||||
pub time_scale: i32, // int32_t
|
||||
pub flags: i32, // int32_t
|
||||
}
|
||||
|
||||
// CVReturn.h
|
||||
|
||||
pub type CVReturn = i32; // int32_t
|
||||
pub const kCVReturnSuccess: CVReturn = 0;
|
||||
|
||||
// CVDisplayLink.h
|
||||
|
||||
pub type CVDisplayLinkRef = *mut c_void;
|
||||
|
||||
extern "C" {
|
||||
pub fn CVDisplayLinkCreateWithCGDisplay(
|
||||
displayID: CGDirectDisplayID,
|
||||
displayLinkOut: *mut CVDisplayLinkRef,
|
||||
) -> CVReturn;
|
||||
pub fn CVDisplayLinkGetNominalOutputVideoRefreshPeriod(
|
||||
displayLink: CVDisplayLinkRef,
|
||||
) -> CVTime;
|
||||
pub fn CVDisplayLinkRelease(displayLink: CVDisplayLinkRef);
|
||||
}
|
||||
}
|
||||
|
||||
pub use core_video::*;
|
||||
#[repr(transparent)]
|
||||
pub struct TISInputSource(std::ffi::c_void);
|
||||
pub type TISInputSourceRef = *mut TISInputSource;
|
||||
|
||||
#[repr(transparent)]
|
||||
pub struct UCKeyboardLayout(std::ffi::c_void);
|
||||
|
||||
pub type OptionBits = u32;
|
||||
pub type UniCharCount = std::os::raw::c_ulong;
|
||||
pub type UniChar = std::os::raw::c_ushort;
|
||||
pub type OSStatus = i32;
|
||||
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const kUCKeyActionDisplay: u16 = 3;
|
||||
#[allow(non_upper_case_globals)]
|
||||
pub const kUCKeyTranslateNoDeadKeysMask: OptionBits = 1;
|
||||
|
||||
#[link(name = "Carbon", kind = "framework")]
|
||||
extern "C" {
|
||||
pub static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn TISGetInputSourceProperty(
|
||||
inputSource: TISInputSourceRef,
|
||||
propertyKey: CFStringRef,
|
||||
) -> *mut c_void;
|
||||
|
||||
pub fn TISCopyCurrentKeyboardLayoutInputSource() -> TISInputSourceRef;
|
||||
|
||||
pub fn LMGetKbdType() -> u8;
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
pub fn UCKeyTranslate(
|
||||
keyLayoutPtr: *const UCKeyboardLayout,
|
||||
virtualKeyCode: u16,
|
||||
keyAction: u16,
|
||||
modifierKeyState: u32,
|
||||
keyboardType: u32,
|
||||
keyTranslateOptions: OptionBits,
|
||||
deadKeyState: *mut u32,
|
||||
maxStringLength: UniCharCount,
|
||||
actualStringLength: *mut UniCharCount,
|
||||
unicodeString: *mut UniChar,
|
||||
) -> OSStatus;
|
||||
}
|
||||
|
||||
// CGWindowLevel.h
|
||||
//
|
||||
// Note: There are two different things at play in this header:
|
||||
// `CGWindowLevel` and `CGWindowLevelKey`.
|
||||
//
|
||||
// It seems like there was a push towards using "key" values instead of the
|
||||
// raw window level values, and then you were supposed to use
|
||||
// `CGWindowLevelForKey` to get the actual level.
|
||||
//
|
||||
// But the values that `NSWindowLevel` has are compiled in, and as such has
|
||||
// to remain ABI compatible, so they're safe for us to hardcode as well.
|
||||
#[allow(dead_code, non_upper_case_globals)]
|
||||
mod window_level {
|
||||
const kCGNumReservedWindowLevels: i32 = 16;
|
||||
const kCGNumReservedBaseWindowLevels: i32 = 5;
|
||||
|
||||
pub const kCGBaseWindowLevel: i32 = i32::MIN;
|
||||
pub const kCGMinimumWindowLevel: i32 = kCGBaseWindowLevel + kCGNumReservedBaseWindowLevels;
|
||||
pub const kCGMaximumWindowLevel: i32 = i32::MAX - kCGNumReservedWindowLevels;
|
||||
|
||||
pub const kCGDesktopWindowLevel: i32 = kCGMinimumWindowLevel + 20;
|
||||
pub const kCGDesktopIconWindowLevel: i32 = kCGDesktopWindowLevel + 20;
|
||||
pub const kCGBackstopMenuLevel: i32 = -20;
|
||||
pub const kCGNormalWindowLevel: i32 = 0;
|
||||
pub const kCGFloatingWindowLevel: i32 = 3;
|
||||
pub const kCGTornOffMenuWindowLevel: i32 = 3;
|
||||
pub const kCGModalPanelWindowLevel: i32 = 8;
|
||||
pub const kCGUtilityWindowLevel: i32 = 19;
|
||||
pub const kCGDockWindowLevel: i32 = 20;
|
||||
pub const kCGMainMenuWindowLevel: i32 = 24;
|
||||
pub const kCGStatusWindowLevel: i32 = 25;
|
||||
pub const kCGPopUpMenuWindowLevel: i32 = 101;
|
||||
pub const kCGOverlayWindowLevel: i32 = 102;
|
||||
pub const kCGHelpWindowLevel: i32 = 200;
|
||||
pub const kCGDraggingWindowLevel: i32 = 500;
|
||||
pub const kCGScreenSaverWindowLevel: i32 = 1000;
|
||||
pub const kCGAssistiveTechHighWindowLevel: i32 = 1500;
|
||||
pub const kCGCursorWindowLevel: i32 = kCGMaximumWindowLevel - 1;
|
||||
}
|
||||
|
||||
pub use window_level::*;
|
||||
107
src/platform_impl/apple/appkit/menu.rs
Normal file
107
src/platform_impl/apple/appkit/menu.rs
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
use objc2::rc::Retained;
|
||||
use objc2::runtime::Sel;
|
||||
use objc2::sel;
|
||||
use objc2_app_kit::{NSApplication, NSEventModifierFlags, NSMenu, NSMenuItem};
|
||||
use objc2_foundation::{ns_string, MainThreadMarker, NSProcessInfo, NSString};
|
||||
|
||||
struct KeyEquivalent<'a> {
|
||||
key: &'a NSString,
|
||||
masks: Option<NSEventModifierFlags>,
|
||||
}
|
||||
|
||||
pub fn initialize(app: &NSApplication) {
|
||||
let mtm = MainThreadMarker::from(app);
|
||||
let menubar = NSMenu::new(mtm);
|
||||
let app_menu_item = NSMenuItem::new(mtm);
|
||||
menubar.addItem(&app_menu_item);
|
||||
|
||||
let app_menu = NSMenu::new(mtm);
|
||||
let process_name = NSProcessInfo::processInfo().processName();
|
||||
|
||||
// About menu item
|
||||
let about_item_title = ns_string!("About ").stringByAppendingString(&process_name);
|
||||
let about_item =
|
||||
menu_item(mtm, &about_item_title, Some(sel!(orderFrontStandardAboutPanel:)), None);
|
||||
|
||||
// Services menu item
|
||||
let services_menu = NSMenu::new(mtm);
|
||||
let services_item = menu_item(mtm, ns_string!("Services"), None, None);
|
||||
services_item.setSubmenu(Some(&services_menu));
|
||||
|
||||
// Separator menu item
|
||||
let sep_first = NSMenuItem::separatorItem(mtm);
|
||||
|
||||
// Hide application menu item
|
||||
let hide_item_title = ns_string!("Hide ").stringByAppendingString(&process_name);
|
||||
let hide_item = menu_item(
|
||||
mtm,
|
||||
&hide_item_title,
|
||||
Some(sel!(hide:)),
|
||||
Some(KeyEquivalent { key: ns_string!("h"), masks: None }),
|
||||
);
|
||||
|
||||
// Hide other applications menu item
|
||||
let hide_others_item_title = ns_string!("Hide Others");
|
||||
let hide_others_item = menu_item(
|
||||
mtm,
|
||||
hide_others_item_title,
|
||||
Some(sel!(hideOtherApplications:)),
|
||||
Some(KeyEquivalent {
|
||||
key: ns_string!("h"),
|
||||
masks: Some(
|
||||
NSEventModifierFlags::NSEventModifierFlagOption
|
||||
| NSEventModifierFlags::NSEventModifierFlagCommand,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
||||
// Show applications menu item
|
||||
let show_all_item_title = ns_string!("Show All");
|
||||
let show_all_item =
|
||||
menu_item(mtm, show_all_item_title, Some(sel!(unhideAllApplications:)), None);
|
||||
|
||||
// Separator menu item
|
||||
let sep = NSMenuItem::separatorItem(mtm);
|
||||
|
||||
// Quit application menu item
|
||||
let quit_item_title = ns_string!("Quit ").stringByAppendingString(&process_name);
|
||||
let quit_item = menu_item(
|
||||
mtm,
|
||||
&quit_item_title,
|
||||
Some(sel!(terminate:)),
|
||||
Some(KeyEquivalent { key: ns_string!("q"), masks: None }),
|
||||
);
|
||||
|
||||
app_menu.addItem(&about_item);
|
||||
app_menu.addItem(&sep_first);
|
||||
app_menu.addItem(&services_item);
|
||||
app_menu.addItem(&hide_item);
|
||||
app_menu.addItem(&hide_others_item);
|
||||
app_menu.addItem(&show_all_item);
|
||||
app_menu.addItem(&sep);
|
||||
app_menu.addItem(&quit_item);
|
||||
app_menu_item.setSubmenu(Some(&app_menu));
|
||||
|
||||
unsafe { app.setServicesMenu(Some(&services_menu)) };
|
||||
app.setMainMenu(Some(&menubar));
|
||||
}
|
||||
|
||||
fn menu_item(
|
||||
mtm: MainThreadMarker,
|
||||
title: &NSString,
|
||||
selector: Option<Sel>,
|
||||
key_equivalent: Option<KeyEquivalent<'_>>,
|
||||
) -> Retained<NSMenuItem> {
|
||||
let (key, masks) = match key_equivalent {
|
||||
Some(ke) => (ke.key, ke.masks),
|
||||
None => (ns_string!(""), None),
|
||||
};
|
||||
let item = unsafe {
|
||||
NSMenuItem::initWithTitle_action_keyEquivalent(mtm.alloc(), title, selector, key)
|
||||
};
|
||||
if let Some(masks) = masks {
|
||||
item.setKeyEquivalentModifierMask(masks)
|
||||
}
|
||||
|
||||
item
|
||||
}
|
||||
61
src/platform_impl/apple/appkit/mod.rs
Normal file
61
src/platform_impl/apple/appkit/mod.rs
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
#[macro_use]
|
||||
mod util;
|
||||
|
||||
mod app;
|
||||
mod app_state;
|
||||
mod cursor;
|
||||
mod event;
|
||||
mod event_handler;
|
||||
mod event_loop;
|
||||
mod ffi;
|
||||
mod menu;
|
||||
mod monitor;
|
||||
mod observer;
|
||||
mod view;
|
||||
mod window;
|
||||
mod window_delegate;
|
||||
|
||||
use std::fmt;
|
||||
|
||||
pub(crate) use self::event::{physicalkey_to_scancode, scancode_to_physicalkey, KeyEventExtra};
|
||||
pub(crate) use self::event_loop::{
|
||||
ActiveEventLoop, EventLoop, EventLoopProxy, OwnedDisplayHandle,
|
||||
PlatformSpecificEventLoopAttributes,
|
||||
};
|
||||
pub(crate) use self::monitor::{MonitorHandle, VideoModeHandle};
|
||||
pub(crate) use self::window::WindowId;
|
||||
pub(crate) use self::window_delegate::PlatformSpecificWindowAttributes;
|
||||
use crate::event::DeviceId as RootDeviceId;
|
||||
|
||||
pub(crate) use self::cursor::CustomCursor as PlatformCustomCursor;
|
||||
pub(crate) use self::window::Window;
|
||||
pub(crate) use crate::cursor::OnlyCursorImageSource as PlatformCustomCursorSource;
|
||||
pub(crate) use crate::icon::NoIcon as PlatformIcon;
|
||||
pub(crate) use crate::platform_impl::Fullscreen;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct DeviceId;
|
||||
|
||||
impl DeviceId {
|
||||
pub const unsafe fn dummy() -> Self {
|
||||
DeviceId
|
||||
}
|
||||
}
|
||||
|
||||
// Constant device ID; to be removed when if backend is updated to report real device IDs.
|
||||
pub(crate) const DEVICE_ID: RootDeviceId = RootDeviceId(DeviceId);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum OsError {
|
||||
CGError(core_graphics::base::CGError),
|
||||
CreationError(&'static str),
|
||||
}
|
||||
|
||||
impl fmt::Display for OsError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
OsError::CGError(e) => f.pad(&format!("CGError {e}")),
|
||||
OsError::CreationError(e) => f.pad(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
351
src/platform_impl/apple/appkit/monitor.rs
Normal file
351
src/platform_impl/apple/appkit/monitor.rs
Normal file
|
|
@ -0,0 +1,351 @@
|
|||
#![allow(clippy::unnecessary_cast)]
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::fmt;
|
||||
|
||||
use core_foundation::array::{CFArrayGetCount, CFArrayGetValueAtIndex};
|
||||
use core_foundation::base::{CFRelease, TCFType};
|
||||
use core_foundation::string::CFString;
|
||||
use core_graphics::display::{
|
||||
CGDirectDisplayID, CGDisplay, CGDisplayBounds, CGDisplayCopyDisplayMode,
|
||||
};
|
||||
use objc2::rc::Retained;
|
||||
use objc2::runtime::AnyObject;
|
||||
use objc2_app_kit::NSScreen;
|
||||
use objc2_foundation::{ns_string, run_on_main, MainThreadMarker, NSNumber, NSPoint, NSRect};
|
||||
|
||||
use super::ffi;
|
||||
use crate::dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct VideoModeHandle {
|
||||
size: PhysicalSize<u32>,
|
||||
bit_depth: u16,
|
||||
refresh_rate_millihertz: u32,
|
||||
pub(crate) monitor: MonitorHandle,
|
||||
pub(crate) native_mode: NativeDisplayMode,
|
||||
}
|
||||
|
||||
impl PartialEq for VideoModeHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.size == other.size
|
||||
&& self.bit_depth == other.bit_depth
|
||||
&& self.refresh_rate_millihertz == other.refresh_rate_millihertz
|
||||
&& self.monitor == other.monitor
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for VideoModeHandle {}
|
||||
|
||||
impl std::hash::Hash for VideoModeHandle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.size.hash(state);
|
||||
self.bit_depth.hash(state);
|
||||
self.refresh_rate_millihertz.hash(state);
|
||||
self.monitor.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for VideoModeHandle {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("VideoModeHandle")
|
||||
.field("size", &self.size)
|
||||
.field("bit_depth", &self.bit_depth)
|
||||
.field("refresh_rate_millihertz", &self.refresh_rate_millihertz)
|
||||
.field("monitor", &self.monitor)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NativeDisplayMode(pub ffi::CGDisplayModeRef);
|
||||
|
||||
unsafe impl Send for NativeDisplayMode {}
|
||||
unsafe impl Sync for NativeDisplayMode {}
|
||||
|
||||
impl Drop for NativeDisplayMode {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
ffi::CGDisplayModeRelease(self.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for NativeDisplayMode {
|
||||
fn clone(&self) -> Self {
|
||||
unsafe {
|
||||
ffi::CGDisplayModeRetain(self.0);
|
||||
}
|
||||
NativeDisplayMode(self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl VideoModeHandle {
|
||||
pub fn size(&self) -> PhysicalSize<u32> {
|
||||
self.size
|
||||
}
|
||||
|
||||
pub fn bit_depth(&self) -> u16 {
|
||||
self.bit_depth
|
||||
}
|
||||
|
||||
pub fn refresh_rate_millihertz(&self) -> u32 {
|
||||
self.refresh_rate_millihertz
|
||||
}
|
||||
|
||||
pub fn monitor(&self) -> MonitorHandle {
|
||||
self.monitor.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MonitorHandle(CGDirectDisplayID);
|
||||
|
||||
// `CGDirectDisplayID` changes on video mode change, so we cannot rely on that
|
||||
// for comparisons, but we can use `CGDisplayCreateUUIDFromDisplayID` to get an
|
||||
// unique identifier that persists even across system reboots
|
||||
impl PartialEq for MonitorHandle {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
unsafe {
|
||||
ffi::CGDisplayCreateUUIDFromDisplayID(self.0)
|
||||
== ffi::CGDisplayCreateUUIDFromDisplayID(other.0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for MonitorHandle {}
|
||||
|
||||
impl PartialOrd for MonitorHandle {
|
||||
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for MonitorHandle {
|
||||
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
|
||||
unsafe {
|
||||
ffi::CGDisplayCreateUUIDFromDisplayID(self.0)
|
||||
.cmp(&ffi::CGDisplayCreateUUIDFromDisplayID(other.0))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for MonitorHandle {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
unsafe {
|
||||
ffi::CGDisplayCreateUUIDFromDisplayID(self.0).hash(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn available_monitors() -> VecDeque<MonitorHandle> {
|
||||
if let Ok(displays) = CGDisplay::active_displays() {
|
||||
let mut monitors = VecDeque::with_capacity(displays.len());
|
||||
for display in displays {
|
||||
monitors.push_back(MonitorHandle(display));
|
||||
}
|
||||
monitors
|
||||
} else {
|
||||
VecDeque::with_capacity(0)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn primary_monitor() -> MonitorHandle {
|
||||
MonitorHandle(CGDisplay::main().id)
|
||||
}
|
||||
|
||||
impl fmt::Debug for MonitorHandle {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("MonitorHandle")
|
||||
.field("name", &self.name())
|
||||
.field("native_identifier", &self.native_identifier())
|
||||
.field("size", &self.size())
|
||||
.field("position", &self.position())
|
||||
.field("scale_factor", &self.scale_factor())
|
||||
.field("refresh_rate_millihertz", &self.refresh_rate_millihertz())
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
}
|
||||
|
||||
impl MonitorHandle {
|
||||
pub fn new(id: CGDirectDisplayID) -> Self {
|
||||
MonitorHandle(id)
|
||||
}
|
||||
|
||||
// TODO: Be smarter about this:
|
||||
// <https://github.com/glfw/glfw/blob/57cbded0760a50b9039ee0cb3f3c14f60145567c/src/cocoa_monitor.m#L44-L126>
|
||||
pub fn name(&self) -> Option<String> {
|
||||
let MonitorHandle(display_id) = *self;
|
||||
let screen_num = CGDisplay::new(display_id).model_number();
|
||||
Some(format!("Monitor #{screen_num}"))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn native_identifier(&self) -> u32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
pub fn size(&self) -> PhysicalSize<u32> {
|
||||
let MonitorHandle(display_id) = *self;
|
||||
let display = CGDisplay::new(display_id);
|
||||
let height = display.pixels_high();
|
||||
let width = display.pixels_wide();
|
||||
PhysicalSize::from_logical::<_, f64>((width as f64, height as f64), self.scale_factor())
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn position(&self) -> PhysicalPosition<i32> {
|
||||
// This is already in screen coordinates. If we were using `NSScreen`,
|
||||
// then a conversion would've been needed:
|
||||
// flip_window_screen_coordinates(self.ns_screen(mtm)?.frame())
|
||||
let bounds = unsafe { CGDisplayBounds(self.native_identifier()) };
|
||||
let position = LogicalPosition::new(bounds.origin.x, bounds.origin.y);
|
||||
position.to_physical(self.scale_factor())
|
||||
}
|
||||
|
||||
pub fn scale_factor(&self) -> f64 {
|
||||
run_on_main(|mtm| {
|
||||
match self.ns_screen(mtm) {
|
||||
Some(screen) => screen.backingScaleFactor() as f64,
|
||||
None => 1.0, // default to 1.0 when we can't find the screen
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn refresh_rate_millihertz(&self) -> Option<u32> {
|
||||
unsafe {
|
||||
let current_display_mode = NativeDisplayMode(CGDisplayCopyDisplayMode(self.0) as _);
|
||||
let refresh_rate = ffi::CGDisplayModeGetRefreshRate(current_display_mode.0);
|
||||
if refresh_rate > 0.0 {
|
||||
return Some((refresh_rate * 1000.0).round() as u32);
|
||||
}
|
||||
|
||||
let mut display_link = std::ptr::null_mut();
|
||||
if ffi::CVDisplayLinkCreateWithCGDisplay(self.0, &mut display_link)
|
||||
!= ffi::kCVReturnSuccess
|
||||
{
|
||||
return None;
|
||||
}
|
||||
let time = ffi::CVDisplayLinkGetNominalOutputVideoRefreshPeriod(display_link);
|
||||
ffi::CVDisplayLinkRelease(display_link);
|
||||
|
||||
// This value is indefinite if an invalid display link was specified
|
||||
if time.flags & ffi::kCVTimeIsIndefinite != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
(time.time_scale as i64).checked_div(time.time_value).map(|v| (v * 1000) as u32)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn video_modes(&self) -> impl Iterator<Item = VideoModeHandle> {
|
||||
let refresh_rate_millihertz = self.refresh_rate_millihertz().unwrap_or(0);
|
||||
let monitor = self.clone();
|
||||
|
||||
unsafe {
|
||||
let modes = {
|
||||
let array = ffi::CGDisplayCopyAllDisplayModes(self.0, std::ptr::null());
|
||||
assert!(!array.is_null(), "failed to get list of display modes");
|
||||
let array_count = CFArrayGetCount(array);
|
||||
let modes: Vec<_> = (0..array_count)
|
||||
.map(move |i| {
|
||||
let mode = CFArrayGetValueAtIndex(array, i) as *mut _;
|
||||
ffi::CGDisplayModeRetain(mode);
|
||||
mode
|
||||
})
|
||||
.collect();
|
||||
CFRelease(array as *const _);
|
||||
modes
|
||||
};
|
||||
|
||||
modes.into_iter().map(move |mode| {
|
||||
let cg_refresh_rate_hertz = ffi::CGDisplayModeGetRefreshRate(mode).round() as i64;
|
||||
|
||||
// CGDisplayModeGetRefreshRate returns 0.0 for any display that
|
||||
// isn't a CRT
|
||||
let refresh_rate_millihertz = if cg_refresh_rate_hertz > 0 {
|
||||
(cg_refresh_rate_hertz * 1000) as u32
|
||||
} else {
|
||||
refresh_rate_millihertz
|
||||
};
|
||||
|
||||
let pixel_encoding =
|
||||
CFString::wrap_under_create_rule(ffi::CGDisplayModeCopyPixelEncoding(mode))
|
||||
.to_string();
|
||||
let bit_depth = if pixel_encoding.eq_ignore_ascii_case(ffi::IO32BitDirectPixels) {
|
||||
32
|
||||
} else if pixel_encoding.eq_ignore_ascii_case(ffi::IO16BitDirectPixels) {
|
||||
16
|
||||
} else if pixel_encoding.eq_ignore_ascii_case(ffi::kIO30BitDirectPixels) {
|
||||
30
|
||||
} else {
|
||||
unimplemented!()
|
||||
};
|
||||
|
||||
VideoModeHandle {
|
||||
size: PhysicalSize::new(
|
||||
ffi::CGDisplayModeGetPixelWidth(mode) as u32,
|
||||
ffi::CGDisplayModeGetPixelHeight(mode) as u32,
|
||||
),
|
||||
refresh_rate_millihertz,
|
||||
bit_depth,
|
||||
monitor: monitor.clone(),
|
||||
native_mode: NativeDisplayMode(mode),
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ns_screen(&self, mtm: MainThreadMarker) -> Option<Retained<NSScreen>> {
|
||||
let uuid = unsafe { ffi::CGDisplayCreateUUIDFromDisplayID(self.0) };
|
||||
NSScreen::screens(mtm).into_iter().find(|screen| {
|
||||
let other_native_id = get_display_id(screen);
|
||||
let other_uuid = unsafe {
|
||||
ffi::CGDisplayCreateUUIDFromDisplayID(other_native_id as CGDirectDisplayID)
|
||||
};
|
||||
uuid == other_uuid
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn get_display_id(screen: &NSScreen) -> u32 {
|
||||
let key = ns_string!("NSScreenNumber");
|
||||
|
||||
objc2::rc::autoreleasepool(|_| {
|
||||
let device_description = screen.deviceDescription();
|
||||
|
||||
// Retrieve the CGDirectDisplayID associated with this screen
|
||||
//
|
||||
// SAFETY: The value from @"NSScreenNumber" in deviceDescription is guaranteed
|
||||
// to be an NSNumber. See documentation for `deviceDescription` for details:
|
||||
// <https://developer.apple.com/documentation/appkit/nsscreen/1388360-devicedescription?language=objc>
|
||||
let obj = device_description
|
||||
.get(key)
|
||||
.expect("failed getting screen display id from device description");
|
||||
let obj: *const AnyObject = obj;
|
||||
let obj: *const NSNumber = obj.cast();
|
||||
let obj: &NSNumber = unsafe { &*obj };
|
||||
|
||||
obj.as_u32()
|
||||
})
|
||||
}
|
||||
|
||||
/// Core graphics screen coordinates are relative to the top-left corner of
|
||||
/// the so-called "main" display, with y increasing downwards - which is
|
||||
/// exactly what we want in Winit.
|
||||
///
|
||||
/// However, `NSWindow` and `NSScreen` changes these coordinates to:
|
||||
/// 1. Be relative to the bottom-left corner of the "main" screen.
|
||||
/// 2. Be relative to the bottom-left corner of the window/screen itself.
|
||||
/// 3. Have y increasing upwards.
|
||||
///
|
||||
/// This conversion happens to be symmetric, so we only need this one function
|
||||
/// to convert between the two coordinate systems.
|
||||
pub(crate) fn flip_window_screen_coordinates(frame: NSRect) -> NSPoint {
|
||||
// It is intentional that we use `CGMainDisplayID` (as opposed to
|
||||
// `NSScreen::mainScreen`), because that's what the screen coordinates
|
||||
// are relative to, no matter which display the window is currently on.
|
||||
let main_screen_height = CGDisplay::main().bounds().size.height;
|
||||
|
||||
let y = main_screen_height - frame.size.height - frame.origin.y;
|
||||
NSPoint::new(frame.origin.x, y)
|
||||
}
|
||||
312
src/platform_impl/apple/appkit/observer.rs
Normal file
312
src/platform_impl/apple/appkit/observer.rs
Normal file
|
|
@ -0,0 +1,312 @@
|
|||
//! Utilities for working with `CFRunLoop`.
|
||||
//!
|
||||
//! See Apple's documentation on Run Loops for details:
|
||||
//! <https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html>
|
||||
use std::cell::Cell;
|
||||
use std::ffi::c_void;
|
||||
use std::panic::{AssertUnwindSafe, UnwindSafe};
|
||||
use std::ptr;
|
||||
use std::rc::Weak;
|
||||
use std::time::Instant;
|
||||
|
||||
use block2::Block;
|
||||
use core_foundation::base::{CFIndex, CFOptionFlags, CFRelease, CFTypeRef};
|
||||
use core_foundation::date::CFAbsoluteTimeGetCurrent;
|
||||
use core_foundation::runloop::{
|
||||
kCFRunLoopAfterWaiting, kCFRunLoopBeforeWaiting, kCFRunLoopCommonModes, kCFRunLoopDefaultMode,
|
||||
kCFRunLoopExit, CFRunLoopActivity, CFRunLoopAddObserver, CFRunLoopAddTimer, CFRunLoopGetMain,
|
||||
CFRunLoopObserverCallBack, CFRunLoopObserverContext, CFRunLoopObserverCreate,
|
||||
CFRunLoopObserverRef, CFRunLoopRef, CFRunLoopTimerCreate, CFRunLoopTimerInvalidate,
|
||||
CFRunLoopTimerRef, CFRunLoopTimerSetNextFireDate, CFRunLoopWakeUp,
|
||||
};
|
||||
use objc2_foundation::MainThreadMarker;
|
||||
use tracing::error;
|
||||
|
||||
use super::app_state::ApplicationDelegate;
|
||||
use super::event_loop::{stop_app_on_panic, PanicInfo};
|
||||
use super::ffi;
|
||||
|
||||
unsafe fn control_flow_handler<F>(panic_info: *mut c_void, f: F)
|
||||
where
|
||||
F: FnOnce(Weak<PanicInfo>) + UnwindSafe,
|
||||
{
|
||||
let info_from_raw = unsafe { Weak::from_raw(panic_info as *mut PanicInfo) };
|
||||
// Asserting unwind safety on this type should be fine because `PanicInfo` is
|
||||
// `RefUnwindSafe` and `Rc<T>` is `UnwindSafe` if `T` is `RefUnwindSafe`.
|
||||
let panic_info = AssertUnwindSafe(Weak::clone(&info_from_raw));
|
||||
// `from_raw` takes ownership of the data behind the pointer.
|
||||
// But if this scope takes ownership of the weak pointer, then
|
||||
// the weak pointer will get free'd at the end of the scope.
|
||||
// However we want to keep that weak reference around after the function.
|
||||
std::mem::forget(info_from_raw);
|
||||
|
||||
let mtm = MainThreadMarker::new().unwrap();
|
||||
stop_app_on_panic(mtm, Weak::clone(&panic_info), move || {
|
||||
let _ = &panic_info;
|
||||
f(panic_info.0)
|
||||
});
|
||||
}
|
||||
|
||||
// begin is queued with the highest priority to ensure it is processed before other observers
|
||||
extern "C" fn control_flow_begin_handler(
|
||||
_: CFRunLoopObserverRef,
|
||||
activity: CFRunLoopActivity,
|
||||
panic_info: *mut c_void,
|
||||
) {
|
||||
unsafe {
|
||||
control_flow_handler(panic_info, |panic_info| {
|
||||
#[allow(non_upper_case_globals)]
|
||||
match activity {
|
||||
kCFRunLoopAfterWaiting => {
|
||||
// trace!("Triggered `CFRunLoopAfterWaiting`");
|
||||
ApplicationDelegate::get(MainThreadMarker::new().unwrap()).wakeup(panic_info);
|
||||
// trace!("Completed `CFRunLoopAfterWaiting`");
|
||||
},
|
||||
_ => unreachable!(),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// end is queued with the lowest priority to ensure it is processed after other observers
|
||||
// without that, LoopExiting would get sent after AboutToWait
|
||||
extern "C" fn control_flow_end_handler(
|
||||
_: CFRunLoopObserverRef,
|
||||
activity: CFRunLoopActivity,
|
||||
panic_info: *mut c_void,
|
||||
) {
|
||||
unsafe {
|
||||
control_flow_handler(panic_info, |panic_info| {
|
||||
#[allow(non_upper_case_globals)]
|
||||
match activity {
|
||||
kCFRunLoopBeforeWaiting => {
|
||||
// trace!("Triggered `CFRunLoopBeforeWaiting`");
|
||||
ApplicationDelegate::get(MainThreadMarker::new().unwrap()).cleared(panic_info);
|
||||
// trace!("Completed `CFRunLoopBeforeWaiting`");
|
||||
},
|
||||
kCFRunLoopExit => (), // unimplemented!(), // not expected to ever happen
|
||||
_ => unreachable!(),
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct RunLoop(CFRunLoopRef);
|
||||
|
||||
impl Default for RunLoop {
|
||||
fn default() -> Self {
|
||||
Self(ptr::null_mut())
|
||||
}
|
||||
}
|
||||
|
||||
impl RunLoop {
|
||||
pub fn main(mtm: MainThreadMarker) -> Self {
|
||||
// SAFETY: We have a MainThreadMarker here, which means we know we're on the main thread, so
|
||||
// scheduling (and scheduling a non-`Send` block) to that thread is allowed.
|
||||
let _ = mtm;
|
||||
RunLoop(unsafe { CFRunLoopGetMain() })
|
||||
}
|
||||
|
||||
pub fn wakeup(&self) {
|
||||
unsafe { CFRunLoopWakeUp(self.0) }
|
||||
}
|
||||
|
||||
unsafe fn add_observer(
|
||||
&self,
|
||||
flags: CFOptionFlags,
|
||||
priority: CFIndex,
|
||||
handler: CFRunLoopObserverCallBack,
|
||||
context: *mut CFRunLoopObserverContext,
|
||||
) {
|
||||
let observer = unsafe {
|
||||
CFRunLoopObserverCreate(
|
||||
ptr::null_mut(),
|
||||
flags,
|
||||
ffi::TRUE, // Indicates we want this to run repeatedly
|
||||
priority, // The lower the value, the sooner this will run
|
||||
handler,
|
||||
context,
|
||||
)
|
||||
};
|
||||
unsafe { CFRunLoopAddObserver(self.0, observer, kCFRunLoopCommonModes) };
|
||||
}
|
||||
|
||||
/// Submit a closure to run on the main thread as the next step in the run loop, before other
|
||||
/// event sources are processed.
|
||||
///
|
||||
/// This is used for running event handlers, as those are not allowed to run re-entrantly.
|
||||
///
|
||||
/// # Implementation
|
||||
///
|
||||
/// This queuing could be implemented in the following several ways with subtle differences in
|
||||
/// timing. This list is sorted in rough order in which they are run:
|
||||
///
|
||||
/// 1. Using `CFRunLoopPerformBlock` or `-[NSRunLoop performBlock:]`.
|
||||
///
|
||||
/// 2. Using `-[NSObject performSelectorOnMainThread:withObject:waitUntilDone:]` or wrapping the
|
||||
/// event in `NSEvent` and posting that to `-[NSApplication postEvent:atStart:]` (both
|
||||
/// creates a custom `CFRunLoopSource`, and signals that to wake up the main event loop).
|
||||
///
|
||||
/// a. `atStart = true`.
|
||||
///
|
||||
/// b. `atStart = false`.
|
||||
///
|
||||
/// 3. `dispatch_async` or `dispatch_async_f`. Note that this may appear before 2b, it does not
|
||||
/// respect the ordering that runloop events have.
|
||||
///
|
||||
/// We choose the first one, both for ease-of-implementation, but mostly for consistency, as we
|
||||
/// want the event to be queued in a way that preserves the order the events originally arrived
|
||||
/// in.
|
||||
///
|
||||
/// As an example, let's assume that we receive two events from the user, a mouse click which we
|
||||
/// handled by queuing it, and a window resize which we handled immediately. If we allowed
|
||||
/// AppKit to choose the ordering when queuing the mouse event, it might get put in the back of
|
||||
/// the queue, and the events would appear out of order to the user of Winit. So we must instead
|
||||
/// put the event at the very front of the queue, to be handled as soon as possible after
|
||||
/// handling whatever event it's currently handling.
|
||||
pub fn queue_closure(&self, closure: impl FnOnce() + 'static) {
|
||||
extern "C" {
|
||||
fn CFRunLoopPerformBlock(rl: CFRunLoopRef, mode: CFTypeRef, block: &Block<dyn Fn()>);
|
||||
}
|
||||
|
||||
// Convert `FnOnce()` to `Block<dyn Fn()>`.
|
||||
let closure = Cell::new(Some(closure));
|
||||
let block = block2::RcBlock::new(move || {
|
||||
if let Some(closure) = closure.take() {
|
||||
closure()
|
||||
} else {
|
||||
error!("tried to execute queued closure on main thread twice");
|
||||
}
|
||||
});
|
||||
|
||||
// There are a few common modes (`kCFRunLoopCommonModes`) defined by Cocoa:
|
||||
// - `NSDefaultRunLoopMode`, alias of `kCFRunLoopDefaultMode`.
|
||||
// - `NSEventTrackingRunLoopMode`, used when mouse-dragging and live-resizing a window.
|
||||
// - `NSModalPanelRunLoopMode`, used when running a modal inside the Winit event loop.
|
||||
// - `NSConnectionReplyMode`: TODO.
|
||||
//
|
||||
// We only want to run event handlers in the default mode, as we support running a blocking
|
||||
// modal inside a Winit event handler (see [#1779]) which outrules the modal panel mode, and
|
||||
// resizing such panel window enters the event tracking run loop mode, so we can't directly
|
||||
// trigger events inside that mode either.
|
||||
//
|
||||
// Any events that are queued while running a modal or when live-resizing will instead wait,
|
||||
// and be delivered to the application afterwards.
|
||||
//
|
||||
// [#1779]: https://github.com/rust-windowing/winit/issues/1779
|
||||
let mode = unsafe { kCFRunLoopDefaultMode as CFTypeRef };
|
||||
|
||||
// SAFETY: The runloop is valid, the mode is a `CFStringRef`, and the block is `'static`.
|
||||
unsafe { CFRunLoopPerformBlock(self.0, mode, &block) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setup_control_flow_observers(mtm: MainThreadMarker, panic_info: Weak<PanicInfo>) {
|
||||
let run_loop = RunLoop::main(mtm);
|
||||
unsafe {
|
||||
let mut context = CFRunLoopObserverContext {
|
||||
info: Weak::into_raw(panic_info) as *mut _,
|
||||
version: 0,
|
||||
retain: None,
|
||||
release: None,
|
||||
copyDescription: None,
|
||||
};
|
||||
run_loop.add_observer(
|
||||
kCFRunLoopAfterWaiting,
|
||||
CFIndex::MIN,
|
||||
control_flow_begin_handler,
|
||||
&mut context as *mut _,
|
||||
);
|
||||
run_loop.add_observer(
|
||||
kCFRunLoopExit | kCFRunLoopBeforeWaiting,
|
||||
CFIndex::MAX,
|
||||
control_flow_end_handler,
|
||||
&mut context as *mut _,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct EventLoopWaker {
|
||||
timer: CFRunLoopTimerRef,
|
||||
|
||||
/// An arbitrary instant in the past, that will trigger an immediate wake
|
||||
/// We save this as the `next_fire_date` for consistency so we can
|
||||
/// easily check if the next_fire_date needs updating.
|
||||
start_instant: Instant,
|
||||
|
||||
/// This is what the `NextFireDate` has been set to.
|
||||
/// `None` corresponds to `waker.stop()` and `start_instant` is used
|
||||
/// for `waker.start()`
|
||||
next_fire_date: Option<Instant>,
|
||||
}
|
||||
|
||||
impl Drop for EventLoopWaker {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
CFRunLoopTimerInvalidate(self.timer);
|
||||
CFRelease(self.timer as _);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventLoopWaker {
|
||||
pub(crate) fn new() -> Self {
|
||||
extern "C" fn wakeup_main_loop(_timer: CFRunLoopTimerRef, _info: *mut c_void) {}
|
||||
unsafe {
|
||||
// Create a timer with a 0.1µs interval (1ns does not work) to mimic polling.
|
||||
// It is initially setup with a first fire time really far into the
|
||||
// future, but that gets changed to fire immediately in did_finish_launching
|
||||
let timer = CFRunLoopTimerCreate(
|
||||
ptr::null_mut(),
|
||||
f64::MAX,
|
||||
0.000_000_1,
|
||||
0,
|
||||
0,
|
||||
wakeup_main_loop,
|
||||
ptr::null_mut(),
|
||||
);
|
||||
CFRunLoopAddTimer(CFRunLoopGetMain(), timer, kCFRunLoopCommonModes);
|
||||
Self { timer, start_instant: Instant::now(), next_fire_date: None }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn stop(&mut self) {
|
||||
if self.next_fire_date.is_some() {
|
||||
self.next_fire_date = None;
|
||||
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MAX) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) {
|
||||
if self.next_fire_date != Some(self.start_instant) {
|
||||
self.next_fire_date = Some(self.start_instant);
|
||||
unsafe { CFRunLoopTimerSetNextFireDate(self.timer, f64::MIN) }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start_at(&mut self, instant: Option<Instant>) {
|
||||
let now = Instant::now();
|
||||
match instant {
|
||||
Some(instant) if now >= instant => {
|
||||
self.start();
|
||||
},
|
||||
Some(instant) => {
|
||||
if self.next_fire_date != Some(instant) {
|
||||
self.next_fire_date = Some(instant);
|
||||
unsafe {
|
||||
let current = CFAbsoluteTimeGetCurrent();
|
||||
let duration = instant - now;
|
||||
let fsecs = duration.subsec_nanos() as f64 / 1_000_000_000.0
|
||||
+ duration.as_secs() as f64;
|
||||
CFRunLoopTimerSetNextFireDate(self.timer, current + fsecs)
|
||||
}
|
||||
}
|
||||
},
|
||||
None => {
|
||||
self.stop();
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/platform_impl/apple/appkit/util.rs
Normal file
28
src/platform_impl/apple/appkit/util.rs
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
use tracing::trace;
|
||||
|
||||
macro_rules! trace_scope {
|
||||
($s:literal) => {
|
||||
let _crate =
|
||||
$crate::platform_impl::platform::appkit::util::TraceGuard::new(module_path!(), $s);
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) struct TraceGuard {
|
||||
module_path: &'static str,
|
||||
called_from_fn: &'static str,
|
||||
}
|
||||
|
||||
impl TraceGuard {
|
||||
#[inline]
|
||||
pub(crate) fn new(module_path: &'static str, called_from_fn: &'static str) -> Self {
|
||||
trace!(target = module_path, "Triggered `{}`", called_from_fn);
|
||||
Self { module_path, called_from_fn }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TraceGuard {
|
||||
#[inline]
|
||||
fn drop(&mut self) {
|
||||
trace!(target = self.module_path, "Completed `{}`", self.called_from_fn);
|
||||
}
|
||||
}
|
||||
1128
src/platform_impl/apple/appkit/view.rs
Normal file
1128
src/platform_impl/apple/appkit/view.rs
Normal file
File diff suppressed because it is too large
Load diff
126
src/platform_impl/apple/appkit/window.rs
Normal file
126
src/platform_impl/apple/appkit/window.rs
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
#![allow(clippy::unnecessary_cast)]
|
||||
|
||||
use objc2::rc::{autoreleasepool, Retained};
|
||||
use objc2::{declare_class, mutability, ClassType, DeclaredClass};
|
||||
use objc2_app_kit::{NSResponder, NSWindow};
|
||||
use objc2_foundation::{MainThreadBound, MainThreadMarker, NSObject};
|
||||
|
||||
use super::event_loop::ActiveEventLoop;
|
||||
use super::window_delegate::WindowDelegate;
|
||||
use crate::error::OsError as RootOsError;
|
||||
use crate::window::WindowAttributes;
|
||||
|
||||
pub(crate) struct Window {
|
||||
window: MainThreadBound<Retained<WinitWindow>>,
|
||||
/// The window only keeps a weak reference to this, so we must keep it around here.
|
||||
delegate: MainThreadBound<Retained<WindowDelegate>>,
|
||||
}
|
||||
|
||||
impl Drop for Window {
|
||||
fn drop(&mut self) {
|
||||
self.window.get_on_main(|window| autoreleasepool(|_| window.close()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Window {
|
||||
pub(crate) fn new(
|
||||
window_target: &ActiveEventLoop,
|
||||
attributes: WindowAttributes,
|
||||
) -> Result<Self, RootOsError> {
|
||||
let mtm = window_target.mtm;
|
||||
let delegate = autoreleasepool(|_| {
|
||||
WindowDelegate::new(window_target.app_delegate(), attributes, mtm)
|
||||
})?;
|
||||
Ok(Window {
|
||||
window: MainThreadBound::new(delegate.window().retain(), mtm),
|
||||
delegate: MainThreadBound::new(delegate, mtm),
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&WindowDelegate) + Send + 'static) {
|
||||
// For now, don't actually do queuing, since it may be less predictable
|
||||
self.maybe_wait_on_main(f)
|
||||
}
|
||||
|
||||
pub(crate) fn maybe_wait_on_main<R: Send>(
|
||||
&self,
|
||||
f: impl FnOnce(&WindowDelegate) -> R + Send,
|
||||
) -> R {
|
||||
self.delegate.get_on_main(|delegate| f(delegate))
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_06")]
|
||||
#[inline]
|
||||
pub(crate) fn raw_window_handle_rwh_06(
|
||||
&self,
|
||||
) -> Result<rwh_06::RawWindowHandle, rwh_06::HandleError> {
|
||||
if let Some(mtm) = MainThreadMarker::new() {
|
||||
Ok(self.delegate.get(mtm).raw_window_handle_rwh_06())
|
||||
} else {
|
||||
Err(rwh_06::HandleError::Unavailable)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "rwh_06")]
|
||||
#[inline]
|
||||
pub(crate) fn raw_display_handle_rwh_06(
|
||||
&self,
|
||||
) -> Result<rwh_06::RawDisplayHandle, rwh_06::HandleError> {
|
||||
Ok(rwh_06::RawDisplayHandle::AppKit(rwh_06::AppKitDisplayHandle::new()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
pub struct WindowId(pub usize);
|
||||
|
||||
impl WindowId {
|
||||
pub const unsafe fn dummy() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<WindowId> for u64 {
|
||||
fn from(window_id: WindowId) -> Self {
|
||||
window_id.0 as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u64> for WindowId {
|
||||
fn from(raw_id: u64) -> Self {
|
||||
Self(raw_id as usize)
|
||||
}
|
||||
}
|
||||
|
||||
declare_class!(
|
||||
#[derive(Debug)]
|
||||
pub struct WinitWindow;
|
||||
|
||||
unsafe impl ClassType for WinitWindow {
|
||||
#[inherits(NSResponder, NSObject)]
|
||||
type Super = NSWindow;
|
||||
type Mutability = mutability::MainThreadOnly;
|
||||
const NAME: &'static str = "WinitWindow";
|
||||
}
|
||||
|
||||
impl DeclaredClass for WinitWindow {}
|
||||
|
||||
unsafe impl WinitWindow {
|
||||
#[method(canBecomeMainWindow)]
|
||||
fn can_become_main_window(&self) -> bool {
|
||||
trace_scope!("canBecomeMainWindow");
|
||||
true
|
||||
}
|
||||
|
||||
#[method(canBecomeKeyWindow)]
|
||||
fn can_become_key_window(&self) -> bool {
|
||||
trace_scope!("canBecomeKeyWindow");
|
||||
true
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
impl WinitWindow {
|
||||
pub(super) fn id(&self) -> WindowId {
|
||||
WindowId(self as *const Self as usize)
|
||||
}
|
||||
}
|
||||
1867
src/platform_impl/apple/appkit/window_delegate.rs
Normal file
1867
src/platform_impl/apple/appkit/window_delegate.rs
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue