diff --git a/examples/window.rs b/examples/window.rs index f9f3c8b84c..6a50ecac2a 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -22,7 +22,9 @@ use winit::event::{DeviceEvent, DeviceId, Ime, MouseButton, MouseScrollDelta, Wi use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::keyboard::{Key, ModifiersState}; #[cfg(macos_platform)] -use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS, WindowExtMacOS}; +use winit::platform::macos::{ + ApplicationHandlerExtMacOS, OptionAsAlt, WindowAttributesExtMacOS, WindowExtMacOS, +}; #[cfg(any(x11_platform, wayland_platform))] use winit::platform::startup_notify::{ self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify, @@ -552,6 +554,23 @@ impl ApplicationHandler for Application { // We must drop the context here. self.context = None; } + + #[cfg(target_os = "macos")] + fn macos_handler(&mut self) -> Option<&mut dyn ApplicationHandlerExtMacOS> { + Some(self) + } +} + +#[cfg(target_os = "macos")] +impl ApplicationHandlerExtMacOS for Application { + fn standard_key_binding( + &mut self, + _event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + action: &str, + ) { + info!(?window_id, ?action, "macOS standard key binding"); + } } /// State of the window. diff --git a/src/application.rs b/src/application.rs index f361ca76c7..320c654e24 100644 --- a/src/application.rs +++ b/src/application.rs @@ -2,6 +2,8 @@ use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; use crate::event_loop::ActiveEventLoop; +#[cfg(any(docsrs, macos_platform))] +use crate::platform::macos::ApplicationHandlerExtMacOS; use crate::window::WindowId; /// The handler of the application events. @@ -343,6 +345,15 @@ pub trait ApplicationHandler { fn memory_warning(&mut self, event_loop: &dyn ActiveEventLoop) { let _ = event_loop; } + + /// The macOS-specific handler. + /// + /// The return value from this should not change at runtime. + #[cfg(any(docsrs, macos_platform))] + #[inline(always)] + fn macos_handler(&mut self) -> Option<&mut dyn ApplicationHandlerExtMacOS> { + None + } } #[deny(clippy::missing_trait_methods)] @@ -411,6 +422,12 @@ impl ApplicationHandler for &mut A { fn memory_warning(&mut self, event_loop: &dyn ActiveEventLoop) { (**self).memory_warning(event_loop); } + + #[cfg(any(docsrs, macos_platform))] + #[inline] + fn macos_handler(&mut self) -> Option<&mut dyn ApplicationHandlerExtMacOS> { + (**self).macos_handler() + } } #[deny(clippy::missing_trait_methods)] @@ -479,4 +496,10 @@ impl ApplicationHandler for Box { fn memory_warning(&mut self, event_loop: &dyn ActiveEventLoop) { (**self).memory_warning(event_loop); } + + #[cfg(any(docsrs, macos_platform))] + #[inline] + fn macos_handler(&mut self) -> Option<&mut dyn ApplicationHandlerExtMacOS> { + (**self).macos_handler() + } } diff --git a/src/changelog/unreleased.md b/src/changelog/unreleased.md index 8a22aed48c..0a3458fc58 100644 --- a/src/changelog/unreleased.md +++ b/src/changelog/unreleased.md @@ -60,6 +60,8 @@ changelog entry. and `Serialize` on many types. - Add `MonitorHandle::current_video_mode()`. - Add basic iOS IME support. The soft keyboard can now be shown using `Window::set_ime_allowed`. +- Add `ApplicationHandlerExtMacOS` trait, and a `macos_handler` method to `ApplicationHandler` which returns a `dyn ApplicationHandlerExtMacOS` which allows for macOS specific extensions to winit. +- Add a `standard_key_binding` method to the `ApplicationHandlerExtMacOS` trait. This allows handling of standard keybindings such as "go to end of line" on macOS. - On macOS, add `WindowExtMacOS::set_borderless_game` and `WindowAttributesExtMacOS::with_borderless_game` to fully disable the menu bar and dock in Borderless Fullscreen as commonly done in games. - Add `WindowId::into_raw()` and `from_raw()`. diff --git a/src/platform/macos.rs b/src/platform/macos.rs index 77fa35dc6c..977327d5e4 100644 --- a/src/platform/macos.rs +++ b/src/platform/macos.rs @@ -75,9 +75,10 @@ use std::os::raw::c_void; #[cfg(feature = "serde")] use serde::{Deserialize, Serialize}; +use crate::application::ApplicationHandler; use crate::event_loop::{ActiveEventLoop, EventLoopBuilder}; use crate::monitor::MonitorHandle; -use crate::window::{Window, WindowAttributes}; +use crate::window::{Window, WindowAttributes, WindowId}; /// Additional methods on [`Window`] that are specific to MacOS. pub trait WindowExtMacOS { @@ -548,3 +549,52 @@ pub enum OptionAsAlt { #[default] None, } + +/// Additional events on [`ApplicationHandler`] that are specific to macOS. +/// +/// This can be registered with [`ApplicationHandler::macos_handler`]. +pub trait ApplicationHandlerExtMacOS: ApplicationHandler { + /// The system interpreted a keypress as a standard key binding command. + /// + /// Examples include inserting tabs and newlines, or moving the insertion point, see + /// [`NSStandardKeyBindingResponding`] for the full list of key bindings. They are often text + /// editing related. + /// + /// This corresponds to the [`doCommandBySelector:`] method on `NSTextInputClient`. + /// + /// The `action` parameter contains the string representation of the selector. Examples include + /// `"insertBacktab:"`, `"indent:"` and `"noop:"`. + /// + /// # Example + /// + /// ```ignore + /// impl ApplicationHandlerExtMacOS for App { + /// fn standard_key_binding( + /// &mut self, + /// event_loop: &dyn ActiveEventLoop, + /// window_id: WindowId, + /// action: &str, + /// ) { + /// match action { + /// "moveBackward:" => self.cursor.position -= 1, + /// "moveForward:" => self.cursor.position += 1, + /// _ => {} // Ignore other actions + /// } + /// } + /// } + /// ``` + /// + /// [`NSStandardKeyBindingResponding`]: https://developer.apple.com/documentation/appkit/nsstandardkeybindingresponding?language=objc + /// [`doCommandBySelector:`]: https://developer.apple.com/documentation/appkit/nstextinputclient/1438256-docommandbyselector?language=objc + #[doc(alias = "doCommandBySelector:")] + fn standard_key_binding( + &mut self, + event_loop: &dyn ActiveEventLoop, + window_id: WindowId, + action: &str, + ) { + let _ = event_loop; + let _ = window_id; + let _ = action; + } +} diff --git a/src/platform_impl/apple/appkit/view.rs b/src/platform_impl/apple/appkit/view.rs index 357e1cd648..e466856cd5 100644 --- a/src/platform_impl/apple/appkit/view.rs +++ b/src/platform_impl/apple/appkit/view.rs @@ -412,8 +412,9 @@ declare_class!( // Basically, we're sent this message whenever a keyboard event that doesn't generate a "human // readable" character happens, i.e. newlines, tabs, and Ctrl+C. #[method(doCommandBySelector:)] - fn do_command_by_selector(&self, _command: Sel) { + fn do_command_by_selector(&self, command: Sel) { trace_scope!("doCommandBySelector:"); + // We shouldn't forward any character from just committed text, since we'll end up sending // it twice with some IMEs like Korean one. We'll also always send `Enter` in that case, // which is not desired given it was used to confirm IME input. @@ -428,6 +429,18 @@ declare_class!( // Leave preedit so that we also report the key-up for this key. self.ivars().ime_state.set(ImeState::Ground); } + + // Send command action to user if they requested it. + let window_id = self.window().id(); + self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| { + if let Some(handler) = app.macos_handler() { + handler.standard_key_binding(event_loop, window_id, command.name()); + } + }); + + // The documentation for `-[NSTextInputClient doCommandBySelector:]` clearly states that + // we should not be forwarding this event up the responder chain, so no calling `super` + // here either. } }