| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374 |
- /* This file is part of DarkFi (https://dark.fi)
- *
- * Copyright (C) 2020-2026 Dyne.org foundation
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License as
- * published by the Free Software Foundation, either version 3 of the
- * License, or (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License
- * along with this program. If not, see <https://www.gnu.org/licenses/>.
- */
- 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::{super::util::get_jni_env, AndroidTextInputState, SharedStatePtr};
- macro_rules! t { ($($arg:tt)*) => { trace!(target: "android::textinput::gametextinput", $($arg)*); } }
- 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,
- selection_end: ndk_sys::jfieldID,
- composing_region_start: ndk_sys::jfieldID,
- composing_region_end: ndk_sys::jfieldID,
- }
- pub struct GameTextInput {
- 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,
- }
- impl GameTextInput {
- pub fn new() -> Self {
- unsafe {
- let env = get_jni_env();
- let find_class = (**env).FindClass.unwrap();
- let state_class_name = b"textinput/State\0";
- let input_connection_class_name = b"textinput/InputConnection\0";
- let state_java_class = find_class(env, state_class_name.as_ptr() as _);
- let input_connection_class = find_class(env, input_connection_class_name.as_ptr() as _);
- let input_connection_class =
- new_global_ref!(env, input_connection_class) as ndk_sys::jclass;
- let get_method_id = (**env).GetMethodID.unwrap();
- let set_state_sig = b"(Ltextinput/State;)V\0";
- let _input_connection_set_state_method = get_method_id(
- env,
- input_connection_class,
- b"setState\0".as_ptr() as _,
- set_state_sig.as_ptr() as _,
- );
- let set_soft_keyboard_active_sig = b"(ZI)V\0";
- let set_soft_keyboard_active_method = get_method_id(
- env,
- input_connection_class,
- b"setSoftKeyboardActive\0".as_ptr() as _,
- set_soft_keyboard_active_sig.as_ptr() as _,
- );
- let restart_input_sig = b"()V\0";
- let restart_input_method = get_method_id(
- env,
- input_connection_class,
- b"restartInput\0".as_ptr() as _,
- restart_input_sig.as_ptr() as _,
- );
- 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_class,
- b"text\0".as_ptr() as _,
- b"Ljava/lang/String;\0".as_ptr() as _,
- );
- let selection_start_field = get_field_id(
- env,
- state_class,
- b"selectionStart\0".as_ptr() as _,
- b"I\0".as_ptr() as _,
- );
- let selection_end_field = get_field_id(
- env,
- state_class,
- b"selectionEnd\0".as_ptr() as _,
- b"I\0".as_ptr() as _,
- );
- let composing_region_start_field = get_field_id(
- env,
- state_class,
- b"composingRegionStart\0".as_ptr() as _,
- b"I\0".as_ptr() as _,
- );
- let composing_region_end_field = get_field_id(
- env,
- 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,
- selection_end: selection_end_field,
- composing_region_start: composing_region_start_field,
- composing_region_end: composing_region_end_field,
- };
- Self {
- 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,
- }
- }
- }
- 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;
- let new_state = new_focus.state.clone();
- drop(new_focus);
- // Push changes to the Java side
- self.push_update(&new_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
- };
- unsafe {
- let env = get_jni_env();
- let jstate = self.state_to_java(state);
- call_void_method!(env, input_connection, "setState", "(Ltextinput/State;)V", jstate);
- let delete_local_ref = (**env).DeleteLocalRef.unwrap();
- delete_local_ref(env, jstate);
- }
- }
- pub fn set_select(&self, start: i32, end: i32) -> Result<(), ()> {
- let Some(input_connection) = *self.input_connection.read() else {
- w!("push_update() - no input_connection set");
- return Err(())
- };
- let is_success = unsafe {
- let env = get_jni_env();
- call_bool_method!(env, input_connection, "setSelection", "(II)Z", start, end)
- };
- if is_success == 0u8 {
- return Err(())
- }
- Ok(())
- }
- pub fn set_input_connection(&self, input_connection: ndk_sys::jobject) {
- unsafe {
- let env = get_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);
- }
- *ic = Some(new_global_ref!(env, input_connection));
- }
- }
- pub fn process_event(&self, event_state: ndk_sys::jobject) {
- let state = self.state_from_java(event_state);
- t!("IME event: {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) {
- let Some(input_connection) = *self.input_connection.read() else {
- w!("show_ime() - no input_connection set");
- return
- };
- unsafe {
- let env = get_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) {
- let Some(input_connection) = *self.input_connection.read() else {
- w!("hide_ime() - no input_connection set");
- return
- };
- unsafe {
- let env = get_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) {
- let Some(input_connection) = *self.input_connection.read() else {
- w!("restart_input() - no input_connection set");
- return
- };
- unsafe {
- let env = get_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 env = get_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 new_object = (**env).NewObject.unwrap();
- let (compose_start, compose_end) = match state.compose {
- Some((start, end)) => (start as i32, end as i32),
- None => (SPAN_UNDEFINED, SPAN_UNDEFINED),
- };
- let jobj = new_object(
- env,
- self.state_class,
- self.state_constructor,
- jtext,
- state.select.0 as i32,
- state.select.1 as i32,
- compose_start,
- compose_end,
- );
- 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 env = get_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!(env, jtext);
- let get_int_field = (**env).GetIntField.unwrap();
- let select_start =
- 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(env, event_state, self.state_class_info.composing_region_start);
- let compose_end =
- get_int_field(env, event_state, self.state_class_info.composing_region_end);
- 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))
- } else {
- assert!(compose_end < 0);
- None
- };
- AndroidTextInputState {
- text,
- select: (select_start as usize, select_end as usize),
- compose,
- }
- }
- }
- }
- impl Drop for GameTextInput {
- fn drop(&mut self) {
- unsafe {
- let env = get_jni_env();
- let delete_global_ref = (**env).DeleteGlobalRef.unwrap();
- if self.input_connection_class != std::ptr::null_mut() {
- 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.read() {
- delete_global_ref(env, input_connection);
- }
- }
- }
- }
- unsafe impl Send for GameTextInput {}
- unsafe impl Sync for GameTextInput {}
|