diff --git a/app/GioView.java b/app/GioView.java index b788caf5..47e293b5 100644 --- a/app/GioView.java +++ b/app/GioView.java @@ -343,12 +343,12 @@ public final class GioView extends SurfaceView { Snippet snip = getSnippet(); editor.inputType = this.keyboardHint; editor.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI; - editor.initialSelStart = snip.toChars(imeSelectionStart(nhandle)); - editor.initialSelEnd = snip.toChars(imeSelectionEnd(nhandle)); + editor.initialSelStart = imeToUTF16(nhandle, imeSelectionStart(nhandle)); + editor.initialSelEnd = imeToUTF16(nhandle, imeSelectionEnd(nhandle)); int selStart = editor.initialSelStart - snip.offset; editor.initialCapsMode = TextUtils.getCapsMode(snip.snippet, selStart, this.keyboardHint); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { - editor.setInitialSurroundingSubText(snip.snippet, snip.toChars(snip.offset)); + editor.setInitialSurroundingSubText(snip.snippet, imeToUTF16(nhandle, snip.offset)); } imeSetComposingRegion(nhandle, -1, -1); return new GioInputConnection(); @@ -435,10 +435,10 @@ public final class GioView extends SurfaceView { void updateSelection() { Snippet snip = getSnippet(); - int selStart = snip.toChars(imeSelectionStart(nhandle)); - int selEnd = snip.toChars(imeSelectionEnd(nhandle)); - int compStart = snip.toChars(imeComposingStart(nhandle)); - int compEnd = snip.toChars(imeComposingEnd(nhandle)); + int selStart = imeToUTF16(nhandle, imeSelectionStart(nhandle)); + int selEnd = imeToUTF16(nhandle, imeSelectionEnd(nhandle)); + int compStart = imeToUTF16(nhandle, imeComposingStart(nhandle)); + int compEnd = imeToUTF16(nhandle, imeComposingEnd(nhandle)); imm.updateSelection(this, selStart, selEnd, compStart, compEnd); } @@ -471,6 +471,10 @@ public final class GioView extends SurfaceView { static private native int imeReplace(long handle, int start, int end, String text); static private native int imeSetSelection(long handle, int start, int end); static private native int imeSetComposingRegion(long handle, int start, int end); + // imeToRunes converts the Java character index into runes (Java code points). + static private native int imeToRunes(long handle, int chars); + // imeToUTF16 converts the rune index into Java characters. + static private native int imeToUTF16(long handle, int runes); private class GioInputConnection implements InputConnection { private int batchDepth; @@ -507,8 +511,8 @@ public final class GioView extends SurfaceView { int selStart = imeSelectionStart(nhandle); int selEnd = imeSelectionEnd(nhandle); Snippet snip = getSnippet(); - int before = selStart - snip.toRunes(snip.toChars(selStart) - beforeChars); - int after = selEnd - snip.toRunes(snip.toChars(selEnd) - afterChars); + int before = selStart - imeToRunes(nhandle, imeToUTF16(nhandle, selStart) - beforeChars); + int after = selEnd - imeToRunes(nhandle, imeToUTF16(nhandle, selEnd) - afterChars); return deleteSurroundingTextInCodePoints(before, after); } @@ -520,7 +524,7 @@ public final class GioView extends SurfaceView { @Override public int getCursorCapsMode(int reqModes) { Snippet snip = getSnippet(); int selStart = imeSelectionStart(nhandle); - return TextUtils.getCapsMode(snip.snippet, snip.toChars(selStart), reqModes); + return TextUtils.getCapsMode(snip.snippet, imeToUTF16(nhandle, selStart), reqModes); } @Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) { @@ -543,7 +547,7 @@ public final class GioView extends SurfaceView { // than wanted. imeSetSnippet(nhandle, selStart - n, selEnd + n); int start = selEnd; - int end = snip.toRunes(snip.toChars(selEnd) + n); + int end = imeToRunes(nhandle, imeToUTF16(nhandle, selEnd) + n); String ret = snip.substringRunes(start, end); return ret; } @@ -555,7 +559,7 @@ public final class GioView extends SurfaceView { // n are in Java characters, but in worst case we'll just ask for more runes // than wanted. imeSetSnippet(nhandle, selStart - n, selEnd + n); - int start = snip.toRunes(snip.toChars(selStart) - n); + int start = imeToRunes(nhandle, imeToUTF16(nhandle, selStart) - n); int end = selStart; String ret = snip.substringRunes(start, end); return ret; @@ -589,8 +593,8 @@ public final class GioView extends SurfaceView { @Override public boolean setComposingRegion(int startChars, int endChars) { Snippet snip = getSnippet(); - int compStart = snip.toRunes(startChars); - int compEnd = snip.toRunes(endChars); + int compStart = imeToRunes(nhandle, startChars); + int compEnd = imeToRunes(nhandle, endChars); imeSetComposingRegion(nhandle, compStart, compEnd); return true; } @@ -614,15 +618,15 @@ public final class GioView extends SurfaceView { // Move cursor. Snippet snip = getSnippet(); - cursor = snip.toRunes(snip.toChars(cursor) + relCursor); + cursor = imeToRunes(nhandle, imeToUTF16(nhandle, cursor) + relCursor); imeSetSelection(nhandle, cursor, cursor); return true; } @Override public boolean setSelection(int startChars, int endChars) { Snippet snip = getSnippet(); - int start = snip.toRunes(startChars); - int end = snip.toRunes(endChars); + int start = imeToRunes(nhandle, startChars); + int end = imeToRunes(nhandle, endChars); imeSetSelection(nhandle, start, end); return true; } @@ -660,7 +664,7 @@ public final class GioView extends SurfaceView { int selEnd = imeSelectionEnd(nhandle); // Expanding in Java characters is ok. imeSetSnippet(nhandle, selStart - beforeChars, selEnd + afterChars); - return new SurroundingText(snip.snippet, snip.toChars(selStart), snip.toChars(selEnd), snip.toChars(snip.offset)); + return new SurroundingText(snip.snippet, imeToUTF16(nhandle, selStart), imeToUTF16(nhandle, selEnd), imeToUTF16(nhandle, snip.offset)); } } @@ -679,47 +683,6 @@ public final class GioView extends SurfaceView { // ever see snippets. int offset; - // toRunes converts the Java character index into runes (Java code points). - int toRunes(int chars) { - if (chars == -1) { - return -1; - } - if (chars < this.offset) { - // Assume runes before offset are one Java character each. - return chars; - } - int runes = this.offset; - chars -= this.offset; - if (chars > snippet.length()) { - // Assume runes after snippets are one Java character each. - runes += chars - snippet.length(); - chars = snippet.length(); - } - runes += snippet.codePointCount(0, chars); - return runes; - } - - // toChars converts the rune index into Java characters. - int toChars(int runes) { - if (runes == -1) { - return -1; - } - if (runes < this.offset) { - // Assume runes before offset are one Java character each. - return runes; - } - int chars = this.offset; - runes -= this.offset; - int n = snippet.codePointCount(0, snippet.length()); - if (runes > n) { - // Assume runes after snippets are one Java character each. - chars += runes - n; - runes = n; - } - chars += snippet.offsetByCodePoints(0, runes); - return chars; - } - // substringRunes returns the substring from start to end in runes. The resuls is // truncated to the snippet. String substringRunes(int start, int end) { diff --git a/app/ime_test.go b/app/ime_test.go index 92a12d6c..b73ae3cb 100644 --- a/app/ime_test.go +++ b/app/ime_test.go @@ -1,9 +1,13 @@ // SPDX-License-Identifier: Unlicense OR MIT +//go:build go1.18 +// +build go1.18 + package app import ( "testing" + "unicode/utf8" "gioui.org/font/gofont" "gioui.org/io/key" @@ -107,3 +111,28 @@ func FuzzIME(f *testing.F) { } }) } + +func TestEditorIndices(t *testing.T) { + var s editorState + const str = "Hello, 😀" + s.Snippet = key.Snippet{ + Text: str, + Range: key.Range{ + Start: 10, + End: utf8.RuneCountInString(str), + }, + } + utf16Indices := [...]struct { + Runes, UTF16 int + }{ + {0, 0}, {10, 10}, {17, 17}, {18, 19}, {30, 31}, + } + for _, p := range utf16Indices { + if want, got := p.UTF16, s.UTF16Index(p.Runes); want != got { + t.Errorf("UTF16Index(%d) = %d, wanted %d", p.Runes, got, want) + } + if want, got := p.Runes, s.RunesIndex(p.UTF16); want != got { + t.Errorf("RunesIndex(%d) = %d, wanted %d", p.UTF16, got, want) + } + } +} diff --git a/app/os_android.go b/app/os_android.go index 543b2d27..e396006d 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -1081,6 +1081,20 @@ func Java_org_gioui_GioView_imeReplace(env *C.JNIEnv, class C.jclass, handle C.j w.callbacks.EditorReplace(r, text) } +//export Java_org_gioui_GioView_imeToRunes +func Java_org_gioui_GioView_imeToRunes(env *C.JNIEnv, class C.jclass, handle C.jlong, chars C.jint) C.jint { + w := views[handle] + state := w.callbacks.EditorState() + return C.jint(state.RunesIndex(int(chars))) +} + +//export Java_org_gioui_GioView_imeToUTF16 +func Java_org_gioui_GioView_imeToUTF16(env *C.JNIEnv, class C.jclass, handle C.jlong, runes C.jint) C.jint { + w := views[handle] + state := w.callbacks.EditorState() + return C.jint(state.UTF16Index(int(runes))) +} + func (w *window) EditorStateChanged(old, new editorState) { runInJVM(javaVM(), func(env *C.JNIEnv) { if old.Snippet != new.Snippet { diff --git a/app/window.go b/app/window.go index a1727dc1..6eaac58a 100644 --- a/app/window.go +++ b/app/window.go @@ -9,6 +9,8 @@ import ( "image/color" "runtime" "time" + "unicode" + "unicode/utf16" "unicode/utf8" "gioui.org/f32" @@ -551,6 +553,56 @@ func (e *editorState) Replace(r key.Range, text string) { e.Snippet = s } +// UTF16Index converts the given index in runes into an index in utf16 characters. +func (e *editorState) UTF16Index(runes int) int { + if runes == -1 { + return -1 + } + if runes < e.Snippet.Start { + // Assume runes before sippet are one UTF-16 character each. + return runes + } + chars := e.Snippet.Start + runes -= e.Snippet.Start + for _, r := range e.Snippet.Text { + if runes == 0 { + break + } + runes-- + chars++ + if r1, _ := utf16.EncodeRune(r); r1 != unicode.ReplacementChar { + chars++ + } + } + // Assume runes after snippets are one UTF-16 character each. + return chars + runes +} + +// RunesIndex converts the given index in utf16 characters to an index in runes. +func (e *editorState) RunesIndex(chars int) int { + if chars == -1 { + return -1 + } + if chars < e.Snippet.Start { + // Assume runes before offset are one UTF-16 character each. + return chars + } + runes := e.Snippet.Start + chars -= e.Snippet.Start + for _, r := range e.Snippet.Text { + if chars == 0 { + break + } + chars-- + runes++ + if r1, _ := utf16.EncodeRune(r); r1 != unicode.ReplacementChar { + chars-- + } + } + // Assume runes after snippets are one UTF-16 character each. + return runes + chars +} + func (w *Window) waitAck(d driver) { for { select {