Fix GPUI keyup events not firing on Windows and macOS (#27290)
While building my own application using GPUI, I found that the `key_up` event doesn't fire on Windows or macOS, with each platform failing for different reasons. These events aren't used anywhere in Zed yet, so it makes sense that the issue hasn't already been caught. I don't have a Linux machine set up right now, so I don't know if these events fire correctly on Linux or not. --- Without this fix, a simple layout like the following: ```rust div() .on_key_down(cx.listener(|_, event, _, _| println!("Key down: {:?}", event))) .on_key_up(cx.listener(|_, event, _, _| println!("Key up: {:?}", event))); ``` ...would result in the following logs if the 'a' key was pressed: ```text Key down: KeyDownEvent { keystroke: Keystroke { modifiers: Modifiers { control: false, alt: false, shift: false, platform: false, function: false }, key: "a", key_char: Some("a") }, is_held: false } <eof> ``` With this fix, the `key_up` event fires correctly, resulting in the following logs: ```text Key down: KeyDownEvent { keystroke: Keystroke { modifiers: Modifiers { control: false, alt: false, shift: false, platform: false, function: false }, key: "a", key_char: Some("a") }, is_held: false } Key up: KeyUpEvent { keystroke: Keystroke { modifiers: Modifiers { control: false, alt: false, shift: false, platform: false, function: false }, key: "a", key_char: None } } <eof> ``` --- I've made the assumption that the `key_char` field shouldn't be set on the `key_up` event since, unlike the `key_down` event, it's not an event that may produce a character. Happy to make any changes to this PR as required. If it would be preferable to test this on Linux as well before it's merged, let me know and I'll sort something out. Hopefully this makes the experience of building new applications on GPUI smoother, and potentially saves the Zed team some time if this event is ever used in the future. Release Notes: - N/A
This commit is contained in:
parent
01b400ea29
commit
bbd1e628f0
2 changed files with 101 additions and 81 deletions
|
@ -108,6 +108,10 @@ unsafe fn build_classes() {
|
|||
sel!(keyDown:),
|
||||
handle_key_down as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(keyUp:),
|
||||
handle_key_up as extern "C" fn(&Object, Sel, id),
|
||||
);
|
||||
decl.add_method(
|
||||
sel!(mouseDown:),
|
||||
handle_view_event as extern "C" fn(&Object, Sel, id),
|
||||
|
@ -1219,6 +1223,10 @@ extern "C" fn handle_key_down(this: &Object, _: Sel, native_event: id) {
|
|||
handle_key_event(this, native_event, false);
|
||||
}
|
||||
|
||||
extern "C" fn handle_key_up(this: &Object, _: Sel, native_event: id) {
|
||||
handle_key_event(this, native_event, false);
|
||||
}
|
||||
|
||||
// Things to test if you're modifying this method:
|
||||
// U.S. layout:
|
||||
// - The IME consumes characters like 'j' and 'k', which makes paging through `less` in
|
||||
|
@ -1251,101 +1259,113 @@ extern "C" fn handle_key_event(this: &Object, native_event: id, key_equivalent:
|
|||
let window_height = lock.content_size().height;
|
||||
let event = unsafe { PlatformInput::from_native(native_event, Some(window_height)) };
|
||||
|
||||
let Some(PlatformInput::KeyDown(mut event)) = event else {
|
||||
let Some(event) = event else {
|
||||
return NO;
|
||||
};
|
||||
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
|
||||
// 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
|
||||
// the "key down" event if we've already just processed its "key equivalent" version.
|
||||
if key_equivalent {
|
||||
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.
|
||||
// We also do this for non-printing keys (like arrow keys and escape) as the IME menu
|
||||
// may need them even if there is no marked text;
|
||||
// however we skip keys with control or the input handler adds control-characters to the buffer.
|
||||
// and keys with function, as the input handler swallows them.
|
||||
if is_composing
|
||||
|| (event.keystroke.key_char.is_none()
|
||||
&& !event.keystroke.modifiers.control
|
||||
&& !event.keystroke.modifiers.function)
|
||||
{
|
||||
{
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
lock.keystroke_for_do_command = Some(event.keystroke.clone());
|
||||
lock.do_command_handled.take();
|
||||
drop(lock);
|
||||
}
|
||||
|
||||
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 let Some(handled) = window_state.as_ref().lock().do_command_handled.take() {
|
||||
return handled as BOOL;
|
||||
} else if handled == YES {
|
||||
return YES;
|
||||
}
|
||||
|
||||
let run_callback = |event: PlatformInput| -> BOOL {
|
||||
let mut callback = window_state.as_ref().lock().event_callback.take();
|
||||
let handled: BOOL = if let Some(callback) = callback.as_mut() {
|
||||
!callback(PlatformInput::KeyDown(event)).propagate as BOOL
|
||||
!callback(event).propagate as BOOL
|
||||
} else {
|
||||
NO
|
||||
};
|
||||
window_state.as_ref().lock().event_callback = callback;
|
||||
return handled as BOOL;
|
||||
}
|
||||
|
||||
let mut callback = window_state.as_ref().lock().event_callback.take();
|
||||
let handled = if let Some(callback) = callback.as_mut() {
|
||||
!callback(PlatformInput::KeyDown(event.clone())).propagate as BOOL
|
||||
} else {
|
||||
NO
|
||||
handled
|
||||
};
|
||||
window_state.as_ref().lock().event_callback = callback;
|
||||
if handled == YES {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if event.is_held {
|
||||
if let Some(key_char) = event.keystroke.key_char.as_ref() {
|
||||
let handled = with_input_handler(&this, |input_handler| {
|
||||
if !input_handler.apple_press_and_hold_enabled() {
|
||||
input_handler.replace_text_in_range(None, &key_char);
|
||||
match event {
|
||||
PlatformInput::KeyDown(mut key_down_event) => {
|
||||
// For certain keystrokes, macOS will first dispatch a "key equivalent" event.
|
||||
// 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
|
||||
// the "key down" event if we've already just processed its "key equivalent" version.
|
||||
if key_equivalent {
|
||||
lock.last_key_equivalent = Some(key_down_event.clone());
|
||||
} else if lock.last_key_equivalent.take().as_ref() == Some(&key_down_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.
|
||||
// We also do this for non-printing keys (like arrow keys and escape) as the IME menu
|
||||
// may need them even if there is no marked text;
|
||||
// however we skip keys with control or the input handler adds control-characters to the buffer.
|
||||
// and keys with function, as the input handler swallows them.
|
||||
if is_composing
|
||||
|| (key_down_event.keystroke.key_char.is_none()
|
||||
&& !key_down_event.keystroke.modifiers.control
|
||||
&& !key_down_event.keystroke.modifiers.function)
|
||||
{
|
||||
{
|
||||
let mut lock = window_state.as_ref().lock();
|
||||
lock.keystroke_for_do_command = Some(key_down_event.keystroke.clone());
|
||||
lock.do_command_handled.take();
|
||||
drop(lock);
|
||||
}
|
||||
|
||||
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 let Some(handled) = window_state.as_ref().lock().do_command_handled.take() {
|
||||
return handled as BOOL;
|
||||
} else if handled == YES {
|
||||
return YES;
|
||||
}
|
||||
NO
|
||||
});
|
||||
if handled == Some(YES) {
|
||||
|
||||
let handled = run_callback(PlatformInput::KeyDown(key_down_event));
|
||||
return handled;
|
||||
}
|
||||
|
||||
let handled = run_callback(PlatformInput::KeyDown(key_down_event.clone()));
|
||||
if handled == YES {
|
||||
return YES;
|
||||
}
|
||||
|
||||
if key_down_event.is_held {
|
||||
if let Some(key_char) = key_down_event.keystroke.key_char.as_ref() {
|
||||
let handled = with_input_handler(&this, |input_handler| {
|
||||
if !input_handler.apple_press_and_hold_enabled() {
|
||||
input_handler.replace_text_in_range(None, &key_char);
|
||||
return YES;
|
||||
}
|
||||
NO
|
||||
});
|
||||
if handled == Some(YES) {
|
||||
return YES;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't send key equivalents to the input handler,
|
||||
// or macOS shortcuts like cmd-` will stop working.
|
||||
if key_equivalent {
|
||||
return NO;
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let input_context: id = msg_send![this, inputContext];
|
||||
msg_send![input_context, handleEvent: native_event]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Don't send key equivalents to the input handler,
|
||||
// or macOS shortcuts like cmd-` will stop working.
|
||||
if key_equivalent {
|
||||
return NO;
|
||||
}
|
||||
PlatformInput::KeyUp(_) => {
|
||||
drop(lock);
|
||||
run_callback(event)
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let input_context: id = msg_send![this, inputContext];
|
||||
msg_send![input_context, handleEvent: native_event]
|
||||
_ => NO,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -361,7 +361,7 @@ fn handle_keydown_msg(
|
|||
lparam: LPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
let Some(keystroke_or_modifier) = parse_keydown_msg_keystroke(wparam) else {
|
||||
let Some(keystroke_or_modifier) = parse_keystroke_from_vkey(wparam, false) else {
|
||||
return Some(1);
|
||||
};
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
|
@ -391,7 +391,7 @@ fn handle_keydown_msg(
|
|||
}
|
||||
|
||||
fn handle_keyup_msg(wparam: WPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
let Some(keystroke_or_modifier) = parse_keydown_msg_keystroke(wparam) else {
|
||||
let Some(keystroke_or_modifier) = parse_keystroke_from_vkey(wparam, true) else {
|
||||
return Some(1);
|
||||
};
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
|
@ -1288,7 +1288,7 @@ enum KeystrokeOrModifier {
|
|||
Modifier(Modifiers),
|
||||
}
|
||||
|
||||
fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
|
||||
fn parse_keystroke_from_vkey(wparam: WPARAM, is_keyup: bool) -> Option<KeystrokeOrModifier> {
|
||||
let vk_code = wparam.loword();
|
||||
|
||||
let modifiers = current_modifiers();
|
||||
|
@ -1316,7 +1316,7 @@ fn parse_keydown_msg_keystroke(wparam: WPARAM) -> Option<KeystrokeOrModifier> {
|
|||
return Some(KeystrokeOrModifier::Modifier(modifiers));
|
||||
}
|
||||
|
||||
if modifiers.control || modifiers.alt {
|
||||
if modifiers.control || modifiers.alt || is_keyup {
|
||||
let basic_key = basic_vkcode_to_string(vk_code, modifiers);
|
||||
if let Some(basic_key) = basic_key {
|
||||
return Some(KeystrokeOrModifier::Keystroke(basic_key));
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue