Skip to content

Commit 0515c33

Browse files
committed
appkit: route IME key events through NSTextInputContext::handleEvent.
1 parent c608c00 commit 0515c33

File tree

4 files changed

+156
-27
lines changed

4 files changed

+156
-27
lines changed

winit-appkit/src/view.rs

Lines changed: 147 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
use std::cell::{Cell, RefCell};
33
use std::collections::{HashMap, VecDeque};
44
use std::ptr;
5-
use std::rc::Rc;
5+
use std::rc::{Rc, Weak};
66

77
use dpi::{LogicalPosition, LogicalSize};
88
use objc2::rc::Retained;
@@ -17,6 +17,7 @@ use objc2_foundation::{
1717
NSArray, NSAttributedString, NSAttributedStringKey, NSCopying, NSMutableAttributedString,
1818
NSNotFound, NSObject, NSPoint, NSRange, NSRect, NSSize, NSString, NSUInteger,
1919
};
20+
use smol_str::SmolStr;
2021
use winit_core::event::{
2122
DeviceEvent, ElementState, Ime, KeyEvent, Modifiers, MouseButton, MouseScrollDelta,
2223
PointerKind, PointerSource, TouchPhase, WindowEvent,
@@ -45,6 +46,24 @@ impl Default for CursorState {
4546
}
4647
}
4748

49+
#[derive(Debug)]
50+
struct EventFilterToken {
51+
deliver: Cell<bool>,
52+
}
53+
54+
impl EventFilterToken {
55+
fn new() -> Self {
56+
Self { deliver: Cell::new(true) }
57+
}
58+
}
59+
60+
#[derive(Debug)]
61+
struct PendingRawCharacter {
62+
serial: u64,
63+
text: SmolStr,
64+
token: Weak<EventFilterToken>,
65+
}
66+
4867
#[derive(Debug, Eq, PartialEq, Clone, Copy, Default)]
4968
enum ImeState {
5069
#[default]
@@ -135,6 +154,10 @@ pub struct ViewState {
135154
marked_text: RefCell<Retained<NSMutableAttributedString>>,
136155
accepts_first_mouse: bool,
137156

157+
current_event_serial: Cell<u64>,
158+
last_handled_event_serial: Cell<u64>,
159+
pending_raw_characters: RefCell<Vec<PendingRawCharacter>>,
160+
138161
/// The state of the `Option` as `Alt`.
139162
option_as_alt: Cell<OptionAsAlt>,
140163
}
@@ -395,6 +418,7 @@ define_class!(
395418

396419
// Commit only if we have marked text.
397420
if self.hasMarkedText() && self.is_ime_enabled() && !is_control {
421+
self.drop_conflicting_raw_characters(&string);
398422
self.queue_event(WindowEvent::Ime(Ime::Preedit(String::new(), None)));
399423
self.queue_event(WindowEvent::Ime(Ime::Commit(string)));
400424
self.ivars().ime_state.set(ImeState::Committed);
@@ -445,6 +469,8 @@ define_class!(
445469
#[unsafe(method(keyDown:))]
446470
fn key_down(&self, event: &NSEvent) {
447471
trace_scope!("keyDown:");
472+
self.begin_key_event();
473+
let mut ime_consumed_event = false;
448474
{
449475
let mut prev_input_source = self.ivars().input_source.borrow_mut();
450476
let current_input_source = self.current_input_source();
@@ -468,8 +494,11 @@ define_class!(
468494
// `doCommandBySelector`. (doCommandBySelector means that the keyboard input
469495
// is not handled by IME and should be handled by the application)
470496
if self.ivars().ime_capabilities.get().is_some() {
471-
let events_for_nsview = NSArray::from_slice(&[&*event]);
472-
self.interpretKeyEvents(&events_for_nsview);
497+
ime_consumed_event = self.handle_text_input_event(&event);
498+
if !ime_consumed_event {
499+
let events_for_nsview = NSArray::from_slice(&[&*event]);
500+
self.interpretKeyEvents(&events_for_nsview);
501+
}
473502

474503
// If the text was committed we must treat the next keyboard event as IME related.
475504
if self.ivars().ime_state.get() == ImeState::Committed {
@@ -491,30 +520,31 @@ define_class!(
491520
_ => old_ime_state != self.ivars().ime_state.get(),
492521
};
493522

494-
if !had_ime_input || self.ivars().forward_key_to_app.get() {
523+
if self.ivars().forward_key_to_app.get() || (!had_ime_input && !ime_consumed_event) {
495524
let key_event = create_key_event(&event, true, event.isARepeat());
496-
self.queue_event(WindowEvent::KeyboardInput {
497-
device_id: None,
498-
event: key_event,
499-
is_synthetic: false,
500-
});
525+
self.queue_keyboard_input_event(key_event, false);
501526
}
502527
}
503528

504529
#[unsafe(method(keyUp:))]
505530
fn key_up(&self, event: &NSEvent) {
506531
trace_scope!("keyUp:");
532+
self.begin_key_event();
533+
let mut ime_consumed_event = false;
507534

508535
let event = replace_event(event, self.option_as_alt());
509536
self.update_modifiers(&event, false);
510537

538+
if self.ivars().ime_capabilities.get().is_some() {
539+
ime_consumed_event = self.handle_text_input_event(&event);
540+
}
541+
511542
// We want to send keyboard input when we are currently in the ground state.
512-
if matches!(self.ivars().ime_state.get(), ImeState::Ground | ImeState::Disabled) {
513-
self.queue_event(WindowEvent::KeyboardInput {
514-
device_id: None,
515-
event: create_key_event(&event, false, false),
516-
is_synthetic: false,
517-
});
543+
if matches!(self.ivars().ime_state.get(), ImeState::Ground | ImeState::Disabled)
544+
&& !ime_consumed_event
545+
{
546+
let key_event = create_key_event(&event, false, false);
547+
self.queue_keyboard_input_event(key_event, false);
518548
}
519549
}
520550

@@ -561,11 +591,7 @@ define_class!(
561591
self.update_modifiers(&event, false);
562592
let event = create_key_event(&event, true, event.isARepeat());
563593

564-
self.queue_event(WindowEvent::KeyboardInput {
565-
device_id: None,
566-
event,
567-
is_synthetic: false,
568-
});
594+
self.queue_keyboard_input_event(event, false);
569595
}
570596

571597
// In the past (?), `mouseMoved:` events were not generated when the
@@ -812,6 +838,9 @@ impl WinitView {
812838
forward_key_to_app: Default::default(),
813839
marked_text: Default::default(),
814840
accepts_first_mouse,
841+
current_event_serial: Cell::new(0),
842+
last_handled_event_serial: Cell::new(0),
843+
pending_raw_characters: RefCell::new(Vec::new()),
815844
option_as_alt: Cell::new(option_as_alt),
816845
});
817846
let this: Retained<Self> = unsafe { msg_send![super(this), init] };
@@ -832,6 +861,98 @@ impl WinitView {
832861
});
833862
}
834863

864+
fn queue_keyboard_input_event(&self, key_event: KeyEvent, is_synthetic: bool) {
865+
self.cleanup_pending_raw_characters();
866+
867+
let serial = self.ivars().current_event_serial.get();
868+
let token = key_event.text.as_ref().map(|text| {
869+
let token = Rc::new(EventFilterToken::new());
870+
self.ivars().pending_raw_characters.borrow_mut().push(PendingRawCharacter {
871+
serial,
872+
text: text.clone(),
873+
token: Rc::downgrade(&token),
874+
});
875+
token
876+
});
877+
878+
let window_event =
879+
WindowEvent::KeyboardInput { device_id: None, event: key_event, is_synthetic };
880+
let window_id = window_id(&self.window());
881+
882+
if let Some(token) = token {
883+
let event_to_dispatch = window_event.clone();
884+
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
885+
if !token.deliver.get() {
886+
return;
887+
}
888+
app.window_event(event_loop, window_id, event_to_dispatch.clone());
889+
});
890+
} else {
891+
let event_to_dispatch = window_event;
892+
self.ivars().app_state.maybe_queue_with_handler(move |app, event_loop| {
893+
app.window_event(event_loop, window_id, event_to_dispatch.clone());
894+
});
895+
}
896+
}
897+
898+
fn begin_key_event(&self) {
899+
let next = self.ivars().current_event_serial.get().wrapping_add(1);
900+
self.ivars().current_event_serial.set(next);
901+
}
902+
903+
fn handle_text_input_event(&self, event: &NSEvent) -> bool {
904+
let Some(input_context) = self.inputContext() else {
905+
return false;
906+
};
907+
908+
let serial = self.ivars().current_event_serial.get();
909+
self.ivars().last_handled_event_serial.set(serial);
910+
911+
input_context.handleEvent(event)
912+
}
913+
914+
fn drop_conflicting_raw_characters(&self, commit: &str) {
915+
let serial = self.ivars().last_handled_event_serial.get();
916+
let mut pending = self.ivars().pending_raw_characters.borrow_mut();
917+
918+
let mut target: Option<(Weak<EventFilterToken>, SmolStr)> = None;
919+
920+
for entry in pending.iter().rev() {
921+
if entry.token.upgrade().is_none() {
922+
continue;
923+
}
924+
925+
if entry.serial == serial {
926+
target = Some((entry.token.clone(), entry.text.clone()));
927+
break;
928+
}
929+
}
930+
931+
if target.is_none() {
932+
if let Some(entry) = pending.iter().rev().find(|entry| entry.token.upgrade().is_some())
933+
{
934+
target = Some((entry.token.clone(), entry.text.clone()));
935+
}
936+
}
937+
938+
if let Some((token, text)) = target {
939+
if text.as_str() != commit {
940+
if let Some(token) = token.upgrade() {
941+
token.deliver.set(false);
942+
}
943+
}
944+
}
945+
946+
pending.retain(|entry| entry.token.upgrade().is_some());
947+
}
948+
949+
fn cleanup_pending_raw_characters(&self) {
950+
self.ivars()
951+
.pending_raw_characters
952+
.borrow_mut()
953+
.retain(|entry| entry.token.upgrade().is_some());
954+
}
955+
835956
fn scale_factor(&self) -> f64 {
836957
self.window().backingScaleFactor() as f64
837958
}
@@ -1030,7 +1151,12 @@ impl WinitView {
10301151
drop(phys_mod_state);
10311152

10321153
for event in events {
1033-
self.queue_event(event);
1154+
match event {
1155+
WindowEvent::KeyboardInput { event: key_event, is_synthetic, .. } => {
1156+
self.queue_keyboard_input_event(key_event, is_synthetic);
1157+
},
1158+
other => self.queue_event(other),
1159+
}
10341160
}
10351161
}
10361162
}

winit-wayland/src/seat/pointer/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,19 @@ use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport;
2222

2323
use sctk::compositor::SurfaceData;
2424
use sctk::globals::GlobalData;
25+
use sctk::seat::SeatState;
2526
use sctk::seat::pointer::{
2627
PointerData, PointerDataExt, PointerEvent, PointerEventKind, PointerHandler,
2728
};
28-
use sctk::seat::SeatState;
2929

3030
use dpi::{LogicalPosition, PhysicalPosition};
3131
use winit_core::event::{
32-
ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource, TouchPhase,
33-
WindowEvent, ButtonSource,
32+
ButtonSource, ElementState, MouseButton, MouseScrollDelta, PointerKind, PointerSource,
33+
TouchPhase, WindowEvent,
3434
};
3535

36-
use crate::state::WinitState;
3736
use crate::WindowId;
37+
use crate::state::WinitState;
3838

3939
pub mod pointer_gesture;
4040
pub mod relative_pointer;

winit-wayland/src/seat/pointer/relative_pointer.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,17 @@
33
use std::ops::Deref;
44

55
use sctk::reexports::client::globals::{BindError, GlobalList};
6-
use sctk::reexports::client::{delegate_dispatch, Dispatch};
76
use sctk::reexports::client::{Connection, QueueHandle};
7+
use sctk::reexports::client::{Dispatch, delegate_dispatch};
88
use sctk::reexports::protocols::wp::relative_pointer::zv1::{
99
client::zwp_relative_pointer_manager_v1::ZwpRelativePointerManagerV1,
1010
client::zwp_relative_pointer_v1::{self, ZwpRelativePointerV1},
1111
};
1212

1313
use sctk::globals::GlobalData;
1414

15-
use winit_core::event::DeviceEvent;
1615
use crate::state::WinitState;
16+
use winit_core::event::DeviceEvent;
1717

1818
/// Wrapper around the relative pointer.
1919
#[derive(Debug)]

winit/src/changelog/unreleased.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,9 @@ changelog entry.
276276
- On macOS, fixed redundant `SurfaceResized` event at window creation.
277277
- On macOS, don't panic on monitors with unknown bit-depths.
278278
- On macOS, fixed crash when closing the window on macOS 26+.
279+
- On macOS, route IME key events through `NSTextInputContext::handleEvent` to avoid duplicate
280+
punctuation and dead key commits produced by third-party input methods, aligning character
281+
delivery with Cocoa.
279282
- On macOS, make AppKit monitor handle tests derive their reference display IDs from
280283
`CGMainDisplayID`, preventing failures on systems where the primary monitor is not display `1`.
281284
- On Windows, account for mouse wheel lines per scroll setting for `WindowEvent::MouseWheel`.

0 commit comments

Comments
 (0)