@@ -138,6 +138,13 @@ pub struct ViewState {
138138 /// True if we're currently processing a key event
139139 in_key_event : Cell < bool > ,
140140
141+ /// Characters of the currently-processed key event, as reported by
142+ /// `NSEvent`. Tuple of (characters, charactersIgnoringModifiers).
143+ /// Used by `insertText:` to distinguish IME direct commits (e.g. SKK
144+ /// hiragana mode) from raw keyboard pass-through (where the IME echoes
145+ /// the same characters).
146+ current_key_event_chars : RefCell < Option < ( String , String ) > > ,
147+
141148 marked_text : RefCell < Retained < NSMutableAttributedString > > ,
142149 accepts_first_mouse : bool ,
143150
@@ -146,6 +153,11 @@ pub struct ViewState {
146153
147154 /// The state of the `Option` as `Alt`.
148155 option_as_alt : Cell < OptionAsAlt > ,
156+
157+ /// Modifier mask deciding when a key event is forwarded to the IME.
158+ /// A key event is forwarded when no modifier is pressed, or when the
159+ /// pressed modifiers intersect this mask.
160+ forward_to_ime_modifier_mask : Cell < ModifiersState > ,
149161}
150162
151163declare_class ! (
@@ -421,23 +433,55 @@ declare_class!(
421433 let has_marked_text = unsafe { self . hasMarkedText( ) } ;
422434 let is_in_key_event = self . ivars( ) . in_key_event. get( ) ;
423435
436+ // Detect IME direct commits (e.g. SKK hiragana mode) that bypass
437+ // preedit: an `insertText:` whose payload differs from the raw
438+ // NSEvent characters is something the IME synthesised, not a
439+ // pass-through of the pressed key.
440+ let is_ime_direct_commit = is_in_key_event
441+ && self
442+ . ivars( )
443+ . current_key_event_chars
444+ . borrow( )
445+ . as_ref( )
446+ . is_some_and( |( chars, chars_unmod) | {
447+ string != chars. as_str( ) && string != chars_unmod. as_str( )
448+ } ) ;
449+
424450 if self . is_ime_enabled( ) {
425451 if has_marked_text && !is_control {
426452 // Clear preedit and commit the text (normal IME flow)
427453 self . queue_event( WindowEvent :: Ime ( Ime :: Preedit ( String :: new( ) , None ) ) ) ;
428454 self . queue_event( WindowEvent :: Ime ( Ime :: Commit ( string) ) ) ;
429455 self . ivars( ) . ime_state. set( ImeState :: Committed ) ;
430- } else if !is_control && !string. is_empty( ) && !is_in_key_event {
431- // Direct input not from keyboard (e.g., emoji picker)
456+ } else if !is_control
457+ && !string. is_empty( )
458+ && ( !is_in_key_event || is_ime_direct_commit)
459+ {
460+ // Direct input not from keyboard (e.g., emoji picker) or
461+ // an IME committing without preedit (e.g., SKK).
432462 self . queue_event( WindowEvent :: Ime ( Ime :: Commit ( string) ) ) ;
433463 self . ivars( ) . ime_state. set( ImeState :: Committed ) ;
464+ } else if is_in_key_event && !is_ime_direct_commit {
465+ // The IME passed the raw key through unchanged (typical
466+ // when the active input source is ASCII-only or the IME
467+ // is in a passthrough mode, e.g. SKK ASCII/eisuu mode).
468+ // Treat this as "IME did not consume the key" so the key
469+ // event is forwarded to the application.
470+ self . ivars( ) . forward_key_to_app. set( true ) ;
434471 }
435- } else if !is_control && !string. is_empty( ) && !is_in_key_event {
472+ } else if !is_control
473+ && !string. is_empty( )
474+ && ( !is_in_key_event || is_ime_direct_commit)
475+ {
436476 // IME is disabled but we got non-keyboard input (e.g., emoji picker)
437- // Temporarily enable IME for this input
477+ // or an IME-style direct commit. Temporarily enable IME for
478+ // this input.
438479 self . queue_event( WindowEvent :: Ime ( Ime :: Enabled ) ) ;
439480 self . queue_event( WindowEvent :: Ime ( Ime :: Commit ( string) ) ) ;
440481 self . ivars( ) . ime_state. set( ImeState :: Committed ) ;
482+ } else if is_in_key_event && !is_ime_direct_commit {
483+ // IME disabled and the IME echoed the raw key — forward it.
484+ self . ivars( ) . forward_key_to_app. set( true ) ;
441485 }
442486 }
443487
@@ -490,13 +534,32 @@ declare_class!(
490534 self . ivars( ) . forward_key_to_app. set( false ) ;
491535 let event = replace_event( event, self . option_as_alt( ) ) ;
492536
537+ // Snapshot the NSEvent characters so `insertText:` can tell IME
538+ // direct commits (e.g. SKK hiragana mode) from raw key pass-through.
539+ let chars = unsafe { event. characters( ) }
540+ . map( |s| s. to_string( ) )
541+ . unwrap_or_default( ) ;
542+ let chars_unmod = unsafe { event. charactersIgnoringModifiers( ) }
543+ . map( |s| s. to_string( ) )
544+ . unwrap_or_default( ) ;
545+ * self . ivars( ) . current_key_event_chars. borrow_mut( ) =
546+ Some ( ( chars, chars_unmod) ) ;
547+
493548 // The `interpretKeyEvents` function might call
494549 // `setMarkedText`, `insertText`, and `doCommandBySelector`.
495550 // It's important that we call this before queuing the KeyboardInput, because
496551 // we must send the `KeyboardInput` event during IME if it triggered
497552 // `doCommandBySelector`. (doCommandBySelector means that the keyboard input
498553 // is not handled by IME and should be handled by the application)
499- if self . ivars( ) . ime_allowed. get( ) {
554+ //
555+ // The IME is bypassed when modifiers are pressed that do not intersect the
556+ // configured `forward_to_ime_modifier_mask`. Events with no modifiers are
557+ // always forwarded.
558+ let mods = event_mods( & event) . state( ) ;
559+ let mask = self . ivars( ) . forward_to_ime_modifier_mask. get( ) ;
560+ let forward_to_ime = mods. is_empty( ) || mods. intersects( mask) ;
561+ let routed_to_ime = self . ivars( ) . ime_allowed. get( ) && forward_to_ime;
562+ if routed_to_ime {
500563 let events_for_nsview = NSArray :: from_slice( & [ & * event] ) ;
501564 unsafe { self . interpretKeyEvents( & events_for_nsview) } ;
502565
@@ -520,7 +583,20 @@ declare_class!(
520583 _ => old_ime_state != self . ivars( ) . ime_state. get( ) ,
521584 } ;
522585
523- if !had_ime_input || self . ivars( ) . forward_key_to_app. get( ) {
586+ // When the event was routed through the IME, only forward it to
587+ // the application if `doCommandBySelector:` explicitly asked us to
588+ // (`forward_key_to_app`). Otherwise the IME is assumed to have
589+ // consumed the key — even when it produced no NSTextInputClient
590+ // callbacks (e.g. SKK switching input mode via
591+ // `IMKTextInput.selectMode()` on Ctrl+J or `q`). When the event
592+ // was *not* routed to the IME, fall back to the historical
593+ // behavior of forwarding it as long as no preedit/commit happened.
594+ let should_forward_key = if routed_to_ime {
595+ self . ivars( ) . forward_key_to_app. get( )
596+ } else {
597+ !had_ime_input
598+ } ;
599+ if should_forward_key {
524600 let key_event = create_key_event( & event, true , unsafe { event. isARepeat( ) } , None ) ;
525601 self . queue_event( WindowEvent :: KeyboardInput {
526602 device_id: DEVICE_ID ,
@@ -531,6 +607,7 @@ declare_class!(
531607
532608 // Clear the flag after processing
533609 self . ivars( ) . in_key_event. set( false ) ;
610+ * self . ivars( ) . current_key_event_chars. borrow_mut( ) = None ;
534611 }
535612
536613 #[ method( keyUp: ) ]
@@ -855,6 +932,8 @@ impl WinitView {
855932 accepts_first_mouse,
856933 _ns_window : WeakId :: new ( & window. retain ( ) ) ,
857934 option_as_alt : Cell :: new ( option_as_alt) ,
935+ forward_to_ime_modifier_mask : Cell :: new ( ModifiersState :: all ( ) ) ,
936+ current_key_event_chars : RefCell :: new ( None ) ,
858937 } ) ;
859938 let this: Retained < Self > = unsafe { msg_send_id ! [ super ( this) , init] } ;
860939
@@ -1022,6 +1101,14 @@ impl WinitView {
10221101 self . ivars ( ) . option_as_alt . get ( )
10231102 }
10241103
1104+ pub ( super ) fn set_forward_to_ime_modifier_mask ( & self , value : ModifiersState ) {
1105+ self . ivars ( ) . forward_to_ime_modifier_mask . set ( value)
1106+ }
1107+
1108+ pub ( super ) fn forward_to_ime_modifier_mask ( & self ) -> ModifiersState {
1109+ self . ivars ( ) . forward_to_ime_modifier_mask . get ( )
1110+ }
1111+
10251112 /// Update modifiers if `event` has something different
10261113 fn update_modifiers ( & self , ns_event : & NSEvent , is_flags_changed_event : bool ) {
10271114 use ElementState :: { Pressed , Released } ;
0 commit comments