app: copy GameTextInput java code into app

This commit is contained in:
jkds
2026-01-02 05:15:49 +01:00
parent 1ee2fe0453
commit 64e38580b2
16 changed files with 1490 additions and 293 deletions

View File

@@ -64,22 +64,5 @@ 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

@@ -2,9 +2,16 @@
import android.view.ViewGroup;
import android.view.WindowInsets.Type;
import android.view.inputmethod.EditorInfo;
import android.text.InputType;
import java.util.HashMap;
import videodecode.VideoDecoder;
import textinput.InputConnection;
import textinput.Settings;
import textinput.Listener;
import textinput.State;
import textinput.GameTextInput;
//% END
@@ -41,6 +48,13 @@ if (true)
//% MAIN_ACTIVITY_BODY
// GameTextInput native bridge functions (following official Android pattern)
native void setInputConnectionNative(textinput.InputConnection c);
native void onTextInputEventNative(textinput.State softKeyboardEvent);
// GameTextInput InputConnection reference (public for QuadSurface access)
public textinput.InputConnection gameTextInputInputConnection;
public String getAppDataPath() {
return getApplicationContext().getDataDir().getAbsolutePath();
}
@@ -81,9 +95,9 @@ public VideoDecoder createVideoDecoder() {
//% MAIN_ACTIVITY_ON_CREATE
view.setFocusable(false);
view.setFocusableInTouchMode(false);
view.clearFocus();
//view.setFocusable(false);
//view.setFocusableInTouchMode(false);
//view.clearFocus();
// Start a foreground service so the app stays awake
Intent serviceIntent = new Intent(this, ForegroundService.class);
@@ -91,3 +105,79 @@ 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

View File

@@ -0,0 +1,52 @@
/*
* 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;
import android.text.Editable;
import android.text.Spanned;
import android.view.inputmethod.EditorInfo;
/*
* Singleton GameTextInput class with helper methods.
*/
public final class GameTextInput {
public final static void copyEditorInfo(EditorInfo from, EditorInfo to) {
if (from == null || to == null)
return;
if (from.hintText != null) {
to.hintText = from.hintText;
}
to.inputType = from.inputType;
to.imeOptions = from.imeOptions;
to.label = from.label;
to.initialCapsMode = from.initialCapsMode;
to.privateImeOptions = from.privateImeOptions;
if (from.packageName != null) {
to.packageName = from.packageName;
}
to.fieldId = from.fieldId;
if (from.fieldName != null) {
to.fieldName = from.fieldName;
}
to.initialSelStart = from.initialSelStart;
to.initialSelEnd = from.initialSelEnd;
}
private GameTextInput() {}
}

View File

@@ -0,0 +1,711 @@
/*
* 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;
import static android.view.inputmethod.EditorInfo.IME_ACTION_UNSPECIFIED;
import android.app.Activity;
import android.content.Context;
import android.os.Bundle;
import android.text.Editable;
import android.text.InputFilter;
import android.text.Selection;
import android.text.SpannableString;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.InputMethodManager;
import androidx.core.graphics.Insets;
import androidx.core.view.ViewCompat;
import androidx.core.view.WindowCompat;
import androidx.core.view.WindowInsetsCompat;
public class InputConnection extends BaseInputConnection implements View.OnKeyListener {
private static final String TAG = "gti.InputConnection";
private final InputMethodManager imm;
private final View targetView;
private final Settings settings;
private final Editable mEditable;
private Listener listener;
private boolean mSoftKeyboardActive;
/*
* This class filters EOL characters from the input. For details of how InputFilter.filter
* function works, refer to its documentation. If the suggested change is accepted without
* modifications, filter() should return null.
*/
private class SingeLineFilter implements InputFilter {
public CharSequence filter(
CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
boolean keepOriginal = true;
StringBuilder builder = new StringBuilder(end - start);
for (int i = start; i < end; i++) {
char c = source.charAt(i);
if (c == '\n') {
keepOriginal = false;
} else {
builder.append(c);
}
}
if (keepOriginal) {
return null;
}
if (source instanceof Spanned) {
SpannableString s = new SpannableString(builder);
TextUtils.copySpansFrom((Spanned) source, start, builder.length(), null, s, 0);
return s;
} else {
return builder;
}
}
}
private static final int MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT = 5000;
/**
* Constructor
*
* @param ctx The app's context
* @param targetView The view created this input connection
* @param settings EditorInfo and other settings needed by this class
* InputConnection.
*/
public InputConnection(Context ctx, View targetView, Settings settings) {
super(targetView, settings.mEditorInfo.inputType != 0);
Log.d(TAG, "InputConnection created");
this.targetView = targetView;
this.settings = settings;
Object imm = ctx.getSystemService(Context.INPUT_METHOD_SERVICE);
if (imm == null) {
throw new java.lang.RuntimeException("Can't get IMM");
} else {
this.imm = (InputMethodManager) imm;
this.mEditable = (Editable) (new SpannableStringBuilder());
}
// Listen for insets changes
WindowCompat.setDecorFitsSystemWindows(((Activity) targetView.getContext()).getWindow(), false);
targetView.setOnKeyListener(this);
// Apply EditorInfo settings
this.setEditorInfo(settings.mEditorInfo);
}
/**
* Restart the input method manager. This is useful to apply changes to the keyboard
* after calling setEditorInfo.
*/
public void restartInput() {
imm.restartInput(targetView);
}
/**
* Get whether the soft keyboard is visible.
*
* @return true if the soft keyboard is visible, false otherwise
*/
public final boolean getSoftKeyboardActive() {
return this.mSoftKeyboardActive;
}
/**
* Request the soft keyboard to become visible or invisible.
*
* @param active True if the soft keyboard should be made visible, otherwise false.
* @param flags See
* https://developer.android.com/reference/android/view/inputmethod/InputMethodManager#showSoftInput(android.view.View,%20int)
*/
public final void setSoftKeyboardActive(boolean active, int flags) {
Log.d(TAG, "setSoftKeyboardActive, active: " + active);
this.mSoftKeyboardActive = active;
if (active) {
this.targetView.setFocusableInTouchMode(true);
this.targetView.requestFocus();
this.imm.showSoftInput(this.targetView, flags);
} else {
this.imm.hideSoftInputFromWindow(this.targetView.getWindowToken(), flags);
}
restartInput();
}
/**
* Get the current EditorInfo used to configure the InputConnection's behaviour.
*
* @return The current EditorInfo.
*/
public final EditorInfo getEditorInfo() {
return this.settings.mEditorInfo;
}
/**
* Set the current EditorInfo used to configure the InputConnection's behaviour.
*
* @param editorInfo The EditorInfo to use
*/
public final void setEditorInfo(EditorInfo editorInfo) {
Log.d(TAG, "setEditorInfo");
settings.mEditorInfo = editorInfo;
// Depending on the multiline state, we might need a different set of filters.
// Filters are being used to filter specific characters for hardware keyboards
// (software input methods already support TYPE_TEXT_FLAG_MULTI_LINE).
if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0) {
mEditable.setFilters(
new InputFilter[] {new InputFilter.LengthFilter(MAX_LENGTH_FOR_SINGLE_LINE_EDIT_TEXT),
new SingeLineFilter()});
} else {
mEditable.setFilters(new InputFilter[] {});
}
}
/**
* Set the text, selection and composing region state.
*
* @param state The state to be used by the IME.
* This replaces any text, selections and composing regions currently active.
*/
public final void setState(State state) {
if (state == null)
return;
Log.d(TAG,
"setState: '" + state.text + "', selection=(" + state.selectionStart + ","
+ state.selectionEnd + "), composing region=(" + state.composingRegionStart + ","
+ state.composingRegionEnd + ")");
mEditable.clear();
mEditable.clearSpans();
mEditable.insert(0, (CharSequence) state.text);
setSelection(state.selectionStart, state.selectionEnd);
if (state.composingRegionStart != state.composingRegionEnd) {
setComposingRegion(state.composingRegionStart, state.composingRegionEnd);
}
restartInput();
}
/**
* Get the current listener for state changes.
*
* @return The current Listener
*/
public final Listener getListener() {
return listener;
}
/**
* Set a listener for state changes.
*
* @param listener
* @return This InputConnection, for setter chaining.
*/
public final InputConnection setListener(Listener listener) {
this.listener = listener;
return this;
}
// From View.OnKeyListener
@Override
public boolean onKey(View view, int i, KeyEvent keyEvent) {
Log.d(TAG, "onKey: " + keyEvent);
if (!getSoftKeyboardActive()) {
return false;
}
// Don't call sendKeyEvent as it might produce an infinite loop.
if (processKeyEvent(keyEvent)) {
// IMM seems to cache the content of Editable, so we update it with restartInput
// Also it caches selection and composing region, so let's notify it about updates.
stateUpdated();
immUpdateSelection();
restartInput();
return true;
}
return false;
}
// From BaseInputConnection
@Override
public Editable getEditable() {
Log.d(TAG, "getEditable");
return mEditable;
}
// From BaseInputConnection
@Override
public boolean setSelection(int start, int end) {
Log.d(TAG, "setSelection: " + start + ":" + end);
return super.setSelection(start, end);
}
// From BaseInputConnection
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
Log.d(
TAG, String.format("setComposingText='%s' newCursorPosition=%d", text, newCursorPosition));
if (text == null) {
return false;
}
return super.setComposingText(text, newCursorPosition);
}
@Override
public boolean setComposingRegion(int start, int end) {
Log.d(TAG, "setComposingRegion: " + start + ":" + end);
return super.setComposingRegion(start, end);
}
// From BaseInputConnection
@Override
public boolean finishComposingText() {
Log.d(TAG, "finishComposingText");
return super.finishComposingText();
}
@Override
public boolean endBatchEdit() {
Log.d(TAG, "endBatchEdit");
stateUpdated();
return super.endBatchEdit();
}
@Override
public boolean commitCompletion(CompletionInfo text) {
Log.d(TAG, "commitCompletion");
return super.commitCompletion(text);
}
@Override
public boolean commitCorrection(CorrectionInfo text) {
Log.d(TAG, "commitCompletion");
return super.commitCorrection(text);
}
// From BaseInputConnection
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
Log.d(TAG,
(new StringBuilder())
.append("commitText: ")
.append(text)
.append(", new pos = ")
.append(newCursorPosition)
.toString());
return super.commitText(text, newCursorPosition);
}
// From BaseInputConnection
@Override
public boolean deleteSurroundingText(int beforeLength, int afterLength) {
Log.d(TAG, "deleteSurroundingText: " + beforeLength + ":" + afterLength);
return super.deleteSurroundingText(beforeLength, afterLength);
}
// From BaseInputConnection
@Override
public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
Log.d(TAG, "deleteSurroundingTextInCodePoints: " + beforeLength + ":" + afterLength);
return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength);
}
// From BaseInputConnection
@Override
public boolean sendKeyEvent(KeyEvent event) {
Log.d(TAG, "sendKeyEvent: " + event);
return super.sendKeyEvent(event);
}
// From BaseInputConnection
@Override
public CharSequence getSelectedText(int flags) {
CharSequence result = super.getSelectedText(flags);
if (result == null) {
result = "";
}
Log.d(TAG, "getSelectedText: " + flags + ", result: " + result);
return result;
}
// From BaseInputConnection
@Override
public CharSequence getTextAfterCursor(int length, int flags) {
Log.d(TAG, "getTextAfterCursor: " + length + ":" + flags);
if (length < 0) {
Log.i(TAG, "getTextAfterCursor: returning null to due to an invalid length=" + length);
return null;
}
return super.getTextAfterCursor(length, flags);
}
// From BaseInputConnection
@Override
public CharSequence getTextBeforeCursor(int length, int flags) {
Log.d(TAG, "getTextBeforeCursor: " + length + ", flags=" + flags);
if (length < 0) {
Log.i(TAG, "getTextBeforeCursor: returning null to due to an invalid length=" + length);
return null;
}
return super.getTextBeforeCursor(length, flags);
}
// From BaseInputConnection
@Override
public boolean requestCursorUpdates(int cursorUpdateMode) {
Log.d(TAG, "Request cursor updates: " + cursorUpdateMode);
return super.requestCursorUpdates(cursorUpdateMode);
}
// From BaseInputConnection
@Override
public void closeConnection() {
Log.d(TAG, "closeConnection");
super.closeConnection();
}
@Override
public boolean setImeConsumesInput(boolean imeConsumesInput) {
Log.d(TAG, "setImeConsumesInput: " + imeConsumesInput);
return super.setImeConsumesInput(imeConsumesInput);
}
@Override
public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
Log.d(TAG, "getExtractedText");
return super.getExtractedText(request, flags);
}
@Override
public boolean performPrivateCommand(String action, Bundle data) {
Log.d(TAG, "performPrivateCommand");
return super.performPrivateCommand(action, data);
}
private void immUpdateSelection() {
Pair selection = this.getSelection();
Pair cr = this.getComposingRegion();
Log.d(TAG,
"immUpdateSelection: " + selection.first + "," + selection.second + ". " + cr.first + ","
+ cr.second);
settings.mEditorInfo.initialSelStart = selection.first;
settings.mEditorInfo.initialSelEnd = selection.second;
imm.updateSelection(targetView, selection.first, selection.second, cr.first, cr.second);
}
private Pair getSelection() {
return new Pair(Selection.getSelectionStart(mEditable), Selection.getSelectionEnd(mEditable));
}
private Pair getComposingRegion() {
return new Pair(getComposingSpanStart(mEditable), getComposingSpanEnd(mEditable));
}
private boolean processKeyEvent(KeyEvent event) {
if (event == null) {
return false;
}
int keyCode = event.getKeyCode();
Log.d(
TAG, String.format("processKeyEvent(key=%d) text=%s", keyCode, this.mEditable.toString()));
// Filter out Enter keys if multi-line mode is disabled.
if ((settings.mEditorInfo.inputType & EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) == 0
&& (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_NUMPAD_ENTER)
&& event.hasNoModifiers()) {
sendEditorAction(settings.mEditorInfo.actionId);
return true;
}
if (event.getAction() != KeyEvent.ACTION_DOWN) {
return false;
}
// If no selection is set, move the selection to the end.
// This is the case when first typing on keys when the selection is not set.
// Note that for InputType.TYPE_CLASS_TEXT, this is not be needed because the
// selection is set in setComposingText.
Pair selection = this.getSelection();
if (selection.first == -1) {
selection.first = this.mEditable.length();
selection.second = this.mEditable.length();
}
if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) {
if (selection.first == selection.second) {
int newIndex = findIndexBackward(mEditable, selection.first, 1);
setSelection(newIndex, newIndex);
} else {
setSelection(selection.first, selection.first);
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
if (selection.first == selection.second) {
int newIndex = findIndexForward(mEditable, selection.second, 1);
setSelection(newIndex, newIndex);
} else {
setSelection(selection.second, selection.second);
}
return true;
}
if (keyCode == KeyEvent.KEYCODE_MOVE_HOME) {
setSelection(0, 0);
return true;
}
if (keyCode == KeyEvent.KEYCODE_MOVE_END) {
setSelection(this.mEditable.length(), this.mEditable.length());
return true;
}
if (keyCode == KeyEvent.KEYCODE_DEL || keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
if (selection.first != selection.second) {
this.mEditable.delete(selection.first, selection.second);
return true;
}
if (keyCode == KeyEvent.KEYCODE_DEL) {
if (selection.first > 0) {
finishComposingText();
deleteSurroundingTextInCodePoints(1, 0);
return true;
}
}
if (keyCode == KeyEvent.KEYCODE_FORWARD_DEL) {
if (selection.first < this.mEditable.length()) {
finishComposingText();
deleteSurroundingTextInCodePoints(0, 1);
return true;
}
}
return false;
}
if (event.getUnicodeChar() == 0) {
return false;
}
if (selection.first != selection.second) {
Log.d(TAG, String.format("processKeyEvent: deleting selection"));
this.mEditable.delete(selection.first, selection.second);
}
String charsToInsert = Character.toString((char) event.getUnicodeChar());
this.mEditable.insert(selection.first, (CharSequence) charsToInsert);
int length = this.mEditable.length();
// Same logic as in setComposingText(): we must update composing region,
// so make sure it points to a valid range.
Pair composingRegion = this.getComposingRegion();
if (composingRegion.first == -1) {
composingRegion = this.getSelection();
if (composingRegion.first == -1) {
composingRegion = new Pair(0, 0);
}
}
composingRegion.second = composingRegion.first + length;
this.setComposingRegion(composingRegion.first, composingRegion.second);
int new_cursor = selection.first + charsToInsert.length();
setSelection(new_cursor, new_cursor);
Log.d(TAG, String.format("processKeyEvent: exit, text=%s", this.mEditable.toString()));
return true;
}
private final void stateUpdated() {
Pair selection = this.getSelection();
Pair cr = this.getComposingRegion();
State state = new State(
this.mEditable.toString(), selection.first, selection.second, cr.first, cr.second);
settings.mEditorInfo.initialSelStart = selection.first;
settings.mEditorInfo.initialSelEnd = selection.second;
// Keep a reference to the listener to avoid a race condition when setting the listener.
Listener listener = this.listener;
// We always propagate state change events because unfortunately keyboard visibility functions
// are unreliable, and text editor logic should not depend on them.
if (listener != null) {
listener.stateChanged(state, /*dismissed=*/false);
}
}
/**
* Get the current IME insets.
*
* @return The current IME insets
*/
public Insets getImeInsets() {
if (this.targetView == null) {
return Insets.NONE;
}
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(this.targetView);
if (insets == null) {
return Insets.NONE;
}
return insets.getInsets(WindowInsetsCompat.Type.ime());
}
/**
* Returns true if software keyboard is visible, false otherwise.
*
* @return whether software IME is visible or not.
*/
public boolean isSoftwareKeyboardVisible() {
if (this.targetView == null) {
return false;
}
WindowInsetsCompat insets = ViewCompat.getRootWindowInsets(this.targetView);
if (insets == null) {
return false;
}
return insets.isVisible(WindowInsetsCompat.Type.ime());
}
/**
* This is an event handler from InputConnection interface.
* It's called when action button is triggered (typically this means Enter was pressed).
*
* @param action Action code, either one from EditorInfo.imeOptions or a custom one.
* @return Returns true on success, false if the input connection is no longer valid.
*/
@Override
public boolean performEditorAction(int action) {
Log.d(TAG, "performEditorAction, action=" + action);
if (action == IME_ACTION_UNSPECIFIED) {
// Super emulates Enter key press/release
return super.performEditorAction(action);
}
return sendEditorAction(action);
}
/**
* Delivers editor action to listener
*
* @param action Action code, either one from EditorInfo.imeOptions or a custom one.
* @return Returns true on success, false if the input connection is no longer valid.
*/
private boolean sendEditorAction(int action) {
Listener listener = this.listener;
if (listener != null) {
listener.onEditorAction(action);
return true;
}
return false;
}
private static int INVALID_INDEX = -1;
// Implementation copy from BaseInputConnection
private static int findIndexBackward(
final CharSequence cs, final int from, final int numCodePoints) {
int currentIndex = from;
boolean waitingHighSurrogate = false;
final int N = cs.length();
if (currentIndex < 0 || N < currentIndex) {
return INVALID_INDEX; // The starting point is out of range.
}
if (numCodePoints < 0) {
return INVALID_INDEX; // Basically this should not happen.
}
int remainingCodePoints = numCodePoints;
while (true) {
if (remainingCodePoints == 0) {
return currentIndex; // Reached to the requested length in code points.
}
--currentIndex;
if (currentIndex < 0) {
if (waitingHighSurrogate) {
return INVALID_INDEX; // An invalid surrogate pair is found.
}
return 0; // Reached to the beginning of the text w/o any invalid surrogate pair.
}
final char c = cs.charAt(currentIndex);
if (waitingHighSurrogate) {
if (!java.lang.Character.isHighSurrogate(c)) {
return INVALID_INDEX; // An invalid surrogate pair is found.
}
waitingHighSurrogate = false;
--remainingCodePoints;
continue;
}
if (!java.lang.Character.isSurrogate(c)) {
--remainingCodePoints;
continue;
}
if (java.lang.Character.isHighSurrogate(c)) {
return INVALID_INDEX; // A invalid surrogate pair is found.
}
waitingHighSurrogate = true;
}
}
// Implementation copy from BaseInputConnection
private static int findIndexForward(
final CharSequence cs, final int from, final int numCodePoints) {
int currentIndex = from;
boolean waitingLowSurrogate = false;
final int N = cs.length();
if (currentIndex < 0 || N < currentIndex) {
return INVALID_INDEX; // The starting point is out of range.
}
if (numCodePoints < 0) {
return INVALID_INDEX; // Basically this should not happen.
}
int remainingCodePoints = numCodePoints;
while (true) {
if (remainingCodePoints == 0) {
return currentIndex; // Reached to the requested length in code points.
}
if (currentIndex >= N) {
if (waitingLowSurrogate) {
return INVALID_INDEX; // An invalid surrogate pair is found.
}
return N; // Reached to the end of the text w/o any invalid surrogate pair.
}
final char c = cs.charAt(currentIndex);
if (waitingLowSurrogate) {
if (!java.lang.Character.isLowSurrogate(c)) {
return INVALID_INDEX; // An invalid surrogate pair is found.
}
--remainingCodePoints;
waitingLowSurrogate = false;
++currentIndex;
continue;
}
if (!java.lang.Character.isSurrogate(c)) {
--remainingCodePoints;
++currentIndex;
continue;
}
if (java.lang.Character.isLowSurrogate(c)) {
return INVALID_INDEX; // A invalid surrogate pair is found.
}
waitingLowSurrogate = true;
++currentIndex;
}
}
}

View File

@@ -0,0 +1,55 @@
/*
* 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;
import androidx.core.graphics.Insets;
/**
* Listener interface for text, selection and composing region changes.
* Also a listener for window insets changes.
*/
public interface Listener {
/*
* Called when the IME text, selection or composing region has changed.
*
* @param newState The updated state
* @param dismmissed Deprecated, don't use
*/
void stateChanged(State newState, boolean dismissed);
/*
* Called when the IME window insets change, i.e. the IME moves into or out of view.
*
* @param insets The new window insets, i.e. the offsets of top, bottom, left and right
* relative to the window
*/
void onImeInsetsChanged(Insets insets);
/*
* Called when the IME window is shown or hidden.
*
* @param insets True is IME is visible, false otherwise.
*/
void onSoftwareKeyboardVisibilityChanged(boolean visible);
/*
* Called when any editor action is performed. Typically this means that
* the Enter button has been pressed.
*
* @param action Code of the action. A default action is IME_ACTION_DONE.
*/
void onEditorAction(int action);
}

View File

@@ -0,0 +1,25 @@
/*
* 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;
}
}

View File

@@ -0,0 +1,29 @@
/*
* 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;
import android.view.inputmethod.EditorInfo;
// Settings for InputConnection
public final class Settings {
EditorInfo mEditorInfo;
boolean mForwardKeyEvents;
public Settings(EditorInfo editorInfo, boolean forwardKeyEvents) {
mEditorInfo = editorInfo;
mForwardKeyEvents = forwardKeyEvents;
}
}

View File

@@ -0,0 +1,34 @@
/*
* 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;
// The state of an editable text region.
public final class State {
public State(String text_in, int selectionStart_in, int selectionEnd_in,
int composingRegionStart_in, int composingRegionEnd_in) {
text = text_in;
selectionStart = selectionStart_in;
selectionEnd = selectionEnd_in;
composingRegionStart = composingRegionStart_in;
composingRegionEnd = composingRegionEnd_in;
}
public String text;
public int selectionStart;
public int selectionEnd;
public int composingRegionStart;
public int composingRegionEnd;
}

View File

@@ -1,6 +1,18 @@
main_activity_inject = "java/MainActivity.java"
java_files = [
"java/ForegroundService.java",
"java/videodecode/VideoDecoder.java"
"java/videodecode/VideoDecoder.java",
"java/textinput/InputConnection.java",
"java/textinput/State.java",
"java/textinput/Listener.java",
"java/textinput/Settings.java",
"java/textinput/GameTextInput.java",
"java/textinput/Pair.java"
]
comptime_jar_files = [
"android-libs/androidx/core-1.9.0.jar"
]
runtime_jar_files = [
"android-libs/androidx/core-1.9.0.jar"
]

View File

@@ -1,111 +0,0 @@
/* 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,52 @@
/* GameTextInput JNI bridge functions */
use crate::android::textinput::{init_game_text_input, GAME_TEXT_INPUT};
use miniquad::native::android::ndk_sys;
/// Set the InputConnection for GameTextInput (called from Java)
///
/// This follows the official Android GameTextInput integration pattern:
/// https://developer.android.com/games/agdk/add-support-for-text-input
///
/// 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);
}
}
/// Process IME state event from Java Listener.stateChanged()
///
/// 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);
}
}

View File

@@ -16,20 +16,36 @@
* 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,
};
use async_channel::Sender as AsyncSender;
use miniquad::native::android::attach_jni_env;
use parking_lot::RwLock;
use std::sync::LazyLock;
mod ffi;
use ffi::{
GameTextInput, GameTextInputSpan, GameTextInputState, GameTextInput_destroy,
GameTextInput_getState, GameTextInput_hideIme, GameTextInput_init,
GameTextInput_setEventCallback, GameTextInput_setState, GameTextInput_showIme,
};
mod jni;
mod state;
use state::GameTextInput;
/// 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()
}
// Text input state exposed to the rest of the app
#[derive(Debug, Clone)]
@@ -39,127 +55,54 @@ pub struct AndroidTextInputState {
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);
impl AndroidTextInputState {
fn new() -> Self {
Self { text: String::new(), select: (0, 0), compose: None }
}
}
pub struct AndroidTextInput {
id: usize,
state: *mut GameTextInput,
state: AndroidTextInputState,
sender: async_channel::Sender<AndroidTextInputState>,
is_focus: bool,
}
// SAFETY: GameTextInput is accessed synchronously through show_ime/hide_ime/set_state/get_state
// The pointer is valid for the lifetime of the AndroidTextInput instance and is only
// accessed from the thread that owns the AndroidTextInput.
unsafe impl Send for AndroidTextInput {}
unsafe impl Sync for AndroidTextInput {}
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 }
Self { state: AndroidTextInputState::new(), sender, is_focus: false }
}
pub fn show_ime(&self) {
unsafe {
GameTextInput_showIme(self.state, 0);
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 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 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 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);
pub fn get_state(&self) -> &AndroidTextInputState {
&self.state
}
}

View File

@@ -0,0 +1,327 @@
/* 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 async_channel::Sender as AsyncSender;
use miniquad::native::android::{ndk_sys, ndk_utils::*};
use parking_lot::Mutex as SyncMutex;
use std::ffi::CString;
use super::AndroidTextInputState;
const DEFAULT_MAX_STRING_SIZE: usize = 1 << 16;
pub const SPAN_UNDEFINED: i32 = -1;
struct StateClassInfo {
text: ndk_sys::jfieldID,
selection_start: ndk_sys::jfieldID,
selection_end: ndk_sys::jfieldID,
composing_region_start: ndk_sys::jfieldID,
composing_region_end: ndk_sys::jfieldID,
}
pub struct GameTextInput {
env: *mut ndk_sys::JNIEnv,
state: SyncMutex<AndroidTextInputState>,
input_connection: Option<ndk_sys::jobject>,
input_connection_class: ndk_sys::jclass,
set_soft_keyboard_active_method: ndk_sys::jmethodID,
restart_input_method: 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 {
unsafe {
let find_class = (**env).FindClass.unwrap();
let state_class_name = b"textinput/State\0";
let input_connection_class_name = b"textinput/InputConnection\0";
let state_java_class = find_class(env, state_class_name.as_ptr() as _);
let input_connection_class = find_class(env, input_connection_class_name.as_ptr() as _);
let input_connection_class =
new_global_ref!(env, input_connection_class) as ndk_sys::jclass;
let get_method_id = (**env).GetMethodID.unwrap();
let set_state_sig = b"(Ltextinput/State;)V\0";
let _input_connection_set_state_method = get_method_id(
env,
input_connection_class,
b"setState\0".as_ptr() as _,
set_state_sig.as_ptr() as _,
);
let set_soft_keyboard_active_sig = b"(ZI)V\0";
let set_soft_keyboard_active_method = get_method_id(
env,
input_connection_class,
b"setSoftKeyboardActive\0".as_ptr() as _,
set_soft_keyboard_active_sig.as_ptr() as _,
);
let restart_input_sig = b"()V\0";
let restart_input_method = get_method_id(
env,
input_connection_class,
b"restartInput\0".as_ptr() as _,
restart_input_sig.as_ptr() as _,
);
let state_java_class = new_global_ref!(env, state_java_class);
let get_field_id = (**env).GetFieldID.unwrap();
let text_field = get_field_id(
env,
state_java_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,
b"selectionStart\0".as_ptr() as _,
b"I\0".as_ptr() as _,
);
let selection_end_field = get_field_id(
env,
state_java_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,
b"composingRegionStart\0".as_ptr() as _,
b"I\0".as_ptr() as _,
);
let composing_region_end_field = get_field_id(
env,
state_java_class,
b"composingRegionEnd\0".as_ptr() as _,
b"I\0".as_ptr() as _,
);
let state_class_info = StateClassInfo {
text: text_field,
selection_start: selection_start_field,
selection_end: selection_end_field,
composing_region_start: composing_region_start_field,
composing_region_end: composing_region_end_field,
};
Self {
env,
state: SyncMutex::new(AndroidTextInputState::new()),
input_connection: None,
input_connection_class,
set_soft_keyboard_active_method,
restart_input_method,
state_class_info,
event_sender: None,
}
}
}
pub fn set_state(&mut self, state: &AndroidTextInputState) {
if let Some(input_connection) = self.input_connection {
unsafe {
let jstate = self.state_to_java(state);
call_void_method!(
self.env,
input_connection,
"setState",
"(Ltextinput/State;)V",
jstate
);
let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
delete_local_ref(self.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) {
unsafe {
if let Some(old_ref) = self.input_connection {
let delete_global_ref = (**self.env).DeleteGlobalRef.unwrap();
delete_global_ref(self.env, old_ref);
}
self.input_connection = Some(new_global_ref!(self.env, input_connection));
}
}
pub fn process_event(&mut 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);
}
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,
);
}
}
}
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,
);
}
}
}
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);
}
}
}
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 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 (compose_start, compose_end) = match state.compose {
Some((start, end)) => (start as i32, end as i32),
None => (SPAN_UNDEFINED, SPAN_UNDEFINED),
};
let jobj = new_object(
self.env,
state_java_class,
constructor,
jtext,
state.select.0 as i32,
state.select.1 as i32,
compose_start,
compose_end,
);
let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
delete_local_ref(self.env, jtext);
delete_local_ref(self.env, state_java_class);
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 text = get_utf_str!(self.env, jtext);
let get_int_field = (**self.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);
let compose_start =
get_int_field(self.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);
let delete_local_ref = (**self.env).DeleteLocalRef.unwrap();
delete_local_ref(self.env, jtext);
let compose = if compose_start >= 0 {
Some((compose_start as usize, compose_end as usize))
} else {
assert!(compose_end < 0);
None
};
AndroidTextInputState {
text,
select: (select_start as usize, select_end as usize),
compose,
}
}
}
}
impl Drop for GameTextInput {
fn drop(&mut self) {
unsafe {
let delete_global_ref = (**self.env).DeleteGlobalRef.unwrap();
if self.input_connection_class != std::ptr::null_mut() {
delete_global_ref(self.env, self.input_connection_class);
}
if let Some(input_connection) = self.input_connection {
delete_global_ref(self.env, input_connection);
}
}
}
}
unsafe impl Send for GameTextInput {}
unsafe impl Sync for GameTextInput {}

View File

@@ -529,23 +529,15 @@ pub async fn make(app: &App, window: SceneNodePtr, i18n_fish: &I18nBabelFish) {
let chatdb_path = get_chatdb_path();
let db = sled::open(chatdb_path).expect("cannot open sleddb");
for channel in CHANNELS {
chat::make(
app,
content.clone(),
channel,
&db,
i18n_fish,
emoji_meshes.clone(),
is_first_time,
)
//for channel in CHANNELS {
chat::make(app, content.clone(), "dev", &db, i18n_fish, emoji_meshes.clone(), is_first_time)
.await;
}
menu::make(app, content.clone(), i18n_fish).await;
//}
//menu::make(app, content.clone(), i18n_fish).await;
// @@@ Debug stuff @@@
//let chatview_node = app.sg_root.lookup_node("/window/content/dev_chat_layer").unwrap();
//chatview_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
let chatview_node = app.sg_root.lookup_node("/window/content/dev_chat_layer").unwrap();
chatview_node.set_property_bool(atom, Role::App, "is_visible", true).unwrap();
//let menu_node = app.sg_root.lookup_node("/window/content/menu_layer").unwrap();
//menu_node.set_property_bool(atom, Role::App, "is_visible", false).unwrap();
}

View File

@@ -53,8 +53,10 @@ impl Editor {
lineheight: PropertyFloat32,
) -> Self {
let (sender, recvr) = async_channel::unbounded();
let input = AndroidTextInput::new(sender);
Self {
input: AndroidTextInput::new(sender),
input,
recvr,
layout: Default::default(),
@@ -70,8 +72,11 @@ impl Editor {
pub async fn on_text_prop_changed(&mut self) {
// Update GameTextInput state
let state = AndroidTextInputState { text: self.text.get(), select: (0, 0), compose: None };
self.input.set_state(&state);
let mut state = self.input.get_state().clone();
state.text = self.text.get();
state.select = (0, 0);
state.compose = None;
self.input.set_state(state);
// Refresh our layout
self.refresh().await;
}
@@ -84,11 +89,11 @@ impl Editor {
self.text.set(atom, &state.text);
}
pub fn focus(&self) {
self.input.show_ime();
pub fn focus(&mut self) {
self.input.show();
}
pub fn unfocus(&self) {
self.input.hide_ime();
pub fn unfocus(&mut self) {
self.input.hide();
}
pub async fn refresh(&mut self) {
@@ -120,16 +125,15 @@ impl Editor {
&self.layout
}
pub fn move_to_pos(&self, pos: Point) {
pub fn move_to_pos(&mut self, pos: Point) {
let cursor = parley::Cursor::from_point(&self.layout, pos.x, pos.y);
let cursor_idx = cursor.index();
t!(" move_to_pos: {cursor_idx}");
let state = AndroidTextInputState {
text: self.text.get(),
select: (cursor_idx, cursor_idx),
compose: None,
};
self.input.set_state(&state);
let mut state = self.input.get_state().clone();
state.text = self.text.get();
state.select = (cursor_idx, cursor_idx);
state.compose = None;
self.input.set_state(state);
}
pub async fn select_word_at_point(&mut self, pos: Point) {
@@ -161,11 +165,11 @@ impl Editor {
pub async fn insert(&mut self, txt: &str, atom: &mut PropertyAtomicGuard) {
// TODO: need to verify this is correct
// Insert text by updating the state
let mut current_state = self.input.get_state();
let mut current_state = self.input.get_state().clone();
current_state.text.push_str(txt);
current_state.select = (current_state.text.len(), current_state.text.len());
current_state.compose = None;
self.input.set_state(&current_state);
self.input.set_state(current_state);
self.on_buffer_changed(atom).await;
}
@@ -213,16 +217,15 @@ impl Editor {
parley::Selection::new(anchor, focus)
}
pub async fn set_selection(&mut self, select_start: usize, select_end: usize) {
let state = AndroidTextInputState {
text: self.text.get(),
select: (select_start, select_end),
compose: None,
};
self.input.set_state(&state);
let mut state = self.input.get_state().clone();
state.text = self.text.get();
state.select = (select_start, select_end);
state.compose = None;
self.input.set_state(state);
}
#[allow(dead_code)]
pub fn buffer(&self) -> String {
self.input.get_state().text
self.input.get_state().text.clone()
}
}

View File

@@ -1271,7 +1271,7 @@ impl BaseEdit {
panic!("self destroyed before insert_text_method_task was stopped!");
};
let editor = self_.lock_editor().await;
let mut editor = self_.lock_editor().await;
editor.focus();
true
}
@@ -1290,7 +1290,7 @@ impl BaseEdit {
panic!("self destroyed before insert_text_method_task was stopped!");
};
let editor = self_.lock_editor().await;
let mut editor = self_.lock_editor().await;
editor.unfocus();
true
}