gametextinput.rs 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use miniquad::native::android::{self, ndk_sys, ndk_utils::*};
  19. use parking_lot::{Mutex as SyncMutex, RwLock};
  20. use std::{
  21. ffi::CString,
  22. sync::{Arc, OnceLock},
  23. };
  24. use super::{super::util::get_jni_env, AndroidTextInputState, SharedStatePtr};
  25. macro_rules! t { ($($arg:tt)*) => { trace!(target: "android::textinput::gametextinput", $($arg)*); } }
  26. macro_rules! w { ($($arg:tt)*) => { warn!(target: "android::textinput::gametextinput", $($arg)*); } }
  27. pub const SPAN_UNDEFINED: i32 = -1;
  28. /// Global GameTextInput instance for JNI bridge
  29. ///
  30. /// Single global instance since only ONE editor is active at a time.
  31. pub static GAME_TEXT_INPUT: OnceLock<GameTextInput> = OnceLock::new();
  32. struct StateClassInfo {
  33. text: ndk_sys::jfieldID,
  34. selection_start: ndk_sys::jfieldID,
  35. selection_end: ndk_sys::jfieldID,
  36. composing_region_start: ndk_sys::jfieldID,
  37. composing_region_end: ndk_sys::jfieldID,
  38. }
  39. pub struct GameTextInput {
  40. state: SyncMutex<Option<SharedStatePtr>>,
  41. input_connection: RwLock<Option<ndk_sys::jobject>>,
  42. input_connection_class: ndk_sys::jclass,
  43. state_class: ndk_sys::jclass,
  44. set_soft_keyboard_active_method: ndk_sys::jmethodID,
  45. restart_input_method: ndk_sys::jmethodID,
  46. state_constructor: ndk_sys::jmethodID,
  47. state_class_info: StateClassInfo,
  48. }
  49. impl GameTextInput {
  50. pub fn new() -> Self {
  51. unsafe {
  52. let env = get_jni_env();
  53. let find_class = (**env).FindClass.unwrap();
  54. let state_class_name = b"textinput/State\0";
  55. let input_connection_class_name = b"textinput/InputConnection\0";
  56. let state_java_class = find_class(env, state_class_name.as_ptr() as _);
  57. let input_connection_class = find_class(env, input_connection_class_name.as_ptr() as _);
  58. let input_connection_class =
  59. new_global_ref!(env, input_connection_class) as ndk_sys::jclass;
  60. let get_method_id = (**env).GetMethodID.unwrap();
  61. let set_state_sig = b"(Ltextinput/State;)V\0";
  62. let _input_connection_set_state_method = get_method_id(
  63. env,
  64. input_connection_class,
  65. b"setState\0".as_ptr() as _,
  66. set_state_sig.as_ptr() as _,
  67. );
  68. let set_soft_keyboard_active_sig = b"(ZI)V\0";
  69. let set_soft_keyboard_active_method = get_method_id(
  70. env,
  71. input_connection_class,
  72. b"setSoftKeyboardActive\0".as_ptr() as _,
  73. set_soft_keyboard_active_sig.as_ptr() as _,
  74. );
  75. let restart_input_sig = b"()V\0";
  76. let restart_input_method = get_method_id(
  77. env,
  78. input_connection_class,
  79. b"restartInput\0".as_ptr() as _,
  80. restart_input_sig.as_ptr() as _,
  81. );
  82. let state_class = new_global_ref!(env, state_java_class) as ndk_sys::jclass;
  83. let get_field_id = (**env).GetFieldID.unwrap();
  84. let text_field = get_field_id(
  85. env,
  86. state_class,
  87. b"text\0".as_ptr() as _,
  88. b"Ljava/lang/String;\0".as_ptr() as _,
  89. );
  90. let selection_start_field = get_field_id(
  91. env,
  92. state_class,
  93. b"selectionStart\0".as_ptr() as _,
  94. b"I\0".as_ptr() as _,
  95. );
  96. let selection_end_field = get_field_id(
  97. env,
  98. state_class,
  99. b"selectionEnd\0".as_ptr() as _,
  100. b"I\0".as_ptr() as _,
  101. );
  102. let composing_region_start_field = get_field_id(
  103. env,
  104. state_class,
  105. b"composingRegionStart\0".as_ptr() as _,
  106. b"I\0".as_ptr() as _,
  107. );
  108. let composing_region_end_field = get_field_id(
  109. env,
  110. state_class,
  111. b"composingRegionEnd\0".as_ptr() as _,
  112. b"I\0".as_ptr() as _,
  113. );
  114. let constructor_sig = b"(Ljava/lang/String;IIII)V\0";
  115. let state_constructor = get_method_id(
  116. env,
  117. state_class,
  118. b"<init>\0".as_ptr() as _,
  119. constructor_sig.as_ptr() as _,
  120. );
  121. let state_class_info = StateClassInfo {
  122. text: text_field,
  123. selection_start: selection_start_field,
  124. selection_end: selection_end_field,
  125. composing_region_start: composing_region_start_field,
  126. composing_region_end: composing_region_end_field,
  127. };
  128. Self {
  129. state: SyncMutex::new(None),
  130. input_connection: RwLock::new(None),
  131. input_connection_class,
  132. state_class,
  133. set_soft_keyboard_active_method,
  134. restart_input_method,
  135. state_constructor,
  136. state_class_info,
  137. }
  138. }
  139. }
  140. pub fn focus(&self, state: SharedStatePtr) {
  141. // Replace old focus
  142. let mut active_focus = self.state.lock();
  143. if let Some(old_focus) = &mut *active_focus {
  144. // Mark old focused state as no longer active
  145. old_focus.lock().is_active = false;
  146. }
  147. *active_focus = Some(state.clone());
  148. drop(active_focus);
  149. let mut new_focus = state.lock();
  150. // Mark new state as active
  151. new_focus.is_active = true;
  152. let new_state = new_focus.state.clone();
  153. drop(new_focus);
  154. // Push changes to the Java side
  155. self.push_update(&new_state);
  156. }
  157. pub fn push_update(&self, state: &AndroidTextInputState) {
  158. let Some(input_connection) = *self.input_connection.read() else {
  159. w!("push_update() - no input_connection set");
  160. return
  161. };
  162. unsafe {
  163. let env = get_jni_env();
  164. let jstate = self.state_to_java(state);
  165. call_void_method!(env, input_connection, "setState", "(Ltextinput/State;)V", jstate);
  166. let delete_local_ref = (**env).DeleteLocalRef.unwrap();
  167. delete_local_ref(env, jstate);
  168. }
  169. }
  170. pub fn set_select(&self, start: i32, end: i32) -> Result<(), ()> {
  171. let Some(input_connection) = *self.input_connection.read() else {
  172. w!("push_update() - no input_connection set");
  173. return Err(())
  174. };
  175. let is_success = unsafe {
  176. let env = get_jni_env();
  177. call_bool_method!(env, input_connection, "setSelection", "(II)Z", start, end)
  178. };
  179. if is_success == 0u8 {
  180. return Err(())
  181. }
  182. Ok(())
  183. }
  184. pub fn set_input_connection(&self, input_connection: ndk_sys::jobject) {
  185. unsafe {
  186. let env = get_jni_env();
  187. let mut ic = self.input_connection.write();
  188. if let Some(old_ref) = *ic {
  189. let delete_global_ref = (**env).DeleteGlobalRef.unwrap();
  190. delete_global_ref(env, old_ref);
  191. }
  192. *ic = Some(new_global_ref!(env, input_connection));
  193. }
  194. }
  195. pub fn process_event(&self, event_state: ndk_sys::jobject) {
  196. let state = self.state_from_java(event_state);
  197. t!("IME event: {state:?}");
  198. let Some(shared) = &*self.state.lock() else {
  199. w!("process_event() - no shared state set");
  200. return
  201. };
  202. let mut inner = shared.lock();
  203. inner.state = state.clone();
  204. let _ = inner.sender.try_send(state);
  205. }
  206. pub fn show_ime(&self, flags: u32) {
  207. let Some(input_connection) = *self.input_connection.read() else {
  208. w!("show_ime() - no input_connection set");
  209. return
  210. };
  211. unsafe {
  212. let env = get_jni_env();
  213. let call_void_method = (**env).CallVoidMethod.unwrap();
  214. call_void_method(
  215. env,
  216. input_connection,
  217. self.set_soft_keyboard_active_method,
  218. 1, // active: true
  219. flags as ndk_sys::jint,
  220. );
  221. }
  222. }
  223. pub fn hide_ime(&self, flags: u32) {
  224. let Some(input_connection) = *self.input_connection.read() else {
  225. w!("hide_ime() - no input_connection set");
  226. return
  227. };
  228. unsafe {
  229. let env = get_jni_env();
  230. let call_void_method = (**env).CallVoidMethod.unwrap();
  231. call_void_method(
  232. env,
  233. input_connection,
  234. self.set_soft_keyboard_active_method,
  235. 0, // active: false
  236. flags as ndk_sys::jint,
  237. );
  238. }
  239. }
  240. pub fn restart_input(&self) {
  241. let Some(input_connection) = *self.input_connection.read() else {
  242. w!("restart_input() - no input_connection set");
  243. return
  244. };
  245. unsafe {
  246. let env = get_jni_env();
  247. let call_void_method = (**env).CallVoidMethod.unwrap();
  248. call_void_method(env, input_connection, self.restart_input_method);
  249. }
  250. }
  251. fn state_to_java(&self, state: &AndroidTextInputState) -> ndk_sys::jobject {
  252. unsafe {
  253. let env = get_jni_env();
  254. let new_string_utf = (**env).NewStringUTF.unwrap();
  255. let text_str = CString::new(state.text.as_str()).unwrap();
  256. let jtext = new_string_utf(env, text_str.as_ptr());
  257. let new_object = (**env).NewObject.unwrap();
  258. let (compose_start, compose_end) = match state.compose {
  259. Some((start, end)) => (start as i32, end as i32),
  260. None => (SPAN_UNDEFINED, SPAN_UNDEFINED),
  261. };
  262. let jobj = new_object(
  263. env,
  264. self.state_class,
  265. self.state_constructor,
  266. jtext,
  267. state.select.0 as i32,
  268. state.select.1 as i32,
  269. compose_start,
  270. compose_end,
  271. );
  272. let delete_local_ref = (**env).DeleteLocalRef.unwrap();
  273. delete_local_ref(env, jtext);
  274. jobj
  275. }
  276. }
  277. fn state_from_java(&self, event_state: ndk_sys::jobject) -> AndroidTextInputState {
  278. unsafe {
  279. let env = get_jni_env();
  280. let get_object_field = (**env).GetObjectField.unwrap();
  281. let jtext =
  282. get_object_field(env, event_state, self.state_class_info.text) as ndk_sys::jstring;
  283. let text = get_utf_str!(env, jtext);
  284. let get_int_field = (**env).GetIntField.unwrap();
  285. let select_start =
  286. get_int_field(env, event_state, self.state_class_info.selection_start);
  287. let select_end = get_int_field(env, event_state, self.state_class_info.selection_end);
  288. let compose_start =
  289. get_int_field(env, event_state, self.state_class_info.composing_region_start);
  290. let compose_end =
  291. get_int_field(env, event_state, self.state_class_info.composing_region_end);
  292. let delete_local_ref = (**env).DeleteLocalRef.unwrap();
  293. delete_local_ref(env, jtext);
  294. let compose = if compose_start >= 0 {
  295. Some((compose_start as usize, compose_end as usize))
  296. } else {
  297. assert!(compose_end < 0);
  298. None
  299. };
  300. AndroidTextInputState {
  301. text,
  302. select: (select_start as usize, select_end as usize),
  303. compose,
  304. }
  305. }
  306. }
  307. }
  308. impl Drop for GameTextInput {
  309. fn drop(&mut self) {
  310. unsafe {
  311. let env = get_jni_env();
  312. let delete_global_ref = (**env).DeleteGlobalRef.unwrap();
  313. if self.input_connection_class != std::ptr::null_mut() {
  314. delete_global_ref(env, self.input_connection_class);
  315. }
  316. if self.state_class != std::ptr::null_mut() {
  317. delete_global_ref(env, self.state_class);
  318. }
  319. if let Some(input_connection) = *self.input_connection.read() {
  320. delete_global_ref(env, input_connection);
  321. }
  322. }
  323. }
  324. }
  325. unsafe impl Send for GameTextInput {}
  326. unsafe impl Sync for GameTextInput {}