Skip to content

Commit 513dc7d

Browse files
committed
Expose input method APIs
This adds `AndroidApp::show/hide_soft_input` APIs for showing or hiding the user's on-screen keyboard. (supported for NativeActivity and GameActivity) 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 1a1ff00 commit 513dc7d

File tree

7 files changed

+305
-8
lines changed

7 files changed

+305
-8
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: 153 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};
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
@@ -309,6 +310,148 @@ impl AndroidAppInner {
309310
}
310311
}
311312

313+
// TODO: move into a trait
314+
pub fn show_soft_input(&self, show_implicit: bool) {
315+
unsafe {
316+
let activity = (*self.native_app.as_ptr()).activity;
317+
let flags = if show_implicit {
318+
ffi::ShowImeFlags_SHOW_IMPLICIT
319+
} else {
320+
0
321+
};
322+
ffi::GameActivity_showSoftInput(activity, flags);
323+
}
324+
}
325+
326+
// TODO: move into a trait
327+
pub fn hide_soft_input(&self, hide_implicit_only: bool) {
328+
unsafe {
329+
let activity = (*self.native_app.as_ptr()).activity;
330+
let flags = if hide_implicit_only {
331+
ffi::HideImeFlags_HIDE_IMPLICIT_ONLY
332+
} else {
333+
0
334+
};
335+
ffi::GameActivity_hideSoftInput(activity, flags);
336+
}
337+
}
338+
339+
unsafe extern "C" fn map_input_state_to_text_event_callback(
340+
context: *mut c_void,
341+
state: *const ffi::GameTextInputState,
342+
) {
343+
// Java uses a modified UTF-8 format, which is a modified cesu8 format
344+
let out_ptr: *mut TextInputState = context.cast();
345+
let text_modified_utf8: *const u8 = (*state).text_UTF8.cast();
346+
let text_modified_utf8 =
347+
std::slice::from_raw_parts(text_modified_utf8, (*state).text_length as usize);
348+
match cesu8::from_java_cesu8(&text_modified_utf8) {
349+
Ok(str) => {
350+
(*out_ptr).text = String::from(str);
351+
(*out_ptr).selection = TextSpan {
352+
start: match (*state).selection.start {
353+
-1 => None,
354+
off => Some(off as usize),
355+
},
356+
end: match (*state).selection.end {
357+
-1 => None,
358+
off => Some(off as usize),
359+
},
360+
};
361+
(*out_ptr).composing_region = TextSpan {
362+
start: match (*state).composingRegion.start {
363+
-1 => None,
364+
off => Some(off as usize),
365+
},
366+
end: match (*state).composingRegion.end {
367+
-1 => None,
368+
off => Some(off as usize),
369+
},
370+
};
371+
}
372+
Err(err) => {
373+
log::error!("Invalid UTF8 text in TextEvent: {}", err);
374+
}
375+
}
376+
}
377+
378+
// TODO: move into a trait
379+
pub fn text_input_state(&self) -> TextInputState {
380+
unsafe {
381+
let activity = (*self.native_app.as_ptr()).activity;
382+
let mut out_state = TextInputState {
383+
text: String::new(),
384+
selection: TextSpan {
385+
start: None,
386+
end: None,
387+
},
388+
composing_region: TextSpan {
389+
start: None,
390+
end: None,
391+
},
392+
};
393+
let out_ptr = &mut out_state as *mut TextInputState;
394+
395+
// NEON WARNING:
396+
//
397+
// It's not clearly documented but the GameActivity API over the
398+
// GameTextInput library directly exposes _modified_ UTF8 text
399+
// from Java so we need to be careful to convert text to and
400+
// from UTF8
401+
//
402+
// GameTextInput also uses a pre-allocated, fixed-sized buffer for the current
403+
// text state but GameTextInput doesn't actually provide it's own thread
404+
// safe API to safely access this state so we have to cooperate with
405+
// the GameActivity code that does locking when reading/writing the state
406+
// (I.e. we can't just punch through to the GameTextInput layer from here).
407+
//
408+
// Overall this is all quite gnarly - and probably a good reminder of why
409+
// we want to use Rust instead of C/C++.
410+
ffi::GameActivity_getTextInputState(
411+
activity,
412+
Some(AndroidAppInner::map_input_state_to_text_event_callback),
413+
out_ptr.cast(),
414+
);
415+
416+
out_state
417+
}
418+
}
419+
420+
// TODO: move into a trait
421+
pub fn set_text_input_state(&self, state: TextInputState) {
422+
unsafe {
423+
let activity = (*self.native_app.as_ptr()).activity;
424+
let modified_utf8 = cesu8::to_java_cesu8(&state.text);
425+
let text_length = modified_utf8.len() as i32;
426+
let modified_utf8_bytes = modified_utf8.as_ptr();
427+
let ffi_state = ffi::GameTextInputState {
428+
text_UTF8: modified_utf8_bytes.cast(), // NB: may be signed or unsigned depending on target
429+
text_length,
430+
selection: ffi::GameTextInputSpan {
431+
start: match state.selection.start {
432+
Some(off) => off as i32,
433+
None => -1,
434+
},
435+
end: match state.selection.end {
436+
Some(off) => off as i32,
437+
None => -1,
438+
},
439+
},
440+
composingRegion: ffi::GameTextInputSpan {
441+
start: match state.composing_region.start {
442+
Some(off) => off as i32,
443+
None => -1,
444+
},
445+
end: match state.composing_region.end {
446+
Some(off) => off as i32,
447+
None => -1,
448+
},
449+
},
450+
};
451+
ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
452+
}
453+
}
454+
312455
pub fn enable_motion_axis(&mut self, axis: Axis) {
313456
unsafe { ffi::GameActivityPointerAxes_enableAxis(axis as i32) }
314457
}
@@ -371,6 +514,15 @@ impl AndroidAppInner {
371514
for motion_event in buf.motion_events_iter() {
372515
callback(&InputEvent::MotionEvent(motion_event));
373516
}
517+
518+
unsafe {
519+
let app_ptr = self.native_app.as_ptr();
520+
if (*app_ptr).textInputState != 0 {
521+
let state = self.text_input_state();
522+
callback(&InputEvent::TextEvent(state));
523+
(*app_ptr).textInputState = 0;
524+
}
525+
}
374526
}
375527

376528
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 composing_region: TextSpan,
23+
}
24+
25+
pub use crate::activity_impl::input::*;

android-activity/src/lib.rs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ compile_error!(
1717
not(any(feature = "game-activity", feature = "native-activity")),
1818
not(doc)
1919
))]
20-
compile_error!(r#"Either \"game-activity\" or \"native-activity\" must be enabled as features
20+
compile_error!(
21+
r#"Either \"game-activity\" or \"native-activity\" must be enabled as features
2122
2223
If you have set one of these features then this error indicates that Cargo is trying to
2324
link together multiple implementations of android-activity (with incompatible versions)
@@ -31,7 +32,8 @@ versions have been resolved.
3132
3233
You may need to add a `[patch]` into your Cargo.toml to ensure a specific version of
3334
android-activity is used across all of your application's crates.
34-
"#);
35+
"#
36+
);
3537

3638
#[cfg(any(feature = "native-activity", doc))]
3739
mod native_activity;
@@ -43,7 +45,7 @@ mod game_activity;
4345
#[cfg(feature = "game-activity")]
4446
use game_activity as activity_impl;
4547

46-
pub use activity_impl::input;
48+
pub mod input;
4749

4850
mod config;
4951
pub use config::ConfigurationRef;
@@ -249,14 +251,62 @@ impl AndroidApp {
249251
self.inner.read().unwrap().asset_manager()
250252
}
251253

254+
/// Enable additional input axis
255+
///
256+
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
257+
/// and other axis should be enabled explicitly.
252258
pub fn enable_motion_axis(&self, axis: input::Axis) {
253259
self.inner.write().unwrap().enable_motion_axis(axis);
254260
}
255261

262+
/// Disable input axis
263+
///
264+
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
265+
/// and other axis should be enabled explicitly.
256266
pub fn disable_motion_axis(&self, axis: input::Axis) {
257267
self.inner.write().unwrap().disable_motion_axis(axis);
258268
}
259269

270+
/// Explicitly request that the current input method's soft input area be
271+
/// shown to the user, if needed.
272+
///
273+
/// Call this if the user interacts with your view in such a way that they
274+
/// have expressed they would like to start performing input into it.
275+
pub fn show_soft_input(&self, show_implicit: bool) {
276+
self.inner.read().unwrap().show_soft_input(show_implicit);
277+
}
278+
279+
/// Request to hide the soft input window from the context of the window
280+
/// that is currently accepting input.
281+
///
282+
/// This should be called as a result of the user doing some action that
283+
/// fairly explicitly requests to have the input window hidden.
284+
pub fn hide_soft_input(&self, hide_implicit_only: bool) {
285+
self.inner
286+
.read()
287+
.unwrap()
288+
.hide_soft_input(hide_implicit_only);
289+
}
290+
291+
/// Fetch the current input text state, as updated by any active IME.
292+
pub fn text_input_state(&self) -> input::TextInputState {
293+
self.inner.read().unwrap().text_input_state()
294+
}
295+
296+
/// Forward the given input text `state` to any active IME.
297+
pub fn set_text_input_state(&self, state: input::TextInputState) {
298+
self.inner.read().unwrap().set_text_input_state(state);
299+
}
300+
301+
/// Query and process all out-standing input event
302+
///
303+
/// Applications are generally either expected to call this in-sync with their rendering or
304+
/// in response to a [`MainEvent::InputAvailable`] event being delivered. _Note though that your
305+
/// application is will only be delivered a single [`MainEvent::InputAvailable`] event between calls
306+
/// to this API._
307+
///
308+
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
309+
/// and other axis should be enabled explicitly via [`Self::enable_motion_axis`].
260310
pub fn input_events<'b, F>(&self, callback: F)
261311
where
262312
F: FnMut(&input::InputEvent),

0 commit comments

Comments
 (0)