diff --git a/android/java/uk/nandi/sleek/SleekActivity.java b/android/java/uk/nandi/sleek/SleekActivity.java index 734c456..7af7c3a 100644 --- a/android/java/uk/nandi/sleek/SleekActivity.java +++ b/android/java/uk/nandi/sleek/SleekActivity.java @@ -10,6 +10,7 @@ import android.net.Uri; import android.os.Build; import android.os.Bundle; import android.provider.Settings; +import android.view.KeyEvent; import android.view.View; import android.view.Window; import android.view.WindowInsets; @@ -57,6 +58,22 @@ public class SleekActivity extends NativeActivity { captureIntent(getIntent()); } + @Override + public boolean dispatchKeyEvent(KeyEvent event) { + // NativeActivity receives hardware / adb keyevents; the hidden IME EditText + // does not. Forward DEL so compose backspace works outside Gboard IC. + if (event != null && event.getAction() == KeyEvent.ACTION_DOWN) { + int code = event.getKeyCode(); + if (code == KeyEvent.KEYCODE_DEL || code == KeyEvent.KEYCODE_FORWARD_DEL) { + android.util.Log.i("SleekIme", "SleekActivity dispatchKeyEvent code=" + code); + if (SleekIme.onHostKeyDelete()) { + return true; + } + } + } + return super.dispatchKeyEvent(event); + } + @Override public void onWindowFocusChanged(boolean hasFocus) { super.onWindowFocusChanged(hasFocus); diff --git a/android/java/uk/nandi/sleek/SleekIme.java b/android/java/uk/nandi/sleek/SleekIme.java index c5a3468..8c2bd93 100644 --- a/android/java/uk/nandi/sleek/SleekIme.java +++ b/android/java/uk/nandi/sleek/SleekIme.java @@ -5,9 +5,11 @@ import android.content.Context; import android.text.Editable; import android.text.InputType; import android.text.Selection; +import android.text.TextWatcher; import android.util.Log; import android.view.View; import android.view.ViewGroup; +import android.view.WindowManager; import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; @@ -23,6 +25,11 @@ import android.widget.FrameLayout; * {@code commitText}) never reaches egui. This invisible editor receives IME * composing events and forwards them to Rust via JNI. * + *

Deletes are forwarded via a {@link TextWatcher}: Gboard may shrink the + * Editable through {@code deleteSurroundingText}, {@code sendKeyEvent}, or + * direct key dispatch — all of those change the Editable without a single + * reliable callback. + * *

Compiled into {@code sleek_activity.dex} (APK {@code classes.dex}). */ public final class SleekIme { @@ -30,6 +37,23 @@ public final class SleekIme { private static volatile ImeBridgeEditText field; + /** True while Gboard holds a composing span — {@link #syncField} must not clobber it. */ + private static volatile boolean composing; + + /** True while {@link #syncField} is rewriting the Editable (do not treat as delete). */ + private static volatile boolean updatingFromSync; + + /** True while BridgeInputConnection is applying an IME mutation that already + * has an explicit JNI forward (preedit/commit). Suppresses TextWatcher deletes. + */ + private static volatile boolean updatingFromIme; + + /** + * Monotonic generation for {@link #syncField} runnables. Bumped on IME edits so + * a stale empty sync posted before commitText cannot wipe the new character. + */ + private static volatile int syncGen; + static { try { System.loadLibrary("sleek"); @@ -58,6 +82,53 @@ public final class SleekIme { private static native void onDeleteSurrounding(int beforeLength, int afterLength); + /** + * Hardware / {@code adb keyevent} DEL is delivered to {@link NativeActivity}, not the + * hidden EditText. Call from {@link SleekActivity#dispatchKeyEvent} while the bridge + * is focused so egui still deletes. + * + * @return true if the delete was handled + */ + public static boolean onHostKeyDelete() { + // NativeActivity often keeps window key-focus even while the bridge + // EditText is the IME target (served=true). Do not require hasFocus(). + if (field == null || field.getVisibility() != View.VISIBLE) { + return false; + } + try { + Editable editable = field.getText(); + if (editable == null) { + return false; + } + int start = Selection.getSelectionStart(editable); + int end = Selection.getSelectionEnd(editable); + if (start < 0) { + start = editable.length(); + end = start; + } + if (end < 0) { + end = start; + } + if (start != end) { + editable.delete(Math.min(start, end), Math.max(start, end)); + Log.i(TAG, "onHostKeyDelete selection"); + return true; + } + if (start > 0) { + editable.delete(start - 1, start); + Log.i(TAG, "onHostKeyDelete char"); + return true; + } + // Already empty — do not synthesize another Backspace (that Disabled + // the IME session and left ime_cursor_range stale for the next commit). + Log.i(TAG, "onHostKeyDelete already empty"); + return true; + } catch (Throwable t) { + Log.w(TAG, "onHostKeyDelete failed", t); + return false; + } + } + /** * Show or hide the IME bridge. Called from Rust when egui wants keyboard input. * @@ -74,14 +145,22 @@ public final class SleekIme { /** * Mirror egui compose text/cursor into the hidden editor so Gboard can glide-type * mid-edit. Must not call {@code restartInput} — that resets the IME each frame. + * Skips while composing so we do not wipe Gboard's composing span mid-swipe. */ public static void syncField(Activity activity, String text, int selStart, int selEnd) { if (activity == null) { return; } final String safeText = text == null ? "" : text; + final int gen = syncGen; activity.runOnUiThread( - () -> syncFieldOnUiThread(activity, safeText, selStart, selEnd)); + () -> { + if (gen != syncGen) { + // Superseded by a newer IME edit or sync. + return; + } + syncFieldOnUiThread(activity, safeText, selStart, selEnd); + }); } private static void setActiveOnUiThread(Activity activity, boolean active) { @@ -98,11 +177,48 @@ public final class SleekIme { return; } if (active) { + // VISIBLE (alpha 0): IMM ignores showSoftInput for INVISIBLE/"not served". + field.setVisibility(View.VISIBLE); + field.setAlpha(0f); + if (activity.getWindow() != null) { + activity + .getWindow() + .setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE + | WindowManager.LayoutParams + .SOFT_INPUT_ADJUST_RESIZE); + } field.requestFocus(); - imm.showSoftInput(field, InputMethodManager.SHOW_IMPLICIT); + // Focus/serve often lands after this frame; post the show. + final ImeBridgeEditText target = field; + target.post( + () -> { + if (!target.hasFocus()) { + target.requestFocus(); + } + boolean shown = + imm.showSoftInput( + target, InputMethodManager.SHOW_IMPLICIT); + if (!shown) { + // Fallback: some NativeActivity focus fights need forced show. + imm.showSoftInput(target, 0); + } + Log.i(TAG, "showSoftInput served=" + target.isFocused() + + " shown=" + shown); + }); } else { + composing = false; imm.hideSoftInputFromWindow(field.getWindowToken(), 0); field.clearFocus(); + field.setVisibility(View.INVISIBLE); + if (activity.getWindow() != null) { + activity + .getWindow() + .setSoftInputMode( + WindowManager.LayoutParams.SOFT_INPUT_STATE_HIDDEN + | WindowManager.LayoutParams + .SOFT_INPUT_ADJUST_RESIZE); + } onPreedit(""); } } catch (Throwable t) { @@ -117,20 +233,70 @@ public final class SleekIme { if (field == null) { return; } - int len = text.length(); - int start = Math.max(0, Math.min(selStart, len)); - int end = Math.max(start, Math.min(selEnd, len)); + // Never overwrite mid-compose: egui's committed text lags Gboard's + // composing span, and replace() clears the span — next swipe dies. + if (composing) { + return; + } Editable editable = field.getText(); if (editable == null) { return; } - if (!text.contentEquals(editable)) { - editable.replace(0, editable.length(), text); + if (BaseInputConnection.getComposingSpanStart(editable) >= 0 + || BaseInputConnection.getComposingSpanEnd(editable) >= 0) { + composing = true; + return; + } + int len = text.length(); + int start = Math.max(0, Math.min(selStart, len)); + int end = Math.max(start, Math.min(selEnd, len)); + boolean changed = !text.contentEquals(editable); + if (changed) { + updatingFromSync = true; + try { + editable.replace(0, editable.length(), text); + } finally { + updatingFromSync = false; + } + } + // Avoid slamming the caret to 0 on a non-empty field (egui sometimes + // reports 0,0 before TextEdit state is ready) — that breaks backspace. + if (start == 0 && end == 0 && len > 0) { + int cur = Selection.getSelectionStart(editable); + if (cur > 0) { + start = cur; + end = Math.max(cur, Selection.getSelectionEnd(editable)); + } else { + start = len; + end = len; + } + } + int curStart = Selection.getSelectionStart(editable); + int curEnd = Selection.getSelectionEnd(editable); + if (curStart != start || curEnd != end) { + Selection.setSelection(editable, start, end); + changed = true; + } + if (changed) { + notifySelection(activity, editable, start, end); } - Selection.setSelection(editable, start, end); } catch (Throwable t) { Log.w(TAG, "syncField failed", t); + updatingFromSync = false; + } + } + + private static void notifySelection( + Activity activity, Editable editable, int start, int end) { + if (field == null || !field.hasFocus()) { + return; + } + InputMethodManager imm = + (InputMethodManager) activity.getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm == null) { + return; } + imm.updateSelection(field, start, end, -1, -1); } private static void ensureField(Activity activity) { @@ -140,9 +306,15 @@ public final class SleekIme { ImeBridgeEditText edit = new ImeBridgeEditText(activity); edit.setFocusable(true); edit.setFocusableInTouchMode(true); - // INVISIBLE (not GONE): IME still attaches; GONE can drop InputConnection. - edit.setVisibility(View.INVISIBLE); + edit.setClickable(true); + edit.setLongClickable(false); + edit.setCursorVisible(false); + // Keep VISIBLE at alpha 0 — IMM on modern Android refuses INVISIBLE views + // ("Ignoring showSoftInput() … is not served"). + edit.setVisibility(View.VISIBLE); + edit.setAlpha(0f); edit.setBackgroundColor(0x00000000); + edit.addTextChangedListener(new DeleteWatcher()); View content = activity.findViewById(android.R.id.content); if (content instanceof FrameLayout) { @@ -160,6 +332,35 @@ public final class SleekIme { field = edit; } + /** + * Forwards Editable shortening to Rust. Covers Gboard {@code deleteSurroundingText}, + * {@code sendKeyEvent(DEL)}, and any other path that mutates the editor. + */ + private static final class DeleteWatcher implements TextWatcher { + private int beforeLen; + + @Override + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + beforeLen = s == null ? 0 : s.length(); + } + + @Override + public void onTextChanged(CharSequence s, int start, int before, int count) {} + + @Override + public void afterTextChanged(Editable s) { + if (updatingFromSync || updatingFromIme || composing) { + return; + } + int afterLen = s == null ? 0 : s.length(); + int deleted = beforeLen - afterLen; + if (deleted > 0) { + Log.i(TAG, "textDeleted count=" + deleted + " len=" + afterLen); + nativeDeleteSurrounding(deleted, 0); + } + } + } + /** Invisible editor whose {@link InputConnection} forwards IME events to Rust. */ private static final class ImeBridgeEditText extends EditText { ImeBridgeEditText(Context context) { @@ -173,6 +374,8 @@ public final class SleekIme { | InputType.TYPE_TEXT_FLAG_MULTI_LINE | InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_EXTRACT_UI; + outAttrs.initialSelStart = getSelectionStart(); + outAttrs.initialSelEnd = getSelectionEnd(); return new BridgeInputConnection(this); } } @@ -185,35 +388,69 @@ public final class SleekIme { @Override public boolean setComposingText(CharSequence text, int newCursorPosition) { - boolean ok = super.setComposingText(text, newCursorPosition); - String s = text == null ? "" : text.toString(); - nativePreedit(s); - return ok; + syncGen++; // invalidate stale syncField runnables + updatingFromIme = true; + try { + composing = text != null && text.length() > 0; + boolean ok = super.setComposingText(text, newCursorPosition); + String s = text == null ? "" : text.toString(); + Log.i(TAG, "setComposingText len=" + s.length() + " composing=" + composing); + nativePreedit(s); + return ok; + } finally { + updatingFromIme = false; + } } @Override - public boolean finishComposingText() { - boolean ok = super.finishComposingText(); - nativePreedit(""); - return ok; + public boolean setComposingRegion(int start, int end) { + composing = end > start; + return super.setComposingRegion(start, end); } @Override - public boolean commitText(CharSequence text, int newCursorPosition) { - boolean ok = super.commitText(text, newCursorPosition); - if (text != null && text.length() > 0) { - nativeCommit(text.toString()); + public boolean finishComposingText() { + updatingFromIme = true; + try { + composing = false; + boolean ok = super.finishComposingText(); + nativePreedit(""); + return ok; + } finally { + updatingFromIme = false; } - return ok; } @Override - public boolean deleteSurroundingText(int beforeLength, int afterLength) { - boolean ok = super.deleteSurroundingText(beforeLength, afterLength); - if (beforeLength > 0 || afterLength > 0) { - nativeDeleteSurrounding(beforeLength, afterLength); + public boolean commitText(CharSequence text, int newCursorPosition) { + syncGen++; // invalidate stale syncField("") from backspace-to-empty + updatingFromIme = true; + try { + composing = false; + boolean ok = super.commitText(text, newCursorPosition); + if (text != null && text.length() > 0) { + String s = text.toString(); + Log.i(TAG, "commitText len=" + s.length()); + nativeCommit(s); + } else { + nativePreedit(""); + } + // Keep IMM selection in sync so the next glide starts at the right cursor. + Editable editable = getEditable(); + if (editable != null && field != null) { + int start = Selection.getSelectionStart(editable); + int end = Selection.getSelectionEnd(editable); + Context ctx = field.getContext(); + if (ctx instanceof Activity) { + notifySelection((Activity) ctx, editable, start, end); + } + } + return ok; + } finally { + updatingFromIme = false; } - return ok; } + + // Deletes: TextWatcher → nativeDeleteSurrounding (do not also call here). } } diff --git a/android/src/android_ime.rs b/android/src/android_ime.rs index 2b8431c..0a7f4b4 100644 --- a/android/src/android_ime.rs +++ b/android/src/android_ime.rs @@ -4,6 +4,7 @@ //! never reaches egui. `SleekIme.java` hosts a hidden `EditText` and forwards //! composing/commit events here; we inject them as `Event::Ime` each frame. +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; use eframe::egui::{self, Event, ImeEvent, Key, Modifiers}; @@ -13,6 +14,8 @@ use crate::android_media::{android_app_handle, load_app_class}; static IME_QUEUE: Mutex> = Mutex::new(Vec::new()); static LAST_ACTIVE: Mutex> = Mutex::new(None); static LAST_SYNC: Mutex> = Mutex::new(None); +/// Gboard has an active composing span — do not mirror egui→Java (clobbers glide). +static COMPOSING: AtomicBool = AtomicBool::new(false); enum QueuedIme { Enabled, @@ -20,6 +23,7 @@ enum QueuedIme { Preedit(String), Commit(String), Backspace, + Delete, } /// Push queued IME events into egui (call once per frame before UI). @@ -40,9 +44,17 @@ pub fn drain_ime_events(ctx: &egui::Context) { i.events.push(Event::Ime(ImeEvent::Preedit(text))); } QueuedIme::Commit(text) => { + // Refresh ime_cursor_range before Commit. egui only inserts when + // cursor_range.secondary == ime_cursor_range.secondary; after + // backspace-to-empty that range is often stale, so the first + // commitText is dropped and only a later preedit/commit works. + i.events.push(Event::Ime(ImeEvent::Enabled)); i.events.push(Event::Ime(ImeEvent::Commit(text))); } QueuedIme::Backspace => { + // Disable IME filtering so Key::Backspace is not stripped, then + // delete. Commit path re-Enables as needed. + i.events.push(Event::Ime(ImeEvent::Disabled)); i.events.push(Event::Key { modifiers: Modifiers::NONE, physical_key: Some(Key::Backspace), @@ -58,6 +70,23 @@ pub fn drain_ime_events(ctx: &egui::Context) { key: Key::Backspace, }); } + QueuedIme::Delete => { + i.events.push(Event::Ime(ImeEvent::Disabled)); + i.events.push(Event::Key { + modifiers: Modifiers::NONE, + physical_key: Some(Key::Delete), + pressed: true, + repeat: false, + key: Key::Delete, + }); + i.events.push(Event::Key { + modifiers: Modifiers::NONE, + physical_key: Some(Key::Delete), + pressed: false, + repeat: false, + key: Key::Delete, + }); + } } } }); @@ -65,8 +94,12 @@ pub fn drain_ime_events(ctx: &egui::Context) { /// Mirror egui compose text into the hidden editor for Gboard glide context. /// Only runs after the bridge is active (keyboard shown) to avoid racing setActive. +/// Skips while Gboard is composing so we never advance LAST_SYNC on a no-op sync. pub fn sync_field_text(text: &str, sel_start: usize, sel_end: usize) { - if LAST_ACTIVE.lock().expect("ime active") != Some(true) { + if *LAST_ACTIVE.lock().expect("ime active") != Some(true) { + return; + } + if COMPOSING.load(Ordering::Relaxed) { return; } let key = (text.to_string(), sel_start, sel_end); @@ -83,21 +116,32 @@ pub fn sync_field_text(text: &str, sel_start: usize, sel_end: usize) { } /// Wire egui-winit IME allow/deny to the Java bridge. -/// Returns false when the bridge is unavailable so winit can fall back to show/hide. +/// +/// Returns `true` when the Java `EditText` bridge owns show/hide so winit does +/// **not** also call NativeActivity `set_ime_allowed` (that dual path steals +/// the `InputConnection` after the first commit and kills multi-word glide). +/// Falls back to winit only if the JNI bridge is unavailable. pub fn set_ime_allowed(allowed: bool) -> bool { let mut last = LAST_ACTIVE.lock().expect("ime active"); if *last == Some(allowed) { + // Still claim ownership so winit never re-enters NativeActivity IME. return true; } - *last = Some(allowed); if allowed { - push_ime(QueuedIme::Enabled); + // Do NOT push ImeEvent::Enabled here. egui TextEdit strips Key::Backspace + // while ime_enabled; Gboard soft/hardware DEL must remain effective after + // typing. Enable only when a real composing session starts (see onPreedit). } else { push_ime(QueuedIme::Disabled); + COMPOSING.store(false, Ordering::Relaxed); *LAST_SYNC.lock().expect("ime sync") = None; } match call_set_active(allowed) { - Ok(()) => true, + Ok(()) => { + *last = Some(allowed); + log::info!("android IME setActive({allowed}) dispatched (java-only)"); + true + } Err(e) => { log::warn!("android IME setActive({allowed}): {e}"); false @@ -168,9 +212,16 @@ fn call_sync_field(text: &str, sel_start: usize, sel_end: usize) -> Result<(), S let vm = unsafe { JavaVM::from_raw(vm_ptr.cast()) }; let start = i32::try_from(sel_start.min(i32::MAX as usize)).unwrap_or(i32::MAX); let end = i32::try_from(sel_end.min(i32::MAX as usize)).unwrap_or(i32::MAX); + let mut err: Option = None; vm.attach_current_thread(|env| -> jni::errors::Result<()> { let activity = unsafe { env.as_cast_raw::>(&activity_ptr)? }; - let cls = load_app_class(env, activity.as_ref(), "uk.nandi.sleek.SleekIme")?; + let cls = match load_app_class(env, activity.as_ref(), "uk.nandi.sleek.SleekIme") { + Ok(c) => c, + Err(e) => { + err = Some(format!("SleekIme class: {e}")); + return Ok(()); + } + }; let jtext = env.new_string(text)?; env.call_static_method( &cls, @@ -185,7 +236,11 @@ fn call_sync_field(text: &str, sel_start: usize, sel_end: usize) -> Result<(), S )?; Ok(()) }) - .map_err(|e| format!("JNI syncField: {e}")) + .map_err(|e| format!("JNI syncField: {e}"))?; + if let Some(e) = err { + return Err(e); + } + Ok(()) } // ── JNI callbacks from SleekIme.java ───────────────────────────────────── @@ -197,7 +252,18 @@ pub extern "system" fn Java_uk_nandi_sleek_SleekIme_onPreedit<'local>( text: jni::objects::JString<'local>, ) { let _ = env.with_env(|_env| -> jni::errors::Result<()> { - push_ime(QueuedIme::Preedit(format!("{text}"))); + let s = format!("{text}"); + let composing = !s.is_empty(); + if composing && !COMPOSING.load(Ordering::Relaxed) { + // First preedit of a glide/compose session — enable IME ordering + // without leaving ime_enabled stuck on after commit. + push_ime(QueuedIme::Enabled); + } + COMPOSING.store(composing, Ordering::Relaxed); + if !composing { + *LAST_SYNC.lock().expect("ime sync") = None; + } + push_ime(QueuedIme::Preedit(s)); Ok(()) }); } @@ -209,6 +275,9 @@ pub extern "system" fn Java_uk_nandi_sleek_SleekIme_onCommit<'local>( text: jni::objects::JString<'local>, ) { let _ = env.with_env(|_env| -> jni::errors::Result<()> { + COMPOSING.store(false, Ordering::Relaxed); + // Force a post-commit syncField after composing ends. + *LAST_SYNC.lock().expect("ime sync") = None; push_ime(QueuedIme::Commit(format!("{text}"))); Ok(()) }); @@ -223,8 +292,17 @@ pub extern "system" fn Java_uk_nandi_sleek_SleekIme_onDeleteSurrounding<'local>( ) { let before = before_length.max(0) as usize; let after = after_length.max(0) as usize; + if before > 0 || after > 0 { + // Allow syncField to mirror egui after the delete is applied. + *LAST_SYNC.lock().expect("ime sync") = None; + } for _ in 0..before { push_ime(QueuedIme::Backspace); } - let _ = after; + // Forward-delete: emulate with Delete key via Backspace at next char — + // egui has no Queued delete-forward; move isn't available here. For after>0 + // push Delete keys instead. + for _ in 0..after { + push_ime(QueuedIme::Delete); + } } diff --git a/android/src/assets/sleek_activity.dex b/android/src/assets/sleek_activity.dex index b660783ee891c447db3d00b93bb20e6edb58a3b2..2259d9f36b921404ef2f9ebf8f0f6f40dcac7272 100644 GIT binary patch delta 18193 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 (usize, usize) { +fn compose_cursor_range(ctx: &egui::Context, field_id: egui::Id, text_len: usize) -> (usize, usize) { + // Never fall back to (0,0) on a non-empty field — that resets the hidden + // EditText caret to the start, and Gboard backspace becomes a no-op. + let fallback = (text_len, text_len); let Some(state) = egui::TextEdit::load_state(ctx, field_id) else { - return (0, 0); + return fallback; }; let Some(range) = state.cursor.char_range() else { - return (0, 0); + return fallback; }; - (range.primary.index, range.secondary.index) + ( + range.primary.index.min(text_len), + range.secondary.index.min(text_len), + ) } fn compose_input_row(