Browse Source

wallet: create a much better autosuggest plugin for Android, and integrate it with the editbox widget.

darkfi 1 year ago
parent
commit
eb5b7dff04

+ 1 - 3
bin/darkwallet/java/MainActivity.java

@@ -12,9 +12,7 @@ if (true) {
         | EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT;
     outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN
         | EditorInfo.IME_ACTION_NONE;
-    // fullEditor is false, but we might set this to true for enabling
-    // text selection, and copy/paste. Lets see.
-    return new CustomInputConnection(this, false);
+    return new CustomInputConnection(this, outAttrs);
 }
 
 //% END

+ 401 - 46
bin/darkwallet/java/autosuggest/CustomInputConnection.java

@@ -18,80 +18,435 @@
 
 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.View;
 import android.view.inputmethod.BaseInputConnection;
-import android.inputmethodservice.InputMethodService;
-
-// setComposingText() - change text being composed
-
-// See 30b299cb04b4ba2330ef61a8a24c1e58513a0af2
-// content/public/android/java/src/org/chromium/content/browser/input/AdapterInputConnection.java
+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 View mInternalView;
+    //private ImeAdapter mImeAdapter;
+    private Editable mEditable;
+    private boolean mSingleLine;
+    private int numBatchEdits;
+    private boolean shouldUpdateImeSelection;
 
     native static void setup();
-    native static void onCommitText(String text);
-    native static void onEndEdit(String text);
+    native static void onCompose(String text, int newCursorPos, boolean isCommit);
+    native static void onSetComposeRegion(int start, int end);
+
+    //private AdapterInputConnection(View view, ImeAdapter imeAdapter, EditorInfo outAttrs) {
+    public CustomInputConnection(View view, EditorInfo outAttrs) {
+        super(view, true);
+        if (DEBUG) Log.d("darkfi", "CustomInputConnection()");
+        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;
+        }
+        */
+        //setup();
+    }
+
+    /**
+     * Updates the AdapterInputConnection's internal representation of the text
+     * being edited and its selection and composition properties. The resulting
+     * Editable is accessible through the getEditable() method.
+     * If the text has not changed, this also calls updateSelection on the InputMethodManager.
+     * @param text The String contents of the field being edited
+     * @param selectionStart The character offset of the selection start, or the caret
+     * position if there is no selection
+     * @param selectionEnd The character offset of the selection end, or the caret
+     * position if there is no selection
+     * @param compositionStart The character offset of the composition start, or -1
+     * if there is no composition
+     * @param compositionEnd The character offset of the composition end, or -1
+     * if there is no selection
+     */
+    public void setEditableText(String text, int selectionStart, int selectionEnd,
+            int compositionStart, int compositionEnd) {
+        if (DEBUG) Log.d("darkfi", "setEditableText(" + text + ", " + selectionStart
+            + ", " + selectionEnd + ", " + compositionStart
+            + ", " + compositionEnd + ")");
+
+        if (mEditable == null) {
+            mEditable = Editable.Factory.getInstance().newEditable("");
+        }
+
+        int prevSelectionStart = Selection.getSelectionStart(mEditable);
+        int prevSelectionEnd = Selection.getSelectionEnd(mEditable);
+        int prevEditableLength = mEditable.length();
+        int prevCompositionStart = getComposingSpanStart(mEditable);
+        int prevCompositionEnd = getComposingSpanEnd(mEditable);
+        String prevText = mEditable.toString();
+
+        selectionStart = Math.min(selectionStart, text.length());
+        selectionEnd = Math.min(selectionEnd, text.length());
+        compositionStart = Math.min(compositionStart, text.length());
+        compositionEnd = Math.min(compositionEnd, text.length());
+
+        boolean textUnchanged = prevText.equals(text);
+
+        if (textUnchanged
+                && prevSelectionStart == selectionStart && prevSelectionEnd == selectionEnd
+                && prevCompositionStart == compositionStart
+                && prevCompositionEnd == compositionEnd) {
+            // Nothing has changed; don't need to do anything
+            return;
+        }
+
+        // When a programmatic change has been made to the editable field, both the start
+        // and end positions for the composition will equal zero. In this case we cancel the
+        // active composition in the editor as this no longer is relevant.
+        if (textUnchanged && compositionStart == 0 && compositionEnd == 0) {
+            cancelComposition();
+        }
+
+        if (!textUnchanged) {
+            mEditable.replace(0, mEditable.length(), text);
+        }
+        Selection.setSelection(mEditable, selectionStart, selectionEnd);
+        super.setComposingRegion(compositionStart, compositionEnd);
+
+        if (textUnchanged || prevText.equals("")) {
+            // updateSelection should be called when a manual selection change occurs.
+            // Should not be called if text is being entered else issues can occur
+            // e.g. backspace to undo autocorrection will not work with the default OSK.
+            getInputMethodManager().updateSelection(mInternalView,
+                    selectionStart, selectionEnd, compositionStart, compositionEnd);
+        }
+    }
+
+    @Override
+    public Editable getEditable() {
+        if (DEBUG) Log.d("darkfi", "getEditable()");
+        if (mEditable == null) {
+            mEditable = Editable.Factory.getInstance().newEditable("");
+            Selection.setSelection(mEditable, 0);
+        }
+        if (DEBUG) Log.d("darkfi", "  -> " + editableToXml(mEditable));
+        return mEditable;
+    }
+
+    @Override
+    public boolean setComposingText(CharSequence text, int newCursorPosition) {
+        if (DEBUG) Log.d("darkfi", "setComposingText(" + text + ", " + newCursorPosition + ")");
+        super.setComposingText(text, newCursorPosition);
+        shouldUpdateImeSelection = true;
+        onCompose(text.toString(), newCursorPosition, false);
+        return true;
+    }
+
+    @Override
+    public boolean commitText(CharSequence text, int newCursorPosition) {
+        if (DEBUG) Log.d("darkfi", "commitText(" + text.toString() + ", " + newCursorPosition + ")");
+        super.commitText(text, newCursorPosition);
+        shouldUpdateImeSelection = true;
+        onCompose(text.toString(), newCursorPosition, text.length() > 0);
+        return true;
+    }
+
+    @Override
+    public boolean performEditorAction(int actionCode) {
+        if (DEBUG) Log.d("darkfi", "performEditorAction(" + actionCode + ")");
+        switch (actionCode) {
+            case EditorInfo.IME_ACTION_NEXT:
+                cancelComposition();
+                // Send TAB key event
+                long timeStampMs = System.currentTimeMillis();
+                //mImeAdapter.sendSyntheticKeyEvent(
+                //        sEventTypeRawKeyDown, timeStampMs, KeyEvent.KEYCODE_TAB, 0);
+                return true;
+            case EditorInfo.IME_ACTION_GO:
+            case EditorInfo.IME_ACTION_SEARCH:
+                //mImeAdapter.dismissInput(true);
+                break;
+        }
+
+        return super.performEditorAction(actionCode);
+    }
+
+    @Override
+    public boolean performContextMenuAction(int id) {
+        if (DEBUG) Log.d("darkfi", "performContextMenuAction(" + id + ")");
+        /*
+        switch (id) {
+            case android.R.id.selectAll:
+                return mImeAdapter.selectAll();
+            case android.R.id.cut:
+                return mImeAdapter.cut();
+            case android.R.id.copy:
+                return mImeAdapter.copy();
+            case android.R.id.paste:
+                return mImeAdapter.paste();
+            default:
+                return false;
+        }
+        */
+        return false;
+    }
 
-    // Android is sending commit("foo") then edit("foo") events which is confusing.
-    // We use this to skip edit("foo") when proceeded by the commit.
-    private String lastCommitText;
+    @Override
+    public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+        if (DEBUG) Log.d("darkfi", "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.flags = mSingleLine ? ExtractedText.FLAG_SINGLE_LINE : 0;
+        return et;
+    }
 
-    public CustomInputConnection(View view, boolean fullEditor) {
-        super(view, fullEditor);
-        lastCommitText = null;
-        setup();
+    @Override
+    public boolean deleteSurroundingText(int leftLength, int rightLength) {
+        if (DEBUG) Log.d("darkfi", "deleteSurroundingText(" + leftLength + ", " + rightLength + ")");
+        if (!super.deleteSurroundingText(leftLength, rightLength)) {
+            return false;
+        }
+        shouldUpdateImeSelection = true;
+        //return mImeAdapter.deleteSurroundingText(leftLength, rightLength);
+        return true;
     }
 
     @Override
     public boolean sendKeyEvent(KeyEvent event) {
         int action = event.getAction();
         int keycode = event.getKeyCode();
-        // If this is backspace/del or if the key has a character representation,
+        if (DEBUG) Log.d("darkfi", "sendKeyEvent()  action=" + action + ", keycode=" + keycode);
+
+        //mImeAdapter.mSelectionHandleController.hideAndDisallowAutomaticShowing();
+        //mImeAdapter.mInsertionHandleController.hideAndDisallowAutomaticShowing();
+
+        // If this is a key-up, and backspace/del or if the key has a character representation,
         // need to update the underlying Editable (i.e. the local representation of the text
-        // being edited).  Some IMEs like Jellybean stock IME and Samsung IME mix in delete
-        // KeyPress events instead of calling deleteSurroundingText.
-        if (action == KeyEvent.ACTION_DOWN && keycode == KeyEvent.KEYCODE_DEL) {
-            deleteSurroundingText(1, 0);
-
-            //String text = getTextBeforeCursor(100, 0).toString();
-            //text = text.substring(0, text.length() - 1);
-            //setComposingText(text, 1);
-        } else if (action == KeyEvent.ACTION_DOWN && keycode == KeyEvent.KEYCODE_FORWARD_DEL) {
-            deleteSurroundingText(0, 1);
-        } else if (action == KeyEvent.ACTION_DOWN && keycode == KeyEvent.KEYCODE_ENTER) {
-            reset();
+        // being edited).
+        if (event.getAction() == KeyEvent.ACTION_UP) {
+            if (event.getKeyCode() == KeyEvent.KEYCODE_DEL) {
+                super.deleteSurroundingText(1, 0);
+            } else if (event.getKeyCode() == KeyEvent.KEYCODE_FORWARD_DEL) {
+                super.deleteSurroundingText(0, 1);
+            } else {
+                int unicodeChar = event.getUnicodeChar();
+                if (unicodeChar != 0) {
+                    Editable editable = getEditable();
+                    int selectionStart = Selection.getSelectionStart(editable);
+                    int selectionEnd = Selection.getSelectionEnd(editable);
+                    if (selectionStart > selectionEnd) {
+                        int temp = selectionStart;
+                        selectionStart = selectionEnd;
+                        selectionEnd = temp;
+                    }
+                    editable.replace(selectionStart, selectionEnd,
+                            Character.toString((char)unicodeChar));
+                }
+            }
         }
+        shouldUpdateImeSelection = true;
+        return super.sendKeyEvent(event);
+    }
+
+    @Override
+    public boolean finishComposingText() {
+        if (DEBUG) Log.d("darkfi", "finishComposingText()");
+        if (mEditable == null
+                || (getComposingSpanStart(mEditable) == getComposingSpanEnd(mEditable))) {
+            return true;
+        }
+        super.finishComposingText();
+        onCompose("", 0, true);
         return true;
     }
 
     @Override
-    public boolean commitText(CharSequence text, int newCursorPosition) {
-        //Log.i("darkfi", String.format("commitText(%s, %d)", text.toString(), newCursorPosition));
-        lastCommitText = text.toString();
-        onCommitText(lastCommitText);
-        return super.commitText(text, newCursorPosition);
+    public boolean setSelection(int start, int end) {
+        if (DEBUG) Log.d("darkfi", "setSelection(" + start + ", " + end + ")");
+        if (start < 0 || end < 0) return true;
+        super.setSelection(start, end);
+        shouldUpdateImeSelection = true;
+        //return mImeAdapter.setEditableSelectionOffsets(start, end);
+        return true;
+    }
+
+    /**
+     * Informs the InputMethodManager and InputMethodSession (i.e. the IME) that there
+     * is no longer a current composition. Note this differs from finishComposingText, which
+     * is called by the IME when it wants to end a composition.
+     */
+    void cancelComposition() {
+        getInputMethodManager().restartInput(mInternalView);
+    }
+
+    @Override
+    public boolean setComposingRegion(int start, int end) {
+        if (DEBUG) Log.d("darkfi", "setComposingRegion(" + start + ", " + end + ")");
+        int a = Math.min(start, end);
+        int b = Math.max(start, end);
+        super.setComposingRegion(a, b);
+        onSetComposeRegion(a, b);
+        return true;
+    }
+
+    boolean isActive() {
+        return getInputMethodManager().isActive();
+    }
+
+    private InputMethodManager getInputMethodManager() {
+        return (InputMethodManager) mInternalView.getContext()
+                .getSystemService(Context.INPUT_METHOD_SERVICE);
+    }
+
+    private void updateImeSelection() {
+        if (DEBUG) Log.d("darkfi", "updateImeSelection()");
+        if (mEditable != null) {
+            getInputMethodManager().updateSelection(mInternalView,
+                    Selection.getSelectionStart(mEditable),
+                    Selection.getSelectionEnd(mEditable),
+                    getComposingSpanStart(mEditable),
+                    getComposingSpanEnd(mEditable));
+        }
+    }
+
+    @Override
+    public boolean beginBatchEdit() {
+        if (DEBUG) Log.d("darkfi", "beginBatchEdit");
+        ++numBatchEdits;
+        return false;
     }
 
     @Override
     public boolean endBatchEdit() {
-        //Log.i("darkfi", "endBatchEdit: " + curr);
-        String text = getTextBeforeCursor(100, 0).toString();
-        if (!text.equals(lastCommitText))
-            onEndEdit(text);
-        lastCommitText = null;
-        return super.endBatchEdit();
+        if (DEBUG) Log.d("darkfi", "endBatchEdit");
+        if (--numBatchEdits == 0 && shouldUpdateImeSelection) {
+            updateImeSelection();
+            shouldUpdateImeSelection = false;
+        }
+        return false;
     }
 
-    public void reset() {
-        setComposingText("", 0);
+    private String editableToXml(Editable editable) {
+        StringBuilder xmlBuilder = new StringBuilder();
+        int length = editable.length();
+
+        Object[] spans = editable.getSpans(0, editable.length(), Object.class);
+
+        for (int i = 0; i < length; i++) {
+            // Find spans starting at this position
+            for (Object span : spans) {
+                if (editable.getSpanStart(span) == i) {
+                    xmlBuilder
+                        .append("<")
+                        .append(span.getClass().getSimpleName())
+                        .append(">");
+                }
+            }
+
+            // Append the character
+            xmlBuilder.append(editable.charAt(i));
+
+            // Find spans ending at this position
+            for (Object span : spans) {
+                if (editable.getSpanEnd(span) == i) {
+                    xmlBuilder
+                        .append("</")
+                        .append(span.getClass().getSimpleName())
+                        .append(">");
+                }
+            }
+        }
+
+        // Find spans starting at this position
+        for (Object span : spans) {
+            if (editable.getSpanStart(span) == length) {
+                xmlBuilder
+                    .append("<")
+                    .append(span.getClass().getSimpleName())
+                    .append(">");
+            }
+        }
+        // Find spans ending at this position
+        for (Object span : spans) {
+            if (editable.getSpanEnd(span) == length) {
+                xmlBuilder
+                    .append("</")
+                    .append(span.getClass().getSimpleName())
+                    .append(">");
+            }
+        }
 
-        // Chromium does this but the above seems to work too.
-        //beginBatchEdit();
-        //finishComposingText();
-        //endBatchEdit();
+        return xmlBuilder.toString();
     }
 }
 

+ 27 - 12
bin/darkwallet/src/android.rs

@@ -42,33 +42,48 @@ pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_setup() {
 }
 
 pub enum AndroidSuggestEvent {
-    CommitText(String),
-    EditText(String),
+    Compose { text: String, cursor_pos: i32, is_commit: bool },
+    ComposeRegion { start: usize, end: usize },
 }
 
 #[no_mangle]
-pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onCommitText(
+pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onCompose(
     env: *mut ndk_sys::JNIEnv,
     _: ndk_sys::jobject,
     text: ndk_sys::jobject,
+    cursor_pos: ndk_sys::jint,
+    is_commit: ndk_sys::jboolean,
 ) {
     let text = ndk_utils::get_utf_str!(env, text);
-    //debug!(target: "android", "onCommitText({text})");
     if let Some(sender) = &GLOBALS.lock().unwrap().sender {
-        let _ = sender.try_send(AndroidSuggestEvent::CommitText(text.to_string()));
+        let _ = sender.try_send(AndroidSuggestEvent::Compose {
+            text: text.to_string(),
+            cursor_pos,
+            is_commit: is_commit != 0,
+        });
     }
 }
 
 #[no_mangle]
-pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onEndEdit(
+pub unsafe extern "C" fn Java_autosuggest_CustomInputConnection_onSetComposeRegion(
     env: *mut ndk_sys::JNIEnv,
     _: ndk_sys::jobject,
-    text: ndk_sys::jobject,
+    start: ndk_sys::jint,
+    end: ndk_sys::jint,
 ) {
-    let text = ndk_utils::get_utf_str!(env, text);
-    //debug!(target: "android", "onEditText({text})");
+    let begin = std::cmp::min(start, end);
+    let end = std::cmp::max(start, end);
+
+    if begin < 0 || end < 0 {
+        warn!(target: "android", "setComposeRegion({start}, {end}) is < 0 so skipping");
+        return
+    }
+
+    let start = begin as usize;
+    let end = end as usize;
+
     if let Some(sender) = &GLOBALS.lock().unwrap().sender {
-        let _ = sender.try_send(AndroidSuggestEvent::EditText(text.to_string()));
+        let _ = sender.try_send(AndroidSuggestEvent::ComposeRegion { start, end });
     }
 }
 
@@ -76,7 +91,7 @@ pub fn set_sender(sender: async_channel::Sender<AndroidSuggestEvent>) {
     GLOBALS.lock().unwrap().sender = Some(sender);
 }
 
-pub fn reset_autosuggest() {
+pub fn cancel_composition() {
     let env = unsafe { android::attach_jni_env() };
     let mut globals = GLOBALS.lock().unwrap();
 
@@ -86,6 +101,6 @@ pub fn reset_autosuggest() {
     }
 
     unsafe {
-        ndk_utils::call_void_method!(env, globals.inp_conn, "reset", "()V");
+        ndk_utils::call_void_method!(env, globals.inp_conn, "cancelComposition", "()V");
     }
 }

+ 77 - 20
bin/darkwallet/src/ui/editbox.rs

@@ -378,8 +378,10 @@ impl EditBox {
         let glyphs = self.glyphs.lock().unwrap().clone();
         let atlas = text::make_texture_atlas(&self.render_api, &glyphs);
 
-        let mut mesh = MeshBuilder::with_clip(clip.clone());
+        //let mut mesh = MeshBuilder::with_clip(clip.clone());
+        let mut mesh = MeshBuilder::new();
         self.draw_selected(&mut mesh, &glyphs, clip.h).unwrap();
+        self.draw_underline(&mut mesh, &glyphs, clip.h).unwrap();
 
         let glyph_pos_iter = GlyphPositionIter::new(font_size, window_scale, &glyphs, baseline);
 
@@ -517,6 +519,65 @@ impl EditBox {
         Ok(())
     }
 
+    fn draw_underline(
+        &self,
+        mesh: &mut MeshBuilder,
+        glyphs: &Vec<Glyph>,
+        clip_h: f32,
+    ) -> Result<()> {
+        if self.underline.is_null(0)? || self.underline.is_null(1)? {
+            // Nothing underline so do nothing
+            return Ok(())
+        }
+        let under_start = self.underline.get_u32(0)? as usize;
+        let under_end = self.underline.get_u32(1)? as usize;
+
+        // Text started but nothing selected yet so do nothing
+        if under_start == under_end {
+            return Ok(())
+        }
+
+        assert!(under_start <= under_end);
+
+        let font_size = self.font_size.get();
+        let window_scale = self.window_scale.get();
+        let baseline = self.baseline.get();
+        let scroll = self.scroll.get();
+        let text_color = self.text_color.get();
+        let glyph_pos_iter = GlyphPositionIter::new(font_size, window_scale, &glyphs, baseline);
+
+        let mut start_x = 0.;
+        let mut end_x = 0.;
+        // When cursor lands at the end of the line
+        let mut rhs = 0.;
+
+        for (glyph_idx, mut glyph_rect) in glyph_pos_iter.enumerate() {
+            glyph_rect.x -= scroll;
+
+            if glyph_idx == under_start {
+                start_x = glyph_rect.x;
+            }
+            if glyph_idx == under_end {
+                end_x = glyph_rect.x;
+            }
+
+            rhs = glyph_rect.rhs();
+        }
+
+        if under_start == 0 {
+            start_x = scroll;
+        }
+
+        if under_end == glyphs.len() {
+            end_x = rhs;
+        }
+
+        // We don't need to do manual clipping since MeshBuilder should do that
+        let underline_rect = Rectangle { x: start_x, y: baseline + 6., w: end_x - start_x, h: 4. };
+        mesh.draw_box(&underline_rect, text_color, &Rectangle::zero());
+        Ok(())
+    }
+
     async fn change_focus(self: Arc<Self>) {
         if !self.is_active.get() {
             return
@@ -907,7 +968,7 @@ impl EditBox {
     fn set_underline_text(&self, suggest_text: &str) {
         if self.underline.is_null(0).unwrap() {
             assert!(self.underline.is_null(1).unwrap());
-            debug!(target: "ui::editbox", "underline is null");
+            //debug!(target: "ui::editbox", "underline is null");
 
             // Underline is not set. Lets insert text before cursor_pos.
             let mut cursor_pos = self.cursor_pos.get();
@@ -925,7 +986,7 @@ impl EditBox {
                 text.push_str(suggest_text);
             }
 
-            debug!(target: "ui::editbox", "setting text = {text}");
+            //debug!(target: "ui::editbox", "setting text = {text}");
             self.text.set(text);
 
             self.underline.set_u32(Role::Internal, 0, cursor_pos).unwrap();
@@ -934,7 +995,7 @@ impl EditBox {
             self.cursor_pos.set(cursor_pos);
         } else {
             assert!(!self.underline.is_null(1).unwrap());
-            debug!(target: "ui::editbox", "underline is NOT null");
+            //debug!(target: "ui::editbox", "underline is NOT null");
 
             // We are going to delete the current underline text and replace it with our new one.
             let mut cursor_pos = self.cursor_pos.get();
@@ -943,7 +1004,7 @@ impl EditBox {
             let underline_start = self.underline.get_u32(0).unwrap() as usize;
             let underline_end = self.underline.get_u32(1).unwrap() as usize;
 
-            debug!(target: "ui::editbox", "inserting underline text at {underline_start}");
+            //debug!(target: "ui::editbox", "inserting underline text at {underline_start}");
 
             let mut text = String::new();
             for (i, glyph) in glyphs.iter().enumerate() {
@@ -960,7 +1021,7 @@ impl EditBox {
                 text.push_str(suggest_text);
             }
 
-            debug!(target: "ui::editbox", "setting text = {text}");
+            //debug!(target: "ui::editbox", "setting text = {text}");
             self.text.set(text);
 
             let underline_end = (underline_start + suggest_text.len()) as u32;
@@ -972,7 +1033,7 @@ impl EditBox {
 
     fn reset_android_autosuggest(&self) {
         #[cfg(target_os = "android")]
-        crate::android::reset_autosuggest();
+        crate::android::cancel_composition();
 
         self.underline.set_null(Role::Internal, 0).unwrap();
         self.underline.set_null(Role::Internal, 1).unwrap();
@@ -1357,8 +1418,8 @@ impl UIObject for EditBox {
         }
     }
 
-    async fn handle_edit_text(&self, suggest_text: &str) -> bool {
-        debug!(target: "ui::editbox", "handle_edit_text({suggest_text})");
+    async fn handle_compose_text(&self, suggest_text: &str, is_commit: bool) -> bool {
+        debug!(target: "ui::editbox", "handle_compose_text({suggest_text}, {is_commit})");
 
         if !self.is_active.get() {
             return false
@@ -1366,28 +1427,24 @@ impl UIObject for EditBox {
 
         self.set_underline_text(suggest_text);
 
+        if is_commit {
+            self.underline.set_null(Role::Internal, 0).unwrap();
+            self.underline.set_null(Role::Internal, 1).unwrap();
+        }
+
         self.regen_glyphs().await;
         //self.apply_cursor_scrolling();
         self.redraw().await;
 
         true
     }
-    async fn handle_commit_text(&self, suggest_text: &str) -> bool {
-        debug!(target: "ui::editbox", "handle_commit_text({suggest_text})");
+    async fn handle_set_compose_region(&self, start: usize, end: usize) -> bool {
+        debug!(target: "ui::editbox", "handle_set_compose_region({start}, {end})");
 
         if !self.is_active.get() {
             return false
         }
 
-        self.set_underline_text(suggest_text);
-
-        self.underline.set_null(Role::Internal, 0).unwrap();
-        self.underline.set_null(Role::Internal, 1).unwrap();
-
-        self.regen_glyphs().await;
-        //self.apply_cursor_scrolling();
-        self.redraw().await;
-
         true
     }
 }

+ 4 - 6
bin/darkwallet/src/ui/layer.rs

@@ -265,27 +265,25 @@ impl UIObject for Layer {
         }
         false
     }
-    async fn handle_edit_text(&self, suggest_text: &str) -> bool {
+    async fn handle_compose_text(&self, suggest_text: &str, is_commit: bool) -> bool {
         if !self.is_visible.get() {
             return false
         }
         for child in self.get_children() {
             let obj = get_ui_object3(&child);
-            if obj.handle_edit_text(suggest_text).await {
-                //debug!(target: "layer", "handle_edit_text({suggest_text}) swallowed by {child:?}");
+            if obj.handle_compose_text(suggest_text, is_commit).await {
                 return true
             }
         }
         false
     }
-    async fn handle_commit_text(&self, suggest_text: &str) -> bool {
+    async fn handle_set_compose_region(&self, start: usize, end: usize) -> bool {
         if !self.is_visible.get() {
             return false
         }
         for child in self.get_children() {
             let obj = get_ui_object3(&child);
-            if obj.handle_commit_text(suggest_text).await {
-                //debug!(target: "layer", "handle_edit_text({suggest_text}) swallowed by {child:?}");
+            if obj.handle_set_compose_region(start, end).await {
                 return true
             }
         }

+ 2 - 2
bin/darkwallet/src/ui/mod.rs

@@ -83,10 +83,10 @@ pub trait UIObject: Sync {
     }
 
     // Android Autosuggest
-    async fn handle_edit_text(&self, text: &str) -> bool {
+    async fn handle_compose_text(&self, text: &str, is_commit: bool) -> bool {
         false
     }
-    async fn handle_commit_text(&self, text: &str) -> bool {
+    async fn handle_set_compose_region(&self, start: usize, end: usize) -> bool {
         false
     }
 }

+ 4 - 2
bin/darkwallet/src/ui/win.rs

@@ -402,8 +402,10 @@ impl Window {
         for child in self.get_children() {
             let obj = get_ui_object3(&child);
             let is_handled = match &ev {
-                EditText(text) => obj.handle_edit_text(&text).await,
-                CommitText(text) => obj.handle_commit_text(&text).await,
+                Compose { text, cursor_pos, is_commit } => {
+                    obj.handle_compose_text(&text, *is_commit).await
+                }
+                ComposeRegion { start, end } => obj.handle_set_compose_region(*start, *end).await,
             };
             if is_handled {
                 return