Basic iOS IME support (#3823)

This implements basic iOS IME support (typing, backspace, support for emojis
etc but no autocomplete or copy / paste menu).

Co-authored-by: Mads Marquart <mads@marquart.dk>
This commit is contained in:
lucasmerlin 2024-08-19 22:04:29 +02:00 committed by GitHub
parent 6e008b39e9
commit 1e1f0fd7e9
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 120 additions and 10 deletions

View file

@ -183,6 +183,8 @@ objc2-ui-kit = { version = "0.2.2", features = [
"UIEvent", "UIEvent",
"UIGeometry", "UIGeometry",
"UIGestureRecognizer", "UIGestureRecognizer",
"UITextInput",
"UITextInputTraits",
"UIOrientation", "UIOrientation",
"UIPanGestureRecognizer", "UIPanGestureRecognizer",
"UIPinchGestureRecognizer", "UIPinchGestureRecognizer",

View file

@ -47,13 +47,13 @@ changelog entry.
`DeviceEvent::MouseMotion` is returning raw data, not OS accelerated, when using `DeviceEvent::MouseMotion` is returning raw data, not OS accelerated, when using
`CursorGrabMode::Locked`. `CursorGrabMode::Locked`.
- On Web, implement `MonitorHandle` and `VideoModeHandle`. - On Web, implement `MonitorHandle` and `VideoModeHandle`.
Without prompting the user for permission, only the current monitor is returned. But when Without prompting the user for permission, only the current monitor is returned. But when
prompting and being granted permission through prompting and being granted permission through
`ActiveEventLoop::request_detailed_monitor_permission()`, access to all monitors and their `ActiveEventLoop::request_detailed_monitor_permission()`, access to all monitors and their
details is available. Handles created with "detailed monitor permissions" can be used in details is available. Handles created with "detailed monitor permissions" can be used in
`Window::set_fullscreen()` as well. `Window::set_fullscreen()` as well.
Keep in mind that handles do not auto-upgrade after permissions are granted and have to be Keep in mind that handles do not auto-upgrade after permissions are granted and have to be
re-created to make full use of this feature. re-created to make full use of this feature.
- Add `Touch::finger_id` with a new type `FingerId`. - Add `Touch::finger_id` with a new type `FingerId`.
@ -62,6 +62,7 @@ changelog entry.
- Implement `Clone`, `Copy`, `Debug`, `Deserialize`, `Eq`, `Hash`, `Ord`, `PartialEq`, `PartialOrd` - Implement `Clone`, `Copy`, `Debug`, `Deserialize`, `Eq`, `Hash`, `Ord`, `PartialEq`, `PartialOrd`
and `Serialize` on many types. and `Serialize` on many types.
- Add `MonitorHandle::current_video_mode()`. - Add `MonitorHandle::current_video_mode()`.
- Add basic iOS IME support. The soft keyboard can now be shown using `Window::set_ime_allowed`.
### Changed ### Changed

View file

@ -4,19 +4,23 @@ use std::cell::{Cell, RefCell};
use objc2::rc::Retained; use objc2::rc::Retained;
use objc2::runtime::{NSObjectProtocol, ProtocolObject}; use objc2::runtime::{NSObjectProtocol, ProtocolObject};
use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass}; use objc2::{declare_class, msg_send, msg_send_id, mutability, sel, ClassType, DeclaredClass};
use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet}; use objc2_foundation::{CGFloat, CGPoint, CGRect, MainThreadMarker, NSObject, NSSet, NSString};
use objc2_ui_kit::{ use objc2_ui_kit::{
UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer, UICoordinateSpace, UIEvent, UIForceTouchCapability, UIGestureRecognizer,
UIGestureRecognizerDelegate, UIGestureRecognizerState, UIPanGestureRecognizer, UIGestureRecognizerDelegate, UIGestureRecognizerState, UIKeyInput, UIPanGestureRecognizer,
UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer, UIPinchGestureRecognizer, UIResponder, UIRotationGestureRecognizer, UITapGestureRecognizer,
UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView, UITextInputTraits, UITouch, UITouchPhase, UITouchType, UITraitEnvironment, UIView,
}; };
use super::app_state::{self, EventWrapper}; use super::app_state::{self, EventWrapper};
use super::window::WinitUIWindow; use super::window::WinitUIWindow;
use super::{FingerId, DEVICE_ID}; use super::{FingerId, DEVICE_ID};
use crate::dpi::PhysicalPosition; use crate::dpi::PhysicalPosition;
use crate::event::{Event, FingerId as RootFingerId, Force, Touch, TouchPhase, WindowEvent}; use crate::event::{
ElementState, Event, FingerId as RootFingerId, Force, KeyEvent, Touch, TouchPhase, WindowEvent,
};
use crate::keyboard::{Key, KeyCode, KeyLocation, NamedKey, NativeKeyCode, PhysicalKey};
use crate::platform_impl::KeyEventExtra;
use crate::window::{WindowAttributes, WindowId as RootWindowId}; use crate::window::{WindowAttributes, WindowId as RootWindowId};
pub struct WinitViewState { pub struct WinitViewState {
@ -314,6 +318,11 @@ declare_class!(
let mtm = MainThreadMarker::new().unwrap(); let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_event(mtm, gesture_event); app_state::handle_nonuser_event(mtm, gesture_event);
} }
#[method(canBecomeFirstResponder)]
fn can_become_first_responder(&self) -> bool {
true
}
} }
unsafe impl NSObjectProtocol for WinitView {} unsafe impl NSObjectProtocol for WinitView {}
@ -324,6 +333,26 @@ declare_class!(
true true
} }
} }
unsafe impl UITextInputTraits for WinitView {
}
unsafe impl UIKeyInput for WinitView {
#[method(hasText)]
fn has_text(&self) -> bool {
true
}
#[method(insertText:)]
fn insert_text(&self, text: &NSString) {
self.handle_insert_text(text)
}
#[method(deleteBackward)]
fn delete_backward(&self) {
self.handle_delete_backward()
}
}
); );
impl WinitView { impl WinitView {
@ -512,4 +541,69 @@ impl WinitView {
let mtm = MainThreadMarker::new().unwrap(); let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_events(mtm, touch_events); app_state::handle_nonuser_events(mtm, touch_events);
} }
fn handle_insert_text(&self, text: &NSString) {
let window = self.window().unwrap();
let window_id = RootWindowId(window.id());
let mtm = MainThreadMarker::new().unwrap();
// send individual events for each character
app_state::handle_nonuser_events(
mtm,
text.to_string().chars().flat_map(|c| {
let text = smol_str::SmolStr::from_iter([c]);
// Emit both press and release events
[ElementState::Pressed, ElementState::Released].map(|state| {
EventWrapper::StaticEvent(Event::WindowEvent {
window_id,
event: WindowEvent::KeyboardInput {
event: KeyEvent {
text: if state == ElementState::Pressed {
Some(text.clone())
} else {
None
},
state,
location: KeyLocation::Standard,
repeat: false,
logical_key: Key::Character(text.clone()),
physical_key: PhysicalKey::Unidentified(
NativeKeyCode::Unidentified,
),
platform_specific: KeyEventExtra {},
},
is_synthetic: false,
device_id: DEVICE_ID,
},
})
})
}),
);
}
fn handle_delete_backward(&self) {
let window = self.window().unwrap();
let window_id = RootWindowId(window.id());
let mtm = MainThreadMarker::new().unwrap();
app_state::handle_nonuser_events(
mtm,
[ElementState::Pressed, ElementState::Released].map(|state| {
EventWrapper::StaticEvent(Event::WindowEvent {
window_id,
event: WindowEvent::KeyboardInput {
device_id: DEVICE_ID,
event: KeyEvent {
state,
logical_key: Key::Named(NamedKey::Backspace),
physical_key: PhysicalKey::Code(KeyCode::Backspace),
platform_specific: KeyEventExtra {},
repeat: false,
location: KeyLocation::Standard,
text: None,
},
is_synthetic: false,
},
})
}),
);
}
} }

View file

@ -370,12 +370,24 @@ impl Inner {
warn!("`Window::set_ime_cursor_area` is ignored on iOS") warn!("`Window::set_ime_cursor_area` is ignored on iOS")
} }
pub fn set_ime_allowed(&self, _allowed: bool) { /// Show / hide the keyboard. To show the keyboard, we call `becomeFirstResponder`,
warn!("`Window::set_ime_allowed` is ignored on iOS") /// requesting focus for the [WinitView]. Since [WinitView] implements
/// [objc2_ui_kit::UIKeyInput], the keyboard will be shown.
/// <https://developer.apple.com/documentation/uikit/uiresponder/1621113-becomefirstresponder>
pub fn set_ime_allowed(&self, allowed: bool) {
if allowed {
unsafe {
self.view.becomeFirstResponder();
}
} else {
unsafe {
self.view.resignFirstResponder();
}
}
} }
pub fn set_ime_purpose(&self, _purpose: ImePurpose) { pub fn set_ime_purpose(&self, _purpose: ImePurpose) {
warn!("`Window::set_ime_allowed` is ignored on iOS") warn!("`Window::set_ime_purpose` is ignored on iOS")
} }
pub fn focus_window(&self) { pub fn focus_window(&self) {

View file

@ -1279,7 +1279,8 @@ impl Window {
/// ///
/// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are /// - **macOS:** IME must be enabled to receive text-input where dead-key sequences are
/// combined. /// combined.
/// - **iOS / Android / Web / Orbital:** Unsupported. /// - **iOS:** This will show / hide the soft keyboard.
/// - **Android / Web / Orbital:** Unsupported.
/// - **X11**: Enabling IME will disable dead keys reporting during compose. /// - **X11**: Enabling IME will disable dead keys reporting during compose.
/// ///
/// [`Ime`]: crate::event::WindowEvent::Ime /// [`Ime`]: crate::event::WindowEvent::Ime