|
|
@@ -16,17 +16,24 @@
|
|
|
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
*/
|
|
|
|
|
|
-use async_channel::Sender as AsyncSender;
|
|
|
-use miniquad::native::android::{ndk_sys, ndk_utils::*};
|
|
|
-use parking_lot::Mutex as SyncMutex;
|
|
|
-use std::ffi::CString;
|
|
|
+use miniquad::native::android::{self, ndk_sys, ndk_utils::*};
|
|
|
+use parking_lot::{Mutex as SyncMutex, RwLock};
|
|
|
+use std::{
|
|
|
+ ffi::CString,
|
|
|
+ sync::{Arc, OnceLock},
|
|
|
+};
|
|
|
|
|
|
-use super::AndroidTextInputState;
|
|
|
+use super::{AndroidTextInputState, SharedStatePtr};
|
|
|
|
|
|
-const DEFAULT_MAX_STRING_SIZE: usize = 1 << 16;
|
|
|
+macro_rules! w { ($($arg:tt)*) => { warn!(target: "android::textinput::gametextinput", $($arg)*); } }
|
|
|
|
|
|
pub const SPAN_UNDEFINED: i32 = -1;
|
|
|
|
|
|
+/// Global GameTextInput instance for JNI bridge
|
|
|
+///
|
|
|
+/// Single global instance since only ONE editor is active at a time.
|
|
|
+pub static GAME_TEXT_INPUT: OnceLock<GameTextInput> = OnceLock::new();
|
|
|
+
|
|
|
struct StateClassInfo {
|
|
|
text: ndk_sys::jfieldID,
|
|
|
selection_start: ndk_sys::jfieldID,
|
|
|
@@ -36,19 +43,21 @@ struct StateClassInfo {
|
|
|
}
|
|
|
|
|
|
pub struct GameTextInput {
|
|
|
- env: *mut ndk_sys::JNIEnv,
|
|
|
- state: SyncMutex<AndroidTextInputState>,
|
|
|
- input_connection: Option<ndk_sys::jobject>,
|
|
|
+ state: SyncMutex<Option<SharedStatePtr>>,
|
|
|
+ input_connection: RwLock<Option<ndk_sys::jobject>>,
|
|
|
input_connection_class: ndk_sys::jclass,
|
|
|
+ state_class: ndk_sys::jclass,
|
|
|
set_soft_keyboard_active_method: ndk_sys::jmethodID,
|
|
|
restart_input_method: ndk_sys::jmethodID,
|
|
|
+ state_constructor: ndk_sys::jmethodID,
|
|
|
state_class_info: StateClassInfo,
|
|
|
- pub event_sender: Option<AsyncSender<AndroidTextInputState>>
|
|
|
}
|
|
|
|
|
|
impl GameTextInput {
|
|
|
- pub fn new(env: *mut ndk_sys::JNIEnv, max_string_size: u32) -> Self {
|
|
|
+ pub fn new() -> Self {
|
|
|
unsafe {
|
|
|
+ let env = android::attach_jni_env();
|
|
|
+
|
|
|
let find_class = (**env).FindClass.unwrap();
|
|
|
|
|
|
let state_class_name = b"textinput/State\0";
|
|
|
@@ -86,41 +95,49 @@ impl GameTextInput {
|
|
|
restart_input_sig.as_ptr() as _,
|
|
|
);
|
|
|
|
|
|
- let state_java_class = new_global_ref!(env, state_java_class);
|
|
|
+ let state_class = new_global_ref!(env, state_java_class) as ndk_sys::jclass;
|
|
|
|
|
|
let get_field_id = (**env).GetFieldID.unwrap();
|
|
|
|
|
|
let text_field = get_field_id(
|
|
|
env,
|
|
|
- state_java_class,
|
|
|
+ state_class,
|
|
|
b"text\0".as_ptr() as _,
|
|
|
b"Ljava/lang/String;\0".as_ptr() as _,
|
|
|
);
|
|
|
let selection_start_field = get_field_id(
|
|
|
env,
|
|
|
- state_java_class,
|
|
|
+ state_class,
|
|
|
b"selectionStart\0".as_ptr() as _,
|
|
|
b"I\0".as_ptr() as _,
|
|
|
);
|
|
|
let selection_end_field = get_field_id(
|
|
|
env,
|
|
|
- state_java_class,
|
|
|
+ state_class,
|
|
|
b"selectionEnd\0".as_ptr() as _,
|
|
|
b"I\0".as_ptr() as _,
|
|
|
);
|
|
|
let composing_region_start_field = get_field_id(
|
|
|
env,
|
|
|
- state_java_class,
|
|
|
+ state_class,
|
|
|
b"composingRegionStart\0".as_ptr() as _,
|
|
|
b"I\0".as_ptr() as _,
|
|
|
);
|
|
|
let composing_region_end_field = get_field_id(
|
|
|
env,
|
|
|
- state_java_class,
|
|
|
+ state_class,
|
|
|
b"composingRegionEnd\0".as_ptr() as _,
|
|
|
b"I\0".as_ptr() as _,
|
|
|
);
|
|
|
|
|
|
+ let constructor_sig = b"(Ljava/lang/String;IIII)V\0";
|
|
|
+ let state_constructor = get_method_id(
|
|
|
+ env,
|
|
|
+ state_class,
|
|
|
+ b"<init>\0".as_ptr() as _,
|
|
|
+ constructor_sig.as_ptr() as _,
|
|
|
+ );
|
|
|
+
|
|
|
let state_class_info = StateClassInfo {
|
|
|
text: text_field,
|
|
|
selection_start: selection_start_field,
|
|
|
@@ -130,124 +147,139 @@ impl GameTextInput {
|
|
|
};
|
|
|
|
|
|
Self {
|
|
|
- env,
|
|
|
- state: SyncMutex::new(AndroidTextInputState::new()),
|
|
|
- input_connection: None,
|
|
|
+ state: SyncMutex::new(None),
|
|
|
+ input_connection: RwLock::new(None),
|
|
|
input_connection_class,
|
|
|
+ state_class,
|
|
|
set_soft_keyboard_active_method,
|
|
|
restart_input_method,
|
|
|
+ state_constructor,
|
|
|
state_class_info,
|
|
|
- event_sender: None,
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- pub fn set_state(&mut self, state: &AndroidTextInputState) {
|
|
|
- if let Some(input_connection) = self.input_connection {
|
|
|
+ pub fn focus(&self, state: SharedStatePtr) {
|
|
|
+ // Replace old focus
|
|
|
+ let mut active_focus = self.state.lock();
|
|
|
+ if let Some(old_focus) = &mut *active_focus {
|
|
|
+ // Mark old focused state as no longer active
|
|
|
+ old_focus.lock().is_active = false;
|
|
|
+ }
|
|
|
+ *active_focus = Some(state.clone());
|
|
|
+ drop(active_focus);
|
|
|
+
|
|
|
+ let mut new_focus = state.lock();
|
|
|
+ // Mark new state as active
|
|
|
+ new_focus.is_active = true;
|
|
|
+ // Push changes to the Java side
|
|
|
+ self.push_update(&new_focus.state);
|
|
|
+ }
|
|
|
+
|
|
|
+ pub fn push_update(&self, state: &AndroidTextInputState) {
|
|
|
+ let Some(input_connection) = *self.input_connection.read() else {
|
|
|
+ w!("push_update() - no input_connection set");
|
|
|
+ return
|
|
|
+ };
|
|
|
+ if let Some(input_connection) = *self.input_connection.read() {
|
|
|
unsafe {
|
|
|
+ let env = android::attach_jni_env();
|
|
|
let jstate = self.state_to_java(state);
|
|
|
call_void_method!(
|
|
|
- self.env,
|
|
|
+ env,
|
|
|
input_connection,
|
|
|
"setState",
|
|
|
"(Ltextinput/State;)V",
|
|
|
jstate
|
|
|
);
|
|
|
- let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
|
|
|
- delete_local_ref(self.env, jstate);
|
|
|
+
|
|
|
+ let delete_local_ref = (**env).DeleteLocalRef.unwrap();
|
|
|
+ delete_local_ref(env, jstate);
|
|
|
}
|
|
|
}
|
|
|
- *self.state.lock() = state.clone();
|
|
|
- }
|
|
|
-
|
|
|
- fn set_state_inner(&mut self, state: AndroidTextInputState) {
|
|
|
- *self.state.lock() = state;
|
|
|
}
|
|
|
|
|
|
- pub fn get_state(&self) -> AndroidTextInputState {
|
|
|
- self.state.lock().clone()
|
|
|
- }
|
|
|
-
|
|
|
- pub fn set_input_connection(&mut self, input_connection: ndk_sys::jobject) {
|
|
|
+ pub fn set_input_connection(&self, input_connection: ndk_sys::jobject) {
|
|
|
unsafe {
|
|
|
- if let Some(old_ref) = self.input_connection {
|
|
|
- let delete_global_ref = (**self.env).DeleteGlobalRef.unwrap();
|
|
|
- delete_global_ref(self.env, old_ref);
|
|
|
+ let env = android::attach_jni_env();
|
|
|
+ let mut ic = self.input_connection.write();
|
|
|
+ if let Some(old_ref) = *ic {
|
|
|
+ let delete_global_ref = (**env).DeleteGlobalRef.unwrap();
|
|
|
+ delete_global_ref(env, old_ref);
|
|
|
}
|
|
|
- self.input_connection = Some(new_global_ref!(self.env, input_connection));
|
|
|
+ *ic = Some(new_global_ref!(env, input_connection));
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- pub fn process_event(&mut self, event_state: ndk_sys::jobject) {
|
|
|
+ pub fn process_event(&self, event_state: ndk_sys::jobject) {
|
|
|
let state = self.state_from_java(event_state);
|
|
|
- if let Some(sender) = &self.event_sender {
|
|
|
- let _ = sender.try_send(state.clone());
|
|
|
- }
|
|
|
- self.set_state_inner(state);
|
|
|
+
|
|
|
+ let Some(shared) = &*self.state.lock() else {
|
|
|
+ w!("process_event() - no shared state set");
|
|
|
+ return
|
|
|
+ };
|
|
|
+
|
|
|
+ let mut inner = shared.lock();
|
|
|
+ inner.state = state.clone();
|
|
|
+ let _ = inner.sender.try_send(state);
|
|
|
}
|
|
|
|
|
|
pub fn show_ime(&self, flags: u32) {
|
|
|
- if let Some(input_connection) = self.input_connection {
|
|
|
- unsafe {
|
|
|
- let call_void_method = (**self.env).CallVoidMethod.unwrap();
|
|
|
- call_void_method(
|
|
|
- self.env,
|
|
|
- input_connection,
|
|
|
- self.set_soft_keyboard_active_method,
|
|
|
- 1, // active: true
|
|
|
- flags as ndk_sys::jint,
|
|
|
- );
|
|
|
- }
|
|
|
+ let Some(input_connection) = *self.input_connection.read() else {
|
|
|
+ w!("show_ime() - no input_connection set");
|
|
|
+ return
|
|
|
+ };
|
|
|
+ unsafe {
|
|
|
+ let env = android::attach_jni_env();
|
|
|
+ let call_void_method = (**env).CallVoidMethod.unwrap();
|
|
|
+ call_void_method(
|
|
|
+ env,
|
|
|
+ input_connection,
|
|
|
+ self.set_soft_keyboard_active_method,
|
|
|
+ 1, // active: true
|
|
|
+ flags as ndk_sys::jint,
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
pub fn hide_ime(&self, flags: u32) {
|
|
|
- if let Some(input_connection) = self.input_connection {
|
|
|
- unsafe {
|
|
|
- let call_void_method = (**self.env).CallVoidMethod.unwrap();
|
|
|
- call_void_method(
|
|
|
- self.env,
|
|
|
- input_connection,
|
|
|
- self.set_soft_keyboard_active_method,
|
|
|
- 0, // active: false
|
|
|
- flags as ndk_sys::jint,
|
|
|
- );
|
|
|
- }
|
|
|
+ let Some(input_connection) = *self.input_connection.read() else {
|
|
|
+ w!("hide_ime() - no input_connection set");
|
|
|
+ return
|
|
|
+ };
|
|
|
+ unsafe {
|
|
|
+ let env = android::attach_jni_env();
|
|
|
+ let call_void_method = (**env).CallVoidMethod.unwrap();
|
|
|
+ call_void_method(
|
|
|
+ env,
|
|
|
+ input_connection,
|
|
|
+ self.set_soft_keyboard_active_method,
|
|
|
+ 0, // active: false
|
|
|
+ flags as ndk_sys::jint,
|
|
|
+ );
|
|
|
}
|
|
|
}
|
|
|
|
|
|
pub fn restart_input(&self) {
|
|
|
- if let Some(input_connection) = self.input_connection {
|
|
|
- unsafe {
|
|
|
- let call_void_method = (**self.env).CallVoidMethod.unwrap();
|
|
|
- call_void_method(self.env, input_connection, self.restart_input_method);
|
|
|
- }
|
|
|
+ let Some(input_connection) = *self.input_connection.read() else {
|
|
|
+ w!("restart_input() - no input_connection set");
|
|
|
+ return
|
|
|
+ };
|
|
|
+ unsafe {
|
|
|
+ let env = android::attach_jni_env();
|
|
|
+ let call_void_method = (**env).CallVoidMethod.unwrap();
|
|
|
+ call_void_method(env, input_connection, self.restart_input_method);
|
|
|
}
|
|
|
}
|
|
|
|
|
|
fn state_to_java(&self, state: &AndroidTextInputState) -> ndk_sys::jobject {
|
|
|
unsafe {
|
|
|
- let new_string_utf = (**self.env).NewStringUTF.unwrap();
|
|
|
- let text_str = CString::new(state.text.as_str()).unwrap_or_else(|_| {
|
|
|
- tracing::error!("Failed to convert text to CString");
|
|
|
- CString::new("").unwrap()
|
|
|
- });
|
|
|
- let jtext = new_string_utf(self.env, text_str.as_ptr());
|
|
|
-
|
|
|
- let new_object = (**self.env).NewObject.unwrap();
|
|
|
- let get_method_id = (**self.env).GetMethodID.unwrap();
|
|
|
- let find_class = (**self.env).FindClass.unwrap();
|
|
|
+ let env = android::attach_jni_env();
|
|
|
+ let new_string_utf = (**env).NewStringUTF.unwrap();
|
|
|
+ let text_str = CString::new(state.text.as_str()).unwrap();
|
|
|
+ let jtext = new_string_utf(env, text_str.as_ptr());
|
|
|
|
|
|
- let state_class_name = b"textinput/State\0";
|
|
|
- let state_java_class = find_class(self.env, state_class_name.as_ptr() as _);
|
|
|
-
|
|
|
- let constructor_sig = b"(Ljava/lang/String;IIII)V\0";
|
|
|
- let constructor = get_method_id(
|
|
|
- self.env,
|
|
|
- state_java_class,
|
|
|
- b"<init>\0".as_ptr() as _,
|
|
|
- constructor_sig.as_ptr() as _,
|
|
|
- );
|
|
|
+ let new_object = (**env).NewObject.unwrap();
|
|
|
|
|
|
let (compose_start, compose_end) = match state.compose {
|
|
|
Some((start, end)) => (start as i32, end as i32),
|
|
|
@@ -255,9 +287,9 @@ impl GameTextInput {
|
|
|
};
|
|
|
|
|
|
let jobj = new_object(
|
|
|
- self.env,
|
|
|
- state_java_class,
|
|
|
- constructor,
|
|
|
+ env,
|
|
|
+ self.state_class,
|
|
|
+ self.state_constructor,
|
|
|
jtext,
|
|
|
state.select.0 as i32,
|
|
|
state.select.1 as i32,
|
|
|
@@ -265,33 +297,32 @@ impl GameTextInput {
|
|
|
compose_end,
|
|
|
);
|
|
|
|
|
|
- let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
|
|
|
- delete_local_ref(self.env, jtext);
|
|
|
- delete_local_ref(self.env, state_java_class);
|
|
|
+ let delete_local_ref = (**env).DeleteLocalRef.unwrap();
|
|
|
+ delete_local_ref(env, jtext);
|
|
|
jobj
|
|
|
}
|
|
|
}
|
|
|
|
|
|
fn state_from_java(&self, event_state: ndk_sys::jobject) -> AndroidTextInputState {
|
|
|
unsafe {
|
|
|
- let get_object_field = (**self.env).GetObjectField.unwrap();
|
|
|
- let jtext = get_object_field(self.env, event_state, self.state_class_info.text)
|
|
|
- as ndk_sys::jstring;
|
|
|
+ let env = android::attach_jni_env();
|
|
|
+ let get_object_field = (**env).GetObjectField.unwrap();
|
|
|
+ let jtext =
|
|
|
+ get_object_field(env, event_state, self.state_class_info.text) as ndk_sys::jstring;
|
|
|
|
|
|
- let text = get_utf_str!(self.env, jtext);
|
|
|
+ let text = get_utf_str!(env, jtext);
|
|
|
|
|
|
- let get_int_field = (**self.env).GetIntField.unwrap();
|
|
|
+ let get_int_field = (**env).GetIntField.unwrap();
|
|
|
let select_start =
|
|
|
- get_int_field(self.env, event_state, self.state_class_info.selection_start);
|
|
|
- let select_end =
|
|
|
- get_int_field(self.env, event_state, self.state_class_info.selection_end);
|
|
|
+ get_int_field(env, event_state, self.state_class_info.selection_start);
|
|
|
+ let select_end = get_int_field(env, event_state, self.state_class_info.selection_end);
|
|
|
let compose_start =
|
|
|
- get_int_field(self.env, event_state, self.state_class_info.composing_region_start);
|
|
|
+ get_int_field(env, event_state, self.state_class_info.composing_region_start);
|
|
|
let compose_end =
|
|
|
- get_int_field(self.env, event_state, self.state_class_info.composing_region_end);
|
|
|
+ get_int_field(env, event_state, self.state_class_info.composing_region_end);
|
|
|
|
|
|
- let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
|
|
|
- delete_local_ref(self.env, jtext);
|
|
|
+ let delete_local_ref = (**env).DeleteLocalRef.unwrap();
|
|
|
+ delete_local_ref(env, jtext);
|
|
|
|
|
|
let compose = if compose_start >= 0 {
|
|
|
Some((compose_start as usize, compose_end as usize))
|
|
|
@@ -312,12 +343,16 @@ impl GameTextInput {
|
|
|
impl Drop for GameTextInput {
|
|
|
fn drop(&mut self) {
|
|
|
unsafe {
|
|
|
- let delete_global_ref = (**self.env).DeleteGlobalRef.unwrap();
|
|
|
+ let env = android::attach_jni_env();
|
|
|
+ let delete_global_ref = (**env).DeleteGlobalRef.unwrap();
|
|
|
if self.input_connection_class != std::ptr::null_mut() {
|
|
|
- delete_global_ref(self.env, self.input_connection_class);
|
|
|
+ delete_global_ref(env, self.input_connection_class);
|
|
|
+ }
|
|
|
+ if self.state_class != std::ptr::null_mut() {
|
|
|
+ delete_global_ref(env, self.state_class);
|
|
|
}
|
|
|
- if let Some(input_connection) = self.input_connection {
|
|
|
- delete_global_ref(self.env, input_connection);
|
|
|
+ if let Some(input_connection) = *self.input_connection.read() {
|
|
|
+ delete_global_ref(env, input_connection);
|
|
|
}
|
|
|
}
|
|
|
}
|