app,widget: implement Editor IME support, add Android implementation

Fixes: https://todo.sr.ht/~eliasnaur/gio/116
References: https://todo.sr.ht/~eliasnaur/gio/246
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2022-01-13 13:04:31 +01:00
parent c22138f5f8
commit 58cdb3e1da
16 changed files with 880 additions and 55 deletions
+312 -14
View File
@@ -17,7 +17,11 @@ import android.graphics.Color;
import android.graphics.Rect;
import android.os.Build;
import android.os.Bundle;
import android.text.Editable;
import android.os.Handler;
import android.os.SystemClock;
import android.text.TextUtils;
import android.text.Selection;
import android.text.SpannableStringBuilder;
import android.util.AttributeSet;
import android.util.TypedValue;
import android.view.KeyCharacterMap;
@@ -33,11 +37,15 @@ import android.view.SurfaceHolder;
import android.view.Window;
import android.view.WindowInsetsController;
import android.view.WindowManager;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.EditorInfo;
import android.text.InputType;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.InputContentInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.SurroundingText;
import android.view.accessibility.AccessibilityNodeProvider;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.accessibility.AccessibilityEvent;
@@ -332,9 +340,18 @@ public final class GioView extends SurfaceView {
}
@Override public InputConnection onCreateInputConnection(EditorInfo editor) {
Snippet snip = getSnippet();
editor.inputType = this.keyboardHint;
editor.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN | EditorInfo.IME_FLAG_NO_EXTRACT_UI;
return new InputConnection(this);
editor.initialSelStart = snip.toChars(imeSelectionStart(nhandle));
editor.initialSelEnd = snip.toChars(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));
}
imeSetComposingRegion(nhandle, -1, -1);
return new GioInputConnection();
}
void setInputHint(int hint) {
@@ -342,7 +359,7 @@ public final class GioView extends SurfaceView {
return;
}
this.keyboardHint = hint;
imm.restartInput(this);
restartInput();
}
void showTextInput() {
@@ -412,6 +429,19 @@ public final class GioView extends SurfaceView {
return onBack(nhandle);
}
void restartInput() {
imm.restartInput(this);
}
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));
imm.updateSelection(this, selStart, selEnd, compStart, compEnd);
}
static private native long onCreateView(GioView view);
static private native void onDestroyView(long handle);
static private native void onStartView(long handle);
@@ -431,19 +461,287 @@ public final class GioView extends SurfaceView {
static private native void onExitTouchExploration(long handle);
static private native void onA11yFocus(long handle, int viewId);
static private native void onClearA11yFocus(long handle, int viewId);
static private native void imeSetSnippet(long handle, int start, int end);
static private native String imeSnippet(long handle);
static private native int imeSnippetStart(long handle);
static private native int imeSelectionStart(long handle);
static private native int imeSelectionEnd(long handle);
static private native int imeComposingStart(long handle);
static private native int imeComposingEnd(long handle);
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);
private static class InputConnection extends BaseInputConnection {
private final Editable editable;
private class GioInputConnection implements InputConnection {
private int batchDepth;
InputConnection(View view) {
// Passing false enables "dummy mode", where the BaseInputConnection
// attempts to convert IME operations to key events.
super(view, false);
editable = Editable.Factory.getInstance().newEditable("");
@Override public boolean beginBatchEdit() {
batchDepth++;
return true;
}
@Override public Editable getEditable() {
return editable;
@Override public boolean endBatchEdit() {
batchDepth--;
return batchDepth > 0;
}
@Override public boolean clearMetaKeyStates(int states) {
return false;
}
@Override public boolean commitCompletion(CompletionInfo text) {
return false;
}
@Override public boolean commitCorrection(CorrectionInfo info) {
return false;
}
@Override public boolean commitText(CharSequence text, int cursor) {
setComposingText(text, cursor);
return finishComposingText();
}
@Override public boolean deleteSurroundingText(int beforeChars, int afterChars) {
// translate before and after to runes.
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);
return deleteSurroundingTextInCodePoints(before, after);
}
@Override public boolean finishComposingText() {
imeSetComposingRegion(nhandle, -1, -1);
return true;
}
@Override public int getCursorCapsMode(int reqModes) {
Snippet snip = getSnippet();
int selStart = imeSelectionStart(nhandle);
return TextUtils.getCapsMode(snip.snippet, snip.toChars(selStart), reqModes);
}
@Override public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
return null;
}
@Override public CharSequence getSelectedText(int flags) {
Snippet snip = getSnippet();
int selStart = imeSelectionStart(nhandle);
int selEnd = imeSelectionEnd(nhandle);
String sub = snip.substringRunes(selStart, selEnd);
return sub;
}
@Override public CharSequence getTextAfterCursor(int n, int flags) {
Snippet snip = getSnippet();
int selStart = imeSelectionStart(nhandle);
int selEnd = imeSelectionEnd(nhandle);
// 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 = selEnd;
int end = snip.toRunes(snip.toChars(selEnd) + n);
String ret = snip.substringRunes(start, end);
return ret;
}
@Override public CharSequence getTextBeforeCursor(int n, int flags) {
Snippet snip = getSnippet();
int selStart = imeSelectionStart(nhandle);
int selEnd = imeSelectionEnd(nhandle);
// 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 end = selStart;
String ret = snip.substringRunes(start, end);
return ret;
}
@Override public boolean performContextMenuAction(int id) {
return false;
}
@Override public boolean performEditorAction(int editorAction) {
long eventTime = SystemClock.uptimeMillis();
// Translate to enter key.
onKeyEvent(nhandle, KeyEvent.KEYCODE_ENTER, '\n', true, eventTime);
onKeyEvent(nhandle, KeyEvent.KEYCODE_ENTER, '\n', false, eventTime);
return true;
}
@Override public boolean performPrivateCommand(String action, Bundle data) {
return false;
}
@Override public boolean reportFullscreenMode(boolean enabled) {
return false;
}
@Override public boolean sendKeyEvent(KeyEvent event) {
boolean pressed = event.getAction() == KeyEvent.ACTION_DOWN;
onKeyEvent(nhandle, event.getKeyCode(), event.getUnicodeChar(), pressed, event.getEventTime());
return true;
}
@Override public boolean setComposingRegion(int startChars, int endChars) {
Snippet snip = getSnippet();
int compStart = snip.toRunes(startChars);
int compEnd = snip.toRunes(endChars);
imeSetComposingRegion(nhandle, compStart, compEnd);
return true;
}
@Override public boolean setComposingText(CharSequence text, int relCursor) {
int start = imeComposingStart(nhandle);
int end = imeComposingEnd(nhandle);
if (start == -1 || end == -1) {
start = imeSelectionStart(nhandle);
end = imeSelectionEnd(nhandle);
}
String str = text.toString();
imeReplace(nhandle, start, end, str);
int cursor = start;
int runes = str.codePointCount(0, str.length());
if (relCursor > 0) {
cursor += runes;
relCursor--;
}
imeSetComposingRegion(nhandle, start, start + runes);
// Move cursor.
Snippet snip = getSnippet();
cursor = snip.toRunes(snip.toChars(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);
imeSetSelection(nhandle, start, end);
return true;
}
/*@Override*/ public boolean requestCursorUpdates(int cursorUpdateMode) {
return false;
}
/*@Override*/ public void closeConnection() {
}
/*@Override*/ public Handler getHandler() {
return null;
}
/*@Override*/ public boolean commitContent(InputContentInfo info, int flags, Bundle opts) {
return false;
}
/*@Override*/ public boolean deleteSurroundingTextInCodePoints(int before, int after) {
if (after > 0) {
int selEnd = imeSelectionEnd(nhandle);
imeReplace(nhandle, selEnd, selEnd + after, "");
}
if (before > 0) {
int selStart = imeSelectionStart(nhandle);
imeReplace(nhandle, selStart - before, selStart, "");
}
return true;
}
/*@Override*/ public SurroundingText getSurroundingText(int beforeChars, int afterChars, int flags) {
Snippet snip = getSnippet();
int selStart = imeSelectionStart(nhandle);
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));
}
}
private Snippet getSnippet() {
Snippet snip = new Snippet();
snip.snippet = imeSnippet(nhandle);
snip.offset = imeSnippetStart(nhandle);
return snip;
}
// Snippet is like android.view.inputmethod.SurroundingText but available for Android < 31.
private static class Snippet {
String snippet;
// offset of snippet into the entire editor content. It is in runes because we won't require
// Gio editors to keep track of UTF-16 offsets. The disctinction won't matter in practice because IMEs only
// 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) {
start -= this.offset;
end -= this.offset;
int runes = snippet.codePointCount(0, snippet.length());
if (start < 0) {
start = 0;
}
if (end < 0) {
end = 0;
}
if (start > runes) {
start = runes;
}
if (end > runes) {
end = runes;
}
return snippet.substring(
snippet.offsetByCodePoints(0, start),
snippet.offsetByCodePoints(0, end)
);
}
}
+109
View File
@@ -0,0 +1,109 @@
// SPDX-License-Identifier: Unlicense OR MIT
package app
import (
"testing"
"gioui.org/font/gofont"
"gioui.org/io/key"
"gioui.org/io/router"
"gioui.org/layout"
"gioui.org/op"
"gioui.org/text"
"gioui.org/unit"
"gioui.org/widget"
)
func FuzzIME(f *testing.F) {
runes := []rune("Hello, 世界! 🤬 علي،الحسنب北查爾斯頓工廠的安全漏洞已")
f.Add([]byte("20\x0010"))
f.Add([]byte("80000"))
f.Add([]byte("2008\"80\r00"))
f.Add([]byte("20007900002\x02000"))
f.Add([]byte("20007800002\x02000"))
f.Add([]byte("200A02000990\x19002\x17\x0200"))
f.Fuzz(func(t *testing.T, cmds []byte) {
cache := text.NewCache(gofont.Collection())
e := new(widget.Editor)
e.Focus()
var r router.Router
gtx := layout.Context{Ops: new(op.Ops), Queue: &r}
// Layout once to register focus.
e.Layout(gtx, cache, text.Font{}, unit.Px(10), nil)
r.Frame(gtx.Ops)
var state editorState
const (
cmdReplace = iota
cmdSelect
cmdSnip
maxCmd
)
const cmdLen = 5
for len(cmds) >= cmdLen {
rng := key.Range{
Start: int(cmds[1]),
End: int(cmds[2]),
}
switch cmds[0] % cmdLen {
case cmdReplace:
rstart := int(cmds[3]) % len(runes)
rend := int(cmds[4]) % len(runes)
if rstart > rend {
rstart, rend = rend, rstart
}
replacement := string(runes[rstart:rend])
state.Replace(rng, replacement)
r.Queue(key.EditEvent{Range: rng, Text: replacement})
r.Queue(key.SnippetEvent(state.Snippet.Range))
case cmdSelect:
r.Queue(key.SelectionEvent(rng))
runes := []rune(e.Text())
if rng.Start < 0 {
rng.Start = 0
}
if rng.End < 0 {
rng.End = 0
}
if rng.Start > len(runes) {
rng.Start = len(runes)
}
if rng.End > len(runes) {
rng.End = len(runes)
}
state.Selection = rng
case cmdSnip:
r.Queue(key.SnippetEvent(rng))
runes := []rune(e.Text())
if rng.Start > rng.End {
rng.Start, rng.End = rng.End, rng.Start
}
if rng.Start < 0 {
rng.Start = 0
}
if rng.End < 0 {
rng.End = 0
}
if rng.Start > len(runes) {
rng.Start = len(runes)
}
if rng.End > len(runes) {
rng.End = len(runes)
}
state.Snippet = key.Snippet{
Range: rng,
Text: string(runes[rng.Start:rng.End]),
}
}
cmds = cmds[cmdLen:]
e.Layout(gtx, cache, text.Font{}, unit.Px(10), nil)
r.Frame(gtx.Ops)
newState := r.EditorState()
if newState != state.EditorState {
t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState)
}
}
})
}
+2 -10
View File
@@ -152,36 +152,28 @@ type driver interface {
// SetAnimating sets the animation flag. When the window is animating,
// FrameEvents are delivered as fast as the display can handle them.
SetAnimating(anim bool)
// ShowTextInput updates the virtual keyboard state.
ShowTextInput(show bool)
SetInputHint(mode key.InputHint)
NewContext() (context, error)
// ReadClipboard requests the clipboard content.
ReadClipboard()
// WriteClipboard requests a clipboard write.
WriteClipboard(s string)
// Configure the window.
Configure([]Option)
// SetCursor updates the current cursor to name.
SetCursor(name pointer.CursorName)
// Raise the window at the top.
Raise()
// Close the window.
Close()
// Wakeup wakes up the event loop and sends a WakeupEvent.
Wakeup()
// Perform actions on the window.
Perform(system.Action)
// EditorStateChanged notifies the driver that the editor state changed.
EditorStateChanged(old, new editorState)
}
type windowRendezvous struct {
+109 -7
View File
@@ -188,6 +188,8 @@ var gioView struct {
sendA11yEvent C.jmethodID
sendA11yChange C.jmethodID
isA11yActive C.jmethodID
restartInput C.jmethodID
updateSelection C.jmethodID
}
// ViewEvent is sent whenever the Window's underlying Android view
@@ -471,6 +473,8 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
m.sendA11yEvent = getMethodID(env, class, "sendA11yEvent", "(II)V")
m.sendA11yChange = getMethodID(env, class, "sendA11yChange", "(I)V")
m.isA11yActive = getMethodID(env, class, "isA11yActive", "()Z")
m.restartInput = getMethodID(env, class, "restartInput", "()V")
m.updateSelection = getMethodID(env, class, "updateSelection", "()V")
})
view = C.jni_NewGlobalRef(env, view)
wopts := <-mainWindow.out
@@ -490,6 +494,7 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
views[handle] = w
w.loadConfig(env, class)
w.Configure(wopts.options)
w.SetInputHint(key.HintAny)
w.setStage(system.StagePaused)
w.callbacks.Event(ViewEvent{View: uintptr(view)})
return handle
@@ -930,7 +935,7 @@ func Java_org_gioui_GioView_onKeyEvent(env *C.JNIEnv, class C.jclass, handle C.j
w.callbacks.Event(key.Event{Name: n, State: state})
}
if pressed == C.JNI_TRUE && r != 0 && r != '\n' { // Checking for "\n" to prevent duplication with key.NameEnter (gio#224).
w.callbacks.Event(key.EditEvent{Text: string(rune(r))})
w.callbacks.EditorInsert(string(rune(r)))
}
}
@@ -988,6 +993,107 @@ func Java_org_gioui_GioView_onTouchEvent(env *C.JNIEnv, class C.jclass, handle C
})
}
//export Java_org_gioui_GioView_imeSelectionStart
func Java_org_gioui_GioView_imeSelectionStart(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := views[handle]
sel := w.callbacks.EditorState().Selection
start := sel.Start
if sel.End < sel.Start {
start = sel.End
}
return C.jint(start)
}
//export Java_org_gioui_GioView_imeSelectionEnd
func Java_org_gioui_GioView_imeSelectionEnd(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := views[handle]
sel := w.callbacks.EditorState().Selection
end := sel.End
if sel.End < sel.Start {
end = sel.Start
}
return C.jint(end)
}
//export Java_org_gioui_GioView_imeComposingStart
func Java_org_gioui_GioView_imeComposingStart(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := views[handle]
comp := w.callbacks.EditorState().compose
start := comp.Start
if e := comp.End; e < start {
start = e
}
return C.jint(start)
}
//export Java_org_gioui_GioView_imeComposingEnd
func Java_org_gioui_GioView_imeComposingEnd(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := views[handle]
comp := w.callbacks.EditorState().compose
end := comp.End
if s := comp.Start; s > end {
end = s
}
return C.jint(end)
}
//export Java_org_gioui_GioView_imeSnippet
func Java_org_gioui_GioView_imeSnippet(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jstring {
w := views[handle]
snip := w.callbacks.EditorState().Snippet.Text
return javaString(env, snip)
}
//export Java_org_gioui_GioView_imeSnippetStart
func Java_org_gioui_GioView_imeSnippetStart(env *C.JNIEnv, class C.jclass, handle C.jlong) C.jint {
w := views[handle]
return C.jint(w.callbacks.EditorState().Snippet.Start)
}
//export Java_org_gioui_GioView_imeSetSnippet
func Java_org_gioui_GioView_imeSetSnippet(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) {
w := views[handle]
r := key.Range{Start: int(start), End: int(end)}
w.callbacks.SetEditorSnippet(r)
}
//export Java_org_gioui_GioView_imeSetSelection
func Java_org_gioui_GioView_imeSetSelection(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) {
w := views[handle]
r := key.Range{Start: int(start), End: int(end)}
w.callbacks.SetEditorSelection(r)
}
//export Java_org_gioui_GioView_imeSetComposingRegion
func Java_org_gioui_GioView_imeSetComposingRegion(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint) {
w := views[handle]
w.callbacks.SetComposingRegion(key.Range{
Start: int(start),
End: int(end),
})
}
//export Java_org_gioui_GioView_imeReplace
func Java_org_gioui_GioView_imeReplace(env *C.JNIEnv, class C.jclass, handle C.jlong, start, end C.jint, jtext C.jstring) {
w := views[handle]
r := key.Range{Start: int(start), End: int(end)}
text := goString(env, jtext)
w.callbacks.EditorReplace(r, text)
}
func (w *window) EditorStateChanged(old, new editorState) {
runInJVM(javaVM(), func(env *C.JNIEnv) {
if old.Snippet != new.Snippet {
callVoidMethod(env, w.view, gioView.restartInput)
return
}
if old.Selection != new.Selection {
w.callbacks.SetComposingRegion(key.Range{Start: -1, End: -1})
}
callVoidMethod(env, w.view, gioView.updateSelection)
})
}
func (w *window) ShowTextInput(show bool) {
runInJVM(javaVM(), func(env *C.JNIEnv) {
if show {
@@ -1002,6 +1108,7 @@ func (w *window) SetInputHint(mode key.InputHint) {
// Constants defined at https://developer.android.com/reference/android/text/InputType.
const (
TYPE_NULL = 0
TYPE_CLASS_TEXT = 1
TYPE_CLASS_NUMBER = 2
TYPE_NUMBER_FLAG_DECIMAL = 8192
TYPE_NUMBER_FLAG_SIGNED = 4096
@@ -1015,14 +1122,9 @@ func (w *window) SetInputHint(mode key.InputHint) {
case key.HintNumeric:
m = TYPE_CLASS_NUMBER | TYPE_NUMBER_FLAG_DECIMAL | TYPE_NUMBER_FLAG_SIGNED
default:
// TYPE_NULL, since TYPE_CLASS_TEXT isn't currently supported.
m = TYPE_NULL
m = TYPE_CLASS_TEXT
}
// The TYPE_TEXT_FLAG_NO_SUGGESTIONS and TYPE_TEXT_VARIATION_VISIBLE_PASSWORD are used to fix the
// Samsung keyboard compatibility, forcing to disable the suggests/auto-complete. gio#116.
m = m | TYPE_TEXT_FLAG_NO_SUGGESTIONS | TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
callVoidMethod(env, w.view, gioView.setInputHint, m)
})
}
+3 -3
View File
@@ -228,9 +228,7 @@ func onDeleteBackward(view C.CFTypeRef) {
//export onText
func onText(view C.CFTypeRef, str *C.char) {
w := views[view]
w.w.Event(key.EditEvent{
Text: C.GoString(str),
})
w.w.EditorInsert(C.GoString(str))
}
//export onTouch
@@ -283,6 +281,8 @@ func (w *window) Configure([]Option) {
}
}
func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) Perform(system.Action) {}
func (w *window) Raise() {}
+3 -1
View File
@@ -300,7 +300,7 @@ func (w *window) addHistory() {
func (w *window) flushInput() {
val := w.tarea.Get("value").String()
w.tarea.Set("value", "")
w.w.Event(key.EditEvent{Text: string(val)})
w.w.EditorInsert(string(val))
}
func (w *window) blur() {
@@ -481,6 +481,8 @@ func (w *window) animCallback() {
}
}
func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) SetAnimating(anim bool) {
w.animating = anim
if anim && !w.animRequested {
+3 -1
View File
@@ -350,6 +350,8 @@ func (w *window) SetCursor(name pointer.CursorName) {
w.cursor = windowSetCursor(w.cursor, name)
}
func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {}
@@ -412,7 +414,7 @@ func gio_onKeys(view C.CFTypeRef, cstr *C.char, ti C.double, mods C.NSUInteger,
func gio_onText(view C.CFTypeRef, cstr *C.char) {
str := C.GoString(cstr)
w := mustView(view)
w.w.Event(key.EditEvent{Text: str})
w.w.EditorInsert(str)
}
//export gio_onMouse
+8 -1
View File
@@ -1202,7 +1202,12 @@ func gio_onKeyboardKey(data unsafe.Pointer, keyboard *C.struct_wl_keyboard, seri
kc := mapXKBKeycode(uint32(keyCode))
ks := mapXKBKeyState(uint32(state))
for _, e := range w.disp.xkb.DispatchKey(kc, ks) {
w.w.Event(e)
if ee, ok := e.(key.EditEvent); ok {
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.w.Event(e)
}
}
if state != C.WL_KEYBOARD_KEY_STATE_PRESSED {
return
@@ -1642,6 +1647,8 @@ func (w *window) ShowTextInput(show bool) {}
func (w *window) SetInputHint(_ key.InputHint) {}
func (w *window) EditorStateChanged(old, new editorState) {}
// Close the window.
func (w *window) Close() {
w.dead = true
+3 -1
View File
@@ -243,7 +243,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
fallthrough
case windows.WM_CHAR:
if r := rune(wParam); unicode.IsPrint(r) {
w.w.Event(key.EditEvent{Text: string(r)})
w.w.EditorInsert(string(r))
}
// The message is processed.
return windows.TRUE
@@ -451,6 +451,8 @@ loop:
return nil
}
func (w *window) EditorStateChanged(old, new editorState) {}
func (w *window) SetAnimating(anim bool) {
w.animating = anim
}
+8 -1
View File
@@ -323,6 +323,8 @@ func (w *x11Window) ShowTextInput(show bool) {}
func (w *x11Window) SetInputHint(_ key.InputHint) {}
func (w *x11Window) EditorStateChanged(old, new editorState) {}
// Close the window.
func (w *x11Window) Close() {
var xev C.XEvent
@@ -534,7 +536,12 @@ func (h *x11EventHandler) handleEvents() bool {
}
kevt := (*C.XKeyPressedEvent)(unsafe.Pointer(xev))
for _, e := range h.w.xkb.DispatchKey(uint32(kevt.keycode), ks) {
w.w.Event(e)
if ee, ok := e.(key.EditEvent); ok {
// There's no support for IME yet.
w.w.EditorInsert(ee.Text)
} else {
w.w.Event(e)
}
}
case C.ButtonPress, C.ButtonRelease:
bevt := (*C.XButtonEvent)(unsafe.Pointer(xev))
+105
View File
@@ -9,12 +9,14 @@ import (
"image/color"
"runtime"
"time"
"unicode/utf8"
"gioui.org/f32"
"gioui.org/font/gofont"
"gioui.org/gpu"
"gioui.org/internal/ops"
"gioui.org/io/event"
"gioui.org/io/key"
"gioui.org/io/pointer"
"gioui.org/io/profile"
"gioui.org/io/router"
@@ -88,6 +90,13 @@ type Window struct {
tree []router.SemanticNode
ids map[router.SemanticID]router.SemanticNode
}
imeState editorState
}
type editorState struct {
router.EditorState
compose key.Range
}
type callbacks struct {
@@ -258,6 +267,13 @@ func (w *Window) processFrame(d driver, frameStart time.Time) {
if q.ReadClipboard() {
d.ReadClipboard()
}
oldState := w.imeState
newState := oldState
newState.EditorState = q.EditorState()
if newState != oldState {
w.imeState = newState
d.EditorStateChanged(oldState, newState)
}
if q.Profiling() && w.gpu != nil {
frameDur := time.Since(frameStart)
frameDur = frameDur.Truncate(100 * time.Microsecond)
@@ -444,6 +460,95 @@ func (c *callbacks) SemanticAt(pos f32.Point) (router.SemanticID, bool) {
return c.w.queue.q.SemanticAt(pos)
}
func (c *callbacks) EditorState() editorState {
return c.w.imeState
}
func (c *callbacks) SetComposingRegion(r key.Range) {
c.w.imeState.compose = r
}
func (c *callbacks) EditorInsert(text string) {
sel := c.w.imeState.Selection
c.EditorReplace(sel, text)
start := sel.Start
if sel.End < start {
start = sel.End
}
sel.Start = start + utf8.RuneCountInString(text)
sel.End = sel.Start
c.SetEditorSelection(sel)
}
func (c *callbacks) EditorReplace(r key.Range, text string) {
c.w.imeState.Replace(r, text)
c.Event(key.EditEvent{Range: r, Text: text})
c.Event(key.SnippetEvent(c.w.imeState.Snippet.Range))
}
func (c *callbacks) SetEditorSelection(r key.Range) {
c.w.imeState.Selection = r
c.Event(key.SelectionEvent(r))
}
func (c *callbacks) SetEditorSnippet(r key.Range) {
if sn := c.EditorState().Snippet.Range; sn == r {
// No need to expand.
return
}
c.Event(key.SnippetEvent(r))
}
func (e *editorState) Replace(r key.Range, text string) {
if r.Start > r.End {
r.Start, r.End = r.End, r.Start
}
runes := []rune(text)
newEnd := r.Start + len(runes)
adjust := func(pos int) int {
switch {
case newEnd < pos && pos <= r.End:
return newEnd
case r.End < pos:
diff := newEnd - r.End
return pos + diff
}
return pos
}
e.Selection.Start = adjust(e.Selection.Start)
e.Selection.End = adjust(e.Selection.End)
e.compose.Start = adjust(e.compose.Start)
e.compose.End = adjust(e.compose.End)
s := e.Snippet
if r.End < s.Start || r.Start > s.End {
// Replacement does not overlap snippet.
e.Snippet.Start = adjust(s.Start)
e.Snippet.End = adjust(s.End)
return
}
// Replacement overlaps; replace content and expand snippet
// to include all of the replacement.
var newSnippet []rune
snippet := []rune(s.Text)
// Append first part of existing snippet.
if end := r.Start - s.Start; end > 0 {
newSnippet = append(newSnippet, snippet[:end]...)
}
// Append replacement.
newSnippet = append(newSnippet, runes...)
// Append last part of existing snippet.
if start := r.End; start < s.End {
newSnippet = append(newSnippet, snippet[start-s.Start:]...)
}
// Adjust snippet range to include replacement.
if r.Start < s.Start {
s.Start = r.Start
}
s.End = s.Start + len(newSnippet)
s.Text = string(newSnippet)
e.Snippet = s
}
func (w *Window) waitAck(d driver) {
for {
select {