mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 09:55:40 +00:00
app: [Android] move UTF-16 to UTF-32 conversion routines to Go
They're easier to test and can be re-used for macOS/Windows. While here, add a Go 1.18 build tag to app/ime_test.go; it relies on Go 1.18 fuzzing. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+22
-59
@@ -343,12 +343,12 @@ public final class GioView extends SurfaceView {
|
|||||||
Snippet snip = getSnippet();
|
Snippet snip = getSnippet();
|
||||||
editor.inputType = this.keyboardHint;
|
editor.inputType = this.keyboardHint;
|
||||||
editor.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
|
editor.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
|
||||||
editor.initialSelStart = snip.toChars(imeSelectionStart(nhandle));
|
editor.initialSelStart = imeToUTF16(nhandle, imeSelectionStart(nhandle));
|
||||||
editor.initialSelEnd = snip.toChars(imeSelectionEnd(nhandle));
|
editor.initialSelEnd = imeToUTF16(nhandle, imeSelectionEnd(nhandle));
|
||||||
int selStart = editor.initialSelStart - snip.offset;
|
int selStart = editor.initialSelStart - snip.offset;
|
||||||
editor.initialCapsMode = TextUtils.getCapsMode(snip.snippet, selStart, this.keyboardHint);
|
editor.initialCapsMode = TextUtils.getCapsMode(snip.snippet, selStart, this.keyboardHint);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
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);
|
imeSetComposingRegion(nhandle, -1, -1);
|
||||||
return new GioInputConnection();
|
return new GioInputConnection();
|
||||||
@@ -435,10 +435,10 @@ public final class GioView extends SurfaceView {
|
|||||||
|
|
||||||
void updateSelection() {
|
void updateSelection() {
|
||||||
Snippet snip = getSnippet();
|
Snippet snip = getSnippet();
|
||||||
int selStart = snip.toChars(imeSelectionStart(nhandle));
|
int selStart = imeToUTF16(nhandle, imeSelectionStart(nhandle));
|
||||||
int selEnd = snip.toChars(imeSelectionEnd(nhandle));
|
int selEnd = imeToUTF16(nhandle, imeSelectionEnd(nhandle));
|
||||||
int compStart = snip.toChars(imeComposingStart(nhandle));
|
int compStart = imeToUTF16(nhandle, imeComposingStart(nhandle));
|
||||||
int compEnd = snip.toChars(imeComposingEnd(nhandle));
|
int compEnd = imeToUTF16(nhandle, imeComposingEnd(nhandle));
|
||||||
imm.updateSelection(this, selStart, selEnd, compStart, compEnd);
|
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 imeReplace(long handle, int start, int end, String text);
|
||||||
static private native int imeSetSelection(long handle, int start, int end);
|
static private native int imeSetSelection(long handle, int start, int end);
|
||||||
static private native int imeSetComposingRegion(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 class GioInputConnection implements InputConnection {
|
||||||
private int batchDepth;
|
private int batchDepth;
|
||||||
@@ -507,8 +511,8 @@ public final class GioView extends SurfaceView {
|
|||||||
int selStart = imeSelectionStart(nhandle);
|
int selStart = imeSelectionStart(nhandle);
|
||||||
int selEnd = imeSelectionEnd(nhandle);
|
int selEnd = imeSelectionEnd(nhandle);
|
||||||
Snippet snip = getSnippet();
|
Snippet snip = getSnippet();
|
||||||
int before = selStart - snip.toRunes(snip.toChars(selStart) - beforeChars);
|
int before = selStart - imeToRunes(nhandle, imeToUTF16(nhandle, selStart) - beforeChars);
|
||||||
int after = selEnd - snip.toRunes(snip.toChars(selEnd) - afterChars);
|
int after = selEnd - imeToRunes(nhandle, imeToUTF16(nhandle, selEnd) - afterChars);
|
||||||
return deleteSurroundingTextInCodePoints(before, after);
|
return deleteSurroundingTextInCodePoints(before, after);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -520,7 +524,7 @@ public final class GioView extends SurfaceView {
|
|||||||
@Override public int getCursorCapsMode(int reqModes) {
|
@Override public int getCursorCapsMode(int reqModes) {
|
||||||
Snippet snip = getSnippet();
|
Snippet snip = getSnippet();
|
||||||
int selStart = imeSelectionStart(nhandle);
|
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) {
|
@Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
|
||||||
@@ -543,7 +547,7 @@ public final class GioView extends SurfaceView {
|
|||||||
// than wanted.
|
// than wanted.
|
||||||
imeSetSnippet(nhandle, selStart - n, selEnd + n);
|
imeSetSnippet(nhandle, selStart - n, selEnd + n);
|
||||||
int start = selEnd;
|
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);
|
String ret = snip.substringRunes(start, end);
|
||||||
return ret;
|
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
|
// n are in Java characters, but in worst case we'll just ask for more runes
|
||||||
// than wanted.
|
// than wanted.
|
||||||
imeSetSnippet(nhandle, selStart - n, selEnd + n);
|
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;
|
int end = selStart;
|
||||||
String ret = snip.substringRunes(start, end);
|
String ret = snip.substringRunes(start, end);
|
||||||
return ret;
|
return ret;
|
||||||
@@ -589,8 +593,8 @@ public final class GioView extends SurfaceView {
|
|||||||
|
|
||||||
@Override public boolean setComposingRegion(int startChars, int endChars) {
|
@Override public boolean setComposingRegion(int startChars, int endChars) {
|
||||||
Snippet snip = getSnippet();
|
Snippet snip = getSnippet();
|
||||||
int compStart = snip.toRunes(startChars);
|
int compStart = imeToRunes(nhandle, startChars);
|
||||||
int compEnd = snip.toRunes(endChars);
|
int compEnd = imeToRunes(nhandle, endChars);
|
||||||
imeSetComposingRegion(nhandle, compStart, compEnd);
|
imeSetComposingRegion(nhandle, compStart, compEnd);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -614,15 +618,15 @@ public final class GioView extends SurfaceView {
|
|||||||
|
|
||||||
// Move cursor.
|
// Move cursor.
|
||||||
Snippet snip = getSnippet();
|
Snippet snip = getSnippet();
|
||||||
cursor = snip.toRunes(snip.toChars(cursor) + relCursor);
|
cursor = imeToRunes(nhandle, imeToUTF16(nhandle, cursor) + relCursor);
|
||||||
imeSetSelection(nhandle, cursor, cursor);
|
imeSetSelection(nhandle, cursor, cursor);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override public boolean setSelection(int startChars, int endChars) {
|
@Override public boolean setSelection(int startChars, int endChars) {
|
||||||
Snippet snip = getSnippet();
|
Snippet snip = getSnippet();
|
||||||
int start = snip.toRunes(startChars);
|
int start = imeToRunes(nhandle, startChars);
|
||||||
int end = snip.toRunes(endChars);
|
int end = imeToRunes(nhandle, endChars);
|
||||||
imeSetSelection(nhandle, start, end);
|
imeSetSelection(nhandle, start, end);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -660,7 +664,7 @@ public final class GioView extends SurfaceView {
|
|||||||
int selEnd = imeSelectionEnd(nhandle);
|
int selEnd = imeSelectionEnd(nhandle);
|
||||||
// Expanding in Java characters is ok.
|
// Expanding in Java characters is ok.
|
||||||
imeSetSnippet(nhandle, selStart - beforeChars, selEnd + afterChars);
|
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.
|
// ever see snippets.
|
||||||
int offset;
|
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
|
// substringRunes returns the substring from start to end in runes. The resuls is
|
||||||
// truncated to the snippet.
|
// truncated to the snippet.
|
||||||
String substringRunes(int start, int end) {
|
String substringRunes(int start, int end) {
|
||||||
|
|||||||
@@ -1,9 +1,13 @@
|
|||||||
// SPDX-License-Identifier: Unlicense OR MIT
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
//go:build go1.18
|
||||||
|
// +build go1.18
|
||||||
|
|
||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
"gioui.org/font/gofont"
|
"gioui.org/font/gofont"
|
||||||
"gioui.org/io/key"
|
"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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1081,6 +1081,20 @@ func Java_org_gioui_GioView_imeReplace(env *C.JNIEnv, class C.jclass, handle C.j
|
|||||||
w.callbacks.EditorReplace(r, text)
|
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) {
|
func (w *window) EditorStateChanged(old, new editorState) {
|
||||||
runInJVM(javaVM(), func(env *C.JNIEnv) {
|
runInJVM(javaVM(), func(env *C.JNIEnv) {
|
||||||
if old.Snippet != new.Snippet {
|
if old.Snippet != new.Snippet {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import (
|
|||||||
"image/color"
|
"image/color"
|
||||||
"runtime"
|
"runtime"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
"unicode/utf16"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
|
|
||||||
"gioui.org/f32"
|
"gioui.org/f32"
|
||||||
@@ -551,6 +553,56 @@ func (e *editorState) Replace(r key.Range, text string) {
|
|||||||
e.Snippet = s
|
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) {
|
func (w *Window) waitAck(d driver) {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|||||||
Reference in New Issue
Block a user