Skip to content

Commit ecb9616

Browse files
committed
Add basic IME support for Android
This adds basic support for Ime events to the Android backend. Note that this will only work when running with the game-activity backend, which uses AGDK GameText to forward Android IME events: https://developer.android.com/games/agdk/add-support-for-text-input Normally on Android, input methods track three things: - Surrounding text - A compose region - A selection Since Winit (0.30) doesn't track surrounding text and therefore also wouldn't be able to handle orthogonal compose + selection regions within some surrounding text, we can treat the whole text region that we edit as the "preedit" string, and we can then treat the compose region as the optional selection within the preedit string. I've tested this with Egui 0.33 I've seem some quirky cases when testing with Egui (such as if you try and move the cursor in the Egui widget while you're in the middle of entering text via a soft keyboard) but I think those are related to general shortcomings of the winit 0.30 IME API and Egui's support for IMEs (there's no way for Egui to notify through Winit that the cursor position has changed).
1 parent f6893a4 commit ecb9616

5 files changed

Lines changed: 137 additions & 3 deletions

File tree

Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,3 +376,6 @@ edition = "2021"
376376
[workspace.dependencies]
377377
serde = { version = "1", features = ["serde_derive"] }
378378
mint = "0.5.6"
379+
380+
[patch.crates-io]
381+
android-activity = { git = "https://github.com/rust-mobile/android-activity.git", branch = "rib/stack/ime-support" }

src/changelog/v0.30.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,10 @@
1+
2+
## 0.30.13
3+
4+
### Added
5+
6+
- On Android, added support for Ime events, for soft keyboard input.
7+
18
## 0.30.12
29

310
### Fixed

src/event.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -227,7 +227,7 @@ pub enum WindowEvent {
227227
///
228228
/// ## Platform-specific
229229
///
230-
/// - **iOS / Android / Web / Orbital:** Unsupported.
230+
/// - **iOS / Web / Orbital:** Unsupported.
231231
Ime(Ime),
232232

233233
/// The cursor has moved on the window.

src/platform_impl/android/keycodes.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,12 @@ pub fn character_map_and_combine_key(
169169
) -> Option<KeyMapChar> {
170170
let device_id = key_event.device_id();
171171

172+
// A device ID of 0 indicates a non-physical device (e.g. software keyboard)
173+
// which we don't expect to have an associated KeyCharacterMap
174+
if device_id == 0 {
175+
return None;
176+
}
177+
172178
let key_map = match app.device_key_character_map(device_id) {
173179
Ok(key_map) => key_map,
174180
Err(err) => {

src/platform_impl/android/mod.rs

Lines changed: 120 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,9 @@ use std::sync::atomic::{AtomicBool, Ordering};
66
use std::sync::{mpsc, Arc, Mutex};
77
use std::time::{Duration, Instant};
88

9-
use android_activity::input::{InputEvent, KeyAction, Keycode, MotionAction};
9+
use android_activity::input::{
10+
InputEvent, KeyAction, Keycode, MotionAction, TextInputAction, TextInputState, TextSpan,
11+
};
1012
use android_activity::{
1113
AndroidApp, AndroidAppWaker, ConfigurationRef, InputStatus, MainEvent, Rect,
1214
};
@@ -131,9 +133,14 @@ impl RedrawRequester {
131133
#[derive(Debug, Clone, Eq, PartialEq, Hash)]
132134
pub struct KeyEventExtra {}
133135

136+
struct ImeState {
137+
ime_allowed: AtomicBool,
138+
}
139+
134140
pub struct EventLoop<T: 'static> {
135141
pub(crate) android_app: AndroidApp,
136142
window_target: event_loop::ActiveEventLoop,
143+
ime_state: Arc<ImeState>,
137144
redraw_flag: SharedFlag,
138145
user_events_sender: mpsc::Sender<T>,
139146
user_events_receiver: PeekableReceiver<T>, // must wake looper whenever something gets sent
@@ -169,6 +176,8 @@ impl<T: 'static> EventLoop<T> {
169176
);
170177
let redraw_flag = SharedFlag::new();
171178

179+
let ime_state = Arc::new(ImeState { ime_allowed: AtomicBool::new(false) });
180+
172181
Ok(Self {
173182
android_app: android_app.clone(),
174183
window_target: event_loop::ActiveEventLoop {
@@ -180,9 +189,11 @@ impl<T: 'static> EventLoop<T> {
180189
&redraw_flag,
181190
android_app.create_waker(),
182191
),
192+
ime_state: Arc::clone(&ime_state),
183193
},
184194
_marker: PhantomData,
185195
},
196+
ime_state,
186197
redraw_flag,
187198
user_events_sender,
188199
user_events_receiver: PeekableReceiver::from_recv(user_events_receiver),
@@ -466,6 +477,94 @@ impl<T: 'static> EventLoop<T> {
466477
},
467478
}
468479
},
480+
InputEvent::TextEvent(input_state) => {
481+
trace!("Received IME text event: {:?}", input_state);
482+
if self.ime_state.ime_allowed.load(Ordering::SeqCst) == false {
483+
trace!("IME input not enabled, ignoring spurious text event");
484+
return InputStatus::Handled;
485+
}
486+
// Note: Winit does not support surrounding text or tracking a selection/cursor that
487+
// may span within the surrounding text and the preedit text.
488+
//
489+
// Since there's no API to specify surrounding text, set_ime_allowed() will reset
490+
// the text to an empty string and we will treat all the text as preedit text.
491+
//
492+
// We map Android's composing region to winit's preedit selection region.
493+
//
494+
// This seems a little odd, since Android's notion of a "composing region" would
495+
// normally be equated with winit's "preedit" text but conceptually we're mapping
496+
// Android's surrounding text + composing region into winit's preedit text +
497+
// selection region.
498+
//
499+
// We ignore the separate selection region that Android supports.
500+
501+
let selection = if let Some(compose_region) = input_state.compose_region {
502+
// Note: Winit uses byte offsets for the preedit selection region and Android
503+
// uses char offsets.
504+
let selection_0 = input_state
505+
.text
506+
.char_indices()
507+
.enumerate()
508+
.find(|(_, (byte_offset, _))| *byte_offset >= compose_region.start)
509+
.map(|(char_idx, _)| char_idx);
510+
let selection_1 = input_state
511+
.text
512+
.char_indices()
513+
.enumerate()
514+
.find(|(_, (byte_offset, _))| *byte_offset >= compose_region.end)
515+
.map(|(char_idx, _)| char_idx);
516+
let selection_0 = selection_0.unwrap_or(input_state.text.len());
517+
let selection_1 = selection_1.unwrap_or(input_state.text.len());
518+
Some((selection_0, selection_1))
519+
} else {
520+
let len = input_state.text.len();
521+
Some((0, len))
522+
};
523+
524+
let event = event::Event::WindowEvent {
525+
window_id: window::WindowId(WindowId),
526+
event: event::WindowEvent::Ime(event::Ime::Preedit(
527+
input_state.text.clone(),
528+
selection,
529+
)),
530+
};
531+
callback(event, self.window_target());
532+
},
533+
InputEvent::TextAction(action) => {
534+
trace!("Received IME text action event: {:?}", action);
535+
if self.ime_state.ime_allowed.load(Ordering::SeqCst) == false {
536+
trace!("IME input not enabled, ignoring spurious text event");
537+
return InputStatus::Handled;
538+
}
539+
540+
// We don't have a way to convey the semantics of the action, so we just
541+
// map them all (except 'None') to a commit of the current text.
542+
if *action != TextInputAction::None {
543+
let latest_ime_state = self.android_app.text_input_state();
544+
545+
// The API docs say that a commit is preceded by an empty Preedit event
546+
let event = event::Event::WindowEvent {
547+
window_id: window::WindowId(WindowId),
548+
event: event::WindowEvent::Ime(event::Ime::Preedit(String::new(), None)),
549+
};
550+
self.android_app.set_text_input_state(TextInputState {
551+
text: String::new(),
552+
selection: TextSpan { start: 0, end: 0 },
553+
compose_region: None,
554+
});
555+
self.android_app.hide_soft_input(true);
556+
callback(event, self.window_target());
557+
558+
let event = event::Event::WindowEvent {
559+
window_id: window::WindowId(WindowId),
560+
event: event::WindowEvent::Ime(event::Ime::Commit(
561+
latest_ime_state.text.clone(),
562+
)),
563+
};
564+
565+
callback(event, self.window_target());
566+
}
567+
},
469568
_ => {
470569
warn!("Unknown android_activity input event {event:?}")
471570
},
@@ -650,6 +749,7 @@ pub struct ActiveEventLoop {
650749
control_flow: Cell<ControlFlow>,
651750
exit: Cell<bool>,
652751
redraw_requester: RedrawRequester,
752+
ime_state: Arc<ImeState>,
653753
}
654754

655755
impl ActiveEventLoop {
@@ -770,6 +870,7 @@ pub struct PlatformSpecificWindowAttributes;
770870
pub(crate) struct Window {
771871
app: AndroidApp,
772872
redraw_requester: RedrawRequester,
873+
ime_state: Arc<ImeState>,
773874
}
774875

775876
impl Window {
@@ -779,7 +880,11 @@ impl Window {
779880
) -> Result<Self, error::OsError> {
780881
// FIXME this ignores requested window attributes
781882

782-
Ok(Self { app: el.app.clone(), redraw_requester: el.redraw_requester.clone() })
883+
Ok(Self {
884+
app: el.app.clone(),
885+
redraw_requester: el.redraw_requester.clone(),
886+
ime_state: Arc::clone(&el.ime_state),
887+
})
783888
}
784889

785890
pub(crate) fn maybe_queue_on_main(&self, f: impl FnOnce(&Self) + Send + 'static) {
@@ -909,11 +1014,24 @@ impl Window {
9091014
pub fn set_ime_cursor_area(&self, _position: Position, _size: Size) {}
9101015

9111016
pub fn set_ime_allowed(&self, allowed: bool) {
1017+
// Request a show/hide regardless of whether the state has changed, since
1018+
// the keyboard may have been dismissed by the user manually while in the
1019+
// middle of text input
9121020
if allowed {
9131021
self.app.show_soft_input(true);
9141022
} else {
9151023
self.app.hide_soft_input(true);
9161024
}
1025+
1026+
if self.ime_state.ime_allowed.swap(allowed, Ordering::SeqCst) == allowed {
1027+
return;
1028+
}
1029+
1030+
self.app.set_text_input_state(TextInputState {
1031+
text: String::new(),
1032+
selection: TextSpan { start: 0, end: 0 },
1033+
compose_region: Some(TextSpan { start: 0, end: 0 }),
1034+
});
9171035
}
9181036

9191037
pub fn set_ime_purpose(&self, _purpose: ImePurpose) {}

0 commit comments

Comments
 (0)