Skip to content

Commit

Permalink
Add WindowExtMacOS::{set_,}option_as_alt
Browse files Browse the repository at this point in the history
This adds an ability to control left and right `Option` keys to be
treated as `Alt`, thus not producing diacritical marks.

Co-authored-by: Kirill Chibisov <[email protected]>
  • Loading branch information
ayax79 and kchibisov authored Jan 31, 2023
1 parent 1361393 commit 180a4c7
Show file tree
Hide file tree
Showing 7 changed files with 236 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ rls/
*.ts
*.js
#*#
.DS_Store
.DS_Store
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ And please only add new entries to the top of this list, right below the `# Unre

# Unreleased

- On macOS, added `WindowExtMacOS::option_as_alt` and `WindowExtMacOS::set_option_as_alt`.
- On Windows, fix window size for maximized, undecorated windows.
- On Windows and macOS, add `WindowBuilder::with_active`.
- Add `Window::is_minimized`.
Expand Down
67 changes: 67 additions & 0 deletions examples/window_option_as_alt.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#![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,
};

/// 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();

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::ReceivedCharacter(c) => println!("ReceivedCharacter: {c:?}"),
WindowEvent::KeyboardInput { .. } => println!("KeyboardInput: {event:?}"),
_ => (),
},
Event::MainEventsCleared => {
window.request_redraw();
}
_ => (),
}
});
}

#[cfg(not(target_os = "macos"))]
fn main() {
println!("This example is only supported on MacOS");
}
60 changes: 59 additions & 1 deletion src/platform/macos.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use objc2::rc::Id;
use std::os::raw::c_void;

use objc2::rc::Id;

use crate::{
event_loop::{EventLoopBuilder, EventLoopWindowTarget},
monitor::MonitorHandle,
Expand Down Expand Up @@ -55,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 {
Expand Down Expand Up @@ -97,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`.
Expand Down Expand Up @@ -142,6 +164,11 @@ 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 whether the `OptionAsAlt` key is interpreted as the `Alt` modifier.
///
/// 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 {
Expand Down Expand Up @@ -201,6 +228,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 {
Expand Down Expand Up @@ -311,3 +344,28 @@ impl<T> EventLoopWindowTargetExtMacOS for EventLoopWindowTarget<T> {
self.p.hide_other_applications()
}
}

/// Option as alt behavior.
///
/// The default is `None`.
#[derive(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.
None,
}

impl Default for OptionAsAlt {
fn default() -> Self {
OptionAsAlt::None
}
}
52 changes: 52 additions & 0 deletions src/platform_impl/macos/appkit/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Self, Shared> {
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;

Expand Down Expand Up @@ -123,13 +152,32 @@ extern_methods!(
#[sel(stage)]
pub fn stage(&self) -> NSInteger;

#[sel(isARepeat)]
pub fn is_a_repeat(&self) -> bool;

#[sel(windowNumber)]
pub fn window_number(&self) -> NSInteger;

#[sel(timestamp)]
pub fn timestamp(&self) -> NSTimeInterval;

pub fn characters(&self) -> Option<Id<NSString, Shared>> {
unsafe { msg_send_id![self, characters] }
}

pub fn charactersIgnoringModifiers(&self) -> Option<Id<NSString, Shared>> {
unsafe { msg_send_id![self, charactersIgnoringModifiers] }
}

pub fn lalt_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICELALTKEYMASK != 0
}

pub fn ralt_pressed(&self) -> bool {
let raw_modifiers = self.modifierFlags().bits() as u32;
raw_modifiers & NX_DEVICERALTKEYMASK != 0
}
}
);

Expand All @@ -138,6 +186,10 @@ unsafe impl NSCopying for NSEvent {
type Output = NSEvent;
}

// The values are from the https://github.com/apple-oss-distributions/IOHIDFamily/blob/19666c840a6d896468416ff0007040a10b7b46b8/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h#L258-L259
const NX_DEVICELALTKEYMASK: u32 = 0x00000020;
const NX_DEVICERALTKEYMASK: u32 = 0x00000040;

bitflags! {
pub struct NSEventModifierFlags: NSUInteger {
const NSAlphaShiftKeyMask = 1 << 16;
Expand Down
41 changes: 38 additions & 3 deletions src/platform_impl/macos/view.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ use super::appkit::{
NSApp, NSCursor, NSEvent, NSEventModifierFlags, NSEventPhase, NSResponder, NSTrackingRectTag,
NSView,
};
use crate::platform::macos::{OptionAsAlt, WindowExtMacOS};
use crate::{
dpi::{LogicalPosition, LogicalSize},
event::{
Expand Down Expand Up @@ -463,7 +464,17 @@ declare_class!(
}
let was_in_preedit = self.state.ime_state == ImeState::Preedit;

let characters = get_characters(event, false);
// Get the characters from the event.
let ev_mods = event_mods(event);
let ignore_alt_characters = match self.window().option_as_alt() {
OptionAsAlt::OnlyLeft if event.lalt_pressed() => true,
OptionAsAlt::OnlyRight if event.ralt_pressed() => true,
OptionAsAlt::Both if ev_mods.alt() => true,
_ => false,
} && !ev_mods.ctrl()
&& !ev_mods.logo();

let characters = get_characters(event, ignore_alt_characters);
self.state.forward_key_to_app = false;

// The `interpretKeyEvents` function might call
Expand All @@ -474,7 +485,13 @@ declare_class!(
// is not handled by IME and should be handled by the application)
let mut text_commited = false;
if self.state.ime_allowed {
let events_for_nsview = NSArray::from_slice(&[event.copy()]);
let new_event = if ignore_alt_characters {
replace_event_chars(event, &characters)
} else {
event.copy()
};

let events_for_nsview = NSArray::from_slice(&[new_event]);
unsafe { self.interpretKeyEvents(&events_for_nsview) };

// If the text was commited we must treat the next keyboard event as IME related.
Expand All @@ -501,7 +518,7 @@ declare_class!(
state: ElementState::Pressed,
scancode,
virtual_keycode,
modifiers: event_mods(event),
modifiers: ev_mods,
},
is_synthetic: false,
});
Expand Down Expand Up @@ -982,3 +999,21 @@ fn mouse_button(event: &NSEvent) -> MouseButton {
n => MouseButton::Other(n as u16),
}
}

fn replace_event_chars(event: &NSEvent, characters: &str) -> Id<NSEvent, Shared> {
let ns_chars = NSString::from_str(characters);
let chars_ignoring_mods = event.charactersIgnoringModifiers().unwrap();

NSEvent::keyEventWithType(
event.type_(),
event.locationInWindow(),
event.modifierFlags(),
event.timestamp(),
event.window_number(),
None,
&ns_chars,
&chars_ignoring_mods,
event.is_a_repeat(),
event.scancode(),
)
}
19 changes: 18 additions & 1 deletion src/platform_impl/macos/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 {
Expand All @@ -100,6 +101,7 @@ impl Default for PlatformSpecificWindowBuilderAttributes {
disallow_hidpi: false,
has_shadow: true,
accepts_first_mouse: true,
option_as_alt: Default::default(),
}
}
}
Expand Down Expand Up @@ -157,6 +159,9 @@ pub struct SharedState {
/// transitioning back to borderless fullscreen.
save_presentation_opts: Option<NSApplicationPresentationOptions>,
pub current_theme: Option<Theme>,

/// The state of the `Option` as `Alt`.
pub(crate) option_as_alt: OptionAsAlt,
}

impl SharedState {
Expand Down Expand Up @@ -368,6 +373,8 @@ impl WinitWindow {
this.center();
}

this.set_option_as_alt(pl_attrs.option_as_alt);

Id::into_shared(this)
})
})
Expand Down Expand Up @@ -1349,6 +1356,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 {
Expand Down

0 comments on commit 180a4c7

Please sign in to comment.