/* 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 .
*/
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 = 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>,
input_connection: RwLock >,
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"\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 {}