Skip to content

Commit

Permalink
Expose TextEvent and input method state
Browse files Browse the repository at this point in the history
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 15, 2022
1 parent 8d30454 commit 96df78d
Show file tree
Hide file tree
Showing 7 changed files with 189 additions and 2 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
128 changes: 127 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, Wind
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 @@ -346,6 +347,122 @@ impl AndroidAppInner {
}
}

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).compose_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,
},
compose_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.compose_region.start {
Some(off) => off as i32,
None => -1,
},
end: match state.compose_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 @@ -408,6 +525,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 compose_region: TextSpan,
}

pub use crate::activity_impl::input::*;
12 changes: 11 additions & 1 deletion android-activity/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,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 @@ -471,6 +471,16 @@ impl AndroidApp {
.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
Expand Down
23 changes: 23 additions & 0 deletions android-activity/src/native_activity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ use ndk::configuration::Configuration;
use ndk::input_queue::InputQueue;
use ndk::native_window::NativeWindow;

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

mod ffi;
Expand All @@ -39,6 +40,7 @@ pub mod input {
pub enum InputEvent {
MotionEvent(self::MotionEvent),
KeyEvent(self::KeyEvent),
TextEvent(crate::input::TextInputState),
}
}

Expand Down Expand Up @@ -405,6 +407,26 @@ impl AndroidAppInner {
}
}

// TODO: move into a trait
pub fn text_input_state(&self) -> TextInputState {
TextInputState {
text: String::new(),
selection: TextSpan {
start: None,
end: None,
},
compose_region: TextSpan {
start: None,
end: None,
},
}
}

// TODO: move into a trait
pub fn set_text_input_state(&self, _state: TextInputState) {
// NOP: Unsupported
}

pub fn enable_motion_axis(&self, _axis: input::Axis) {
// NOP - The InputQueue API doesn't let us optimize which axis values are read
}
Expand Down Expand Up @@ -449,6 +471,7 @@ impl AndroidAppInner {
let ndk_event = match event {
input::InputEvent::MotionEvent(e) => ndk::event::InputEvent::MotionEvent(e),
input::InputEvent::KeyEvent(e) => ndk::event::InputEvent::KeyEvent(e),
_ => unreachable!(),
};

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

0 comments on commit 96df78d

Please sign in to comment.