Add support for InputEvent::TextAction events

This exposes IME actions via an InputEvent::TextAction event so that
it's possible to recognise when text entry via an input method is
finished.

This adds a `TextInputAction` enum to represent the action key on a soft
keyboard, such as "Done".

For example, this makes it possible to emit Ime::Commit events in Winit.
This commit is contained in:
Robert Bragg
2026-01-07 12:05:52 +00:00
parent fe2c50ccc6
commit 7e8990fd92
5 changed files with 70 additions and 9 deletions
+2
View File
@@ -8,6 +8,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- input: TextInputAction enum representing action button types on soft keyboards.
- input: InputEvent::TextAction event for handling action button presses from soft keyboards.
- The `ndk` and `ndk-sys` crates are now re-exported under `android_activity::ndk` and `android_activity::ndk_sys` ([#194](https://github.com/rust-mobile/android-activity/pull/194))
### Changed
@@ -27,6 +27,7 @@ pub enum InputEvent<'a> {
MotionEvent(MotionEvent<'a>),
KeyEvent(KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
TextAction(crate::input::TextInputAction),
}
/// A motion event.
+39 -9
View File
@@ -21,7 +21,7 @@ use ndk::configuration::Configuration;
use ndk::native_window::NativeWindow;
use crate::error::InternalResult;
use crate::input::{Axis, KeyCharacterMap, KeyCharacterMapBinding};
use crate::input::{Axis, KeyCharacterMap, KeyCharacterMapBinding, TextInputAction};
use crate::jni_utils::{self, CloneJavaVM};
use crate::util::{abort_on_panic, forward_stdio_to_logcat, log_panic, try_get_path_from_ptr};
use crate::{
@@ -174,9 +174,6 @@ impl NativeAppGlue {
};
let out_ptr = &mut out_state as *mut TextInputState;
let app_ptr = self.as_ptr();
(*app_ptr).textInputState = 0;
// NEON WARNING:
//
// It's not clearly documented but the GameActivity API over the
@@ -204,6 +201,14 @@ impl NativeAppGlue {
}
}
pub fn take_text_input_state(&self) -> TextInputState {
unsafe {
let app_ptr = self.as_ptr();
(*app_ptr).textInputState = 0;
}
self.text_input_state()
}
// TODO: move into a trait
pub fn set_text_input_state(&self, state: TextInputState) {
unsafe {
@@ -247,6 +252,18 @@ impl NativeAppGlue {
ffi::GameActivity_setTextInputState(activity, &ffi_state as *const _);
}
}
pub fn take_pending_editor_action(&self) -> Option<i32> {
unsafe {
let app_ptr = self.as_ptr();
if (*app_ptr).pendingEditorAction {
(*app_ptr).pendingEditorAction = false;
Some((*app_ptr).editorAction)
} else {
None
}
}
}
}
#[derive(Debug)]
@@ -804,7 +821,8 @@ impl<'a> From<Arc<InputReceiver>> for InputIteratorInner<'a> {
_receiver: receiver,
buffered,
native_app,
text_event_checked: false,
ime_text_input_state_checked: false,
ime_editor_action_checked: false,
}
}
}
@@ -821,7 +839,8 @@ pub(crate) struct InputIteratorInner<'a> {
buffered: Option<BufferedEvents<'a>>,
native_app: NativeAppGlue,
text_event_checked: bool,
ime_text_input_state_checked: bool,
ime_editor_action_checked: bool,
}
impl InputIteratorInner<'_> {
@@ -841,8 +860,10 @@ impl InputIteratorInner<'_> {
self.buffered = None;
}
if !self.text_event_checked {
self.text_event_checked = true;
// We make sure any input state changes are sent before we check
// for editor actions, so actions will apply to the latest state.
if !self.ime_text_input_state_checked {
self.ime_text_input_state_checked = true;
unsafe {
let app_ptr = self.native_app.as_ptr();
@@ -854,12 +875,21 @@ impl InputIteratorInner<'_> {
// the compiler isn't reordering code so this gets flagged
// before the java main thread really updates the state.
if (*app_ptr).textInputState != 0 {
let state = self.native_app.text_input_state(); // Will clear .textInputState
let state = self.native_app.take_text_input_state(); // Will clear .textInputState
let _ = callback(&InputEvent::TextEvent(state));
return true;
}
}
}
if !self.ime_editor_action_checked {
self.ime_editor_action_checked = true;
if let Some(action) = self.native_app.take_pending_editor_action() {
let _ = callback(&InputEvent::TextAction(TextInputAction::from(action)));
return true;
}
}
false
}
}
+27
View File
@@ -907,6 +907,33 @@ pub struct TextInputState {
pub compose_region: Option<TextSpan>,
}
// Represents the action button on a soft keyboard.
#[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive, num_enum::IntoPrimitive)]
#[non_exhaustive]
#[repr(i32)]
pub enum TextInputAction {
/// Let receiver decide what logical action to perform
Unspecified = 0,
/// No action - receiver could instead interpret as an "enter" key that inserts a newline character
None = 1,
/// Navigate to the input location (such as a URL)
Go = 2,
/// Search based on the input text
Search = 3,
/// Send the input to the target
Send = 4,
/// Move to the next input field
Next = 5,
/// Indicate that input is done
Done = 6,
/// Move to the previous input field
Previous = 7,
#[doc(hidden)]
#[num_enum(catch_all)]
__Unknown(i32),
}
/// An exclusive, lending iterator for input events
pub struct InputIterator<'a> {
pub(crate) inner: crate::activity_impl::InputIteratorInner<'a>,
@@ -434,4 +434,5 @@ pub enum InputEvent<'a> {
MotionEvent(self::MotionEvent<'a>),
KeyEvent(self::KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
TextAction(crate::input::TextInputAction),
}