forked from joejulian/gio
app,widget,io: implement IME positioning
This change implements reporting of the caret position from Editor, as well as Windows, macOS, Android support. As a result, the IME composition window on Windows and macOS is now positioned correctly. References: https://todo.sr.ht/~eliasnaur/gio/246 Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+34
-6
@@ -14,6 +14,7 @@ import android.app.FragmentTransaction;
|
||||
import android.content.Context;
|
||||
import android.graphics.Canvas;
|
||||
import android.graphics.Color;
|
||||
import android.graphics.Matrix;
|
||||
import android.graphics.Rect;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@@ -37,14 +38,15 @@ import android.view.SurfaceHolder;
|
||||
import android.view.Window;
|
||||
import android.view.WindowInsetsController;
|
||||
import android.view.WindowManager;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.CorrectionInfo;
|
||||
import android.view.inputmethod.CompletionInfo;
|
||||
import android.view.inputmethod.InputContentInfo;
|
||||
import android.view.inputmethod.CursorAnchorInfo;
|
||||
import android.view.inputmethod.EditorInfo;
|
||||
import android.view.inputmethod.ExtractedText;
|
||||
import android.view.inputmethod.ExtractedTextRequest;
|
||||
import android.view.inputmethod.InputConnection;
|
||||
import android.view.inputmethod.InputMethodManager;
|
||||
import android.view.inputmethod.InputContentInfo;
|
||||
import android.view.inputmethod.SurroundingText;
|
||||
import android.view.accessibility.AccessibilityNodeProvider;
|
||||
import android.view.accessibility.AccessibilityNodeInfo;
|
||||
@@ -441,6 +443,31 @@ public final class GioView extends SurfaceView {
|
||||
imm.updateSelection(this, selStart, selEnd, compStart, compEnd);
|
||||
}
|
||||
|
||||
void updateCaret(float m00, float m01, float m02, float m10, float m11, float m12, float caretX, float caretTop, float caretBase, float caretBottom) {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
|
||||
return;
|
||||
}
|
||||
Matrix m = new Matrix();
|
||||
m.setValues(new float[]{m00, m01, m02, m10, m11, m12, 0.0f, 0.0f, 1.0f});
|
||||
m.setConcat(getMatrix(), m);
|
||||
int selStart = imeSelectionStart(nhandle);
|
||||
int selEnd = imeSelectionEnd(nhandle);
|
||||
int compStart = imeComposingStart(nhandle);
|
||||
int compEnd = imeComposingEnd(nhandle);
|
||||
Snippet snip = getSnippet();
|
||||
String composing = "";
|
||||
if (compStart != -1) {
|
||||
composing = snip.substringRunes(compStart, compEnd);
|
||||
}
|
||||
CursorAnchorInfo inf = new CursorAnchorInfo.Builder()
|
||||
.setMatrix(m)
|
||||
.setComposingText(imeToUTF16(nhandle, compStart), composing)
|
||||
.setSelectionRange(imeToUTF16(nhandle, selStart), imeToUTF16(nhandle, selEnd))
|
||||
.setInsertionMarkerLocation(caretX, caretTop, caretBase, caretBottom, 0)
|
||||
.build();
|
||||
imm.updateCursorAnchorInfo(this, inf);
|
||||
}
|
||||
|
||||
static private native long onCreateView(GioView view);
|
||||
static private native void onDestroyView(long handle);
|
||||
static private native void onStartView(long handle);
|
||||
@@ -628,7 +655,8 @@ public final class GioView extends SurfaceView {
|
||||
}
|
||||
|
||||
/*@Override*/ public boolean requestCursorUpdates(int cursorUpdateMode) {
|
||||
return false;
|
||||
// We always provide cursor updates.
|
||||
return true;
|
||||
}
|
||||
|
||||
/*@Override*/ public void closeConnection() {
|
||||
@@ -675,7 +703,7 @@ public final class GioView extends SurfaceView {
|
||||
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
|
||||
// Gio editors to keep track of UTF-16 offsets. The distinction won't matter in practice because IMEs only
|
||||
// ever see snippets.
|
||||
int offset;
|
||||
|
||||
|
||||
+3
-1
@@ -78,7 +78,7 @@ func FuzzIME(f *testing.F) {
|
||||
if rng.End > len(runes) {
|
||||
rng.End = len(runes)
|
||||
}
|
||||
state.Selection = rng
|
||||
state.Selection.Range = rng
|
||||
case cmdSnip:
|
||||
r.Queue(key.SnippetEvent(rng))
|
||||
runes := []rune(e.Text())
|
||||
@@ -106,6 +106,8 @@ func FuzzIME(f *testing.F) {
|
||||
e.Layout(gtx, cache, text.Font{}, unit.Px(10), nil)
|
||||
r.Frame(gtx.Ops)
|
||||
newState := r.EditorState()
|
||||
// We don't track caret position.
|
||||
state.Selection.Caret = newState.Selection.Caret
|
||||
if newState != state.EditorState {
|
||||
t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState)
|
||||
}
|
||||
|
||||
@@ -15,6 +15,19 @@ import (
|
||||
syscall "golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
type CompositionForm struct {
|
||||
dwStyle uint32
|
||||
ptCurrentPos Point
|
||||
rcArea Rect
|
||||
}
|
||||
|
||||
type CandidateForm struct {
|
||||
dwIndex uint32
|
||||
dwStyle uint32
|
||||
ptCurrentPos Point
|
||||
rcArea Rect
|
||||
}
|
||||
|
||||
type Rect struct {
|
||||
Left, Top, Right, Bottom int32
|
||||
}
|
||||
@@ -95,6 +108,9 @@ const (
|
||||
GCS_RESULTREADSTR = 0x0200
|
||||
GCS_RESULTSTR = 0x0800
|
||||
|
||||
CFS_POINT = 0x0002
|
||||
CFS_CANDIDATEPOS = 0x0040
|
||||
|
||||
HWND_TOPMOST = ^(uint32(1) - 1) // -1
|
||||
|
||||
HTCLIENT = 1
|
||||
@@ -187,40 +203,41 @@ const (
|
||||
|
||||
UNICODE_NOCHAR = 65535
|
||||
|
||||
WM_CANCELMODE = 0x001F
|
||||
WM_CHAR = 0x0102
|
||||
WM_CLOSE = 0x0010
|
||||
WM_CREATE = 0x0001
|
||||
WM_DPICHANGED = 0x02E0
|
||||
WM_DESTROY = 0x0002
|
||||
WM_ERASEBKGND = 0x0014
|
||||
WM_GETMINMAXINFO = 0x0024
|
||||
WM_IME_COMPOSITION = 0x010F
|
||||
WM_IME_ENDCOMPOSITION = 0x010E
|
||||
WM_KEYDOWN = 0x0100
|
||||
WM_KEYUP = 0x0101
|
||||
WM_KILLFOCUS = 0x0008
|
||||
WM_LBUTTONDOWN = 0x0201
|
||||
WM_LBUTTONUP = 0x0202
|
||||
WM_MBUTTONDOWN = 0x0207
|
||||
WM_MBUTTONUP = 0x0208
|
||||
WM_MOUSEMOVE = 0x0200
|
||||
WM_MOUSEWHEEL = 0x020A
|
||||
WM_MOUSEHWHEEL = 0x020E
|
||||
WM_PAINT = 0x000F
|
||||
WM_QUIT = 0x0012
|
||||
WM_SETCURSOR = 0x0020
|
||||
WM_SETFOCUS = 0x0007
|
||||
WM_SHOWWINDOW = 0x0018
|
||||
WM_SIZE = 0x0005
|
||||
WM_SYSKEYDOWN = 0x0104
|
||||
WM_SYSKEYUP = 0x0105
|
||||
WM_RBUTTONDOWN = 0x0204
|
||||
WM_RBUTTONUP = 0x0205
|
||||
WM_TIMER = 0x0113
|
||||
WM_UNICHAR = 0x0109
|
||||
WM_USER = 0x0400
|
||||
WM_WINDOWPOSCHANGED = 0x0047
|
||||
WM_CANCELMODE = 0x001F
|
||||
WM_CHAR = 0x0102
|
||||
WM_CLOSE = 0x0010
|
||||
WM_CREATE = 0x0001
|
||||
WM_DPICHANGED = 0x02E0
|
||||
WM_DESTROY = 0x0002
|
||||
WM_ERASEBKGND = 0x0014
|
||||
WM_GETMINMAXINFO = 0x0024
|
||||
WM_IME_COMPOSITION = 0x010F
|
||||
WM_IME_ENDCOMPOSITION = 0x010E
|
||||
WM_IME_STARTCOMPOSITION = 0x010D
|
||||
WM_KEYDOWN = 0x0100
|
||||
WM_KEYUP = 0x0101
|
||||
WM_KILLFOCUS = 0x0008
|
||||
WM_LBUTTONDOWN = 0x0201
|
||||
WM_LBUTTONUP = 0x0202
|
||||
WM_MBUTTONDOWN = 0x0207
|
||||
WM_MBUTTONUP = 0x0208
|
||||
WM_MOUSEMOVE = 0x0200
|
||||
WM_MOUSEWHEEL = 0x020A
|
||||
WM_MOUSEHWHEEL = 0x020E
|
||||
WM_PAINT = 0x000F
|
||||
WM_QUIT = 0x0012
|
||||
WM_SETCURSOR = 0x0020
|
||||
WM_SETFOCUS = 0x0007
|
||||
WM_SHOWWINDOW = 0x0018
|
||||
WM_SIZE = 0x0005
|
||||
WM_SYSKEYDOWN = 0x0104
|
||||
WM_SYSKEYUP = 0x0105
|
||||
WM_RBUTTONDOWN = 0x0204
|
||||
WM_RBUTTONUP = 0x0205
|
||||
WM_TIMER = 0x0113
|
||||
WM_UNICHAR = 0x0109
|
||||
WM_USER = 0x0400
|
||||
WM_WINDOWPOSCHANGED = 0x0047
|
||||
|
||||
WS_CLIPCHILDREN = 0x00010000
|
||||
WS_CLIPSIBLINGS = 0x04000000
|
||||
@@ -338,6 +355,8 @@ var (
|
||||
_ImmGetCompositionString = imm32.NewProc("ImmGetCompositionStringW")
|
||||
_ImmNotifyIME = imm32.NewProc("ImmNotifyIME")
|
||||
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
|
||||
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
|
||||
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
|
||||
)
|
||||
|
||||
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
|
||||
@@ -536,6 +555,26 @@ func ImmGetCompositionValue(imc syscall.Handle, key int) int {
|
||||
return int(int32(val))
|
||||
}
|
||||
|
||||
func ImmSetCompositionWindow(imc syscall.Handle, x, y int) {
|
||||
f := CompositionForm{
|
||||
dwStyle: CFS_POINT,
|
||||
ptCurrentPos: Point{
|
||||
X: int32(x), Y: int32(y),
|
||||
},
|
||||
}
|
||||
_ImmSetCompositionWindow.Call(uintptr(imc), uintptr(unsafe.Pointer(&f)))
|
||||
}
|
||||
|
||||
func ImmSetCandidateWindow(imc syscall.Handle, x, y int) {
|
||||
f := CandidateForm{
|
||||
dwStyle: CFS_CANDIDATEPOS,
|
||||
ptCurrentPos: Point{
|
||||
X: int32(x), Y: int32(y),
|
||||
},
|
||||
}
|
||||
_ImmSetCandidateWindow.Call(uintptr(imc), uintptr(unsafe.Pointer(&f)))
|
||||
}
|
||||
|
||||
func SetWindowLong(hwnd syscall.Handle, idx uint32, style uintptr) {
|
||||
if runtime.GOARCH == "386" {
|
||||
_SetWindowLong32.Call(uintptr(hwnd), uintptr(idx), style)
|
||||
|
||||
+14
-2
@@ -123,6 +123,7 @@ import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"math"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
@@ -190,6 +191,7 @@ var gioView struct {
|
||||
isA11yActive C.jmethodID
|
||||
restartInput C.jmethodID
|
||||
updateSelection C.jmethodID
|
||||
updateCaret C.jmethodID
|
||||
}
|
||||
|
||||
// ViewEvent is sent whenever the Window's underlying Android view
|
||||
@@ -475,6 +477,7 @@ func Java_org_gioui_GioView_onCreateView(env *C.JNIEnv, class C.jclass, view C.j
|
||||
m.isA11yActive = getMethodID(env, class, "isA11yActive", "()Z")
|
||||
m.restartInput = getMethodID(env, class, "restartInput", "()V")
|
||||
m.updateSelection = getMethodID(env, class, "updateSelection", "()V")
|
||||
m.updateCaret = getMethodID(env, class, "updateCaret", "(FFFFFFFFFF)V")
|
||||
})
|
||||
view = C.jni_NewGlobalRef(env, view)
|
||||
wopts := <-mainWindow.out
|
||||
@@ -1101,10 +1104,19 @@ func (w *window) EditorStateChanged(old, new editorState) {
|
||||
callVoidMethod(env, w.view, gioView.restartInput)
|
||||
return
|
||||
}
|
||||
if old.Selection != new.Selection {
|
||||
if old.Selection.Range != new.Selection.Range {
|
||||
w.callbacks.SetComposingRegion(key.Range{Start: -1, End: -1})
|
||||
callVoidMethod(env, w.view, gioView.updateSelection)
|
||||
}
|
||||
if old.Selection.Transform != new.Selection.Transform || old.Selection.Caret != new.Selection.Caret {
|
||||
sel := new.Selection
|
||||
m00, m01, m02, m10, m11, m12 := sel.Transform.Elems()
|
||||
f := func(v float32) jvalue {
|
||||
return jvalue(math.Float32bits(v))
|
||||
}
|
||||
c := sel.Caret
|
||||
callVoidMethod(env, w.view, gioView.updateCaret, f(m00), f(m01), f(m02), f(m10), f(m11), f(m12), f(c.Pos.X), f(c.Pos.Y-c.Ascent), f(c.Pos.Y), f(c.Pos.Y+c.Descent))
|
||||
}
|
||||
callVoidMethod(env, w.view, gioView.updateSelection)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
+48
-4
@@ -172,6 +172,16 @@ static void discardMarkedText(CFTypeRef viewRef) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void invalidateCharacterCoordinates(CFTypeRef viewRef) {
|
||||
@autoreleasepool {
|
||||
id<NSTextInputClient> view = (__bridge id<NSTextInputClient>)viewRef;
|
||||
NSTextInputContext *ctx = [NSTextInputContext currentInputContext];
|
||||
if (view == [ctx client]) {
|
||||
[ctx invalidateCharacterCoordinates];
|
||||
}
|
||||
}
|
||||
}
|
||||
*/
|
||||
import "C"
|
||||
|
||||
@@ -363,8 +373,13 @@ func (w *window) SetCursor(name pointer.CursorName) {
|
||||
}
|
||||
|
||||
func (w *window) EditorStateChanged(old, new editorState) {
|
||||
C.discardMarkedText(w.view)
|
||||
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
||||
if old.Selection.Range != new.Selection.Range || old.Snippet != new.Snippet {
|
||||
C.discardMarkedText(w.view)
|
||||
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
||||
}
|
||||
if old.Selection.Caret != new.Selection.Caret || old.Selection.Transform != new.Selection.Transform {
|
||||
C.invalidateCharacterCoordinates(w.view)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) ShowTextInput(show bool) {}
|
||||
@@ -546,7 +561,7 @@ func gio_setMarkedText(view, cstr C.CFTypeRef, selRange C.NSRange, replaceRange
|
||||
state := w.w.EditorState()
|
||||
rng := state.compose
|
||||
if rng.Start == -1 {
|
||||
rng = state.Selection
|
||||
rng = state.Selection.Range
|
||||
}
|
||||
if replaceRange.location != C.NSNotFound {
|
||||
// replaceRange is relative to marked (or selected) text.
|
||||
@@ -606,7 +621,7 @@ func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) {
|
||||
state := w.w.EditorState()
|
||||
rng := state.compose
|
||||
if rng.Start == -1 {
|
||||
rng = state.Selection
|
||||
rng = state.Selection.Range
|
||||
}
|
||||
if crng.location != C.NSNotFound {
|
||||
rng = key.Range{
|
||||
@@ -621,6 +636,35 @@ func gio_insertText(view, cstr C.CFTypeRef, crng C.NSRange) {
|
||||
w.w.SetEditorSelection(key.Range{Start: pos, End: pos})
|
||||
}
|
||||
|
||||
//export gio_characterIndexForPoint
|
||||
func gio_characterIndexForPoint(view C.CFTypeRef, p C.NSPoint) C.NSUInteger {
|
||||
return C.NSNotFound
|
||||
}
|
||||
|
||||
//export gio_firstRectForCharacterRange
|
||||
func gio_firstRectForCharacterRange(view C.CFTypeRef, crng C.NSRange, actual C.NSRangePointer) C.NSRect {
|
||||
w := mustView(view)
|
||||
state := w.w.EditorState()
|
||||
sel := state.Selection
|
||||
u16start := state.UTF16Index(sel.Start)
|
||||
actual.location = C.NSUInteger(u16start)
|
||||
actual.length = 0
|
||||
// Transform to NSView local coordinates (lower left origin, undo backing scale).
|
||||
scale := 1. / float32(C.getViewBackingScale(w.view))
|
||||
height := float32(C.viewHeight(w.view))
|
||||
local := f32.Affine2D{}.Scale(f32.Pt(0, 0), f32.Pt(scale, -scale)).Offset(f32.Pt(0, height))
|
||||
t := local.Mul(sel.Transform)
|
||||
bounds := f32.Rectangle{
|
||||
Min: t.Transform(sel.Pos.Sub(f32.Pt(0, sel.Ascent))),
|
||||
Max: t.Transform(sel.Pos.Add(f32.Pt(0, sel.Descent))),
|
||||
}.Canon()
|
||||
sz := bounds.Size()
|
||||
return C.NSMakeRect(
|
||||
C.CGFloat(bounds.Min.X), C.CGFloat(bounds.Min.Y),
|
||||
C.CGFloat(sz.X), C.CGFloat(sz.Y),
|
||||
)
|
||||
}
|
||||
|
||||
func (w *window) draw() {
|
||||
w.scale = float32(C.getViewBackingScale(w.view))
|
||||
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(w.view))
|
||||
|
||||
+7
-5
@@ -176,12 +176,14 @@ static void handleMouse(NSView *view, NSEvent *event, int typ, CGFloat dx, CGFlo
|
||||
}
|
||||
gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange);
|
||||
}
|
||||
- (NSUInteger)characterIndexForPoint:(NSPoint)point {
|
||||
return NSNotFound;
|
||||
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
|
||||
return gio_characterIndexForPoint((__bridge CFTypeRef)self, p);
|
||||
}
|
||||
- (NSRect)firstRectForCharacterRange:(NSRange)range
|
||||
actualRange:(NSRangePointer)actualRange {
|
||||
return NSZeroRect;
|
||||
- (NSRect)firstRectForCharacterRange:(NSRange)rng
|
||||
actualRange:(NSRangePointer)actual {
|
||||
NSRect r = gio_firstRectForCharacterRange((__bridge CFTypeRef)self, rng, actual);
|
||||
r = [self convertRect:r toView:nil];
|
||||
return [[self window] convertRectToScreen:r];
|
||||
}
|
||||
@end
|
||||
|
||||
|
||||
+15
-2
@@ -349,6 +349,17 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
}
|
||||
case _WM_WAKEUP:
|
||||
w.w.Event(wakeupEvent{})
|
||||
case windows.WM_IME_STARTCOMPOSITION:
|
||||
imc := windows.ImmGetContext(w.hwnd)
|
||||
if imc == 0 {
|
||||
return windows.TRUE
|
||||
}
|
||||
defer windows.ImmReleaseContext(w.hwnd, imc)
|
||||
sel := w.w.EditorState().Selection
|
||||
caret := sel.Transform.Transform(sel.Caret.Pos.Add(f32.Pt(0, sel.Caret.Descent)))
|
||||
icaret := image.Pt(int(caret.X+.5), int(caret.Y+.5))
|
||||
windows.ImmSetCompositionWindow(imc, icaret.X, icaret.Y)
|
||||
windows.ImmSetCandidateWindow(imc, icaret.X, icaret.Y)
|
||||
case windows.WM_IME_COMPOSITION:
|
||||
imc := windows.ImmGetContext(w.hwnd)
|
||||
if imc == 0 {
|
||||
@@ -358,7 +369,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
||||
state := w.w.EditorState()
|
||||
rng := state.compose
|
||||
if rng.Start == -1 {
|
||||
rng = state.Selection
|
||||
rng = state.Selection.Range
|
||||
}
|
||||
if rng.Start > rng.End {
|
||||
rng.Start, rng.End = rng.End, rng.Start
|
||||
@@ -499,7 +510,9 @@ func (w *window) EditorStateChanged(old, new editorState) {
|
||||
return
|
||||
}
|
||||
defer windows.ImmReleaseContext(w.hwnd, imc)
|
||||
windows.ImmNotifyIME(imc, windows.NI_COMPOSITIONSTR, windows.CPS_CANCEL, 0)
|
||||
if old.Selection.Range != new.Selection.Range || old.Snippet != new.Snippet {
|
||||
windows.ImmNotifyIME(imc, windows.NI_COMPOSITIONSTR, windows.CPS_CANCEL, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *window) SetAnimating(anim bool) {
|
||||
|
||||
+2
-2
@@ -474,7 +474,7 @@ func (c *callbacks) SetComposingRegion(r key.Range) {
|
||||
}
|
||||
|
||||
func (c *callbacks) EditorInsert(text string) {
|
||||
sel := c.w.imeState.Selection
|
||||
sel := c.w.imeState.Selection.Range
|
||||
c.EditorReplace(sel, text)
|
||||
start := sel.Start
|
||||
if sel.End < start {
|
||||
@@ -492,7 +492,7 @@ func (c *callbacks) EditorReplace(r key.Range, text string) {
|
||||
}
|
||||
|
||||
func (c *callbacks) SetEditorSelection(r key.Range) {
|
||||
c.w.imeState.Selection = r
|
||||
c.w.imeState.Selection.Range = r
|
||||
c.Event(key.SelectionEvent(r))
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -157,7 +157,7 @@ const (
|
||||
TypeSemanticSelectedLen = 2
|
||||
TypeSemanticDisabledLen = 2
|
||||
TypeSnippetLen = 1 + 4 + 4
|
||||
TypeSelectionLen = 1 + 4 + 4
|
||||
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
|
||||
)
|
||||
|
||||
func (op *ClipOp) Decode(data []byte) {
|
||||
|
||||
@@ -12,8 +12,10 @@ package key
|
||||
import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math"
|
||||
"strings"
|
||||
|
||||
"gioui.org/f32"
|
||||
"gioui.org/internal/ops"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/op"
|
||||
@@ -45,6 +47,7 @@ type FocusOp struct {
|
||||
type SelectionOp struct {
|
||||
Tag event.Tag
|
||||
Range
|
||||
Caret
|
||||
}
|
||||
|
||||
// SnippetOp updates the content snippet for an input handler.
|
||||
@@ -67,6 +70,16 @@ type Snippet struct {
|
||||
Text string
|
||||
}
|
||||
|
||||
// Caret represents the position of a caret.
|
||||
type Caret struct {
|
||||
// Pos is the intersection point of the caret and its baseline.
|
||||
Pos f32.Point
|
||||
// Ascent is the length of the caret above its baseline.
|
||||
Ascent float32
|
||||
// Descent is the length of the caret below its baseline.
|
||||
Descent float32
|
||||
}
|
||||
|
||||
// SelectionEvent is generated when an input method changes the selection.
|
||||
type SelectionEvent Range
|
||||
|
||||
@@ -229,6 +242,10 @@ func (s SelectionOp) Add(o *op.Ops) {
|
||||
bo := binary.LittleEndian
|
||||
bo.PutUint32(data[1:], uint32(s.Start))
|
||||
bo.PutUint32(data[5:], uint32(s.End))
|
||||
bo.PutUint32(data[9:], math.Float32bits(s.Pos.X))
|
||||
bo.PutUint32(data[13:], math.Float32bits(s.Pos.Y))
|
||||
bo.PutUint32(data[17:], math.Float32bits(s.Ascent))
|
||||
bo.PutUint32(data[21:], math.Float32bits(s.Descent))
|
||||
}
|
||||
|
||||
func (EditEvent) ImplementsEvent() {}
|
||||
|
||||
+11
-4
@@ -3,14 +3,19 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"gioui.org/f32"
|
||||
"gioui.org/io/event"
|
||||
"gioui.org/io/key"
|
||||
)
|
||||
|
||||
// EditorState represents the state of an editor needed by input handlers.
|
||||
type EditorState struct {
|
||||
Selection key.Range
|
||||
Snippet key.Snippet
|
||||
Selection struct {
|
||||
Transform f32.Affine2D
|
||||
key.Range
|
||||
key.Caret
|
||||
}
|
||||
Snippet key.Snippet
|
||||
}
|
||||
|
||||
type TextInputState uint8
|
||||
@@ -143,9 +148,11 @@ func (k *keyCollector) inputOp(op key.InputOp) {
|
||||
h.hint = op.Hint
|
||||
}
|
||||
|
||||
func (k *keyCollector) selectionOp(op key.SelectionOp) {
|
||||
func (k *keyCollector) selectionOp(t f32.Affine2D, op key.SelectionOp) {
|
||||
if op.Tag == k.q.focus {
|
||||
k.q.content.Selection = op.Range
|
||||
k.q.content.Selection.Range = op.Range
|
||||
k.q.content.Selection.Caret = op.Caret
|
||||
k.q.content.Selection.Transform = t
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+10
-1
@@ -14,6 +14,7 @@ import (
|
||||
"encoding/binary"
|
||||
"image"
|
||||
"io"
|
||||
"math"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -335,8 +336,16 @@ func (q *Router) collect() {
|
||||
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
|
||||
End: int(int32(bo.Uint32(encOp.Data[5:]))),
|
||||
},
|
||||
Caret: key.Caret{
|
||||
Pos: f32.Point{
|
||||
X: math.Float32frombits(bo.Uint32(encOp.Data[9:])),
|
||||
Y: math.Float32frombits(bo.Uint32(encOp.Data[13:])),
|
||||
},
|
||||
Ascent: math.Float32frombits(bo.Uint32(encOp.Data[17:])),
|
||||
Descent: math.Float32frombits(bo.Uint32(encOp.Data[21:])),
|
||||
},
|
||||
}
|
||||
kc.selectionOp(op)
|
||||
kc.selectionOp(t, op)
|
||||
|
||||
// Semantic ops.
|
||||
case ops.TypeSemanticLabel:
|
||||
|
||||
+60
-38
@@ -103,7 +103,10 @@ type Editor struct {
|
||||
}
|
||||
|
||||
type imeState struct {
|
||||
selection key.Range
|
||||
selection struct {
|
||||
rng key.Range
|
||||
caret key.Caret
|
||||
}
|
||||
snippet key.Snippet
|
||||
start, end int
|
||||
}
|
||||
@@ -524,30 +527,40 @@ func (e *Editor) Layout(gtx layout.Context, sh text.Shaper, font text.Font, size
|
||||
e.processEvents(gtx)
|
||||
e.makeValid()
|
||||
|
||||
if e.focused {
|
||||
// Notify IME of selection if it changed.
|
||||
newSel := key.Range{
|
||||
Start: e.caret.start,
|
||||
End: e.caret.end,
|
||||
}
|
||||
if newSel != e.ime.selection {
|
||||
e.ime.selection = newSel
|
||||
key.SelectionOp{
|
||||
Tag: &e.eventKey,
|
||||
Range: newSel,
|
||||
}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
e.updateSnippet(gtx, e.ime.start, e.ime.end)
|
||||
}
|
||||
|
||||
if viewSize := gtx.Constraints.Constrain(e.dims.Size); viewSize != e.viewSize {
|
||||
e.viewSize = viewSize
|
||||
e.invalidate()
|
||||
}
|
||||
e.makeValid()
|
||||
|
||||
return e.layout(gtx, content)
|
||||
dims := e.layout(gtx, content)
|
||||
|
||||
if e.focused {
|
||||
// Notify IME of selection if it changed.
|
||||
newSel := e.ime.selection
|
||||
newSel.rng = key.Range{
|
||||
Start: e.caret.start,
|
||||
End: e.caret.end,
|
||||
}
|
||||
caretPos, carAsc, carDesc := e.caretInfo()
|
||||
newSel.caret = key.Caret{
|
||||
Pos: layout.FPt(caretPos),
|
||||
Ascent: float32(carAsc),
|
||||
Descent: float32(carDesc),
|
||||
}
|
||||
if newSel != e.ime.selection {
|
||||
e.ime.selection = newSel
|
||||
key.SelectionOp{
|
||||
Tag: &e.eventKey,
|
||||
Range: newSel.rng,
|
||||
Caret: newSel.caret,
|
||||
}.Add(gtx.Ops)
|
||||
}
|
||||
|
||||
e.updateSnippet(gtx, e.ime.start, e.ime.end)
|
||||
}
|
||||
|
||||
return dims
|
||||
}
|
||||
|
||||
// updateSnippet adds a key.SnippetOp if the snippet content or position
|
||||
@@ -710,29 +723,23 @@ func (e *Editor) PaintCaret(gtx layout.Context) {
|
||||
if !e.caret.on {
|
||||
return
|
||||
}
|
||||
carWidth := fixed.I(gtx.Px(unit.Dp(1)))
|
||||
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
|
||||
carX := caretStart.x
|
||||
carY := caretStart.y
|
||||
|
||||
carX -= carWidth / 2
|
||||
carAsc, carDesc := -e.lines[caretStart.lineCol.Y].Bounds.Min.Y, e.lines[caretStart.lineCol.Y].Bounds.Max.Y
|
||||
carRect := image.Rectangle{
|
||||
Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()},
|
||||
Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()},
|
||||
carWidth2 := gtx.Px(unit.Dp(1)) / 2
|
||||
if carWidth2 < 1 {
|
||||
carWidth2 = 1
|
||||
}
|
||||
caretPos, carAsc, carDesc := e.caretInfo()
|
||||
|
||||
carRect := image.Rectangle{
|
||||
Min: caretPos.Sub(image.Pt(carWidth2, carAsc)),
|
||||
Max: caretPos.Add(image.Pt(carWidth2, carDesc)),
|
||||
}
|
||||
carRect = carRect.Add(image.Point{
|
||||
X: -e.scrollOff.X,
|
||||
Y: -e.scrollOff.Y,
|
||||
})
|
||||
cl := textPadding(e.lines)
|
||||
// Account for caret width to each side.
|
||||
whalf := (carWidth / 2).Ceil()
|
||||
if cl.Max.X < whalf {
|
||||
cl.Max.X = whalf
|
||||
if cl.Max.X < carWidth2 {
|
||||
cl.Max.X = carWidth2
|
||||
}
|
||||
if cl.Min.X > -whalf {
|
||||
cl.Min.X = -whalf
|
||||
if cl.Min.X > -carWidth2 {
|
||||
cl.Min.X = -carWidth2
|
||||
}
|
||||
cl.Max = cl.Max.Add(e.viewSize)
|
||||
carRect = cl.Intersect(carRect)
|
||||
@@ -742,6 +749,21 @@ func (e *Editor) PaintCaret(gtx layout.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Editor) caretInfo() (pos image.Point, ascent, descent int) {
|
||||
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
|
||||
carX := caretStart.x
|
||||
carY := caretStart.y
|
||||
|
||||
ascent = -e.lines[caretStart.lineCol.Y].Bounds.Min.Y.Ceil()
|
||||
descent = e.lines[caretStart.lineCol.Y].Bounds.Max.Y.Ceil()
|
||||
pos = image.Point{
|
||||
X: carX.Round(),
|
||||
Y: carY,
|
||||
}
|
||||
pos = pos.Sub(e.scrollOff)
|
||||
return
|
||||
}
|
||||
|
||||
// TODO: copied from package math. Remove when Go 1.18 is minimum.
|
||||
const (
|
||||
intSize = 32 << (^uint(0) >> 63) // 32 or 64
|
||||
|
||||
Reference in New Issue
Block a user