From d131828285a332e01de336b2327daad4da7421dc Mon Sep 17 00:00:00 2001 From: jkds Date: Sat, 3 Jan 2026 11:22:03 +0100 Subject: [PATCH] app: reduce JNI overhead when adjusting a selection by creating a special setSelection() fn so we dont have to pass the entire state over the bridge. --- bin/app/java/textinput/InputConnection.java | 73 ++++++++++--------- .../src/android/textinput/gametextinput.rs | 20 ++++- bin/app/src/android/textinput/mod.rs | 18 +++++ bin/app/src/app/schema/chat.rs | 14 +++- bin/app/src/app/schema/mod.rs | 20 +++-- bin/app/src/text2/editor/android.rs | 9 ++- bin/app/src/ui/edit/mod.rs | 9 ++- 7 files changed, 110 insertions(+), 53 deletions(-) diff --git a/bin/app/java/textinput/InputConnection.java b/bin/app/java/textinput/InputConnection.java index 5f411d870..5b4d4929c 100644 --- a/bin/app/java/textinput/InputConnection.java +++ b/bin/app/java/textinput/InputConnection.java @@ -45,7 +45,6 @@ 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; private final View targetView; private final Settings settings; @@ -53,6 +52,10 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi private Listener listener; private boolean mSoftKeyboardActive; + private void log(String text) { + //Log.d("darkfi", text); + } + /* * 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 @@ -100,7 +103,7 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi */ public InputConnection(Context ctx, View targetView, Settings settings) { super(targetView, settings.mEditorInfo.inputType != 0); - Log.d(TAG, "InputConnection created"); + log("InputConnection created"); this.targetView = targetView; this.settings = settings; @@ -143,7 +146,7 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi * 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); + log("setSoftKeyboardActive, active: " + active); this.mSoftKeyboardActive = active; if (active) { @@ -171,7 +174,7 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi * @param editorInfo The EditorInfo to use */ public final void setEditorInfo(EditorInfo editorInfo) { - Log.d(TAG, "setEditorInfo"); + log("setEditorInfo"); settings.mEditorInfo = editorInfo; // Depending on the multiline state, we might need a different set of filters. @@ -195,7 +198,7 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi public final void setState(State state) { if (state == null) return; - Log.d(TAG, + log( "setState: '" + state.text + "', selection=(" + state.selectionStart + "," + state.selectionEnd + "), composing region=(" + state.composingRegionStart + "," + state.composingRegionEnd + ")"); @@ -232,7 +235,7 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi // From View.OnKeyListener @Override public boolean onKey(View view, int i, KeyEvent keyEvent) { - Log.d(TAG, "onKey: " + keyEvent); + log("onKey: " + keyEvent); if (!getSoftKeyboardActive()) { return false; } @@ -251,22 +254,22 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi // From BaseInputConnection @Override public Editable getEditable() { - Log.d(TAG, "getEditable"); + log("getEditable"); return mEditable; } // From BaseInputConnection @Override public boolean setSelection(int start, int end) { - Log.d(TAG, "setSelection: " + start + ":" + end); + log("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)); + log( + "setComposingText='" + text + "' newCursorPosition=" + newCursorPosition); if (text == null) { return false; } @@ -275,40 +278,40 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi @Override public boolean setComposingRegion(int start, int end) { - Log.d(TAG, "setComposingRegion: " + start + ":" + end); + log("setComposingRegion: " + start + ":" + end); return super.setComposingRegion(start, end); } // From BaseInputConnection @Override public boolean finishComposingText() { - Log.d(TAG, "finishComposingText"); + log("finishComposingText"); return super.finishComposingText(); } @Override public boolean endBatchEdit() { - Log.d(TAG, "endBatchEdit"); + log("endBatchEdit"); stateUpdated(); return super.endBatchEdit(); } @Override public boolean commitCompletion(CompletionInfo text) { - Log.d(TAG, "commitCompletion"); + log("commitCompletion"); return super.commitCompletion(text); } @Override public boolean commitCorrection(CorrectionInfo text) { - Log.d(TAG, "commitCompletion"); + log("commitCompletion"); return super.commitCorrection(text); } // From BaseInputConnection @Override public boolean commitText(CharSequence text, int newCursorPosition) { - Log.d(TAG, + log( (new StringBuilder()) .append("commitText: ") .append(text) @@ -321,21 +324,21 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi // From BaseInputConnection @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { - Log.d(TAG, "deleteSurroundingText: " + beforeLength + ":" + afterLength); + log("deleteSurroundingText: " + beforeLength + ":" + afterLength); return super.deleteSurroundingText(beforeLength, afterLength); } // From BaseInputConnection @Override public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { - Log.d(TAG, "deleteSurroundingTextInCodePoints: " + beforeLength + ":" + afterLength); + log("deleteSurroundingTextInCodePoints: " + beforeLength + ":" + afterLength); return super.deleteSurroundingTextInCodePoints(beforeLength, afterLength); } // From BaseInputConnection @Override public boolean sendKeyEvent(KeyEvent event) { - Log.d(TAG, "sendKeyEvent: " + event); + log("sendKeyEvent: " + event); return super.sendKeyEvent(event); } @@ -346,16 +349,16 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi if (result == null) { result = ""; } - Log.d(TAG, "getSelectedText: " + flags + ", result: " + result); + log("getSelectedText: " + flags + ", result: " + result); return result; } // From BaseInputConnection @Override public CharSequence getTextAfterCursor(int length, int flags) { - Log.d(TAG, "getTextAfterCursor: " + length + ":" + flags); + log("getTextAfterCursor: " + length + ":" + flags); if (length < 0) { - Log.i(TAG, "getTextAfterCursor: returning null to due to an invalid length=" + length); + Log.i("darkfi", "getTextAfterCursor: returning null to due to an invalid length=" + length); return null; } return super.getTextAfterCursor(length, flags); @@ -364,9 +367,9 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi // From BaseInputConnection @Override public CharSequence getTextBeforeCursor(int length, int flags) { - Log.d(TAG, "getTextBeforeCursor: " + length + ", flags=" + flags); + log("getTextBeforeCursor: " + length + ", flags=" + flags); if (length < 0) { - Log.i(TAG, "getTextBeforeCursor: returning null to due to an invalid length=" + length); + Log.i("darkfi", "getTextBeforeCursor: returning null to due to an invalid length=" + length); return null; } return super.getTextBeforeCursor(length, flags); @@ -375,39 +378,39 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi // From BaseInputConnection @Override public boolean requestCursorUpdates(int cursorUpdateMode) { - Log.d(TAG, "Request cursor updates: " + cursorUpdateMode); + log("Request cursor updates: " + cursorUpdateMode); return super.requestCursorUpdates(cursorUpdateMode); } // From BaseInputConnection @Override public void closeConnection() { - Log.d(TAG, "closeConnection"); + log("closeConnection"); super.closeConnection(); } @Override public boolean setImeConsumesInput(boolean imeConsumesInput) { - Log.d(TAG, "setImeConsumesInput: " + imeConsumesInput); + log("setImeConsumesInput: " + imeConsumesInput); return super.setImeConsumesInput(imeConsumesInput); } @Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { - Log.d(TAG, "getExtractedText"); + log("getExtractedText"); return super.getExtractedText(request, flags); } @Override public boolean performPrivateCommand(String action, Bundle data) { - Log.d(TAG, "performPrivateCommand"); + log("performPrivateCommand"); return super.performPrivateCommand(action, data); } private void immUpdateSelection() { Pair selection = this.getSelection(); Pair cr = this.getComposingRegion(); - Log.d(TAG, + log( "immUpdateSelection: " + selection.first + "," + selection.second + ". " + cr.first + "," + cr.second); settings.mEditorInfo.initialSelStart = selection.first; @@ -428,8 +431,8 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi return false; } int keyCode = event.getKeyCode(); - Log.d( - TAG, String.format("processKeyEvent(key=%d) text=%s", keyCode, this.mEditable.toString())); + log( + "processKeyEvent(key=" + keyCode + ") text=" + 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) @@ -503,7 +506,7 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi } if (selection.first != selection.second) { - Log.d(TAG, String.format("processKeyEvent: deleting selection")); + log("processKeyEvent: deleting selection"); this.mEditable.delete(selection.first, selection.second); } @@ -525,7 +528,7 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi 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())); + log("processKeyEvent: exit, text=" + this.mEditable.toString()); return true; } @@ -594,7 +597,7 @@ public class InputConnection extends BaseInputConnection implements View.OnKeyLi */ @Override public boolean performEditorAction(int action) { - Log.d(TAG, "performEditorAction, action=" + action); + log("performEditorAction, action=" + action); if (action == IME_ACTION_UNSPECIFIED) { // Super emulates Enter key press/release return super.performEditorAction(action); diff --git a/bin/app/src/android/textinput/gametextinput.rs b/bin/app/src/android/textinput/gametextinput.rs index e3294c8f2..18481e918 100644 --- a/bin/app/src/android/textinput/gametextinput.rs +++ b/bin/app/src/android/textinput/gametextinput.rs @@ -173,8 +173,11 @@ impl GameTextInput { let mut new_focus = state.lock(); // Mark new state as active new_focus.is_active = true; + let new_state = new_focus.state.clone(); + drop(new_focus); + // Push changes to the Java side - self.push_update(&new_focus.state); + self.push_update(&new_state); } pub fn push_update(&self, state: &AndroidTextInputState) { @@ -192,6 +195,21 @@ impl GameTextInput { } } + pub fn set_select(&self, start: i32, end: i32) -> Result<(), ()> { + let Some(input_connection) = *self.input_connection.read() else { + w!("push_update() - no input_connection set"); + return Err(()) + }; + let is_success = unsafe { + let env = android::attach_jni_env(); + call_bool_method!(env, input_connection, "setSelection", "(II)Z", start, end) + }; + if is_success == 0u8 { + return Err(()) + } + Ok(()) + } + pub fn set_input_connection(&self, input_connection: ndk_sys::jobject) { unsafe { let env = android::attach_jni_env(); diff --git a/bin/app/src/android/textinput/mod.rs b/bin/app/src/android/textinput/mod.rs index b41c19c4f..bacd76b31 100644 --- a/bin/app/src/android/textinput/mod.rs +++ b/bin/app/src/android/textinput/mod.rs @@ -87,4 +87,22 @@ impl AndroidTextInput { gti.push_update(&state); } } + + pub fn set_select(&self, select_start: usize, select_end: usize) { + //t!("set_select({select_start}, {select_end})"); + // Always update our own state. + let mut ours = self.state.lock(); + let state = &mut ours.state; + assert!(select_start <= state.text.len()); + assert!(select_end <= state.text.len()); + state.select = (select_start, select_end); + let is_active = ours.is_active; + drop(ours); + + // Only update java state when this input is active + if is_active { + let gti = GAME_TEXT_INPUT.get().unwrap(); + gti.set_select(select_start as i32, select_end as i32).unwrap(); + } + } } diff --git a/bin/app/src/app/schema/chat.rs b/bin/app/src/app/schema/chat.rs index 17d82f0cf..f88e25234 100644 --- a/bin/app/src/app/schema/chat.rs +++ b/bin/app/src/app/schema/chat.rs @@ -1559,13 +1559,19 @@ pub async fn make( let atom = &mut render_api.make_guard(gfxtag!("edit select task")); if editz_select_text.is_null(0).unwrap() { info!(target: "app::chat", "selection changed: null"); - actions_is_visible.set(atom, false); - pasta_is_visible2.set(atom, false); + // Avoid triggering unecessary redraws + if actions_is_visible.get() { + actions_is_visible.set(atom, false); + pasta_is_visible2.set(atom, false); + } } else { let select_text = editz_select_text.get_str(0).unwrap(); info!(target: "app::chat", "selection changed: {select_text}"); - actions_is_visible.set(atom, true); - pasta_is_visible2.set(atom, false); + // Avoid triggering unecessary redraws + if !actions_is_visible.get() { + actions_is_visible.set(atom, true); + pasta_is_visible2.set(atom, false); + } } } }); diff --git a/bin/app/src/app/schema/mod.rs b/bin/app/src/app/schema/mod.rs index c73333f47..8bd265689 100644 --- a/bin/app/src/app/schema/mod.rs +++ b/bin/app/src/app/schema/mod.rs @@ -529,15 +529,23 @@ 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(), "dev", &db, i18n_fish, emoji_meshes.clone(), is_first_time) + for channel in CHANNELS { + chat::make( + app, + content.clone(), + channel, + &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(); } diff --git a/bin/app/src/text2/editor/android.rs b/bin/app/src/text2/editor/android.rs index 33f7d99ff..c0905565d 100644 --- a/bin/app/src/text2/editor/android.rs +++ b/bin/app/src/text2/editor/android.rs @@ -132,9 +132,10 @@ impl Editor { let cursor_idx = cursor.index(); t!(" move_to_pos: {cursor_idx}"); assert!(cursor_idx <= self.state.text.len()); + assert_eq!(self.state.text, self.text.get()); self.state.select = (cursor_idx, cursor_idx); self.state.compose = None; - self.input.set_state(self.state.clone()); + self.input.set_select(cursor_idx, cursor_idx); } pub async fn select_word_at_point(&mut self, pos: Point) { @@ -216,10 +217,12 @@ impl Editor { parley::Selection::new(anchor, focus) } pub async fn set_selection(&mut self, select_start: usize, select_end: usize) { - self.state.text = self.text.get(); + assert!(select_start <= self.state.text.len()); + assert!(select_end <= self.state.text.len()); + assert_eq!(self.state.text, self.text.get()); self.state.select = (select_start, select_end); self.state.compose = None; - self.input.set_state(self.state.clone()); + self.input.set_select(select_start, select_end); } #[allow(dead_code)] diff --git a/bin/app/src/ui/edit/mod.rs b/bin/app/src/ui/edit/mod.rs index 1411bb3f5..7e1947127 100644 --- a/bin/app/src/ui/edit/mod.rs +++ b/bin/app/src/ui/edit/mod.rs @@ -1363,16 +1363,17 @@ impl UIObject for BaseEdit { fn init(&self) { let mut guard = self.editor.lock_blocking(); assert!(guard.is_none()); - let mut editor = Editor::new( + let editor = Editor::new( self.text.clone(), self.font_size.clone(), self.text_color.clone(), self.window_scale.clone(), self.lineheight.clone(), ); - let atom = &mut PropertyAtomicGuard::none(); - self.text.set(atom, "the quick brown fox jumped over the"); - smol::block_on(editor.on_text_prop_changed()); + // For Android you can do this: + //let atom = &mut PropertyAtomicGuard::none(); + //self.text.set(atom, "the quick brown fox jumped over the"); + //smol::block_on(editor.on_text_prop_changed()); *guard = Some(editor); }