Skip to content

Commit 96df78d

Browse files
committed
Expose TextEvent and input method state
This also adds `InputEvent::TextEvent` for notifying applications of IME state changes as well as explicit getter/setter APIs for tracking IME selection + compose region state. (only supported with GameActivity) Fixes: #18
1 parent 8d30454 commit 96df78d

File tree

7 files changed

+189
-2
lines changed

7 files changed

+189
-2
lines changed

android-activity/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ native-activity = []
2424
[dependencies]
2525
log = "0.4"
2626
jni-sys = "0.3"
27+
cesu8 = "1"
2728
ndk = "0.7"
2829
ndk-sys = "0.4"
2930
ndk-context = "0.1"

android-activity/game-activity-csrc/game-activity/native_app_glue/android_native_app_glue.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -601,6 +601,7 @@ static void onTextInputEvent(GameActivity* activity,
601601
pthread_mutex_lock(&android_app->mutex);
602602

603603
android_app->textInputState = 1;
604+
notifyInput(android_app);
604605
pthread_mutex_unlock(&android_app->mutex);
605606
}
606607

android-activity/src/game_activity/input.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ use bitflags::bitflags;
2626
pub enum InputEvent {
2727
MotionEvent(MotionEvent),
2828
KeyEvent(KeyEvent),
29+
TextEvent(crate::input::TextInputState),
2930
}
3031

3132
/// An enum representing the source of an [`MotionEvent`] or [`KeyEvent`]

android-activity/src/game_activity/mod.rs

Lines changed: 127 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use std::fs::File;
55
use std::io::{BufRead, BufReader};
66
use std::marker::PhantomData;
77
use std::ops::Deref;
8-
use std::os::raw;
8+
use std::os::raw::{self, c_void};
99
use std::os::unix::prelude::*;
1010
use std::ptr::NonNull;
1111
use std::sync::{Arc, RwLock};
@@ -28,6 +28,7 @@ use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect, Wind
2828
mod ffi;
2929

3030
pub mod input;
31+
use crate::input::{TextInputState, TextSpan};
3132
use input::{Axis, InputEvent, KeyEvent, MotionEvent};
3233

3334
// The only time it's safe to update the android_app->savedState pointer is
@@ -346,6 +347,122 @@ impl AndroidAppInner {
346347
}
347348
}
348349

350+
unsafe extern "C" fn map_input_state_to_text_event_callback(
351+
context: *mut c_void,
352+
state: *const ffi::GameTextInputState,
353+
) {
354+
// Java uses a modified UTF-8 format, which is a modified cesu8 format
355+
let out_ptr: *mut TextInputState = context.cast();
356+
let text_modified_utf8: *const u8 = (*state).text_UTF8.cast();
357+
let text_modified_utf8 =
358+
std::slice::from_raw_parts(text_modified_utf8, (*state).text_length as usize);
359+
match cesu8::from_java_cesu8(&text_modified_utf8) {
360+
Ok(str) => {
361+
(*out_ptr).text = String::from(str);
362+
(*out_ptr).selection = TextSpan {
363+
start: match (*state).selection.start {
364+
-1 => None,
365+
off => Some(off as usize),
366+
},
367+
end: match (*state).selection.end {
368+
-1 => None,
369+
off => Some(off as usize),
370+
},
371+
};
372+
(*out_ptr).compose_region = TextSpan {
373+
start: match (*state).composingRegion.start {
374+
-1 => None,
375+
off => Some(off as usize),
376+
},
377+
end: match (*state).composingRegion.end {
378+
-1 => None,
379+
off => Some(off as usize),
380+
},
381+
};
382+
}
383+
Err(err) => {
384+
log::error!("Invalid UTF8 text in TextEvent: {}", err);
385+
}
386+
}
387+
}
388+
389+
// TODO: move into a trait
390+
pub fn text_input_state(&self) -> TextInputState {
391+
unsafe {
392+
let activity = (*self.native_app.as_ptr()).activity;
393+
let mut out_state = TextInputState {
394+
text: String::new(),
395+
selection: TextSpan {
396+
start: None,
397+
end: None,
398+
},
399+
compose_region: TextSpan {
400+
start: None,
401+
end: None,
402+
},
403+
};
404+
let out_ptr = &mut out_state as *mut TextInputState;
405+
406+
// NEON WARNING:
407+
//
408+
// It's not clearly documented but the GameActivity API over the
409+
// GameTextInput library directly exposes _modified_ UTF8 text
410+
// from Java so we need to be careful to convert text to and
411+
// from UTF8
412+
//
413+
// GameTextInput also uses a pre-allocated, fixed-sized buffer for the current
414+
// text state but GameTextInput doesn't actually provide it's own thread
415+
// safe API to safely access this state so we have to cooperate with
416+
// the GameActivity code that does locking when reading/writing the state
417+
// (I.e. we can't just punch through to the GameTextInput layer from here).
418+
//
419+
// Overall this is all quite gnarly - and probably a good reminder of why
420+
// we want to use Rust instead of C/C++.
421+
ffi::GameActivity_getTextInputState(
422+
activity,
423+
Some(AndroidAppInner::map_input_state_to_text_event_callback),
424+
out_ptr.cast(),
425+
);
426+
427+
out_state
428+
}
429+
}
430+
431+
// TODO: move into a trait
432+
pub fn set_text_input_state(&self, state: TextInputState) {
433+
unsafe {
434+
let activity = (*self.native_app.as_ptr()).activity;
435+
let modified_utf8 = cesu8::to_java_cesu8(&state.text);
436+
let text_length = modified_utf8.len() as i32;
437+
let modified_utf8_bytes = modified_utf8.as_ptr();
438+
let ffi_state = ffi::GameTextInputState {
439+
text_UTF8: modified_utf8_bytes.cast(), // NB: may be signed or unsigned depending on target
440+
text_length,
441+
selection: ffi::GameTextInputSpan {
442+
start: match state.selection.start {
443+
Some(off) => off as i32,
444+
None => -1,
445+
},
446+
end: match state.selection.end {
447+
Some(off) => off as i32,
448+
None => -1,
449+
},
450+
},
451+
composingRegion: ffi::GameTextInputSpan {
452+
start: match state.compose_region.start {
453+
Some(off) => off as i32,
454+
None => -1,
455+
},
456+
end: match state.compose_region.end {
457+
Some(off) => off as i32,
458+
None => -1,
459+
},
460+
},
461+
};
462+
ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
463+
}
464+
}
465+
349466
pub fn enable_motion_axis(&mut self, axis: Axis) {
350467
unsafe { ffi::GameActivityPointerAxes_enableAxis(axis as i32) }
351468
}
@@ -408,6 +525,15 @@ impl AndroidAppInner {
408525
for motion_event in buf.motion_events_iter() {
409526
callback(&InputEvent::MotionEvent(motion_event));
410527
}
528+
529+
unsafe {
530+
let app_ptr = self.native_app.as_ptr();
531+
if (*app_ptr).textInputState != 0 {
532+
let state = self.text_input_state();
533+
callback(&InputEvent::TextEvent(state));
534+
(*app_ptr).textInputState = 0;
535+
}
536+
}
411537
}
412538

413539
pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {

android-activity/src/input.rs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/// This struct holds a span within a region of text from `start` (inclusive) to
2+
/// `end` (exclusive).
3+
///
4+
/// An empty span or cursor position is specified with `Some(start) == Some(end)`.
5+
///
6+
/// An undefined span is specified with start = end = `None`.
7+
#[derive(Debug, Clone, Copy)]
8+
pub struct TextSpan {
9+
/// The start of the span (inclusive)
10+
pub start: Option<usize>,
11+
12+
/// The end of the span (exclusive)
13+
pub end: Option<usize>,
14+
}
15+
16+
#[derive(Debug, Clone)]
17+
pub struct TextInputState {
18+
pub text: String,
19+
/// A selection defined on the text.
20+
pub selection: TextSpan,
21+
/// A composing region defined on the text.
22+
pub compose_region: TextSpan,
23+
}
24+
25+
pub use crate::activity_impl::input::*;

android-activity/src/lib.rs

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ mod game_activity;
4747
#[cfg(feature = "game-activity")]
4848
use game_activity as activity_impl;
4949

50-
pub use activity_impl::input;
50+
pub mod input;
5151

5252
mod config;
5353
pub use config::ConfigurationRef;
@@ -471,6 +471,16 @@ impl AndroidApp {
471471
.hide_soft_input(hide_implicit_only);
472472
}
473473

474+
/// Fetch the current input text state, as updated by any active IME.
475+
pub fn text_input_state(&self) -> input::TextInputState {
476+
self.inner.read().unwrap().text_input_state()
477+
}
478+
479+
/// Forward the given input text `state` to any active IME.
480+
pub fn set_text_input_state(&self, state: input::TextInputState) {
481+
self.inner.read().unwrap().set_text_input_state(state);
482+
}
483+
474484
/// Query and process all out-standing input event
475485
///
476486
/// Applications are generally either expected to call this in-sync with their rendering or

android-activity/src/native_activity/mod.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use ndk::configuration::Configuration;
2121
use ndk::input_queue::InputQueue;
2222
use ndk::native_window::NativeWindow;
2323

24+
use crate::input::{TextInputState, TextSpan};
2425
use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect, WindowManagerFlags};
2526

2627
mod ffi;
@@ -39,6 +40,7 @@ pub mod input {
3940
pub enum InputEvent {
4041
MotionEvent(self::MotionEvent),
4142
KeyEvent(self::KeyEvent),
43+
TextEvent(crate::input::TextInputState),
4244
}
4345
}
4446

@@ -405,6 +407,26 @@ impl AndroidAppInner {
405407
}
406408
}
407409

410+
// TODO: move into a trait
411+
pub fn text_input_state(&self) -> TextInputState {
412+
TextInputState {
413+
text: String::new(),
414+
selection: TextSpan {
415+
start: None,
416+
end: None,
417+
},
418+
compose_region: TextSpan {
419+
start: None,
420+
end: None,
421+
},
422+
}
423+
}
424+
425+
// TODO: move into a trait
426+
pub fn set_text_input_state(&self, _state: TextInputState) {
427+
// NOP: Unsupported
428+
}
429+
408430
pub fn enable_motion_axis(&self, _axis: input::Axis) {
409431
// NOP - The InputQueue API doesn't let us optimize which axis values are read
410432
}
@@ -449,6 +471,7 @@ impl AndroidAppInner {
449471
let ndk_event = match event {
450472
input::InputEvent::MotionEvent(e) => ndk::event::InputEvent::MotionEvent(e),
451473
input::InputEvent::KeyEvent(e) => ndk::event::InputEvent::KeyEvent(e),
474+
_ => unreachable!(),
452475
};
453476

454477
// Always report events as 'handled'. This means we won't get

0 commit comments

Comments
 (0)