mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-06 21:34:00 -05:00
app: do a big cleanup of java GameTextInput code and make everything have a sane API.
This commit is contained in:
2
bin/app/Cargo.lock
generated
2
bin/app/Cargo.lock
generated
@@ -3708,7 +3708,7 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
[[package]]
|
||||
name = "miniquad"
|
||||
version = "0.4.8"
|
||||
source = "git+https://github.com/narodnik/miniquad#f9bf3a41e40f8b15032c159e24dec8e40c7adfa5"
|
||||
source = "git+https://github.com/not-fl3/miniquad#dd44380a7ace19462bb63e53dce6093d28219aa2"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"ndk-sys",
|
||||
|
||||
@@ -9,8 +9,7 @@ homepage = "https://dark.fi"
|
||||
repository = "https://codeberg.org/darkrenaissance/darkfi"
|
||||
|
||||
[dependencies]
|
||||
#miniquad = { git = "https://github.com/not-fl3/miniquad" }
|
||||
miniquad = { git = "https://github.com/narodnik/miniquad" }
|
||||
miniquad = { git = "https://github.com/not-fl3/miniquad" }
|
||||
|
||||
# Currently latest version links to freetype-sys 0.19 but we use 0.21
|
||||
#harfbuzz-sys = "0.6.1"
|
||||
|
||||
@@ -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 gametextinput
|
||||
android-release: $(SRC) fonts assets/forest_720x1280.mp4
|
||||
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 gametextinput
|
||||
android-debug: $(SRC) fonts assets/forest_720x1280.mp4
|
||||
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,18 +69,6 @@ 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:
|
||||
@@ -99,7 +87,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 gametextinput
|
||||
apk: $(SRC) fonts assets/forest_720x1280.mp4
|
||||
cargo quad-apk build $(DEV_FEATURES)
|
||||
$(MAKE) install-apk
|
||||
|
||||
@@ -107,6 +95,9 @@ install-apk:
|
||||
-mv $(DEBUG_APK) .
|
||||
-adb $(ADB_DEVICE) uninstall darkfi.darkfi_app
|
||||
adb $(ADB_DEVICE) install -r darkfi-app.apk
|
||||
$(MAKE) log-apk
|
||||
|
||||
log-apk:
|
||||
reset
|
||||
adb $(ADB_DEVICE) logcat -c
|
||||
adb $(ADB_DEVICE) shell monkey -p darkfi.darkfi_app -c android.intent.category.LAUNCHER 1
|
||||
|
||||
@@ -4,10 +4,10 @@ import android.view.ViewGroup;
|
||||
import android.view.WindowInsets.Type;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import java.util.HashMap;
|
||||
|
||||
import videodecode.VideoDecoder;
|
||||
import textinput.InputConnection;
|
||||
import textinput.Settings;
|
||||
import textinput.Listener;
|
||||
import textinput.State;
|
||||
@@ -15,6 +15,67 @@ import textinput.GameTextInput;
|
||||
|
||||
//% END
|
||||
|
||||
//% QUAD_SURFACE_ON_CREATE_INPUT_CONNECTION
|
||||
|
||||
MainActivity main = (MainActivity)getContext();
|
||||
// Create InputConnection if it doesn't exist yet
|
||||
if (main.inpcon == null) {
|
||||
EditorInfo editorInfo = new EditorInfo();
|
||||
editorInfo.inputType = InputType.TYPE_CLASS_TEXT |
|
||||
InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
|
||||
editorInfo.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||
|
||||
main.inpcon = new textinput.InputConnection(
|
||||
getContext(),
|
||||
this,
|
||||
new Settings(editorInfo, true)
|
||||
);
|
||||
|
||||
// Pass the InputConnection to native GameTextInput library
|
||||
main.setInputConnectionNative(main.inpcon);
|
||||
}
|
||||
|
||||
// Set the listener to receive IME state changes
|
||||
main.inpcon.setListener(new Listener() {
|
||||
@Override
|
||||
public void stateChanged(State newState, boolean dismissed) {
|
||||
// Called when the IME sends new text state
|
||||
// Forward to native code which triggers Rust callback
|
||||
Log.d("darkfi", "stateChanged: text=" + newState.toString());
|
||||
main.onTextInputEventNative(newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImeInsetsChanged(androidx.core.graphics.Insets insets) {
|
||||
// Called when IME insets change (e.g., keyboard height changes)
|
||||
// Optional: can be used for dynamic layout adjustment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoftwareKeyboardVisibilityChanged(boolean visible) {
|
||||
// Called when keyboard is shown or hidden
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditorAction(int actionCode) {
|
||||
// Called when user presses action button (Done, Next, etc.)
|
||||
// Optional: handle specific editor actions
|
||||
}
|
||||
});
|
||||
|
||||
// Copy EditorInfo from GameTextInput to configure IME
|
||||
if (outAttrs != null) {
|
||||
GameTextInput.copyEditorInfo(
|
||||
main.inpcon.getEditorInfo(),
|
||||
outAttrs
|
||||
);
|
||||
}
|
||||
|
||||
// Return the GameTextInput InputConnection to IME
|
||||
if (true) return main.inpcon;
|
||||
|
||||
//% END
|
||||
|
||||
//% RESIZING_LAYOUT_BODY
|
||||
|
||||
native static void onApplyInsets(
|
||||
@@ -53,7 +114,7 @@ native void setInputConnectionNative(textinput.InputConnection c);
|
||||
native void onTextInputEventNative(textinput.State softKeyboardEvent);
|
||||
|
||||
// GameTextInput InputConnection reference (public for QuadSurface access)
|
||||
public textinput.InputConnection gameTextInputInputConnection;
|
||||
public textinput.InputConnection inpcon;
|
||||
|
||||
public String getAppDataPath() {
|
||||
return getApplicationContext().getDataDir().getAbsolutePath();
|
||||
@@ -95,89 +156,9 @@ public VideoDecoder createVideoDecoder() {
|
||||
|
||||
//% MAIN_ACTIVITY_ON_CREATE
|
||||
|
||||
//view.setFocusable(false);
|
||||
//view.setFocusableInTouchMode(false);
|
||||
//view.clearFocus();
|
||||
|
||||
// Start a foreground service so the app stays awake
|
||||
Intent serviceIntent = new Intent(this, ForegroundService.class);
|
||||
startForegroundService(serviceIntent);
|
||||
|
||||
//% END
|
||||
|
||||
|
||||
//% QUAD_SURFACE_ON_CREATE_INPUT_CONNECTION
|
||||
|
||||
// Get reference to MainActivity
|
||||
if (getContext() == null)
|
||||
Log.i("darkfi", "getCTX (on creat) is nulllll!!!!!!!!!!!!!!!!!!");
|
||||
MainActivity mainActivity = (MainActivity)getContext();
|
||||
|
||||
android.util.Log.d("darkfi", "onCreateInputConnection called");
|
||||
|
||||
// Create InputConnection if it doesn't exist yet
|
||||
if (mainActivity.gameTextInputInputConnection == null) {
|
||||
android.util.Log.d("darkfi", "Creating new InputConnection");
|
||||
// Create InputConnection with Context (from QuadSurface)
|
||||
android.view.inputmethod.EditorInfo editorInfo = new android.view.inputmethod.EditorInfo();
|
||||
editorInfo.inputType = android.text.InputType.TYPE_CLASS_TEXT |
|
||||
android.text.InputType.TYPE_TEXT_FLAG_AUTO_CORRECT;
|
||||
editorInfo.imeOptions = android.view.inputmethod.EditorInfo.IME_FLAG_NO_FULLSCREEN;
|
||||
|
||||
if (mainActivity == null)
|
||||
Log.i("darkfi", "mainact is NULLLL");
|
||||
mainActivity.gameTextInputInputConnection = new textinput.InputConnection(
|
||||
getContext(),
|
||||
this,
|
||||
new textinput.Settings(editorInfo, true)
|
||||
);
|
||||
|
||||
// Pass the InputConnection to native GameTextInput library
|
||||
android.util.Log.d("darkfi", "InputConnection created and passed to native");
|
||||
mainActivity.setInputConnectionNative(mainActivity.gameTextInputInputConnection);
|
||||
} else {
|
||||
android.util.Log.d("darkfi", "Reusing existing InputConnection");
|
||||
}
|
||||
|
||||
// Set the listener to receive IME state changes
|
||||
mainActivity.gameTextInputInputConnection.setListener(new textinput.Listener() {
|
||||
@Override
|
||||
public void stateChanged(textinput.State newState, boolean dismissed) {
|
||||
// Called when the IME sends new text state
|
||||
// Forward to native code which triggers Rust callback
|
||||
android.util.Log.d("darkfi", "stateChanged: text=" + newState.toString());
|
||||
mainActivity.onTextInputEventNative(newState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImeInsetsChanged(androidx.core.graphics.Insets insets) {
|
||||
// Called when IME insets change (e.g., keyboard height changes)
|
||||
// Optional: can be used for dynamic layout adjustment
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSoftwareKeyboardVisibilityChanged(boolean visible) {
|
||||
// Called when keyboard is shown or hidden
|
||||
android.util.Log.d("darkfi", "onSoftwareKeyboardVisibilityChanged: " + visible);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEditorAction(int actionCode) {
|
||||
// Called when user presses action button (Done, Next, etc.)
|
||||
// Optional: handle specific editor actions
|
||||
}
|
||||
});
|
||||
|
||||
// Copy EditorInfo from GameTextInput to configure IME
|
||||
if (outAttrs != null) {
|
||||
textinput.GameTextInput.copyEditorInfo(
|
||||
mainActivity.gameTextInputInputConnection.getEditorInfo(),
|
||||
outAttrs
|
||||
);
|
||||
}
|
||||
|
||||
// Return the GameTextInput InputConnection to IME
|
||||
if (true) return mainActivity.gameTextInputInputConnection;
|
||||
return mainActivity.gameTextInputInputConnection;
|
||||
|
||||
//% END
|
||||
|
||||
@@ -48,5 +48,14 @@ public final class GameTextInput {
|
||||
to.initialSelEnd = from.initialSelEnd;
|
||||
}
|
||||
|
||||
public static final class Pair {
|
||||
int first, second;
|
||||
|
||||
Pair(int f, int s) {
|
||||
first = f;
|
||||
second = s;
|
||||
}
|
||||
}
|
||||
|
||||
private GameTextInput() {}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,8 @@ import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import textinput.GameTextInput.Pair;
|
||||
|
||||
public class InputConnection extends BaseInputConnection implements View.OnKeyListener {
|
||||
private static final String TAG = "gti.InputConnection";
|
||||
private final InputMethodManager imm;
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2021 The Android Open Source Project
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package textinput;
|
||||
|
||||
public final class Pair {
|
||||
public int first, second;
|
||||
|
||||
public Pair(int f, int s) {
|
||||
first = f;
|
||||
second = s;
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
#!/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"
|
||||
|
||||
@@ -6,8 +6,7 @@ java_files = [
|
||||
"java/textinput/State.java",
|
||||
"java/textinput/Listener.java",
|
||||
"java/textinput/Settings.java",
|
||||
"java/textinput/GameTextInput.java",
|
||||
"java/textinput/Pair.java"
|
||||
"java/textinput/GameTextInput.java"
|
||||
]
|
||||
comptime_jar_files = [
|
||||
"android-libs/androidx/core-1.9.0.jar"
|
||||
|
||||
@@ -22,6 +22,7 @@ use std::{collections::HashMap, path::PathBuf, sync::LazyLock};
|
||||
|
||||
pub mod insets;
|
||||
pub mod textinput;
|
||||
mod util;
|
||||
pub mod vid;
|
||||
|
||||
macro_rules! call_mainactivity_int_method {
|
||||
|
||||
@@ -16,17 +16,24 @@
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
use async_channel::Sender as AsyncSender;
|
||||
use miniquad::native::android::{ndk_sys, ndk_utils::*};
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use std::ffi::CString;
|
||||
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::AndroidTextInputState;
|
||||
use super::{AndroidTextInputState, SharedStatePtr};
|
||||
|
||||
const DEFAULT_MAX_STRING_SIZE: usize = 1 << 16;
|
||||
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<GameTextInput> = OnceLock::new();
|
||||
|
||||
struct StateClassInfo {
|
||||
text: ndk_sys::jfieldID,
|
||||
selection_start: ndk_sys::jfieldID,
|
||||
@@ -36,19 +43,21 @@ struct StateClassInfo {
|
||||
}
|
||||
|
||||
pub struct GameTextInput {
|
||||
env: *mut ndk_sys::JNIEnv,
|
||||
state: SyncMutex<AndroidTextInputState>,
|
||||
input_connection: Option<ndk_sys::jobject>,
|
||||
state: SyncMutex<Option<SharedStatePtr>>,
|
||||
input_connection: RwLock<Option<ndk_sys::jobject>>,
|
||||
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,
|
||||
pub event_sender: Option<AsyncSender<AndroidTextInputState>>
|
||||
}
|
||||
|
||||
impl GameTextInput {
|
||||
pub fn new(env: *mut ndk_sys::JNIEnv, max_string_size: u32) -> Self {
|
||||
pub fn new() -> Self {
|
||||
unsafe {
|
||||
let env = android::attach_jni_env();
|
||||
|
||||
let find_class = (**env).FindClass.unwrap();
|
||||
|
||||
let state_class_name = b"textinput/State\0";
|
||||
@@ -86,41 +95,49 @@ impl GameTextInput {
|
||||
restart_input_sig.as_ptr() as _,
|
||||
);
|
||||
|
||||
let state_java_class = new_global_ref!(env, state_java_class);
|
||||
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_java_class,
|
||||
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_java_class,
|
||||
state_class,
|
||||
b"selectionStart\0".as_ptr() as _,
|
||||
b"I\0".as_ptr() as _,
|
||||
);
|
||||
let selection_end_field = get_field_id(
|
||||
env,
|
||||
state_java_class,
|
||||
state_class,
|
||||
b"selectionEnd\0".as_ptr() as _,
|
||||
b"I\0".as_ptr() as _,
|
||||
);
|
||||
let composing_region_start_field = get_field_id(
|
||||
env,
|
||||
state_java_class,
|
||||
state_class,
|
||||
b"composingRegionStart\0".as_ptr() as _,
|
||||
b"I\0".as_ptr() as _,
|
||||
);
|
||||
let composing_region_end_field = get_field_id(
|
||||
env,
|
||||
state_java_class,
|
||||
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"<init>\0".as_ptr() as _,
|
||||
constructor_sig.as_ptr() as _,
|
||||
);
|
||||
|
||||
let state_class_info = StateClassInfo {
|
||||
text: text_field,
|
||||
selection_start: selection_start_field,
|
||||
@@ -130,124 +147,139 @@ impl GameTextInput {
|
||||
};
|
||||
|
||||
Self {
|
||||
env,
|
||||
state: SyncMutex::new(AndroidTextInputState::new()),
|
||||
input_connection: None,
|
||||
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,
|
||||
event_sender: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: &AndroidTextInputState) {
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
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;
|
||||
// Push changes to the Java side
|
||||
self.push_update(&new_focus.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
|
||||
};
|
||||
if let Some(input_connection) = *self.input_connection.read() {
|
||||
unsafe {
|
||||
let env = android::attach_jni_env();
|
||||
let jstate = self.state_to_java(state);
|
||||
call_void_method!(
|
||||
self.env,
|
||||
env,
|
||||
input_connection,
|
||||
"setState",
|
||||
"(Ltextinput/State;)V",
|
||||
jstate
|
||||
);
|
||||
let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
|
||||
delete_local_ref(self.env, jstate);
|
||||
|
||||
let delete_local_ref = (**env).DeleteLocalRef.unwrap();
|
||||
delete_local_ref(env, jstate);
|
||||
}
|
||||
}
|
||||
*self.state.lock() = state.clone();
|
||||
}
|
||||
|
||||
fn set_state_inner(&mut self, state: AndroidTextInputState) {
|
||||
*self.state.lock() = state;
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> AndroidTextInputState {
|
||||
self.state.lock().clone()
|
||||
}
|
||||
|
||||
pub fn set_input_connection(&mut self, input_connection: ndk_sys::jobject) {
|
||||
pub fn set_input_connection(&self, input_connection: ndk_sys::jobject) {
|
||||
unsafe {
|
||||
if let Some(old_ref) = self.input_connection {
|
||||
let delete_global_ref = (**self.env).DeleteGlobalRef.unwrap();
|
||||
delete_global_ref(self.env, old_ref);
|
||||
let env = android::attach_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);
|
||||
}
|
||||
self.input_connection = Some(new_global_ref!(self.env, input_connection));
|
||||
*ic = Some(new_global_ref!(env, input_connection));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn process_event(&mut self, event_state: ndk_sys::jobject) {
|
||||
pub fn process_event(&self, event_state: ndk_sys::jobject) {
|
||||
let state = self.state_from_java(event_state);
|
||||
if let Some(sender) = &self.event_sender {
|
||||
let _ = sender.try_send(state.clone());
|
||||
}
|
||||
self.set_state_inner(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) {
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
unsafe {
|
||||
let call_void_method = (**self.env).CallVoidMethod.unwrap();
|
||||
call_void_method(
|
||||
self.env,
|
||||
input_connection,
|
||||
self.set_soft_keyboard_active_method,
|
||||
1, // active: true
|
||||
flags as ndk_sys::jint,
|
||||
);
|
||||
}
|
||||
let Some(input_connection) = *self.input_connection.read() else {
|
||||
w!("show_ime() - no input_connection set");
|
||||
return
|
||||
};
|
||||
unsafe {
|
||||
let env = android::attach_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) {
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
unsafe {
|
||||
let call_void_method = (**self.env).CallVoidMethod.unwrap();
|
||||
call_void_method(
|
||||
self.env,
|
||||
input_connection,
|
||||
self.set_soft_keyboard_active_method,
|
||||
0, // active: false
|
||||
flags as ndk_sys::jint,
|
||||
);
|
||||
}
|
||||
let Some(input_connection) = *self.input_connection.read() else {
|
||||
w!("hide_ime() - no input_connection set");
|
||||
return
|
||||
};
|
||||
unsafe {
|
||||
let env = android::attach_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) {
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
unsafe {
|
||||
let call_void_method = (**self.env).CallVoidMethod.unwrap();
|
||||
call_void_method(self.env, input_connection, self.restart_input_method);
|
||||
}
|
||||
let Some(input_connection) = *self.input_connection.read() else {
|
||||
w!("restart_input() - no input_connection set");
|
||||
return
|
||||
};
|
||||
unsafe {
|
||||
let env = android::attach_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 new_string_utf = (**self.env).NewStringUTF.unwrap();
|
||||
let text_str = CString::new(state.text.as_str()).unwrap_or_else(|_| {
|
||||
tracing::error!("Failed to convert text to CString");
|
||||
CString::new("").unwrap()
|
||||
});
|
||||
let jtext = new_string_utf(self.env, text_str.as_ptr());
|
||||
let env = android::attach_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 = (**self.env).NewObject.unwrap();
|
||||
let get_method_id = (**self.env).GetMethodID.unwrap();
|
||||
let find_class = (**self.env).FindClass.unwrap();
|
||||
|
||||
let state_class_name = b"textinput/State\0";
|
||||
let state_java_class = find_class(self.env, state_class_name.as_ptr() as _);
|
||||
|
||||
let constructor_sig = b"(Ljava/lang/String;IIII)V\0";
|
||||
let constructor = get_method_id(
|
||||
self.env,
|
||||
state_java_class,
|
||||
b"<init>\0".as_ptr() as _,
|
||||
constructor_sig.as_ptr() as _,
|
||||
);
|
||||
let new_object = (**env).NewObject.unwrap();
|
||||
|
||||
let (compose_start, compose_end) = match state.compose {
|
||||
Some((start, end)) => (start as i32, end as i32),
|
||||
@@ -255,9 +287,9 @@ impl GameTextInput {
|
||||
};
|
||||
|
||||
let jobj = new_object(
|
||||
self.env,
|
||||
state_java_class,
|
||||
constructor,
|
||||
env,
|
||||
self.state_class,
|
||||
self.state_constructor,
|
||||
jtext,
|
||||
state.select.0 as i32,
|
||||
state.select.1 as i32,
|
||||
@@ -265,33 +297,32 @@ impl GameTextInput {
|
||||
compose_end,
|
||||
);
|
||||
|
||||
let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
|
||||
delete_local_ref(self.env, jtext);
|
||||
delete_local_ref(self.env, state_java_class);
|
||||
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 get_object_field = (**self.env).GetObjectField.unwrap();
|
||||
let jtext = get_object_field(self.env, event_state, self.state_class_info.text)
|
||||
as ndk_sys::jstring;
|
||||
let env = android::attach_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!(self.env, jtext);
|
||||
let text = get_utf_str!(env, jtext);
|
||||
|
||||
let get_int_field = (**self.env).GetIntField.unwrap();
|
||||
let get_int_field = (**env).GetIntField.unwrap();
|
||||
let select_start =
|
||||
get_int_field(self.env, event_state, self.state_class_info.selection_start);
|
||||
let select_end =
|
||||
get_int_field(self.env, event_state, self.state_class_info.selection_end);
|
||||
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(self.env, event_state, self.state_class_info.composing_region_start);
|
||||
get_int_field(env, event_state, self.state_class_info.composing_region_start);
|
||||
let compose_end =
|
||||
get_int_field(self.env, event_state, self.state_class_info.composing_region_end);
|
||||
get_int_field(env, event_state, self.state_class_info.composing_region_end);
|
||||
|
||||
let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
|
||||
delete_local_ref(self.env, jtext);
|
||||
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))
|
||||
@@ -312,12 +343,16 @@ impl GameTextInput {
|
||||
impl Drop for GameTextInput {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
let delete_global_ref = (**self.env).DeleteGlobalRef.unwrap();
|
||||
let env = android::attach_jni_env();
|
||||
let delete_global_ref = (**env).DeleteGlobalRef.unwrap();
|
||||
if self.input_connection_class != std::ptr::null_mut() {
|
||||
delete_global_ref(self.env, self.input_connection_class);
|
||||
delete_global_ref(env, self.input_connection_class);
|
||||
}
|
||||
if let Some(input_connection) = self.input_connection {
|
||||
delete_global_ref(self.env, input_connection);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,25 @@
|
||||
/* GameTextInput JNI bridge functions */
|
||||
/* 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 crate::android::textinput::{init_game_text_input, GAME_TEXT_INPUT};
|
||||
use miniquad::native::android::ndk_sys;
|
||||
|
||||
use super::gametextinput::{GameTextInput, GAME_TEXT_INPUT};
|
||||
|
||||
/// Set the InputConnection for GameTextInput (called from Java)
|
||||
///
|
||||
/// This follows the official Android GameTextInput integration pattern:
|
||||
@@ -10,24 +27,16 @@ use miniquad::native::android::ndk_sys;
|
||||
///
|
||||
/// Called from MainActivity when the InputConnection is created. It passes
|
||||
/// the Java InputConnection object to the native GameTextInput library.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `env` - JNI environment pointer
|
||||
/// * `_class` - JNI class reference (unused)
|
||||
/// * `input_connection` - Java InputConnection object from textinput.InputConnection
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_darkfi_darkfi_1app_MainActivity_setInputConnectionNative(
|
||||
_env: *mut ndk_sys::JNIEnv,
|
||||
_class: ndk_sys::jclass,
|
||||
input_connection: ndk_sys::jobject,
|
||||
) {
|
||||
// Initialize GameTextInput first
|
||||
init_game_text_input();
|
||||
|
||||
// Now set the InputConnection
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.set_input_connection(input_connection);
|
||||
}
|
||||
debug!(target: "android::textinput::jni", "Setting input connection");
|
||||
// Initialize GameTextInput on first call
|
||||
let gti = GAME_TEXT_INPUT.get_or_init(|| GameTextInput::new());
|
||||
gti.set_input_connection(input_connection);
|
||||
}
|
||||
|
||||
/// Process IME state event from Java Listener.stateChanged()
|
||||
@@ -35,18 +44,12 @@ pub extern "C" fn Java_darkfi_darkfi_1app_MainActivity_setInputConnectionNative(
|
||||
/// This follows the official Android GameTextInput integration pattern.
|
||||
/// Called from the Java InputConnection's Listener whenever the IME sends
|
||||
/// a state change (text typed, cursor moved, etc.).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `env` - JNI environment pointer
|
||||
/// * `_class` - JNI class reference (unused)
|
||||
/// * `soft_keyboard_event` - Java State object from textinput.State
|
||||
#[no_mangle]
|
||||
pub extern "C" fn Java_darkfi_darkfi_1app_MainActivity_onTextInputEventNative(
|
||||
_env: *mut ndk_sys::JNIEnv,
|
||||
_class: ndk_sys::jclass,
|
||||
soft_keyboard_event: ndk_sys::jobject,
|
||||
) {
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.process_event(soft_keyboard_event);
|
||||
}
|
||||
let gti = GAME_TEXT_INPUT.get().unwrap();
|
||||
gti.process_event(soft_keyboard_event);
|
||||
}
|
||||
|
||||
@@ -17,92 +17,76 @@
|
||||
*/
|
||||
|
||||
use async_channel::Sender as AsyncSender;
|
||||
use miniquad::native::android::attach_jni_env;
|
||||
use parking_lot::RwLock;
|
||||
use std::sync::LazyLock;
|
||||
use parking_lot::Mutex as SyncMutex;
|
||||
use std::sync::Arc;
|
||||
|
||||
mod gametextinput;
|
||||
mod jni;
|
||||
mod state;
|
||||
|
||||
use state::GameTextInput;
|
||||
use gametextinput::{GameTextInput, GAME_TEXT_INPUT};
|
||||
|
||||
/// Global GameTextInput instance for JNI bridge
|
||||
///
|
||||
/// Single global instance since only ONE editor is active at a time.
|
||||
pub(self) static GAME_TEXT_INPUT: LazyLock<RwLock<Option<GameTextInput>>> =
|
||||
LazyLock::new(|| RwLock::new(None));
|
||||
|
||||
pub(self) fn init_game_text_input() {
|
||||
debug!("AndroidTextInput: Initializing GameTextInput");
|
||||
|
||||
let env = unsafe { attach_jni_env() };
|
||||
let mut gti = GameTextInput::new(env, 0);
|
||||
// Store globally for JNI bridge access
|
||||
*GAME_TEXT_INPUT.write() = Some(gti);
|
||||
|
||||
debug!("AndroidTextInput: GameTextInput initialized");
|
||||
}
|
||||
|
||||
fn is_init() -> bool {
|
||||
GAME_TEXT_INPUT.read().is_some()
|
||||
}
|
||||
macro_rules! t { ($($arg:tt)*) => { trace!(target: "android::textinput", $($arg)*); } }
|
||||
|
||||
// Text input state exposed to the rest of the app
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct AndroidTextInputState {
|
||||
pub text: String,
|
||||
pub select: (usize, usize),
|
||||
pub compose: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
impl AndroidTextInputState {
|
||||
fn new() -> Self {
|
||||
Self { text: String::new(), select: (0, 0), compose: None }
|
||||
struct SharedState {
|
||||
state: AndroidTextInputState,
|
||||
/// Used so we know whether to also update the GameTextInput in Android.
|
||||
/// We should only do so when active.
|
||||
is_active: bool,
|
||||
sender: AsyncSender<AndroidTextInputState>,
|
||||
}
|
||||
|
||||
impl SharedState {
|
||||
fn new(sender: AsyncSender<AndroidTextInputState>) -> Self {
|
||||
Self { state: Default::default(), is_active: false, sender }
|
||||
}
|
||||
}
|
||||
|
||||
pub(self) type SharedStatePtr = Arc<SyncMutex<SharedState>>;
|
||||
|
||||
pub struct AndroidTextInput {
|
||||
state: AndroidTextInputState,
|
||||
sender: async_channel::Sender<AndroidTextInputState>,
|
||||
is_focus: bool,
|
||||
state: SharedStatePtr,
|
||||
}
|
||||
|
||||
impl AndroidTextInput {
|
||||
pub fn new(sender: async_channel::Sender<AndroidTextInputState>) -> Self {
|
||||
Self { state: AndroidTextInputState::new(), sender, is_focus: false }
|
||||
pub fn new(sender: AsyncSender<AndroidTextInputState>) -> Self {
|
||||
Self { state: Arc::new(SyncMutex::new(SharedState::new(sender))) }
|
||||
}
|
||||
|
||||
pub fn show(&mut self) {
|
||||
if !is_init() {
|
||||
return;
|
||||
}
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.event_sender = Some(self.sender.clone());
|
||||
gti.set_state(&self.state);
|
||||
gti.show_ime(0);
|
||||
}
|
||||
self.is_focus = true;
|
||||
pub fn show(&self) {
|
||||
t!("show IME");
|
||||
let gti = GAME_TEXT_INPUT.get().unwrap();
|
||||
gti.focus(self.state.clone());
|
||||
gti.show_ime(0);
|
||||
}
|
||||
|
||||
pub fn hide(&mut self) {
|
||||
if !is_init() {
|
||||
return;
|
||||
}
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.event_sender = None;
|
||||
gti.hide_ime(0);
|
||||
}
|
||||
self.is_focus = false;
|
||||
pub fn hide(&self) {
|
||||
t!("hide IME");
|
||||
let gti = GAME_TEXT_INPUT.get().unwrap();
|
||||
gti.hide_ime(0);
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: AndroidTextInputState) {
|
||||
self.state = state;
|
||||
if let Some(gti) = &mut *GAME_TEXT_INPUT.write() {
|
||||
gti.set_state(&self.state);
|
||||
pub fn set_state(&self, state: AndroidTextInputState) {
|
||||
t!("set_state({state:?})");
|
||||
// Always update our own state.
|
||||
let mut ours = self.state.lock();
|
||||
ours.state = state.clone();
|
||||
|
||||
// Only update java state when this input is active
|
||||
if ours.is_active {
|
||||
let gti = GAME_TEXT_INPUT.get().unwrap();
|
||||
gti.push_update(&state);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_state(&self) -> &AndroidTextInputState {
|
||||
&self.state
|
||||
pub fn get_state(&self) -> AndroidTextInputState {
|
||||
self.state.lock().state.clone()
|
||||
}
|
||||
}
|
||||
|
||||
36
bin/app/src/android/util.rs
Normal file
36
bin/app/src/android/util.rs
Normal file
@@ -0,0 +1,36 @@
|
||||
/* 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;
|
||||
|
||||
/// Check for pending Java exceptions and log them
|
||||
///
|
||||
/// This function should be called after any JNI call that might throw an exception.
|
||||
/// It will use ExceptionDescribe to print the exception to logcat, then clear it.
|
||||
unsafe fn check_except(env: *mut ndk_sys::JNIEnv, context: &str) {
|
||||
let exception_check = (**env).ExceptionCheck.unwrap();
|
||||
if exception_check(env) != 0 {
|
||||
// Use ExceptionDescribe to print the exception stack trace to logcat
|
||||
// This is safe to call even with a pending exception and handles StackOverflowError gracefully
|
||||
let exception_describe = (**env).ExceptionDescribe.unwrap();
|
||||
exception_describe(env);
|
||||
|
||||
let exception_clear = (**env).ExceptionClear.unwrap();
|
||||
exception_clear(env);
|
||||
}
|
||||
}
|
||||
@@ -1311,8 +1311,8 @@ impl BaseEdit {
|
||||
|
||||
let mut editor = self.lock_editor().await;
|
||||
editor.on_buffer_changed(atom).await;
|
||||
|
||||
drop(editor);
|
||||
|
||||
self.eval_rect().await;
|
||||
self.behave.apply_cursor_scroll().await;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user