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

This commit is contained in:
jkds
2026-01-01 11:39:58 +01:00
parent 49bcfb575f
commit 0a5bc2772c
9 changed files with 356 additions and 17 deletions

View File

@@ -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

View File

@@ -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");
}
}

View File

@@ -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"

View File

@@ -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 {

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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;

View File

@@ -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},

View File

@@ -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,