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 an `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 a8637e3f25
commit 4827e0b17b
5 changed files with 73 additions and 9 deletions
+5
View File
@@ -6,6 +6,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Added
- input: TextInputAction enum representing action button types on soft keyboards.
- input: InputEvent::TextAction event for handling action button presses from soft keyboards.
### Changed
- input: Replaced custom types with their `ndk` crate equivalent.
> [!NOTE]
@@ -29,6 +29,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
@@ -23,7 +23,7 @@ use ndk::configuration::Configuration;
use ndk::native_window::NativeWindow;
use crate::error::InternalResult;
use crate::input::{device_key_character_map, Axis, KeyCharacterMap};
use crate::input::{device_key_character_map, Axis, KeyCharacterMap, TextInputAction};
use crate::util::{
abort_on_panic, forward_stdio_to_logcat, init_android_main_thread, log_panic,
try_get_path_from_ptr,
@@ -177,9 +177,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
@@ -207,6 +204,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 {
@@ -250,6 +255,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)]
@@ -778,7 +795,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,
}
}
}
@@ -795,7 +813,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<'_> {
@@ -815,8 +834,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();
@@ -828,12 +849,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
@@ -78,6 +78,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>,
@@ -406,4 +406,5 @@ pub enum InputEvent<'a> {
MotionEvent(self::MotionEvent<'a>),
KeyEvent(self::KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
TextAction(crate::input::TextInputAction),
}