Просмотр исходного кода

app/android: integrate GameTextInput into build and provide an interface

jkds 7 месяцев назад
Родитель
Сommit
0a5bc2772c

+ 15 - 3
bin/app/Makefile

@@ -45,10 +45,10 @@ win-release: $(SRC) fonts
 win-debug: $(SRC) fonts
 	$(CARGO) build $(DEBUG_FEATURES)
 	-mv target/debug/darkfi-app.exe .
-android-release: $(SRC) fonts assets/forest_720x1280.mp4
+android-release: $(SRC) fonts assets/forest_720x1280.mp4 gametextinput
 	podman run -v $(shell pwd)/../../:/root/darkfi -w /root/darkfi/bin/app/ -t apk cargo quad-apk build --release $(RELEASE_FEATURES)
 	-mv $(RELEASE_APK) darkfi-app.apk
-android-debug: $(SRC) fonts assets/forest_720x1280.mp4
+android-debug: $(SRC) fonts assets/forest_720x1280.mp4 gametextinput
 	podman run -v $(shell pwd)/../../:/root/darkfi -w /root/darkfi/bin/app/ -t apk cargo quad-apk build $(DEBUG_FEATURES)
 	-mv $(DEBUG_APK) darkfi-app_debug.apk
 
@@ -69,6 +69,18 @@ ibm-plex-mono-regular.otf:
 NotoColorEmoji.ttf:
 	wget -c https://dark.fi/assets/NotoColorEmoji.ttf
 
+# Download Android GameTextInput library
+
+GAMETEXTINPUT_INCLUDE = src/android/textinput/include/game-text-input/game-text-input/gametextinput.h
+GAMETEXTINPUT_LIBS = pkg/android/libs/android.arm64-v8a/libgame-text-input.a
+
+gametextinput: $(GAMETEXTINPUT_INCLUDE) $(GAMETEXTINPUT_LIBS)
+
+$(GAMETEXTINPUT_INCLUDE):
+	pkg/android/dl-textinput-lib.sh
+$(GAMETEXTINPUT_LIBS):
+	pkg/android/dl-textinput-lib.sh
+
 # App data
 
 assets/forest_1920x1080.ivf:
@@ -87,7 +99,7 @@ dev: $(SRC) fonts assets/forest_1920x1080.ivf
 	./darkfi-app
 
 # Users should use the android-release and android-debug targets instead.
-apk: $(SRC) fonts assets/forest_720x1280.mp4
+apk: $(SRC) fonts assets/forest_720x1280.mp4 gametextinput
 	cargo quad-apk build $(DEV_FEATURES)
 	$(MAKE) install-apk
 

+ 17 - 0
bin/app/build.rs

@@ -64,5 +64,22 @@ fn main() {
                 "cargo:warning='leaving linker args for {target_os}:{target_arch} unchanged"
             ),
         }
+
+        // Link GameTextInput static library
+        let gametextinput_lib_dir = match target_arch.as_str() {
+            "aarch64" => "pkg/android/libs/android.arm64-v8a",
+            "arm" => "pkg/android/libs/android.armeabi-v7a",
+            "i686" => "pkg/android/libs/android.x86",
+            "x86_64" => "pkg/android/libs/android.x86_64",
+            _ => {
+                println!("cargo:warning=Unknown target arch for GameTextInput: {target_arch}");
+                "pkg/android/libs"
+            }
+        };
+        println!("cargo:rustc-link-search=native={gametextinput_lib_dir}");
+        println!("cargo:rustc-link-lib=static=game-text-input");
+
+        // Rebuild if headers change
+        println!("cargo:rerun-if-changed=src/android/textinput/include");
     }
 }

+ 41 - 0
bin/app/pkg/android/dl-textinput-lib.sh

@@ -0,0 +1,41 @@
+#!/bin/bash
+# Download GameTextInput library from Android Maven repository
+# This script downloads and extracts the GameTextInput headers and libraries
+
+set -e
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+PROJECT_ROOT=$(cd "$SCRIPT_DIR/../.." && pwd)
+
+LIBS_DIR=$SCRIPT_DIR/libs
+INCLUDE_DIR=$PROJECT_ROOT/src/android/textinput/include
+
+VERSION=4.0.0
+AAR=games-text-input-$VERSION.aar
+URL=https://dl.google.com/android/maven2/androidx/games/games-text-input/$VERSION/$AAR
+TMPDIR=/tmp/games-text-input-$VERSION
+
+cleanup() {
+    rm -rf $TMPDIR
+}
+trap cleanup EXIT
+
+# Clean existing files
+rm -rf $LIBS_DIR
+rm -rf $INCLUDE_DIR/game-text-input
+
+# Download AAR
+mkdir -p $TMPDIR
+cd $TMPDIR
+wget $URL
+unzip $AAR
+# Copy libs
+mv prefab/modules/game-text-input/libs $LIBS_DIR/
+# Copy headers
+mkdir -p $INCLUDE_DIR/game-text-input
+mv prefab/modules/game-text-input/include/* $INCLUDE_DIR/game-text-input/
+
+echo "GameTextInput ${GAMETEXTINPUT_VERSION} installation complete!"
+echo "  Libraries: $LIBS_DIR"
+echo "  Headers:  $INCLUDE_DIR"
+

+ 11 - 1
bin/app/src/android/mod.rs

@@ -20,9 +20,19 @@ use miniquad::native::android::{self, ndk_sys, ndk_utils};
 use parking_lot::Mutex as SyncMutex;
 use std::{collections::HashMap, path::PathBuf, sync::LazyLock};
 
-use crate::AndroidSuggestEvent;
+// TODO: Remove this enum after migration to GameTextInput is complete
+#[derive(Debug)]
+pub enum AndroidSuggestEvent {
+    Init,
+    CreateInputConnect,
+    Compose { text: String, cursor_pos: i32, is_commit: bool },
+    ComposeRegion { start: usize, end: usize },
+    FinishCompose,
+    DeleteSurroundingText { left: usize, right: usize },
+}
 
 pub mod insets;
+pub mod textinput;
 pub mod vid;
 
 macro_rules! call_mainactivity_int_method {

+ 111 - 0
bin/app/src/android/textinput/ffi.rs

@@ -0,0 +1,111 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 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::ndk_sys;
+use std::ffi::{c_char, c_void, CStr};
+
+use super::AndroidTextInputState;
+
+// Opaque type from GameTextInput C API
+#[repr(C)]
+pub struct GameTextInput(c_void);
+
+// Span type used by GameTextInput
+#[repr(C)]
+#[derive(Clone, Copy, Debug)]
+pub struct GameTextInputSpan {
+    pub start: i32,
+    pub end: i32,
+}
+
+// State structure matching the C header (gametextinput.h:67-85)
+#[repr(C)]
+pub struct GameTextInputState {
+    pub text_utf8: *const c_char,
+    pub text_length: i32,
+    pub select: GameTextInputSpan,
+    pub compose: GameTextInputSpan,
+}
+
+impl GameTextInputState {
+    pub fn to_owned(&self) -> AndroidTextInputState {
+        let text = unsafe { CStr::from_ptr(self.text_utf8) }.to_str().unwrap().to_string();
+
+        let select = (self.select.start as usize, self.select.end as usize);
+
+        let compose = if self.compose.start >= 0 {
+            assert!(self.compose.end >= 0);
+            Some((self.compose.start as usize, self.compose.end as usize))
+        } else {
+            assert!(self.compose.end < 0);
+            None
+        };
+
+        AndroidTextInputState { text, select, compose }
+    }
+}
+
+// Callback type definitions (gametextinput.h:93-94, 221-222)
+pub type GameTextInputGetStateCallback =
+    unsafe extern "C" fn(*mut c_void, *const GameTextInputState);
+
+pub type GameTextInputEventCallback = unsafe extern "C" fn(*mut c_void, *const GameTextInputState);
+
+// FFI bindings to GameTextInput C library
+extern "C" {
+    // gametextinput.h:111
+    pub fn GameTextInput_init(
+        env: *mut ndk_sys::JNIEnv,
+        max_string_size: u32,
+    ) -> *mut GameTextInput;
+
+    // gametextinput.h:140
+    pub fn GameTextInput_destroy(state: *mut GameTextInput);
+
+    // gametextinput.h:235-237
+    pub fn GameTextInput_setEventCallback(
+        state: *mut GameTextInput,
+        callback: Option<GameTextInputEventCallback>,
+        context: *mut c_void,
+    );
+
+    // gametextinput.h:161
+    pub fn GameTextInput_showIme(state: *mut GameTextInput, flags: u32);
+
+    // gametextinput.h:182
+    pub fn GameTextInput_hideIme(state: *mut GameTextInput, flags: u32);
+
+    // gametextinput.h:211-212
+    pub fn GameTextInput_setState(state: *mut GameTextInput, state: *const GameTextInputState);
+
+    // gametextinput.h:200-202
+    pub fn GameTextInput_getState(
+        state: *const GameTextInput,
+        callback: GameTextInputGetStateCallback,
+        context: *mut c_void,
+    );
+
+    // gametextinput.h:121-122
+    pub fn GameTextInput_setInputConnection(
+        state: *mut GameTextInput,
+        input_connection: *mut c_void,
+    );
+
+    // gametextinput.h:132
+    pub fn GameTextInput_processEvent(state: *mut GameTextInput, event_state: *mut c_void);
+}

+ 159 - 0
bin/app/src/android/textinput/mod.rs

@@ -0,0 +1,159 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 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};
+use parking_lot::Mutex as SyncMutex;
+use std::{
+    collections::HashMap,
+    ffi::{c_char, c_void, CString},
+    sync::LazyLock,
+};
+
+mod ffi;
+use ffi::{
+    GameTextInput, GameTextInputSpan, GameTextInputState, GameTextInput_destroy,
+    GameTextInput_getState, GameTextInput_hideIme, GameTextInput_init,
+    GameTextInput_setEventCallback, GameTextInput_setState, GameTextInput_showIme,
+};
+
+// Text input state exposed to the rest of the app
+#[derive(Debug, Clone)]
+pub struct AndroidTextInputState {
+    pub text: String,
+    pub select: (usize, usize),
+    pub compose: Option<(usize, usize)>,
+}
+
+struct Globals {
+    next_id: usize,
+    senders: HashMap<usize, async_channel::Sender<AndroidTextInputState>>,
+}
+
+static GLOBALS: LazyLock<SyncMutex<Globals>> =
+    LazyLock::new(|| SyncMutex::new(Globals { next_id: 0, senders: HashMap::new() }));
+
+// Callback implementation - sends state update event
+extern "C" fn game_text_input_callback(ctx: *mut c_void, state: *const GameTextInputState) {
+    // Ensures we can use the void* pointer to store a usize
+    assert_eq!(std::mem::size_of::<usize>(), std::mem::size_of::<*mut c_void>());
+    // ctx is the usize id we passed as void* pointer
+    let id = ctx as usize;
+    let text_state = unsafe { &(*state) }.to_owned();
+
+    let globals = GLOBALS.lock();
+    if let Some(sender) = globals.senders.get(&id) {
+        let _ = sender.try_send(text_state);
+    }
+}
+
+pub struct AndroidTextInput {
+    id: usize,
+    state: *mut GameTextInput,
+}
+
+impl AndroidTextInput {
+    pub fn new(sender: async_channel::Sender<AndroidTextInputState>) -> Self {
+        let id = {
+            let mut globals = GLOBALS.lock();
+            let id = globals.next_id;
+            globals.next_id += 1;
+            globals.senders.insert(id, sender);
+            id
+        };
+
+        let state = unsafe {
+            let env = android::attach_jni_env();
+            let state = GameTextInput_init(env, 0);
+            // Ensures we can use the void* pointer to store a usize
+            assert_eq!(std::mem::size_of::<usize>(), std::mem::size_of::<*mut c_void>());
+            GameTextInput_setEventCallback(
+                state,
+                Some(game_text_input_callback),
+                id as *mut c_void,
+            );
+            state
+        };
+
+        Self { id, state }
+    }
+
+    pub fn show_ime(&self) {
+        unsafe {
+            GameTextInput_showIme(self.state, 0);
+        }
+    }
+
+    pub fn hide_ime(&self) {
+        unsafe {
+            GameTextInput_hideIme(self.state, 0);
+        }
+    }
+
+    pub fn set_state(&self, state: &AndroidTextInputState) {
+        let ctext = CString::new(state.text.as_str()).unwrap();
+
+        let select = GameTextInputSpan { start: state.select.0 as i32, end: state.select.1 as i32 };
+
+        let compose = match state.compose {
+            Some((start, end)) => GameTextInputSpan { start: start as i32, end: end as i32 },
+            None => GameTextInputSpan { start: -1, end: -1 },
+        };
+
+        let gt_state = GameTextInputState {
+            text_utf8: ctext.as_ptr(),
+            text_length: state.text.len() as i32,
+            select,
+            compose,
+        };
+
+        unsafe {
+            GameTextInput_setState(self.state, &gt_state);
+        }
+    }
+
+    pub fn get_state(&self) -> AndroidTextInputState {
+        let mut state =
+            AndroidTextInputState { text: String::new(), select: (0, 0), compose: None };
+
+        // This is guaranteed by GameTextInput_getState() to be called sync
+        // so what we are doing is legit here.
+        extern "C" fn callback(ctx: *mut c_void, game_state: *const GameTextInputState) {
+            let state = unsafe { &mut *(ctx as *mut AndroidTextInputState) };
+            *state = unsafe { &(*game_state) }.to_owned();
+        }
+
+        unsafe {
+            GameTextInput_getState(
+                self.state,
+                callback,
+                &mut state as *mut AndroidTextInputState as *mut c_void,
+            );
+        }
+
+        state
+    }
+}
+
+impl Drop for AndroidTextInput {
+    fn drop(&mut self) {
+        unsafe {
+            GameTextInput_destroy(self.state);
+        }
+        GLOBALS.lock().senders.remove(&self.id);
+    }
+}

+ 0 - 10
bin/app/src/main.rs

@@ -27,16 +27,6 @@ use std::sync::{Arc, OnceLock};
 #[macro_use]
 extern crate tracing;
 
-#[derive(Debug)]
-pub enum AndroidSuggestEvent {
-    Init,
-    CreateInputConnect,
-    Compose { text: String, cursor_pos: i32, is_commit: bool },
-    ComposeRegion { start: usize, end: usize },
-    FinishCompose,
-    DeleteSurroundingText { left: usize, right: usize },
-}
-
 #[cfg(target_os = "android")]
 mod android;
 mod app;

+ 1 - 2
bin/app/src/text2/editor/android.rs

@@ -17,12 +17,11 @@
  */
 
 use crate::{
-    android,
+    android::{self, AndroidSuggestEvent},
     gfx::Point,
     mesh::Color,
     prop::{PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyStr},
     text2::{TextContext, TEXT_CTX},
-    AndroidSuggestEvent,
 };
 use std::{
     cmp::{max, min},

+ 1 - 1
bin/app/src/ui/edit/mod.rs

@@ -37,7 +37,7 @@ use std::{
 use tracing::instrument;
 
 #[cfg(target_os = "android")]
-use crate::AndroidSuggestEvent;
+use crate::android::AndroidSuggestEvent;
 use crate::{
     gfx::{gfxtag, DrawCall, DrawInstruction, DrawMesh, Point, Rectangle, RenderApi, Vertex},
     mesh::MeshBuilder,