Add new Ime event for desktop platforms

This commit brings new Ime event to account for preedit state of input
method, also adding `Window::set_ime_allowed` to toggle IME input on
the particular window.

This commit implements API as designed in #1497 for desktop platforms.

Co-authored-by: Artur Kovacs <kovacs.artur.barnabas@gmail.com>
Co-authored-by: Markus Siglreithmaier <m.siglreith@gmail.com>
Co-authored-by: Murarth <murarth@gmail.com>
Co-authored-by: Yusuke Kominami <yukke.konan@gmail.com>
Co-authored-by: moko256 <koutaro.mo@gmail.com>
This commit is contained in:
Kirill Chibisov 2022-05-07 05:29:25 +03:00 committed by GitHub
parent b4175c1454
commit f04fa5d54f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1346 additions and 311 deletions

View file

@ -4,6 +4,7 @@ mod cursor;
pub use self::{cursor::*, r#async::*};
use std::ops::{BitAnd, Deref};
use std::os::raw::c_uchar;
use cocoa::{
appkit::{NSApp, NSWindowStyleMask},
@ -11,7 +12,7 @@ use cocoa::{
foundation::{NSPoint, NSRect, NSString, NSUInteger},
};
use core_graphics::display::CGDisplay;
use objc::runtime::{Class, Object};
use objc::runtime::{Class, Object, BOOL, NO};
use crate::dpi::LogicalPosition;
use crate::platform_impl::platform::ffi;
@ -165,3 +166,21 @@ pub unsafe fn toggle_style_mask(window: id, view: id, mask: NSWindowStyleMask, o
// If we don't do this, key handling will break. Therefore, never call `setStyleMask` directly!
window.makeFirstResponder_(view);
}
/// For invalid utf8 sequences potentially returned by `UTF8String`,
/// it behaves identically to `String::from_utf8_lossy`
///
/// Safety: Assumes that `string` is an instance of `NSAttributedString` or `NSString`
pub unsafe fn id_to_string_lossy(string: id) -> String {
let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)];
let characters = if has_attr != NO {
// This is a *mut NSAttributedString
msg_send![string, string]
} else {
// This is already a *mut NSString
string
};
let utf8_sequence =
std::slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len());
String::from_utf8_lossy(utf8_sequence).into_owned()
}

View file

@ -3,7 +3,10 @@ use std::{
collections::VecDeque,
os::raw::*,
ptr, slice, str,
sync::{Arc, Mutex, Weak},
sync::{
atomic::{compiler_fence, Ordering},
Arc, Mutex, Weak,
},
};
use cocoa::{
@ -19,7 +22,7 @@ use objc::{
use crate::{
dpi::LogicalPosition,
event::{
DeviceEvent, ElementState, Event, KeyboardInput, ModifiersState, MouseButton,
DeviceEvent, ElementState, Event, Ime, KeyboardInput, ModifiersState, MouseButton,
MouseScrollDelta, TouchPhase, VirtualKeyCode, WindowEvent,
},
platform_impl::platform::{
@ -29,7 +32,7 @@ use crate::{
scancode_to_keycode, EventWrapper,
},
ffi::*,
util::{self, IdRef},
util::{self, id_to_string_lossy, IdRef},
window::get_window_id,
DEVICE_ID,
},
@ -50,20 +53,42 @@ impl Default for CursorState {
}
}
#[derive(Eq, PartialEq)]
enum ImeState {
Disabled,
Enabled,
Preedit,
}
pub(super) struct ViewState {
ns_window: id,
pub cursor_state: Arc<Mutex<CursorState>>,
/// The position of the candidate window.
ime_position: LogicalPosition<f64>,
raw_characters: Option<String>,
pub(super) modifiers: ModifiersState,
tracking_rect: Option<NSInteger>,
ime_state: ImeState,
input_source: String,
/// True iff the application wants IME events.
///
/// Can be set using `set_ime_allowed`
ime_allowed: bool,
/// True if the current key event should be forwarded
/// to the application, even during IME
forward_key_to_app: bool,
}
impl ViewState {
fn get_scale_factor(&self) -> f64 {
(unsafe { NSWindow::backingScaleFactor(self.ns_window) }) as f64
}
fn is_ime_enabled(&self) -> bool {
match self.ime_state {
ImeState::Disabled => false,
_ => true,
}
}
}
pub fn new_view(ns_window: id) -> (IdRef, Weak<Mutex<CursorState>>) {
@ -72,11 +97,13 @@ pub fn new_view(ns_window: id) -> (IdRef, Weak<Mutex<CursorState>>) {
let state = ViewState {
ns_window,
cursor_state,
// By default, open the candidate window in the top left corner
ime_position: LogicalPosition::new(0.0, 0.0),
raw_characters: None,
modifiers: Default::default(),
tracking_rect: None,
ime_state: ImeState::Disabled,
input_source: String::new(),
ime_allowed: false,
forward_key_to_app: false,
};
unsafe {
// This is free'd in `dealloc`
@ -97,6 +124,33 @@ pub unsafe fn set_ime_position(ns_view: id, position: LogicalPosition<f64>) {
let _: () = msg_send![input_context, invalidateCharacterCoordinates];
}
pub unsafe fn set_ime_allowed(ns_view: id, ime_allowed: bool) {
let state_ptr: *mut c_void = *(*ns_view).get_mut_ivar("winitState");
let state = &mut *(state_ptr as *mut ViewState);
if state.ime_allowed == ime_allowed {
return;
}
state.ime_allowed = ime_allowed;
if state.ime_allowed {
return;
}
let marked_text_ref: &mut id = (*ns_view).get_mut_ivar("markedText");
// Clear markedText
let _: () = msg_send![*marked_text_ref, release];
let marked_text =
<id as NSMutableAttributedString>::init(NSMutableAttributedString::alloc(nil));
*marked_text_ref = marked_text;
if state.ime_state != ImeState::Disabled {
state.ime_state = ImeState::Disabled;
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::Ime(Ime::Disabled),
}));
}
}
struct ViewClass(*const Class);
unsafe impl Send for ViewClass {}
unsafe impl Sync for ViewClass {}
@ -130,6 +184,9 @@ lazy_static! {
sel!(resetCursorRects),
reset_cursor_rects as extern "C" fn(&Object, Sel),
);
// ------------------------------------------------------------------
// NSTextInputClient
decl.add_method(
sel!(hasMarkedText),
has_marked_text as extern "C" fn(&Object, Sel) -> BOOL,
@ -173,6 +230,8 @@ lazy_static! {
sel!(doCommandBySelector:),
do_command_by_selector as extern "C" fn(&Object, Sel, Sel),
);
// ------------------------------------------------------------------
decl.add_method(sel!(keyDown:), key_down as extern "C" fn(&Object, Sel, id));
decl.add_method(sel!(keyUp:), key_up as extern "C" fn(&Object, Sel, id));
decl.add_method(
@ -266,9 +325,9 @@ lazy_static! {
extern "C" fn dealloc(this: &Object, _sel: Sel) {
unsafe {
let state: *mut c_void = *this.get_ivar("winitState");
let marked_text: id = *this.get_ivar("markedText");
let _: () = msg_send![marked_text, release];
let state: *mut c_void = *this.get_ivar("winitState");
Box::from_raw(state as *mut ViewState);
}
}
@ -285,15 +344,19 @@ extern "C" fn init_with_winit(this: &Object, _sel: Sel, state: *mut c_void) -> i
let notification_center: &Object =
msg_send![class!(NSNotificationCenter), defaultCenter];
let notification_name =
// About frame change
let frame_did_change_notification_name =
IdRef::new(NSString::alloc(nil).init_str("NSViewFrameDidChangeNotification"));
let _: () = msg_send![
notification_center,
addObserver: this
selector: sel!(frameDidChange:)
name: notification_name
name: frame_did_change_notification_name
object: this
];
let winit_state = &mut *(state as *mut ViewState);
winit_state.input_source = current_input_source(this);
}
this
}
@ -402,7 +465,7 @@ extern "C" fn marked_range(this: &Object, _sel: Sel) -> NSRange {
let marked_text: id = *this.get_ivar("markedText");
let length = marked_text.length();
if length > 0 {
NSRange::new(0, length - 1)
NSRange::new(0, length)
} else {
util::EMPTY_RANGE
}
@ -414,6 +477,13 @@ extern "C" fn selected_range(_this: &Object, _sel: Sel) -> NSRange {
util::EMPTY_RANGE
}
/// Safety: Assumes that `view` is an instance of `VIEW_CLASS` from winit.
unsafe fn current_input_source(view: *const Object) -> String {
let input_context: id = msg_send![view, inputContext];
let input_source: id = msg_send![input_context, selectedKeyboardInputSource];
id_to_string_lossy(input_source)
}
extern "C" fn set_marked_text(
this: &mut Object,
_sel: Sel,
@ -423,7 +493,10 @@ extern "C" fn set_marked_text(
) {
trace_scope!("setMarkedText:selectedRange:replacementRange:");
unsafe {
// Get pre-edit text
let marked_text_ref: &mut id = this.get_mut_ivar("markedText");
// Update markedText
let _: () = msg_send![(*marked_text_ref), release];
let marked_text = NSMutableAttributedString::alloc(nil);
let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)];
@ -433,6 +506,33 @@ extern "C" fn set_marked_text(
marked_text.initWithString(string);
};
*marked_text_ref = marked_text;
// Update ViewState with new marked text
let state_ptr: *mut c_void = *this.get_ivar("winitState");
let state = &mut *(state_ptr as *mut ViewState);
let preedit_string = id_to_string_lossy(string);
// Notify IME is active if application still doesn't know it.
if state.ime_state == ImeState::Disabled {
state.input_source = current_input_source(this);
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::Ime(Ime::Enabled),
}));
}
let cursor_start = preedit_string.len();
let cursor_end = preedit_string.len();
state.ime_state = ImeState::Preedit;
// Send WindowEvent for updating marked text
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::Ime(Ime::Preedit(
preedit_string,
Some((cursor_start, cursor_end)),
)),
}));
}
}
@ -446,6 +546,19 @@ extern "C" fn unmark_text(this: &Object, _sel: Sel) {
let _: () = msg_send![s, release];
let input_context: id = msg_send![this, inputContext];
let _: () = msg_send![input_context, discardMarkedText];
let state_ptr: *mut c_void = *this.get_ivar("winitState");
let state = &mut *(state_ptr as *mut ViewState);
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::Ime(Ime::Preedit(String::new(), Some((0, 0)))),
}));
if state.is_ime_enabled() {
// Leave the Preedit state
state.ime_state = ImeState::Enabled;
} else {
warn!("Expected to have IME enabled when receiving unmarkText");
}
}
}
@ -499,35 +612,24 @@ extern "C" fn insert_text(this: &Object, _sel: Sel, string: id, _replacement_ran
let state_ptr: *mut c_void = *this.get_ivar("winitState");
let state = &mut *(state_ptr as *mut ViewState);
let has_attr: BOOL = msg_send![string, isKindOfClass: class!(NSAttributedString)];
let characters = if has_attr != NO {
// This is a *mut NSAttributedString
msg_send![string, string]
} else {
// This is already a *mut NSString
string
};
let string = id_to_string_lossy(string);
let slice =
slice::from_raw_parts(characters.UTF8String() as *const c_uchar, characters.len());
let string = str::from_utf8_unchecked(slice);
let is_control = string.chars().next().map_or(false, |c| c.is_control());
// We don't need this now, but it's here if that changes.
//let event: id = msg_send![NSApp(), currentEvent];
let mut events = VecDeque::with_capacity(characters.len());
for character in string.chars().filter(|c| !is_corporate_character(*c)) {
events.push_back(EventWrapper::StaticEvent(Event::WindowEvent {
if state.is_ime_enabled() && !is_control {
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::ReceivedCharacter(character),
event: WindowEvent::Ime(Ime::Commit(string)),
}));
state.ime_state = ImeState::Enabled;
}
AppState::queue_events(events);
}
}
extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) {
extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, _command: Sel) {
trace_scope!("doCommandBySelector:");
// 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.
@ -535,31 +637,15 @@ extern "C" fn do_command_by_selector(this: &Object, _sel: Sel, command: Sel) {
let state_ptr: *mut c_void = *this.get_ivar("winitState");
let state = &mut *(state_ptr as *mut ViewState);
let mut events = VecDeque::with_capacity(1);
if command == sel!(insertNewline:) {
// The `else` condition would emit the same character, but I'm keeping this here both...
// 1) as a reminder for how `doCommandBySelector` works
// 2) to make our use of carriage return explicit
events.push_back(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::ReceivedCharacter('\r'),
}));
} else {
let raw_characters = state.raw_characters.take();
if let Some(raw_characters) = raw_characters {
for character in raw_characters
.chars()
.filter(|c| !is_corporate_character(*c))
{
events.push_back(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::ReceivedCharacter(character),
}));
}
}
};
state.forward_key_to_app = true;
AppState::queue_events(events);
let has_marked_text: BOOL = msg_send![this, hasMarkedText];
if has_marked_text == NO {
if state.ime_state == ImeState::Preedit {
// Leave preedit so that we also report the keyup for this key
state.ime_state = ImeState::Enabled;
}
}
}
}
@ -637,54 +723,71 @@ extern "C" fn key_down(this: &Object, _sel: Sel, event: id) {
let state_ptr: *mut c_void = *this.get_ivar("winitState");
let state = &mut *(state_ptr as *mut ViewState);
let window_id = WindowId(get_window_id(state.ns_window));
let characters = get_characters(event, false);
state.raw_characters = Some(characters.clone());
let input_source = current_input_source(this);
if state.input_source != input_source && state.is_ime_enabled() {
state.ime_state = ImeState::Disabled;
state.input_source = input_source;
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::Ime(Ime::Disabled),
}));
}
let was_in_preedit = state.ime_state == ImeState::Preedit;
let characters = get_characters(event, false);
state.forward_key_to_app = false;
// The `interpretKeyEvents` function might call
// `setMarkedText`, `insertText`, and `doCommandBySelector`.
// It's important that we call this before queuing the KeyboardInput, because
// we must send the `KeyboardInput` event during IME if it triggered
// `doCommandBySelector`. (doCommandBySelector means that the keyboard input
// is not handled by IME and should be handled by the application)
if state.ime_allowed {
let events_for_nsview: id = msg_send![class!(NSArray), arrayWithObject: event];
let _: () = msg_send![this, interpretKeyEvents: events_for_nsview];
// Using a compiler fence because `interpretKeyEvents` might call
// into functions that modify the `ViewState`, but the compiler
// doesn't know this. Without the fence, the compiler may think that
// some of the reads (eg `state.ime_state`) that happen after this
// point are not needed.
compiler_fence(Ordering::SeqCst);
}
let now_in_preedit = state.ime_state == ImeState::Preedit;
let scancode = get_scancode(event) as u32;
let virtual_keycode = retrieve_keycode(event);
let is_repeat: BOOL = msg_send![event, isARepeat];
update_potentially_stale_modifiers(state, event);
#[allow(deprecated)]
let window_event = Event::WindowEvent {
window_id,
event: WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
input: KeyboardInput {
state: ElementState::Pressed,
scancode,
virtual_keycode,
modifiers: event_mods(event),
let preedit_related = was_in_preedit || now_in_preedit;
if !preedit_related || state.forward_key_to_app || !state.ime_allowed {
#[allow(deprecated)]
let window_event = Event::WindowEvent {
window_id,
event: WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
input: KeyboardInput {
state: ElementState::Pressed,
scancode,
virtual_keycode,
modifiers: event_mods(event),
},
is_synthetic: false,
},
is_synthetic: false,
},
};
};
let pass_along = {
AppState::queue_event(EventWrapper::StaticEvent(window_event));
// Emit `ReceivedCharacter` for key repeats
if is_repeat != NO {
for character in characters.chars().filter(|c| !is_corporate_character(*c)) {
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
window_id,
event: WindowEvent::ReceivedCharacter(character),
}));
}
false
} else {
true
}
};
if pass_along {
// Some keys (and only *some*, with no known reason) don't trigger `insertText`, while others do...
// So, we don't give repeats the opportunity to trigger that, since otherwise our hack will cause some
// keys to generate twice as many characters.
let array: id = msg_send![class!(NSArray), arrayWithObject: event];
let _: () = msg_send![this, interpretKeyEvents: array];
for character in characters.chars().filter(|c| !is_corporate_character(*c)) {
AppState::queue_event(EventWrapper::StaticEvent(Event::WindowEvent {
window_id,
event: WindowEvent::ReceivedCharacter(character),
}));
}
}
}
}
@ -700,22 +803,25 @@ extern "C" fn key_up(this: &Object, _sel: Sel, event: id) {
update_potentially_stale_modifiers(state, event);
#[allow(deprecated)]
let window_event = Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
input: KeyboardInput {
state: ElementState::Released,
scancode,
virtual_keycode,
modifiers: event_mods(event),
// We want to send keyboard input when we are not currently in preedit
if state.ime_state != ImeState::Preedit {
#[allow(deprecated)]
let window_event = Event::WindowEvent {
window_id: WindowId(get_window_id(state.ns_window)),
event: WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
input: KeyboardInput {
state: ElementState::Released,
scancode,
virtual_keycode,
modifiers: event_mods(event),
},
is_synthetic: false,
},
is_synthetic: false,
},
};
};
AppState::queue_event(EventWrapper::StaticEvent(window_event));
AppState::queue_event(EventWrapper::StaticEvent(window_event));
}
}
}

View file

@ -461,7 +461,7 @@ impl UnownedWindow {
if maximized {
window.set_maximized(maximized);
}
trace!("Done unowned window::new");
Ok((window, delegate))
}
@ -1054,6 +1054,13 @@ impl UnownedWindow {
unsafe { view::set_ime_position(*self.ns_view, logical_spot) };
}
#[inline]
pub fn set_ime_allowed(&self, allowed: bool) {
unsafe {
view::set_ime_allowed(*self.ns_view, allowed);
}
}
#[inline]
pub fn focus_window(&self) {
let is_minimized: BOOL = unsafe { msg_send![*self.ns_window, isMiniaturized] };