mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-08 22:28:12 -05:00
app/android: integrate GameTextInput into build and provide an interface
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
bin/app/pkg/android/dl-textinput-lib.sh
Executable file
41
bin/app/pkg/android/dl-textinput-lib.sh
Executable 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"
|
||||
|
||||
@@ -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
bin/app/src/android/textinput/ffi.rs
Normal file
111
bin/app/src/android/textinput/ffi.rs
Normal 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);
|
||||
}
|
||||
159
bin/app/src/android/textinput/mod.rs
Normal file
159
bin/app/src/android/textinput/mod.rs
Normal 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, >_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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user