diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9229c5f2..ab27f2d9 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -38,7 +38,6 @@ And please only add new entries to the top of this list, right below the `# Unre
portable) interpretations of a given key-press.
- Add `KeyCodeExtScancode`, which lets you convert between raw keycodes and
`KeyCode`.
- - Remove `WindowExtMacOS::option_as_alt` and `WindowExtMacOS::set_option_as_alt`.
- `ModifiersChanged` now uses dedicated `Modifiers` struct.
- On Orbital, fix `ModifiersChanged` not being sent.
- **Breaking:** `CursorIcon` is now used from the `cursor-icon` crate.
diff --git a/examples/window_option_as_alt.rs b/examples/window_option_as_alt.rs
new file mode 100644
index 00000000..fc2e4582
--- /dev/null
+++ b/examples/window_option_as_alt.rs
@@ -0,0 +1,75 @@
+#![allow(clippy::single_match)]
+
+#[cfg(target_os = "macos")]
+use winit::platform::macos::{OptionAsAlt, WindowExtMacOS};
+
+#[cfg(target_os = "macos")]
+use winit::{
+ event::ElementState,
+ event::{Event, MouseButton, WindowEvent},
+ event_loop::EventLoop,
+ window::WindowBuilder,
+};
+
+#[cfg(target_os = "macos")]
+#[path = "util/fill.rs"]
+mod fill;
+
+/// Prints the keyboard events characters received when option_is_alt is true versus false.
+/// A left mouse click will toggle option_is_alt.
+#[cfg(target_os = "macos")]
+fn main() {
+ let event_loop = EventLoop::new();
+
+ let window = WindowBuilder::new()
+ .with_title("A fantastic window!")
+ .with_inner_size(winit::dpi::LogicalSize::new(128.0, 128.0))
+ .build(&event_loop)
+ .unwrap();
+
+ window.set_ime_allowed(true);
+
+ let mut option_as_alt = window.option_as_alt();
+
+ event_loop.run(move |event, _, control_flow| {
+ control_flow.set_wait();
+
+ match event {
+ Event::WindowEvent {
+ event: WindowEvent::CloseRequested,
+ window_id,
+ } if window_id == window.id() => control_flow.set_exit(),
+ Event::WindowEvent { event, .. } => match event {
+ WindowEvent::MouseInput {
+ state: ElementState::Pressed,
+ button: MouseButton::Left,
+ ..
+ } => {
+ option_as_alt = match option_as_alt {
+ OptionAsAlt::None => OptionAsAlt::OnlyLeft,
+ OptionAsAlt::OnlyLeft => OptionAsAlt::OnlyRight,
+ OptionAsAlt::OnlyRight => OptionAsAlt::Both,
+ OptionAsAlt::Both => OptionAsAlt::None,
+ };
+
+ println!("Received Mouse click, toggling option_as_alt to: {option_as_alt:?}");
+ window.set_option_as_alt(option_as_alt);
+ }
+ WindowEvent::KeyboardInput { .. } => println!("KeyboardInput: {event:?}"),
+ _ => (),
+ },
+ Event::MainEventsCleared => {
+ window.request_redraw();
+ }
+ Event::RedrawRequested(_) => {
+ fill::fill_window(&window);
+ }
+ _ => (),
+ }
+ });
+}
+
+#[cfg(not(target_os = "macos"))]
+fn main() {
+ println!("This example is only supported on MacOS");
+}
diff --git a/src/platform/macos.rs b/src/platform/macos.rs
index e4da83bd..18d4745d 100644
--- a/src/platform/macos.rs
+++ b/src/platform/macos.rs
@@ -56,6 +56,17 @@ pub trait WindowExtMacOS {
/// Put the window in a state which indicates a file save is required.
fn set_document_edited(&self, edited: bool);
+
+ /// Set option as alt behavior as described in [`OptionAsAlt`].
+ ///
+ /// This will ignore diacritical marks and accent characters from
+ /// being processed as received characters. Instead, the input
+ /// device's raw character will be placed in event queues with the
+ /// Alt modifier set.
+ fn set_option_as_alt(&self, option_as_alt: OptionAsAlt);
+
+ /// Getter for the [`WindowExtMacOS::set_option_as_alt`].
+ fn option_as_alt(&self) -> OptionAsAlt;
}
impl WindowExtMacOS for Window {
@@ -98,6 +109,16 @@ impl WindowExtMacOS for Window {
fn set_document_edited(&self, edited: bool) {
self.window.set_document_edited(edited)
}
+
+ #[inline]
+ fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) {
+ self.window.set_option_as_alt(option_as_alt)
+ }
+
+ #[inline]
+ fn option_as_alt(&self) -> OptionAsAlt {
+ self.window.option_as_alt()
+ }
}
/// Corresponds to `NSApplicationActivationPolicy`.
@@ -140,6 +161,10 @@ pub trait WindowBuilderExtMacOS {
fn with_has_shadow(self, has_shadow: bool) -> WindowBuilder;
/// Window accepts click-through mouse events.
fn with_accepts_first_mouse(self, accepts_first_mouse: bool) -> WindowBuilder;
+ /// Set how the Option keys are interpreted.
+ ///
+ /// See [`WindowExtMacOS::set_option_as_alt`] for details on what this means if set.
+ fn with_option_as_alt(self, option_as_alt: OptionAsAlt) -> WindowBuilder;
}
impl WindowBuilderExtMacOS for WindowBuilder {
@@ -199,6 +224,12 @@ impl WindowBuilderExtMacOS for WindowBuilder {
self.platform_specific.accepts_first_mouse = accepts_first_mouse;
self
}
+
+ #[inline]
+ fn with_option_as_alt(mut self, option_as_alt: OptionAsAlt) -> WindowBuilder {
+ self.platform_specific.option_as_alt = option_as_alt;
+ self
+ }
}
pub trait EventLoopBuilderExtMacOS {
@@ -309,3 +340,23 @@ impl EventLoopWindowTargetExtMacOS for EventLoopWindowTarget {
self.p.hide_other_applications()
}
}
+
+/// Option as alt behavior.
+///
+/// The default is `None`.
+#[derive(Default, Debug, Clone, Copy, PartialEq, Eq)]
+#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
+pub enum OptionAsAlt {
+ /// The left `Option` key is treated as `Alt`.
+ OnlyLeft,
+
+ /// The right `Option` key is treated as `Alt`.
+ OnlyRight,
+
+ /// Both `Option` keys are treated as `Alt`.
+ Both,
+
+ /// No special handling is applied for `Option` key.
+ #[default]
+ None,
+}
diff --git a/src/platform_impl/macos/appkit/event.rs b/src/platform_impl/macos/appkit/event.rs
index e8992585..5952c43c 100644
--- a/src/platform_impl/macos/appkit/event.rs
+++ b/src/platform_impl/macos/appkit/event.rs
@@ -67,6 +67,35 @@ extern_methods!(
}
}
+ pub fn keyEventWithType(
+ type_: NSEventType,
+ location: NSPoint,
+ modifier_flags: NSEventModifierFlags,
+ timestamp: NSTimeInterval,
+ window_num: NSInteger,
+ context: Option<&NSObject>,
+ characters: &NSString,
+ characters_ignoring_modifiers: &NSString,
+ is_a_repeat: bool,
+ scancode: c_ushort,
+ ) -> Id {
+ unsafe {
+ msg_send_id![
+ Self::class(),
+ keyEventWithType: type_,
+ location: location,
+ modifierFlags: modifier_flags,
+ timestamp: timestamp,
+ windowNumber: window_num,
+ context: context,
+ characters: characters,
+ charactersIgnoringModifiers: characters_ignoring_modifiers,
+ isARepeat: is_a_repeat,
+ keyCode: scancode,
+ ]
+ }
+ }
+
#[sel(locationInWindow)]
pub fn locationInWindow(&self) -> NSPoint;
diff --git a/src/platform_impl/macos/view.rs b/src/platform_impl/macos/view.rs
index faecdd15..0ae81874 100644
--- a/src/platform_impl/macos/view.rs
+++ b/src/platform_impl/macos/view.rs
@@ -28,6 +28,7 @@ use crate::{
TouchPhase, WindowEvent,
},
keyboard::{Key, KeyCode, KeyLocation, ModifiersState},
+ platform::macos::{OptionAsAlt, WindowExtMacOS},
platform::scancode::KeyCodeExtScancode,
platform_impl::platform::{
app_state::AppState,
@@ -482,6 +483,7 @@ declare_class!(
// Get the characters from the event.
let old_ime_state = self.state.ime_state;
self.state.forward_key_to_app = false;
+ let event = replace_event(event, self.window().option_as_alt());
// The `interpretKeyEvents` function might call
// `setMarkedText`, `insertText`, and `doCommandBySelector`.
@@ -500,7 +502,7 @@ declare_class!(
}
}
- self.update_modifiers(event, false);
+ self.update_modifiers(&event, false);
let had_ime_input = match self.state.ime_state {
ImeState::Commited => {
@@ -514,7 +516,7 @@ declare_class!(
};
if !had_ime_input || self.state.forward_key_to_app {
- let key_event = create_key_event(event, true, event.is_a_repeat(), None);
+ let key_event = create_key_event(&event, true, event.is_a_repeat(), None);
self.queue_event(WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
event: key_event,
@@ -527,13 +529,14 @@ declare_class!(
fn key_up(&mut self, event: &NSEvent) {
trace_scope!("keyUp:");
- self.update_modifiers(event, false);
+ let event = replace_event(event, self.window().option_as_alt());
+ self.update_modifiers(&event, false);
// We want to send keyboard input when we are currently in the ground state.
if matches!(self.state.ime_state, ImeState::Ground | ImeState::Disabled) {
self.queue_event(WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
- event: create_key_event(event, false, false, None),
+ event: create_key_event(&event, false, false, None),
is_synthetic: false,
});
}
@@ -1038,3 +1041,38 @@ fn mouse_button(event: &NSEvent) -> MouseButton {
n => MouseButton::Other(n as u16),
}
}
+
+// NOTE: to get option as alt working we need to rewrite events
+// we're getting from the operating system, which makes it
+// impossible to provide such events as extra in `KeyEvent`.
+fn replace_event(event: &NSEvent, option_as_alt: OptionAsAlt) -> Id {
+ let ev_mods = event_mods(event).state;
+ let ignore_alt_characters = match option_as_alt {
+ OptionAsAlt::OnlyLeft if event.lalt_pressed() => true,
+ OptionAsAlt::OnlyRight if event.ralt_pressed() => true,
+ OptionAsAlt::Both if ev_mods.alt_key() => true,
+ _ => false,
+ } && !ev_mods.control_key()
+ && !ev_mods.super_key();
+
+ if ignore_alt_characters {
+ let ns_chars = event
+ .charactersIgnoringModifiers()
+ .expect("expected characters to be non-null");
+
+ NSEvent::keyEventWithType(
+ event.type_(),
+ event.locationInWindow(),
+ event.modifierFlags(),
+ event.timestamp(),
+ event.window_number(),
+ None,
+ &ns_chars,
+ &ns_chars,
+ event.is_a_repeat(),
+ event.key_code(),
+ )
+ } else {
+ event.copy()
+ }
+}
diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs
index 05aad7b1..2ba6acaa 100644
--- a/src/platform_impl/macos/window.rs
+++ b/src/platform_impl/macos/window.rs
@@ -21,7 +21,7 @@ use crate::{
error::{ExternalError, NotSupportedError, OsError as RootOsError},
event::WindowEvent,
icon::Icon,
- platform::macos::WindowExtMacOS,
+ platform::macos::{OptionAsAlt, WindowExtMacOS},
platform_impl::platform::{
app_state::AppState,
appkit::NSWindowOrderingMode,
@@ -85,6 +85,7 @@ pub struct PlatformSpecificWindowBuilderAttributes {
pub disallow_hidpi: bool,
pub has_shadow: bool,
pub accepts_first_mouse: bool,
+ pub option_as_alt: OptionAsAlt,
}
impl Default for PlatformSpecificWindowBuilderAttributes {
@@ -100,6 +101,7 @@ impl Default for PlatformSpecificWindowBuilderAttributes {
disallow_hidpi: false,
has_shadow: true,
accepts_first_mouse: true,
+ option_as_alt: Default::default(),
}
}
}
@@ -160,6 +162,8 @@ pub struct SharedState {
/// The current resize incerments for the window content.
pub(crate) resize_increments: NSSize,
+ /// The state of the `Option` as `Alt`.
+ pub(crate) option_as_alt: OptionAsAlt,
}
impl SharedState {
@@ -369,6 +373,8 @@ impl WinitWindow {
this.center();
}
+ this.set_option_as_alt(pl_attrs.option_as_alt);
+
Id::into_shared(this)
})
})
@@ -1376,6 +1382,16 @@ impl WindowExtMacOS for WinitWindow {
fn set_document_edited(&self, edited: bool) {
self.setDocumentEdited(edited)
}
+
+ fn set_option_as_alt(&self, option_as_alt: OptionAsAlt) {
+ let mut shared_state_lock = self.shared_state.lock().unwrap();
+ shared_state_lock.option_as_alt = option_as_alt;
+ }
+
+ fn option_as_alt(&self) -> OptionAsAlt {
+ let shared_state_lock = self.shared_state.lock().unwrap();
+ shared_state_lock.option_as_alt
+ }
}
pub(super) fn get_ns_theme() -> Theme {