From 729db3a7e7c6d36c9c5d3707c27d57bdddd5825a Mon Sep 17 00:00:00 2001 From: Robert Bragg Date: Wed, 7 Jan 2026 12:05:52 +0000 Subject: [PATCH] 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. --- android-activity/CHANGELOG.md | 2 + android-activity/src/game_activity/input.rs | 1 + android-activity/src/game_activity/mod.rs | 48 +++++++++++++++---- android-activity/src/input.rs | 27 +++++++++++ android-activity/src/native_activity/input.rs | 1 + 5 files changed, 70 insertions(+), 9 deletions(-) diff --git a/android-activity/CHANGELOG.md b/android-activity/CHANGELOG.md index e0c31706..3da59ff3 100644 --- a/android-activity/CHANGELOG.md +++ b/android-activity/CHANGELOG.md @@ -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 diff --git a/android-activity/src/game_activity/input.rs b/android-activity/src/game_activity/input.rs index 66b94fe9..167b782c 100644 --- a/android-activity/src/game_activity/input.rs +++ b/android-activity/src/game_activity/input.rs @@ -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. diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs index 9c84b6f6..11382647 100644 --- a/android-activity/src/game_activity/mod.rs +++ b/android-activity/src/game_activity/mod.rs @@ -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 { + 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> 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>, 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 } } diff --git a/android-activity/src/input.rs b/android-activity/src/input.rs index 96c7d68b..aae6292c 100644 --- a/android-activity/src/input.rs +++ b/android-activity/src/input.rs @@ -907,6 +907,33 @@ pub struct TextInputState { pub compose_region: Option, } +// 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>, diff --git a/android-activity/src/native_activity/input.rs b/android-activity/src/native_activity/input.rs index 96187ecd..00e088b5 100644 --- a/android-activity/src/native_activity/input.rs +++ b/android-activity/src/native_activity/input.rs @@ -434,4 +434,5 @@ pub enum InputEvent<'a> { MotionEvent(self::MotionEvent<'a>), KeyEvent(self::KeyEvent<'a>), TextEvent(crate::input::TextInputState), + TextAction(crate::input::TextInputAction), }