macOS: Improve deadkeys (#20515)

Closes #19738

This change refactors how we handle input on macOS to avoid simulating
our own IME. This fixes a number of small edge-cases, and also lets us
remove a bunch of code that had been added to work around bugs in the
previous version.

Release Notes:

- On macOS: Keyboard shortcuts are now handled before activating the IME
system, this enables using vim's default mode on keyboards that use IME
menus (like Japanese).
- On macOS: Improvements to handling of dead-keys. For example when
typing `""` on a Brazillian keyboard, you now get a committed " and a
new marked ", as happens in other apps. Also, you can now type cmd-^ on
an AZERTY keyboard for indent; and ^ on a QWERTZ keyboard now goes to
the beginning of line in vim normal mode, or `d i "` no requires no
space to delete within quotes on Brazilian keyboards (though `d f "
space` is still required as `f` relies on the input handler, not a
binding).
- On macOS: In the terminal pane, holding down a key will now repeat
that key (as happens in iTerm2) instead of opening the character
selector.
This commit is contained in:
Conrad Irwin 2024-11-11 16:34:36 -07:00 committed by GitHub
parent 38f2a919f8
commit 2ea4ede08e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 228 additions and 218 deletions

2
Cargo.lock generated
View file

@ -5657,7 +5657,7 @@ dependencies = [
"httpdate", "httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"socket2 0.4.10", "socket2 0.5.7",
"tokio", "tokio",
"tower-service", "tower-service",
"tracing", "tracing",

View file

@ -580,7 +580,7 @@ impl Render for InputExample {
.children(self.recent_keystrokes.iter().rev().map(|ks| { .children(self.recent_keystrokes.iter().rev().map(|ks| {
format!( format!(
"{:} {}", "{:} {}",
ks, ks.unparse(),
if let Some(ime_key) = ks.ime_key.as_ref() { if let Some(ime_key) = ks.ime_key.as_ref() {
format!("-> {}", ime_key) format!("-> {}", ime_key)
} else { } else {

View file

@ -683,6 +683,11 @@ impl PlatformInputHandler {
.flatten() .flatten()
} }
#[allow(dead_code)]
fn apple_press_and_hold_enabled(&mut self) -> bool {
self.handler.apple_press_and_hold_enabled()
}
pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) { pub(crate) fn dispatch_input(&mut self, input: &str, cx: &mut WindowContext) {
self.handler.replace_text_in_range(None, input, cx); self.handler.replace_text_in_range(None, input, cx);
} }
@ -780,6 +785,15 @@ pub trait InputHandler: 'static {
range_utf16: Range<usize>, range_utf16: Range<usize>,
cx: &mut WindowContext, cx: &mut WindowContext,
) -> Option<Bounds<Pixels>>; ) -> Option<Bounds<Pixels>>;
/// Allows a given input context to opt into getting raw key repeats instead of
/// sending these to the platform.
/// TODO: Ideally we should be able to set ApplePressAndHoldEnabled in NSUserDefaults
/// (which is how iTerm does it) but it doesn't seem to work for me.
#[allow(dead_code)]
fn apple_press_and_hold_enabled(&mut self) -> bool {
true
}
} }
/// The variables that can be configured when creating a new window /// The variables that can be configured when creating a new window

View file

@ -124,6 +124,9 @@ impl Keystroke {
/// Produces a representation of this key that Parse can understand. /// Produces a representation of this key that Parse can understand.
pub fn unparse(&self) -> String { pub fn unparse(&self) -> String {
let mut str = String::new(); let mut str = String::new();
if self.modifiers.function {
str.push_str("fn-");
}
if self.modifiers.control { if self.modifiers.control {
str.push_str("ctrl-"); str.push_str("ctrl-");
} }

View file

@ -1,20 +1,20 @@
use crate::{ use crate::{
platform::mac::NSStringExt, point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, platform::mac::{
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent, kTISPropertyUnicodeKeyLayoutData, LMGetKbdType, NSStringExt,
MouseUpEvent, NavigationDirection, Pixels, PlatformInput, ScrollDelta, ScrollWheelEvent, TISCopyCurrentKeyboardLayoutInputSource, TISGetInputSourceProperty, UCKeyTranslate,
TouchPhase, },
point, px, KeyDownEvent, KeyUpEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton,
MouseDownEvent, MouseExitEvent, MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels,
PlatformInput, ScrollDelta, ScrollWheelEvent, TouchPhase,
}; };
use cocoa::{ use cocoa::{
appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType}, appkit::{NSEvent, NSEventModifierFlags, NSEventPhase, NSEventType},
base::{id, YES}, base::{id, YES},
}; };
use core_graphics::{ use core_foundation::data::{CFDataGetBytePtr, CFDataRef};
event::{CGEvent, CGEventFlags, CGKeyCode}, use core_graphics::event::CGKeyCode;
event_source::{CGEventSource, CGEventSourceStateID}, use objc::{msg_send, sel, sel_impl};
}; use std::{borrow::Cow, ffi::c_void};
use metal::foreign_types::ForeignType as _;
use objc::{class, msg_send, sel, sel_impl};
use std::{borrow::Cow, mem, ptr, sync::Once};
const BACKSPACE_KEY: u16 = 0x7f; const BACKSPACE_KEY: u16 = 0x7f;
const SPACE_KEY: u16 = b' ' as u16; const SPACE_KEY: u16 = b' ' as u16;
@ -24,24 +24,6 @@ const ESCAPE_KEY: u16 = 0x1b;
const TAB_KEY: u16 = 0x09; const TAB_KEY: u16 = 0x09;
const SHIFT_TAB_KEY: u16 = 0x19; const SHIFT_TAB_KEY: u16 = 0x19;
fn synthesize_keyboard_event(code: CGKeyCode) -> CGEvent {
static mut EVENT_SOURCE: core_graphics::sys::CGEventSourceRef = ptr::null_mut();
static INIT_EVENT_SOURCE: Once = Once::new();
INIT_EVENT_SOURCE.call_once(|| {
let source = CGEventSource::new(CGEventSourceStateID::Private).unwrap();
unsafe {
EVENT_SOURCE = source.as_ptr();
};
mem::forget(source);
});
let source = unsafe { core_graphics::event_source::CGEventSource::from_ptr(EVENT_SOURCE) };
let event = CGEvent::new_keyboard_event(source.clone(), code, true).unwrap();
mem::forget(source);
event
}
pub fn key_to_native(key: &str) -> Cow<str> { pub fn key_to_native(key: &str) -> Cow<str> {
use cocoa::appkit::*; use cocoa::appkit::*;
let code = match key { let code = match key {
@ -259,8 +241,9 @@ impl PlatformInput {
unsafe fn parse_keystroke(native_event: id) -> Keystroke { unsafe fn parse_keystroke(native_event: id) -> Keystroke {
use cocoa::appkit::*; use cocoa::appkit::*;
let mut chars_ignoring_modifiers = chars_for_modified_key(native_event.keyCode(), false, false); let mut characters = native_event.characters().to_str().to_string();
let first_char = chars_ignoring_modifiers.chars().next().map(|ch| ch as u16); let mut ime_key = None;
let first_char = characters.chars().next().map(|ch| ch as u16);
let modifiers = native_event.modifierFlags(); let modifiers = native_event.modifierFlags();
let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask); let control = modifiers.contains(NSEventModifierFlags::NSControlKeyMask);
@ -321,6 +304,9 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
// * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/) // * Norwegian 7 | 7 | cmd-7 | cmd-/ (macOS reports cmd-shift-7 instead of cmd-/)
// * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout) // * Russian 7 | 7 | cmd-7 | cmd-& (shift-7 is . but when cmd is down, should use cmd layout)
// * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z) // * German QWERTZ ; | ö | cmd-ö | cmd-Ö (Zed's shift special case only applies to a-z)
//
let mut chars_ignoring_modifiers =
chars_for_modified_key(native_event.keyCode(), false, false);
let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), false, true); let mut chars_with_shift = chars_for_modified_key(native_event.keyCode(), false, true);
// Handle Dvorak+QWERTY / Russian / Armeniam // Handle Dvorak+QWERTY / Russian / Armeniam
@ -341,14 +327,24 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
chars_ignoring_modifiers = chars_with_cmd; chars_ignoring_modifiers = chars_with_cmd;
} }
if shift && chars_ignoring_modifiers == chars_with_shift.to_ascii_lowercase() { let mut key = if shift
&& chars_ignoring_modifiers
.chars()
.all(|c| c.is_ascii_lowercase())
{
chars_ignoring_modifiers chars_ignoring_modifiers
} else if shift { } else if shift {
shift = false; shift = false;
chars_with_shift chars_with_shift
} else { } else {
chars_ignoring_modifiers chars_ignoring_modifiers
} };
if characters.len() > 0 && characters != key {
ime_key = Some(characters.clone());
};
key
} }
}; };
@ -361,50 +357,81 @@ unsafe fn parse_keystroke(native_event: id) -> Keystroke {
function, function,
}, },
key, key,
ime_key: None, ime_key,
} }
} }
fn always_use_command_layout() -> bool { fn always_use_command_layout() -> bool {
// look at the key to the right of "tab" ('a' in QWERTY) if chars_for_modified_key(0, false, false).is_ascii() {
// if it produces a non-ASCII character, but with command held produces ASCII,
// we default to the command layout for our keyboard system.
let event = synthesize_keyboard_event(0);
let without_cmd = unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
};
if without_cmd.is_ascii() {
return false; return false;
} }
event.set_flags(CGEventFlags::CGEventFlagCommand); chars_for_modified_key(0, true, false).is_ascii()
let with_cmd = unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event];
event.characters().to_str().to_string()
};
with_cmd.is_ascii()
} }
fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String { fn chars_for_modified_key(code: CGKeyCode, cmd: bool, shift: bool) -> String {
// Ideally, we would use `[NSEvent charactersByApplyingModifiers]` but that // Values from: https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/Carbon.framework/Versions/A/Frameworks/HIToolbox.framework/Versions/A/Headers/Events.h#L126
// always returns an empty string with certain keyboards, e.g. Japanese. Synthesizing // shifted >> 8 for UCKeyTranslate
// an event with the given flags instead lets us access `characters`, which always const CMD_MOD: u32 = 1;
// returns a valid string. const SHIFT_MOD: u32 = 2;
let event = synthesize_keyboard_event(code); const CG_SPACE_KEY: u16 = 49;
// https://github.com/phracker/MacOSX-SDKs/blob/master/MacOSX10.6.sdk/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/CarbonCore.framework/Versions/A/Headers/UnicodeUtilities.h#L278
#[allow(non_upper_case_globals)]
const kUCKeyActionDown: u16 = 0;
#[allow(non_upper_case_globals)]
const kUCKeyTranslateNoDeadKeysMask: u32 = 0;
let mut flags = CGEventFlags::empty(); let keyboard_type = unsafe { LMGetKbdType() as u32 };
if cmd { const BUFFER_SIZE: usize = 4;
flags |= CGEventFlags::CGEventFlagCommand; let mut dead_key_state = 0;
let mut buffer: [u16; BUFFER_SIZE] = [0; BUFFER_SIZE];
let mut buffer_size: usize = 0;
let keyboard = unsafe { TISCopyCurrentKeyboardLayoutInputSource() };
if keyboard.is_null() {
return "".to_string();
} }
if shift { let layout_data = unsafe {
flags |= CGEventFlags::CGEventFlagShift; TISGetInputSourceProperty(keyboard, kTISPropertyUnicodeKeyLayoutData as *const c_void)
as CFDataRef
};
if layout_data.is_null() {
unsafe {
let _: () = msg_send![keyboard, release];
}
return "".to_string();
} }
event.set_flags(flags); let keyboard_layout = unsafe { CFDataGetBytePtr(layout_data) };
let modifiers = if cmd { CMD_MOD } else { 0 } | if shift { SHIFT_MOD } else { 0 };
unsafe { unsafe {
let event: id = msg_send![class!(NSEvent), eventWithCGEvent: &*event]; UCKeyTranslate(
event.characters().to_str().to_string() keyboard_layout as *const c_void,
code,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
if dead_key_state != 0 {
UCKeyTranslate(
keyboard_layout as *const c_void,
CG_SPACE_KEY,
kUCKeyActionDown,
modifiers,
keyboard_type,
kUCKeyTranslateNoDeadKeysMask,
&mut dead_key_state,
BUFFER_SIZE,
&mut buffer_size as *mut usize,
&mut buffer as *mut u16,
);
}
let _: () = msg_send![keyboard, release];
} }
String::from_utf16(&buffer[..buffer_size]).unwrap_or_default()
} }

View file

@ -1448,13 +1448,27 @@ unsafe fn ns_url_to_path(url: id) -> Result<PathBuf> {
#[link(name = "Carbon", kind = "framework")] #[link(name = "Carbon", kind = "framework")]
extern "C" { extern "C" {
fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object; pub(super) fn TISCopyCurrentKeyboardLayoutInputSource() -> *mut Object;
fn TISGetInputSourceProperty( pub(super) fn TISGetInputSourceProperty(
inputSource: *mut Object, inputSource: *mut Object,
propertyKey: *const c_void, propertyKey: *const c_void,
) -> *mut Object; ) -> *mut Object;
pub static kTISPropertyInputSourceID: CFStringRef; pub(super) fn UCKeyTranslate(
keyLayoutPtr: *const ::std::os::raw::c_void,
virtualKeyCode: u16,
keyAction: u16,
modifierKeyState: u32,
keyboardType: u32,
keyTranslateOptions: u32,
deadKeyState: *mut u32,
maxStringLength: usize,
actualStringLength: *mut usize,
unicodeString: *mut u16,
) -> u32;
pub(super) fn LMGetKbdType() -> u16;
pub(super) static kTISPropertyUnicodeKeyLayoutData: CFStringRef;
pub(super) static kTISPropertyInputSourceID: CFStringRef;
} }
mod security { mod security {

View file

@ -38,7 +38,6 @@ use std::{
cell::Cell, cell::Cell,
ffi::{c_void, CStr}, ffi::{c_void, CStr},
mem, mem,
ops::Range,
path::PathBuf, path::PathBuf,
ptr::{self, NonNull}, ptr::{self, NonNull},
rc::Rc, rc::Rc,
@ -310,14 +309,6 @@ unsafe fn build_window_class(name: &'static str, superclass: &Class) -> *const C
decl.register() decl.register()
} }
#[allow(clippy::enum_variant_names)]
#[derive(Clone, Debug)]
enum ImeInput {
InsertText(String, Option<Range<usize>>),
SetMarkedText(String, Option<Range<usize>>, Option<Range<usize>>),
UnmarkText,
}
struct MacWindowState { struct MacWindowState {
handle: AnyWindowHandle, handle: AnyWindowHandle,
executor: ForegroundExecutor, executor: ForegroundExecutor,
@ -338,14 +329,11 @@ struct MacWindowState {
synthetic_drag_counter: usize, synthetic_drag_counter: usize,
traffic_light_position: Option<Point<Pixels>>, traffic_light_position: Option<Point<Pixels>>,
previous_modifiers_changed_event: Option<PlatformInput>, previous_modifiers_changed_event: Option<PlatformInput>,
// State tracking what the IME did after the last request keystroke_for_do_command: Option<Keystroke>,
last_ime_inputs: Option<SmallVec<[(String, Option<Range<usize>>); 1]>>,
previous_keydown_inserted_text: Option<String>,
external_files_dragged: bool, external_files_dragged: bool,
// Whether the next left-mouse click is also the focusing click. // Whether the next left-mouse click is also the focusing click.
first_mouse: bool, first_mouse: bool,
fullscreen_restore_bounds: Bounds<Pixels>, fullscreen_restore_bounds: Bounds<Pixels>,
ime_composing: bool,
} }
impl MacWindowState { impl MacWindowState {
@ -619,12 +607,10 @@ impl MacWindow {
.as_ref() .as_ref()
.and_then(|titlebar| titlebar.traffic_light_position), .and_then(|titlebar| titlebar.traffic_light_position),
previous_modifiers_changed_event: None, previous_modifiers_changed_event: None,
last_ime_inputs: None, keystroke_for_do_command: None,
previous_keydown_inserted_text: None,
external_files_dragged: false, external_files_dragged: false,
first_mouse: false, first_mouse: false,
fullscreen_restore_bounds: Bounds::default(), fullscreen_restore_bounds: Bounds::default(),
ime_composing: false,
}))); })));
(*native_window).set_ivar( (*native_window).set_ivar(
@ -1226,9 +1212,9 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
// Brazilian layout: // Brazilian layout:
// - `" space` should create an unmarked quote // - `" space` should create an unmarked quote
// - `" backspace` should delete the marked quote // - `" backspace` should delete the marked quote
// - `" "`should create an unmarked quote and a second marked quote
// - `" up` should insert a quote, unmark it, and move up one line // - `" up` should insert a quote, unmark it, and move up one line
// - `" cmd-down` should insert a quote, unmark it, and move to the end of the file // - `" cmd-down` should insert a quote, unmark it, and move to the end of the file
// - NOTE: The current implementation does not move the selection to the end of the file
// - `cmd-ctrl-space` and clicking on an emoji should type it // - `cmd-ctrl-space` and clicking on an emoji should type it
// Czech (QWERTY) layout: // Czech (QWERTY) layout:
// - in vim mode `option-4` should go to end of line (same as $) // - in vim mode `option-4` should go to end of line (same as $)
@ -1241,95 +1227,80 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
let window_height = lock.content_size().height; let window_height = lock.content_size().height;
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) }; let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
if let Some(PlatformInput::KeyDown(mut event)) = event { let Some(PlatformInput::KeyDown(mut event)) = event else {
// For certain keystrokes, macOS will first dispatch a "key equivalent" event. return NO;
// If that event isn't handled, it will then dispatch a "key down" event. GPUI };
// makes no distinction between these two types of events, so we need to ignore // For certain keystrokes, macOS will first dispatch a "key equivalent" event.
// the "key down" event if we've already just processed its "key equivalent" version. // If that event isn't handled, it will then dispatch a "key down" event. GPUI
if key_equivalent { // makes no distinction between these two types of events, so we need to ignore
lock.last_key_equivalent = Some(event.clone()); // the "key down" event if we've already just processed its "key equivalent" version.
} else if lock.last_key_equivalent.take().as_ref() == Some(&event) { if key_equivalent {
return NO; lock.last_key_equivalent = Some(event.clone());
} else if lock.last_key_equivalent.take().as_ref() == Some(&event) {
return NO;
}
drop(lock);
let is_composing = with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.is_some();
// If we're composing, send the key to the input handler first;
// otherwise we only send to the input handler if we don't have a matching binding.
// The input handler may call `do_command_by_selector` if it doesn't know how to handle
// a key. If it does so, it will return YES so we won't send the key twice.
if is_composing || event.keystroke.key.is_empty() {
window_state.as_ref().lock().keystroke_for_do_command = Some(event.keystroke.clone());
let handled: BOOL = unsafe {
let input_context: id = msg_send![this, inputContext];
msg_send![input_context, handleEvent: native_event]
};
window_state.as_ref().lock().keystroke_for_do_command.take();
if handled {
return YES;
} }
let keydown = event.keystroke.clone(); let mut callback = window_state.as_ref().lock().event_callback.take();
let fn_modifier = keydown.modifiers.function; let handled = if let Some(callback) = callback.as_mut() {
lock.last_ime_inputs = Some(Default::default()); !callback(PlatformInput::KeyDown(event)).propagate
drop(lock); } else {
false
};
window_state.as_ref().lock().event_callback = callback;
return handled;
}
// Send the event to the input context for IME handling, unless the `fn` modifier is let mut callback = window_state.as_ref().lock().event_callback.take();
// being pressed. let handled = if let Some(callback) = callback.as_mut() {
// this will call back into `insert_text`, etc. !callback(PlatformInput::KeyDown(event.clone())).propagate
if !fn_modifier {
unsafe {
let input_context: id = msg_send![this, inputContext];
let _: BOOL = msg_send![input_context, handleEvent: native_event];
}
}
let mut handled = false;
let mut lock = window_state.lock();
let previous_keydown_inserted_text = lock.previous_keydown_inserted_text.take();
let mut last_inserts = lock.last_ime_inputs.take().unwrap();
let ime_composing = std::mem::take(&mut lock.ime_composing);
let mut callback = lock.event_callback.take();
drop(lock);
let last_insert = last_inserts.pop();
// on a brazilian keyboard typing `"` and then hitting `up` will cause two IME
// events, one to unmark the quote, and one to send the up arrow.
for (text, range) in last_inserts {
send_to_input_handler(this, ImeInput::InsertText(text, range));
}
let is_composing =
with_input_handler(this, |input_handler| input_handler.marked_text_range())
.flatten()
.is_some()
|| ime_composing;
if let Some((text, range)) = last_insert {
if !is_composing {
window_state.lock().previous_keydown_inserted_text = Some(text.clone());
if let Some(callback) = callback.as_mut() {
event.keystroke.ime_key = Some(text.clone());
handled = !callback(PlatformInput::KeyDown(event)).propagate;
}
}
if !handled {
handled = true;
send_to_input_handler(this, ImeInput::InsertText(text, range));
}
} else if !is_composing {
let is_held = event.is_held;
if let Some(callback) = callback.as_mut() {
handled = !callback(PlatformInput::KeyDown(event)).propagate;
}
if !handled && is_held {
if let Some(text) = previous_keydown_inserted_text {
// macOS IME is a bit funky, and even when you've told it there's nothing to
// enter it will still swallow certain keys (e.g. 'f', 'j') and not others
// (e.g. 'n'). This is a problem for certain kinds of views, like the terminal.
with_input_handler(this, |input_handler| {
if input_handler.selected_text_range(false).is_none() {
handled = true;
input_handler.replace_text_in_range(None, &text)
}
});
window_state.lock().previous_keydown_inserted_text = Some(text);
}
}
}
window_state.lock().event_callback = callback;
handled as BOOL
} else { } else {
NO false
};
window_state.as_ref().lock().event_callback = callback;
if handled {
return handled;
}
if event.is_held {
let handled = with_input_handler(&this, |input_handler| {
if !input_handler.apple_press_and_hold_enabled() {
input_handler.replace_text_in_range(
None,
&event.keystroke.ime_key.unwrap_or(event.keystroke.key),
);
return true;
}
false
});
if handled == Some(true) {
return YES;
}
}
unsafe {
let input_context: id = msg_send![this, inputContext];
msg_send![input_context, handleEvent: native_event]
} }
} }
@ -1741,10 +1712,9 @@ extern "C" fn insert_text(this: &Object, _: Sel, text: id, replacement_range: NS
let text = text.to_str(); let text = text.to_str();
let replacement_range = replacement_range.to_range(); let replacement_range = replacement_range.to_range();
send_to_input_handler( with_input_handler(this, |input_handler| {
this, input_handler.replace_text_in_range(replacement_range, &text)
ImeInput::InsertText(text.to_string(), replacement_range), });
);
} }
} }
@ -1766,15 +1736,13 @@ extern "C" fn set_marked_text(
let selected_range = selected_range.to_range(); let selected_range = selected_range.to_range();
let replacement_range = replacement_range.to_range(); let replacement_range = replacement_range.to_range();
let text = text.to_str(); let text = text.to_str();
with_input_handler(this, |input_handler| {
send_to_input_handler( input_handler.replace_and_mark_text_in_range(replacement_range, &text, selected_range)
this, });
ImeInput::SetMarkedText(text.to_string(), replacement_range, selected_range),
);
} }
} }
extern "C" fn unmark_text(this: &Object, _: Sel) { extern "C" fn unmark_text(this: &Object, _: Sel) {
send_to_input_handler(this, ImeInput::UnmarkText); with_input_handler(this, |input_handler| input_handler.unmark_text());
} }
extern "C" fn attributed_substring_for_proposed_range( extern "C" fn attributed_substring_for_proposed_range(
@ -1800,7 +1768,24 @@ extern "C" fn attributed_substring_for_proposed_range(
.unwrap_or(nil) .unwrap_or(nil)
} }
extern "C" fn do_command_by_selector(_: &Object, _: Sel, _: Sel) {} // We ignore which selector it asks us to do because the user may have
// bound the shortcut to something else.
extern "C" fn do_command_by_selector(this: &Object, _: Sel, _: Sel) {
let state = unsafe { get_window_state(this) };
let mut lock = state.as_ref().lock();
let keystroke = lock.keystroke_for_do_command.take();
let mut event_callback = lock.event_callback.take();
drop(lock);
if let Some((keystroke, mut callback)) = keystroke.zip(event_callback.as_mut()) {
(callback)(PlatformInput::KeyDown(KeyDownEvent {
keystroke,
is_held: false,
}));
}
state.as_ref().lock().event_callback = event_callback;
}
extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) { extern "C" fn view_did_change_effective_appearance(this: &Object, _: Sel) {
unsafe { unsafe {
@ -1950,43 +1935,6 @@ where
} }
} }
fn send_to_input_handler(window: &Object, ime: ImeInput) {
unsafe {
let window_state = get_window_state(window);
let mut lock = window_state.lock();
if let Some(mut input_handler) = lock.input_handler.take() {
match ime {
ImeInput::InsertText(text, range) => {
if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
ime_input.push((text, range));
lock.input_handler = Some(input_handler);
return;
}
drop(lock);
input_handler.replace_text_in_range(range, &text)
}
ImeInput::SetMarkedText(text, range, marked_range) => {
lock.ime_composing = true;
drop(lock);
input_handler.replace_and_mark_text_in_range(range, &text, marked_range)
}
ImeInput::UnmarkText => {
drop(lock);
input_handler.unmark_text()
}
}
window_state.lock().input_handler = Some(input_handler);
} else {
if let ImeInput::InsertText(text, range) = ime {
if let Some(ime_input) = lock.last_ime_inputs.as_mut() {
ime_input.push((text, range));
}
}
}
}
}
unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID { unsafe fn display_id_for_screen(screen: id) -> CGDirectDisplayID {
let device_description = NSScreen::deviceDescription(screen); let device_description = NSScreen::deviceDescription(screen);
let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber"); let screen_number_key: id = NSString::alloc(nil).init_str("NSScreenNumber");

View file

@ -1044,6 +1044,10 @@ impl InputHandler for TerminalInputHandler {
) -> Option<Bounds<Pixels>> { ) -> Option<Bounds<Pixels>> {
self.cursor_bounds self.cursor_bounds
} }
fn apple_press_and_hold_enabled(&mut self) -> bool {
false
}
} }
pub fn is_blank(cell: &IndexedCell) -> bool { pub fn is_blank(cell: &IndexedCell) -> bool {