Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions android-activity/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
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 @@ -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.
Expand Down
48 changes: 39 additions & 9 deletions android-activity/src/game_activity/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -782,7 +799,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,
}
}
}
Expand All @@ -799,7 +817,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<'_> {
Expand All @@ -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();

Expand All @@ -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
}
}
Expand Down
27 changes: 27 additions & 0 deletions android-activity/src/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down
1 change: 1 addition & 0 deletions android-activity/src/native_activity/input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -406,4 +406,5 @@ pub enum InputEvent<'a> {
MotionEvent(self::MotionEvent<'a>),
KeyEvent(self::KeyEvent<'a>),
TextEvent(crate::input::TextInputState),
TextAction(crate::input::TextInputAction),
}