macOS: add a way to hook standard keybinding events

Add macOS specific application handler to deliver macOS specific
events.

Co-authored-by: Mads Marquart <mads@marquart.dk>
This commit is contained in:
Nico Burns 2024-10-23 17:33:42 +09:00 committed by GitHub
parent a5f5ce6a3d
commit c913cdab0b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 110 additions and 3 deletions

View file

@ -22,7 +22,9 @@ use winit::event::{DeviceEvent, DeviceId, Ime, MouseButton, MouseScrollDelta, Wi
use winit::event_loop::{ActiveEventLoop, EventLoop}; use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::{Key, ModifiersState}; use winit::keyboard::{Key, ModifiersState};
#[cfg(macos_platform)] #[cfg(macos_platform)]
use winit::platform::macos::{OptionAsAlt, WindowAttributesExtMacOS, WindowExtMacOS}; use winit::platform::macos::{
ApplicationHandlerExtMacOS, OptionAsAlt, WindowAttributesExtMacOS, WindowExtMacOS,
};
#[cfg(any(x11_platform, wayland_platform))] #[cfg(any(x11_platform, wayland_platform))]
use winit::platform::startup_notify::{ use winit::platform::startup_notify::{
self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify, self, EventLoopExtStartupNotify, WindowAttributesExtStartupNotify, WindowExtStartupNotify,
@ -552,6 +554,23 @@ impl ApplicationHandler for Application {
// We must drop the context here. // We must drop the context here.
self.context = None; 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. /// State of the window.

View file

@ -2,6 +2,8 @@
use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent}; use crate::event::{DeviceEvent, DeviceId, StartCause, WindowEvent};
use crate::event_loop::ActiveEventLoop; use crate::event_loop::ActiveEventLoop;
#[cfg(any(docsrs, macos_platform))]
use crate::platform::macos::ApplicationHandlerExtMacOS;
use crate::window::WindowId; use crate::window::WindowId;
/// The handler of the application events. /// The handler of the application events.
@ -343,6 +345,15 @@ pub trait ApplicationHandler {
fn memory_warning(&mut self, event_loop: &dyn ActiveEventLoop) { fn memory_warning(&mut self, event_loop: &dyn ActiveEventLoop) {
let _ = event_loop; 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)] #[deny(clippy::missing_trait_methods)]
@ -411,6 +422,12 @@ impl<A: ?Sized + ApplicationHandler> ApplicationHandler for &mut A {
fn memory_warning(&mut self, event_loop: &dyn ActiveEventLoop) { fn memory_warning(&mut self, event_loop: &dyn ActiveEventLoop) {
(**self).memory_warning(event_loop); (**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)] #[deny(clippy::missing_trait_methods)]
@ -479,4 +496,10 @@ impl<A: ?Sized + ApplicationHandler> ApplicationHandler for Box<A> {
fn memory_warning(&mut self, event_loop: &dyn ActiveEventLoop) { fn memory_warning(&mut self, event_loop: &dyn ActiveEventLoop) {
(**self).memory_warning(event_loop); (**self).memory_warning(event_loop);
} }
#[cfg(any(docsrs, macos_platform))]
#[inline]
fn macos_handler(&mut self) -> Option<&mut dyn ApplicationHandlerExtMacOS> {
(**self).macos_handler()
}
} }

View file

@ -60,6 +60,8 @@ changelog entry.
and `Serialize` on many types. and `Serialize` on many types.
- Add `MonitorHandle::current_video_mode()`. - Add `MonitorHandle::current_video_mode()`.
- Add basic iOS IME support. The soft keyboard can now be shown using `Window::set_ime_allowed`. - 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` - 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. to fully disable the menu bar and dock in Borderless Fullscreen as commonly done in games.
- Add `WindowId::into_raw()` and `from_raw()`. - Add `WindowId::into_raw()` and `from_raw()`.

View file

@ -75,9 +75,10 @@ use std::os::raw::c_void;
#[cfg(feature = "serde")] #[cfg(feature = "serde")]
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::application::ApplicationHandler;
use crate::event_loop::{ActiveEventLoop, EventLoopBuilder}; use crate::event_loop::{ActiveEventLoop, EventLoopBuilder};
use crate::monitor::MonitorHandle; use crate::monitor::MonitorHandle;
use crate::window::{Window, WindowAttributes}; use crate::window::{Window, WindowAttributes, WindowId};
/// Additional methods on [`Window`] that are specific to MacOS. /// Additional methods on [`Window`] that are specific to MacOS.
pub trait WindowExtMacOS { pub trait WindowExtMacOS {
@ -548,3 +549,52 @@ pub enum OptionAsAlt {
#[default] #[default]
None, 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;
}
}

View file

@ -412,8 +412,9 @@ declare_class!(
// Basically, we're sent this message whenever a keyboard event that doesn't generate a "human // 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. // readable" character happens, i.e. newlines, tabs, and Ctrl+C.
#[method(doCommandBySelector:)] #[method(doCommandBySelector:)]
fn do_command_by_selector(&self, _command: Sel) { fn do_command_by_selector(&self, command: Sel) {
trace_scope!("doCommandBySelector:"); trace_scope!("doCommandBySelector:");
// We shouldn't forward any character from just committed text, since we'll end up sending // 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, // 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. // 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. // Leave preedit so that we also report the key-up for this key.
self.ivars().ime_state.set(ImeState::Ground); 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.
} }
} }