Эх сурвалжийг харах

android: improve editor so it works in an uninitialized state. introduce request_focus/focus targets

darkfi 1 жил өмнө
parent
commit
0aefb38307

+ 30 - 3
bin/app/java/MainActivity.java

@@ -63,6 +63,7 @@ public boolean focus(final int id) {
     return true;
 }
 
+/*
 public CustomInputConnection getInputConnect(int id) {
     InvisibleInputView iv = editors.get(id);
     if (iv == null) {
@@ -70,16 +71,42 @@ public CustomInputConnection getInputConnect(int id) {
     }
     return iv.inputConnection;
 }
+*/
+public InvisibleInputView getInputView(int id) {
+    return editors.get(id);
+}
 
-public boolean setText(final int id, final String txt) {
-    final InvisibleInputView iv = editors.get(id);
-    if (iv == null || iv.inputConnection == null) {
+public boolean setText(int id, String txt) {
+    InvisibleInputView iv = editors.get(id);
+    if (iv == null) {
         return false;
     }
 
+    // If inputConnection is not yet ready, then setup the editable directly.
+    if (iv.inputConnection == null) {
+        iv.setEditableText(txt);
+        return true;
+    }
+
     // Maybe do this on the UI thread?
     iv.inputConnection.setEditableText(txt, txt.length(), txt.length(), 0, 0);
+    return true;
+}
+public boolean setSelection(int id, int start, int end) {
+    InvisibleInputView iv = editors.get(id);
+    if (iv == null) {
+        return false;
+    }
+
+    // If inputConnection is not yet ready, then setup the sel directly.
+    if (iv.inputConnection == null) {
+        iv.setSelection(start, end);
+        return true;
+    }
 
+    iv.inputConnection.beginBatchEdit();
+    iv.inputConnection.setSelection(start, end);
+    iv.inputConnection.endBatchEdit();
     return true;
 }
 

+ 11 - 112
bin/app/java/autosuggest/CustomInputConnection.java

@@ -19,36 +19,23 @@
 package autosuggest;
 
 import android.content.Context;
-import android.os.Bundle;
-import android.os.Handler;
 import android.text.Editable;
 import android.text.Selection;
-import android.text.SpannableStringBuilder;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.inputmethod.BaseInputConnection;
-import android.view.inputmethod.InputConnection;
-import android.view.inputmethod.InputContentInfo;
 import android.view.inputmethod.EditorInfo;
 import android.view.View;
-import android.view.inputmethod.CompletionInfo;
-import android.view.inputmethod.CorrectionInfo;
 import android.view.inputmethod.ExtractedText;
 import android.view.inputmethod.ExtractedTextRequest;
 import android.view.inputmethod.SurroundingText;
 import android.view.inputmethod.InputMethodManager;
-//import android.view.inputmethod.TextSnapshot;
-//import android.view.inputmethod.TextAttribute;
 
-// This InputConnection is created by ContentView.onCreateInputConnection.
-// It then adapts android's IME to chrome's RenderWidgetHostView using the
-// native ImeAdapterAndroid via the outer class ImeAdapter.
 public class CustomInputConnection extends BaseInputConnection {
-    private static final boolean DEBUG = false;
+    private static final boolean DEBUG = true;
     private int id = -1;
 
     private View mInternalView;
-    //private ImeAdapter mImeAdapter;
     private Editable mEditable;
     private boolean mSingleLine;
     private int numBatchEdits;
@@ -59,63 +46,13 @@ public class CustomInputConnection extends BaseInputConnection {
     native static void onFinishCompose(int id);
     native static void onDeleteSurroundingText(int id, int left, int right);
 
-    //private AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
-    public CustomInputConnection(int id, View view, EditorInfo outAttrs) {
+    public CustomInputConnection(int id, Editable editable, View view) {
         super(view, true);
-        this.id = id;
         log("CustomInputConnection()");
+        this.id = id;
+        mEditable = editable;
         mInternalView = view;
-        //mImeAdapter = imeAdapter;
-        //mImeAdapter.setInputConnection(this);
-        mSingleLine = true;
-        outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
-        outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
-                | EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
-            /*
-        if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeText) {
-        */
-            // Normal text field
-            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
-            /*
-        } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeTextArea ||
-                imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeContentEditable) {
-            // TextArea or contenteditable.
-            outAttrs.inputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE
-                    | EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES
-                    | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
-            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NONE;
-            mSingleLine = false;
-        } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypePassword) {
-            // Password
-            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
-                    | InputType.TYPE_TEXT_VARIATION_WEB_PASSWORD;
-            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
-        } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeSearch) {
-            // Search
-            outAttrs.imeOptions |= EditorInfo.IME_ACTION_SEARCH;
-        } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeUrl) {
-            // Url
-            // TYPE_TEXT_VARIATION_URI prevents Tab key from showing, so
-            // exclude it for now.
-            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
-        } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeEmail) {
-            // Email
-            outAttrs.inputType = InputType.TYPE_CLASS_TEXT
-                    | InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS;
-            outAttrs.imeOptions |= EditorInfo.IME_ACTION_GO;
-        } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeTel) {
-            // Telephone
-            // Number and telephone do not have both a Tab key and an
-            // action in default OSK, so set the action to NEXT
-            outAttrs.inputType = InputType.TYPE_CLASS_PHONE;
-            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
-        } else if (imeAdapter.mTextInputType == ImeAdapter.sTextInputTypeNumber) {
-            // Number
-            outAttrs.inputType = InputType.TYPE_CLASS_NUMBER
-                    | InputType.TYPE_NUMBER_VARIATION_NORMAL;
-            outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
-        }
-        */
+        mSingleLine = false;
     }
 
     private void log(String fstr, Object... args) {
@@ -145,11 +82,6 @@ public class CustomInputConnection extends BaseInputConnection {
             selectionStart, selectionEnd,
             compositionStart, compositionEnd);
 
-        if (mEditable == null) {
-            log("setEditableText creating new editable");
-            mEditable = Editable.Factory.getInstance().newEditable("");
-        }
-
         int prevSelectionStart = Selection.getSelectionStart(mEditable);
         int prevSelectionEnd = Selection.getSelectionEnd(mEditable);
         int prevEditableLength = mEditable.length();
@@ -198,11 +130,6 @@ public class CustomInputConnection extends BaseInputConnection {
 
     @Override
     public Editable getEditable() {
-        if (mEditable == null) {
-            log("getEditable() [create new]");
-            mEditable = Editable.Factory.getInstance().newEditable("");
-            Selection.setSelection(mEditable, 0);
-        }
         log("getEditable() -> %s", editableToXml(mEditable));
         return mEditable;
     }
@@ -290,14 +217,10 @@ public class CustomInputConnection extends BaseInputConnection {
     public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
         log("getExtractedText(...)");
         ExtractedText et = new ExtractedText();
-        if (mEditable == null) {
-            et.text = "";
-        } else {
-            et.text = mEditable.toString();
-            et.partialEndOffset = mEditable.length();
-            et.selectionStart = Selection.getSelectionStart(mEditable);
-            et.selectionEnd = Selection.getSelectionEnd(mEditable);
-        }
+        et.text = mEditable.toString();
+        et.partialEndOffset = mEditable.length();
+        et.selectionStart = Selection.getSelectionStart(mEditable);
+        et.selectionEnd = Selection.getSelectionEnd(mEditable);
         et.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
         return et;
     }
@@ -354,8 +277,7 @@ public class CustomInputConnection extends BaseInputConnection {
     @Override
     public boolean finishComposingText() {
         log("finishComposingText()");
-        if (mEditable == null
-                || (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable))) {
+        if (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable)) {
             return true;
         }
         super.finishComposingText();
@@ -408,10 +330,6 @@ public class CustomInputConnection extends BaseInputConnection {
 
     private void updateImeSelection() {
         log("updateImeSelection()");
-        if (mEditable == null) {
-            return;
-        }
-
         getInputMethodManager().updateSelection(
             mInternalView,
             Selection.getSelectionStart(mEditable),
@@ -440,7 +358,7 @@ public class CustomInputConnection extends BaseInputConnection {
         return false;
     }
 
-    private String editableToXml(Editable editable) {
+    public static String editableToXml(Editable editable) {
         StringBuilder xmlBuilder = new StringBuilder();
         int length = editable.length();
 
@@ -500,24 +418,5 @@ public class CustomInputConnection extends BaseInputConnection {
 
         return xmlBuilder.toString();
     }
-
-    public String debugEditableStr() {
-        return editableToXml(mEditable);
-    }
-    public String rawText() {
-        return mEditable.toString();
-    }
-    public int getSelectionStart() {
-        return Selection.getSelectionStart(mEditable);
-    }
-    public int getSelectionEnd() {
-        return Selection.getSelectionEnd(mEditable);
-    }
-    public int getComposeStart() {
-        return getComposingSpanStart(mEditable);
-    }
-    public int getComposeEnd() {
-        return getComposingSpanEnd(mEditable);
-    }
 }
 

+ 41 - 8
bin/app/java/autosuggest/InvisibleInputView.java

@@ -19,11 +19,13 @@
 package autosuggest;
 
 import android.content.Context;
-import android.graphics.Canvas;
 import android.graphics.Rect;
+import android.text.Editable;
+import android.text.Selection;
 import android.util.Log;
 import android.view.View;
 import android.view.ViewGroup;
+import android.view.inputmethod.BaseInputConnection;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputConnection;
 
@@ -32,6 +34,7 @@ import autosuggest.CustomInputConnection;
 public class InvisibleInputView extends View {
     public CustomInputConnection inputConnection;
     public int id = -1;
+    public Editable editable;
 
     native static void onCreateInputConnect(int id);
 
@@ -44,14 +47,20 @@ public class InvisibleInputView extends View {
         //setAlpha(0f);
         setLayoutParams(new ViewGroup.LayoutParams(400, 200));
         this.id = id;
+        editable = Editable.Factory.getInstance().newEditable("");
+        Selection.setSelection(editable, 0);
     }
 
-    /*
-    @Override
-    protected void onDraw(Canvas canvas) {
-        Log.d("darkfi", "InvisibleInputView skipping onDraw()");
+    // Maybe move CustomInputConnection.setEditableText() to here?
+    // For now this is called when the InputConnection is not yet available.
+    public void setEditableText(String text) {
+        editable.replace(0, editable.length(), text);
+        Selection.setSelection(editable, text.length(), text.length());
+    }
+    // Same as above
+    public void setSelection(int start, int end) {
+        Selection.setSelection(editable, start, end);
     }
-    */
 
     @Override
     protected void onAttachedToWindow() {
@@ -74,9 +83,14 @@ public class InvisibleInputView extends View {
 
         outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT
             | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
+            //| EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT;
         outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
-            | EditorInfo.IME_ACTION_NONE;
-        inputConnection = new CustomInputConnection(id, this, outAttrs);
+            //| EditorInfo.IME_ACTION_NONE;
+            | EditorInfo.IME_ACTION_GO;
+        outAttrs.initialSelStart = getSelectionStart();
+        outAttrs.initialSelEnd = getSelectionEnd();
+
+        inputConnection = new CustomInputConnection(id, editable, this);
         onCreateInputConnect(id);
         return inputConnection;
     }
@@ -86,5 +100,24 @@ public class InvisibleInputView extends View {
         super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
         Log.d("darkfi", "onFocusChanged: " + gainFocus);
     }
+
+    public String debugEditableStr() {
+        return CustomInputConnection.editableToXml(editable);
+    }
+    public String rawText() {
+        return editable.toString();
+    }
+    public int getSelectionStart() {
+        return Selection.getSelectionStart(editable);
+    }
+    public int getSelectionEnd() {
+        return Selection.getSelectionEnd(editable);
+    }
+    public int getComposeStart() {
+        return BaseInputConnection.getComposingSpanStart(editable);
+    }
+    public int getComposeEnd() {
+        return BaseInputConnection.getComposingSpanEnd(editable);
+    }
 }
 

+ 21 - 31
bin/app/src/android.rs

@@ -207,31 +207,23 @@ pub fn set_text(id: usize, text: &str) -> Option<()> {
 
 pub fn set_selection(id: usize, select_start: usize, select_end: usize) -> Option<()> {
     //trace!(target: "android", "set_selection({id}, {select_start}, {select_end})");
-    unsafe {
+    let is_success = unsafe {
         let env = android::attach_jni_env();
-        let input_connect = ndk_utils::call_object_method!(
-            env,
-            android::ACTIVITY,
-            "getInputConnect",
-            "(I)Lautosuggest/CustomInputConnection;",
-            id as i32
-        );
-        if input_connect.is_null() {
-            return None
-        }
-
-        ndk_utils::call_bool_method!(env, input_connect, "beginBatchEdit", "()Z");
         ndk_utils::call_bool_method!(
             env,
-            input_connect,
+            android::ACTIVITY,
             "setSelection",
-            "(II)Z",
-            select_start,
-            select_end
-        );
-        ndk_utils::call_bool_method!(env, input_connect, "endBatchEdit", "()Z");
+            "(III)Z",
+            id as i32,
+            select_start as i32,
+            select_end as i32
+        )
+    };
+    if is_success == 0u8 {
+        None
+    } else {
+        Some(())
     }
-    Some(())
 }
 
 pub struct Editable {
@@ -246,31 +238,29 @@ pub fn get_editable(id: usize) -> Option<Editable> {
     //trace!(target: "android", "get_editable({id})");
     unsafe {
         let env = android::attach_jni_env();
-        let input_connect = ndk_utils::call_object_method!(
+        let input_view = ndk_utils::call_object_method!(
             env,
             android::ACTIVITY,
-            "getInputConnect",
-            "(I)Lautosuggest/CustomInputConnection;",
+            "getInputView",
+            "(I)Lautosuggest/InvisibleInputView;",
             id as i32
         );
-        if input_connect.is_null() {
+        if input_view.is_null() {
             return None
         }
 
         let buffer =
-            ndk_utils::call_object_method!(env, input_connect, "rawText", "()Ljava/lang/String;");
+            ndk_utils::call_object_method!(env, input_view, "rawText", "()Ljava/lang/String;");
         assert!(!buffer.is_null());
         let buffer = ndk_utils::get_utf_str!(env, buffer).to_string();
 
-        let select_start =
-            ndk_utils::call_int_method!(env, input_connect, "getSelectionStart", "()I");
+        let select_start = ndk_utils::call_int_method!(env, input_view, "getSelectionStart", "()I");
 
-        let select_end = ndk_utils::call_int_method!(env, input_connect, "getSelectionEnd", "()I");
+        let select_end = ndk_utils::call_int_method!(env, input_view, "getSelectionEnd", "()I");
 
-        let compose_start =
-            ndk_utils::call_int_method!(env, input_connect, "getComposeStart", "()I");
+        let compose_start = ndk_utils::call_int_method!(env, input_view, "getComposeStart", "()I");
 
-        let compose_end = ndk_utils::call_int_method!(env, input_connect, "getComposeEnd", "()I");
+        let compose_end = ndk_utils::call_int_method!(env, input_view, "getComposeEnd", "()I");
 
         assert!(select_start >= 0);
         assert!(select_end >= 0);

+ 2 - 1
bin/app/src/app/node.rs

@@ -447,11 +447,12 @@ pub fn create_chatedit(name: &str) -> SceneNode {
     node.add_property(prop).unwrap();
 
     node.add_signal("enter_pressed", "Enter key pressed", vec![]).unwrap();
-    node.add_signal("keyboard_request", "Request to show keyboard", vec![]).unwrap();
+    node.add_signal("focus_request", "Request to gain focus", vec![]).unwrap();
     node.add_signal("paste_request", "Request to show paste dialog", vec![]).unwrap();
 
     // Used by emoji_picker
     node.add_method("insert_text", vec![("text", "Text", CallArgType::Str)], None).unwrap();
+    node.add_method("focus", vec![], None).unwrap();
 
     node
 }

+ 7 - 5
bin/app/src/app/schema/chat.rs

@@ -70,7 +70,7 @@ mod android_ui_consts {
     pub const CHATEDIT_HANDLE_DESCENT: f32 = 10.;
     pub const CHATEDIT_NEG_W: f32 = 300.;
     pub const CHATEDIT_LHS_PAD: f32 = 150.;
-    pub const TEXTBAR_BASELINE: f32 = 60.;
+    pub const TEXTBAR_BASELINE: f32 = 40.;
     pub const EMOJI_BTN_X: f32 = 60.;
     pub const EMOJI_BG_W: f32 = 120.;
     pub const EMOJI_SCALE: f32 = 40.;
@@ -975,19 +975,21 @@ pub async fn make(
     prop.clone().set_f32(atom, Role::App, 3, EMOJIBTN_BOX[3]).unwrap();
 
     let (slot, recvr) = Slot::new("reqkeyb");
-    chatedit_node.register("keyboard_request", slot).unwrap();
+    chatedit_node.register("focus_request", slot).unwrap();
+    let chatedit_node2 = chatedit_node.clone();
     let emoji_btn_is_visible2 = emoji_btn_is_visible.clone();
     let listen_click = app.ex.spawn(async move {
         while let Ok(_) = recvr.recv().await {
             if emoji_btn_is_visible2.get() {
                 debug!(target: "app::chat", "Emoji picker not visible so showing keyboard");
-                miniquad::window::show_keyboard(true);
+                chatedit_node2.call_method("focus", vec![]).await.unwrap();
             }
         }
     });
     app.tasks.lock().unwrap().push(listen_click);
 
     let (slot, recvr) = Slot::new("emoji_clicked");
+    let chatedit_node2 = chatedit_node.clone();
     node.register("click", slot).unwrap();
     let listen_click = app.ex.spawn(async move {
         let mut panel_height = if cfg!(target_os = "android") {
@@ -1013,7 +1015,7 @@ pub async fn make(
             }
 
             if emoji_btn_is_visible.get() {
-                miniquad::window::show_keyboard(false);
+                chatedit_node2.call_method("focus", vec![]).await.unwrap();
 
                 assert!(!emoji_close_is_visible.get());
                 assert!(emoji_h_prop.get() < 0.001);
@@ -1025,7 +1027,7 @@ pub async fn make(
                 //    msleep(10).await;
                 //}
             } else {
-                miniquad::window::show_keyboard(true);
+                chatedit_node2.call_method("focus", vec![]).await.unwrap();
 
                 assert!(emoji_close_is_visible.get());
                 assert!(emoji_h_prop.get() > 0.);

+ 36 - 19
bin/app/src/text2/editor/android.rs

@@ -23,6 +23,7 @@ use crate::{
     prop::{PropertyAtomicGuard, PropertyColor, PropertyFloat32, PropertyStr},
     text2::{TextContext, TEXT_CTX},
 };
+use std::sync::atomic::{AtomicBool, Ordering};
 
 macro_rules! t { ($($arg:tt)*) => { trace!(target: "text::editor::android", $($arg)*); } }
 macro_rules! w { ($($arg:tt)*) => { warn!(target: "text::editor::android", $($arg)*) } }
@@ -43,6 +44,10 @@ fn byte_to_char16_index(s: &str, byte_idx: usize) -> Option<usize> {
 pub struct Editor {
     pub composer_id: usize,
     is_init: bool,
+    is_setup: bool,
+    /// We cannot receive focus until `AndroidSuggestEvent::Init` has finished.
+    /// We use this flag to delay calling `android::focus()` until the init has completed.
+    is_focus_req: AtomicBool,
 
     layout: parley::Layout<Color>,
     width: Option<f32>,
@@ -65,6 +70,8 @@ impl Editor {
         Self {
             composer_id: usize::MAX,
             is_init: false,
+            is_setup: false,
+            is_focus_req: AtomicBool::new(false),
 
             layout: Default::default(),
             width: None,
@@ -77,18 +84,37 @@ impl Editor {
         }
     }
 
+    /// Called on `AndroidSuggestEvent::Init` after the View has been added to the main hierarchy
+    /// and is ready to receive commands such as focus.
     pub fn init(&mut self) {
-        android::focus(self.composer_id).unwrap();
+        self.is_init = true;
+
+        // Perform any focus requests.
+        let is_focus_req = self.is_focus_req.swap(false, Ordering::SeqCst);
+        if is_focus_req {
+            android::focus(self.composer_id).unwrap();
+        }
     }
+    /// Called on `AndroidSuggestEvent::CreateInputConnect`, which only happens after the View
+    /// is focused for the first time.
     pub fn setup(&mut self) {
+        assert!(self.is_init);
+        self.is_setup = true;
+
         assert!(self.composer_id != usize::MAX);
         t!("Initialized composer [{}]", self.composer_id);
         //let atxt = "A berry is small 😊 and pulpy.";
         //let atxt = "A berry is a small, pulpy, and often edible fruit. Typically, berries are juicy, rounded, brightly colored, sweet, sour or tart, and do not have a stone or pit, although many pips or seeds may be present. Common examples of berries in the culinary sense are strawberries, raspberries, blueberries, blackberries, white currants, blackcurrants, and redcurrants. In Britain, soft fruit is a horticultural term for such fruits. The common usage of the term berry is different from the scientific or botanical definition of a berry, which refers to a fruit produced from the ovary of a single flower where the outer layer of the ovary wall develops into an edible fleshy portion (pericarp). The botanical definition includes many fruits that are not commonly known or referred to as berries, such as grapes, tomatoes, cucumbers, eggplants, bananas, and chili peppers.";
-        // This will initialize the editable and set the cursor.
-        // Otherwise get_editable() will segfault.
-        android::set_text(self.composer_id, "").unwrap();
-        self.is_init = true;
+    }
+
+    /// Can only be called after AndroidSuggestEvent::Init.
+    pub fn focus(&self) {
+        // We're not yet ready to receive focus
+        if !self.is_init {
+            self.is_focus_req.store(true, Ordering::SeqCst);
+            return
+        }
+        android::focus(self.composer_id).unwrap();
     }
 
     pub async fn refresh(&mut self, atom: &mut PropertyAtomicGuard) {
@@ -97,10 +123,7 @@ impl Editor {
         let window_scale = self.window_scale.get();
         let lineheight = self.lineheight.get();
 
-        let Some(edit) = android::get_editable(self.composer_id) else {
-            w!("refresh(): editable composer_id={} not initialized yet", self.composer_id);
-            return
-        };
+        let edit = android::get_editable(self.composer_id).unwrap();
 
         let mut underlines = vec![];
         if let Some(compose_start) = edit.compose_start {
@@ -135,8 +158,8 @@ impl Editor {
         let edit = android::get_editable(self.composer_id).unwrap();
         let cursor_idx = cursor.index();
         let pos = byte_to_char16_index(&edit.buffer, cursor_idx).unwrap();
-        android::set_selection(self.composer_id, pos, pos);
         t!("  {cursor_idx} => {pos}");
+        android::set_selection(self.composer_id, pos, pos);
     }
 
     pub fn select_word_at_point(&self, pos: Point) {
@@ -146,11 +169,7 @@ impl Editor {
         self.set_selection(select.start, select.end);
     }
 
-    pub fn get_cursor_pos(&self) -> Option<Point> {
-        if !self.is_init {
-            return None
-        }
-
+    pub fn get_cursor_pos(&self) -> Point {
         let lineheight = self.lineheight.get();
         let edit = android::get_editable(self.composer_id).unwrap();
 
@@ -170,9 +189,7 @@ impl Editor {
             )
         };
         let cursor_rect = cursor.geometry(&self.layout, lineheight);
-
-        let cursor_pos = Point::new(cursor_rect.x0 as f32, cursor_rect.y0 as f32);
-        Some(cursor_pos)
+        Point::new(cursor_rect.x0 as f32, cursor_rect.y0 as f32)
     }
 
     pub async fn driver<'a>(
@@ -199,7 +216,7 @@ impl Editor {
         Some(edit.buffer[select_start..select_end].to_string())
     }
     pub fn selection(&self) -> parley::Selection {
-        let Some(edit) = android::get_editable(self.composer_id) else { return Default::default() };
+        let edit = android::get_editable(self.composer_id).unwrap();
 
         let select_start = char16_to_byte_index(&edit.buffer, edit.select_start).unwrap();
         let select_end = char16_to_byte_index(&edit.buffer, edit.select_end).unwrap();

+ 29 - 6
bin/app/src/ui/chatedit.rs

@@ -899,7 +899,7 @@ impl ChatEdit {
         }
 
         let node = self.node.upgrade().unwrap();
-        node.trigger("keyboard_request", vec![]).await.unwrap();
+        node.trigger("focus_request", vec![]).await.unwrap();
 
         true
     }
@@ -993,9 +993,7 @@ impl ChatEdit {
         let draw_main = vec![(
             self.content_dc_key,
             GfxDrawCall {
-                instrs: vec![
-                    GfxDrawInstruction::Move(Point::new(0., -scroll)),
-                ],
+                instrs: vec![GfxDrawInstruction::Move(Point::new(0., -scroll))],
                 dcs: vec![self.text_dc_key, self.cursor_dc_key, self.select_dc_key],
                 z_index: self.z_index.get(),
             },
@@ -1031,7 +1029,7 @@ impl ChatEdit {
 
         let mut cursor_instrs = vec![];
 
-        let Some(mut cursor_pos) = self.editor.lock().await.get_cursor_pos() else { return vec![] };
+        let mut cursor_pos = self.editor.lock().await.get_cursor_pos();
         cursor_pos += self.inner_pos();
         cursor_instrs.push(GfxDrawInstruction::Move(cursor_pos));
 
@@ -1244,6 +1242,26 @@ impl ChatEdit {
         true
     }
 
+    async fn process_focus_method(me: &Weak<Self>, sub: &MethodCallSub) -> bool {
+        let Ok(method_call) = sub.receive().await else {
+            debug!(target: "ui::chatedit", "Event relayer closed");
+            return false
+        };
+
+        t!("method called: focus({method_call:?})");
+        assert!(method_call.send_res.is_none());
+        assert!(method_call.data.is_empty());
+
+        let Some(self_) = me.upgrade() else {
+            // Should not happen
+            panic!("self destroyed before insert_text_method_task was stopped!");
+        };
+
+        let mut editor = self_.editor.lock().await;
+        editor.focus();
+        true
+    }
+
     async fn handle_android_event(&self, ev: AndroidSuggestEvent) {
         t!("handle_android_event({ev:?})");
         if !self.is_active.get() {
@@ -1300,6 +1318,11 @@ impl UIObject for ChatEdit {
                 async move { while Self::process_insert_text_method(&me2, &method_sub).await {} },
             );
 
+        let method_sub = node_ref.subscribe_method_call("focus").unwrap();
+        let me2 = me.clone();
+        let focus_task =
+            ex.spawn(async move { while Self::process_focus_method(&me2, &method_sub).await {} });
+
         let mut on_modify = OnModify::new(ex.clone(), self.node.clone(), me.clone());
         on_modify.when_change(self.is_focused.prop(), Self::change_focus);
 
@@ -1384,7 +1407,7 @@ impl UIObject for ChatEdit {
             }
         });
 
-        let mut tasks = vec![insert_text_task, blinking_cursor_task];
+        let mut tasks = vec![insert_text_task, focus_task, blinking_cursor_task];
         tasks.append(&mut on_modify.tasks);
 
         #[cfg(target_os = "android")]