Skip to content

Commit

Permalink
Expose input method APIs
Browse files Browse the repository at this point in the history
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
  • Loading branch information
rib committed Sep 12, 2022
1 parent 1a1ff00 commit 5a91699
Show file tree
Hide file tree
Showing 7 changed files with 297 additions and 8 deletions.
1 change: 1 addition & 0 deletions android-activity/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ native-activity = []
[dependencies]
log = "0.4"
jni-sys = "0.3"
cesu8 = "1"
ndk = "0.7"
ndk-sys = "0.4"
ndk-context = "0.1"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -601,6 +601,7 @@ static void onTextInputEvent(GameActivity* activity,
pthread_mutex_lock(&android_app->mutex);

android_app->textInputState = 1;
notifyInput(android_app);
pthread_mutex_unlock(&android_app->mutex);
}

Expand Down
1 change: 1 addition & 0 deletions android-activity/src/game_activity/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use bitflags::bitflags;
pub enum InputEvent {
MotionEvent(MotionEvent),
KeyEvent(KeyEvent),
TextEvent(crate::input::TextInputState),
}

/// An enum representing the source of an [`MotionEvent`] or [`KeyEvent`]
Expand Down
154 changes: 153 additions & 1 deletion android-activity/src/game_activity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use std::fs::File;
use std::io::{BufRead, BufReader};
use std::marker::PhantomData;
use std::ops::Deref;
use std::os::raw;
use std::os::raw::{self, c_void};
use std::os::unix::prelude::*;
use std::ptr::NonNull;
use std::sync::{Arc, RwLock};
Expand All @@ -28,6 +28,7 @@ use crate::{util, AndroidApp, ConfigurationRef, MainEvent, PollEvent, Rect};
mod ffi;

pub mod input;
use crate::input::{TextInputState, TextSpan};
use input::{Axis, InputEvent, KeyEvent, MotionEvent};

// The only time it's safe to update the android_app->savedState pointer is
Expand Down Expand Up @@ -309,6 +310,148 @@ impl AndroidAppInner {
}
}

// TODO: move into a trait
pub fn show_soft_input(&self, show_implicit: bool) {
unsafe {
let activity = (*self.native_app.as_ptr()).activity;
let flags = if show_implicit {
ffi::ShowImeFlags_SHOW_IMPLICIT
} else {
0
};
ffi::GameActivity_showSoftInput(activity, flags);
}
}

// TODO: move into a trait
pub fn hide_soft_input(&self, hide_implicit_only: bool) {
unsafe {
let activity = (*self.native_app.as_ptr()).activity;
let flags = if hide_implicit_only {
ffi::HideImeFlags_HIDE_IMPLICIT_ONLY
} else {
0
};
ffi::GameActivity_hideSoftInput(activity, flags);
}
}

unsafe extern "C" fn map_input_state_to_text_event_callback(
context: *mut c_void,
state: *const ffi::GameTextInputState,
) {
// Java uses a modified UTF-8 format, which is a modified cesu8 format
let out_ptr: *mut TextInputState = context.cast();
let text_modified_utf8: *const u8 = (*state).text_UTF8.cast();
let text_modified_utf8 =
std::slice::from_raw_parts(text_modified_utf8, (*state).text_length as usize);
match cesu8::from_java_cesu8(&text_modified_utf8) {
Ok(str) => {
(*out_ptr).text = String::from(str);
(*out_ptr).selection = TextSpan {
start: match (*state).selection.start {
-1 => None,
off => Some(off as usize),
},
end: match (*state).selection.end {
-1 => None,
off => Some(off as usize),
},
};
(*out_ptr).composing_region = TextSpan {
start: match (*state).composingRegion.start {
-1 => None,
off => Some(off as usize),
},
end: match (*state).composingRegion.end {
-1 => None,
off => Some(off as usize),
},
};
}
Err(err) => {
log::error!("Invalid UTF8 text in TextEvent: {}", err);
}
}
}

// TODO: move into a trait
pub fn text_input_state(&self) -> TextInputState {
unsafe {
let activity = (*self.native_app.as_ptr()).activity;
let mut out_state = TextInputState {
text: String::new(),
selection: TextSpan {
start: None,
end: None,
},
composing_region: TextSpan {
start: None,
end: None,
},
};
let out_ptr = &mut out_state as *mut TextInputState;

// NEON WARNING:
//
// It's not clearly documented but the GameActivity API over the
// GameTextInput library directly exposes _modified_ UTF8 text
// from Java so we need to be careful to convert text to and
// from UTF8
//
// GameTextInput also uses a pre-allocated, fixed-sized buffer for the current
// text state but GameTextInput doesn't actually provide it's own thread
// safe API to safely access this state so we have to cooperate with
// the GameActivity code that does locking when reading/writing the state
// (I.e. we can't just punch through to the GameTextInput layer from here).
//
// Overall this is all quite gnarly - and probably a good reminder of why
// we want to use Rust instead of C/C++.
ffi::GameActivity_getTextInputState(
activity,
Some(AndroidAppInner::map_input_state_to_text_event_callback),
out_ptr.cast(),
);

out_state
}
}

// TODO: move into a trait
pub fn set_text_input_state(&self, state: TextInputState) {
unsafe {
let activity = (*self.native_app.as_ptr()).activity;
let modified_utf8 = cesu8::to_java_cesu8(&state.text);
let text_length = modified_utf8.len() as i32;
let modified_utf8_bytes = modified_utf8.as_ptr();
let ffi_state = ffi::GameTextInputState {
text_UTF8: modified_utf8_bytes.cast(), // NB: may be signed or unsigned depending on target
text_length,
selection: ffi::GameTextInputSpan {
start: match state.selection.start {
Some(off) => off as i32,
None => -1,
},
end: match state.selection.end {
Some(off) => off as i32,
None => -1,
},
},
composingRegion: ffi::GameTextInputSpan {
start: match state.composing_region.start {
Some(off) => off as i32,
None => -1,
},
end: match state.composing_region.end {
Some(off) => off as i32,
None => -1,
},
},
};
ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
}
}

pub fn enable_motion_axis(&mut self, axis: Axis) {
unsafe { ffi::GameActivityPointerAxes_enableAxis(axis as i32) }
}
Expand Down Expand Up @@ -371,6 +514,15 @@ impl AndroidAppInner {
for motion_event in buf.motion_events_iter() {
callback(&InputEvent::MotionEvent(motion_event));
}

unsafe {
let app_ptr = self.native_app.as_ptr();
if (*app_ptr).textInputState != 0 {
let state = self.text_input_state();
callback(&InputEvent::TextEvent(state));
(*app_ptr).textInputState = 0;
}
}
}

pub fn internal_data_path(&self) -> Option<std::path::PathBuf> {
Expand Down
25 changes: 25 additions & 0 deletions android-activity/src/input.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/// This struct holds a span within a region of text from `start` (inclusive) to
/// `end` (exclusive).
///
/// An empty span or cursor position is specified with `Some(start) == Some(end)`.
///
/// An undefined span is specified with start = end = `None`.
#[derive(Debug, Clone, Copy)]
pub struct TextSpan {
/// The start of the span (inclusive)
pub start: Option<usize>,

/// The end of the span (exclusive)
pub end: Option<usize>,
}

#[derive(Debug, Clone)]
pub struct TextInputState {
pub text: String,
/// A selection defined on the text.
pub selection: TextSpan,
/// A composing region defined on the text.
pub composing_region: TextSpan,
}

pub use crate::activity_impl::input::*;
56 changes: 53 additions & 3 deletions android-activity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ compile_error!(
not(any(feature = "game-activity", feature = "native-activity")),
not(doc)
))]
compile_error!(r#"Either \"game-activity\" or \"native-activity\" must be enabled as features
compile_error!(
r#"Either \"game-activity\" or \"native-activity\" must be enabled as features
If you have set one of these features then this error indicates that Cargo is trying to
link together multiple implementations of android-activity (with incompatible versions)
Expand All @@ -31,7 +32,8 @@ versions have been resolved.
You may need to add a `[patch]` into your Cargo.toml to ensure a specific version of
android-activity is used across all of your application's crates.
"#);
"#
);

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

pub use activity_impl::input;
pub mod input;

mod config;
pub use config::ConfigurationRef;
Expand Down Expand Up @@ -249,14 +251,62 @@ impl AndroidApp {
self.inner.read().unwrap().asset_manager()
}

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

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

/// Explicitly request that the current input method's soft input area be
/// shown to the user, if needed.
///
/// Call this if the user interacts with your view in such a way that they
/// have expressed they would like to start performing input into it.
pub fn show_soft_input(&self, show_implicit: bool) {
self.inner.read().unwrap().show_soft_input(show_implicit);
}

/// Request to hide the soft input window from the context of the window
/// that is currently accepting input.
///
/// This should be called as a result of the user doing some action that
/// fairly explicitly requests to have the input window hidden.
pub fn hide_soft_input(&self, hide_implicit_only: bool) {
self.inner
.read()
.unwrap()
.hide_soft_input(hide_implicit_only);
}

/// Fetch the current input text state, as updated by any active IME.
pub fn text_input_state(&self) -> input::TextInputState {
self.inner.read().unwrap().text_input_state()
}

/// Forward the given input text `state` to any active IME.
pub fn set_text_input_state(&self, state: input::TextInputState) {
self.inner.read().unwrap().set_text_input_state(state);
}

/// Query and process all out-standing input event
///
/// Applications are generally either expected to call this in-sync with their rendering or
/// in response to a [`MainEvent::InputAvailable`] event being delivered. _Note though that your
/// application is will only be delivered a single [`MainEvent::InputAvailable`] event between calls
/// to this API._
///
/// To reduce overhead, by default only [`input::Axis::X`] and [`input::Axis::Y`] are enabled
/// and other axis should be enabled explicitly via [`Self::enable_motion_axis`].
pub fn input_events<'b, F>(&self, callback: F)
where
F: FnMut(&input::InputEvent),
Expand Down
Loading

0 comments on commit 5a91699

Please sign in to comment.