diff --git a/android-activity/CHANGELOG.md b/android-activity/CHANGELOG.md index be0c1ac..f6d2026 100644 --- a/android-activity/CHANGELOG.md +++ b/android-activity/CHANGELOG.md @@ -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] diff --git a/android-activity/src/game_activity/input.rs b/android-activity/src/game_activity/input.rs index f8fac2b..54daec4 100644 --- a/android-activity/src/game_activity/input.rs +++ b/android-activity/src/game_activity/input.rs @@ -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. diff --git a/android-activity/src/game_activity/mod.rs b/android-activity/src/game_activity/mod.rs index 129175d..993f161 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)] @@ -782,7 +799,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, } } } @@ -799,7 +817,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<'_> { @@ -819,8 +838,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(); @@ -832,12 +853,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 909f895..8e1101f 100644 --- a/android-activity/src/input.rs +++ b/android-activity/src/input.rs @@ -78,6 +78,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 1559108..056baa1 100644 --- a/android-activity/src/native_activity/input.rs +++ b/android-activity/src/native_activity/input.rs @@ -406,4 +406,5 @@ pub enum InputEvent<'a> { MotionEvent(self::MotionEvent<'a>), KeyEvent(self::KeyEvent<'a>), TextEvent(crate::input::TextInputState), + TextAction(crate::input::TextInputAction), }