diff --git a/app/GioView.java b/app/GioView.java index 435b588b..e6903ba4 100644 --- a/app/GioView.java +++ b/app/GioView.java @@ -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; diff --git a/app/ime_test.go b/app/ime_test.go index 4092c44e..ec17d1e0 100644 --- a/app/ime_test.go +++ b/app/ime_test.go @@ -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) } diff --git a/app/internal/windows/windows.go b/app/internal/windows/windows.go index f9f39670..215d9a10 100644 --- a/app/internal/windows/windows.go +++ b/app/internal/windows/windows.go @@ -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) diff --git a/app/os_android.go b/app/os_android.go index e396006d..5e704fd2 100644 --- a/app/os_android.go +++ b/app/os_android.go @@ -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) }) } diff --git a/app/os_macos.go b/app/os_macos.go index 50994fb7..ed1dd77b 100644 --- a/app/os_macos.go +++ b/app/os_macos.go @@ -172,6 +172,16 @@ static void discardMarkedText(CFTypeRef viewRef) { } } } + +static void invalidateCharacterCoordinates(CFTypeRef viewRef) { + @autoreleasepool { + id view = (__bridge id)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)) diff --git a/app/os_macos.m b/app/os_macos.m index 6ec2e8a0..99624085 100644 --- a/app/os_macos.m +++ b/app/os_macos.m @@ -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 diff --git a/app/os_windows.go b/app/os_windows.go index 9bc5fe8f..4af0bb14 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -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) { diff --git a/app/window.go b/app/window.go index 8753c2e2..ba9832ee 100644 --- a/app/window.go +++ b/app/window.go @@ -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)) } diff --git a/internal/ops/ops.go b/internal/ops/ops.go index 21a73eb3..daf1b62d 100644 --- a/internal/ops/ops.go +++ b/internal/ops/ops.go @@ -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) { diff --git a/io/key/key.go b/io/key/key.go index c984265f..3775a4c8 100644 --- a/io/key/key.go +++ b/io/key/key.go @@ -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() {} diff --git a/io/router/key.go b/io/router/key.go index 6677361b..fa8347aa 100644 --- a/io/router/key.go +++ b/io/router/key.go @@ -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 } } diff --git a/io/router/router.go b/io/router/router.go index 768402c1..daf498dc 100644 --- a/io/router/router.go +++ b/io/router/router.go @@ -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: diff --git a/widget/editor.go b/widget/editor.go index 13f01fe9..748a973f 100644 --- a/widget/editor.go +++ b/widget/editor.go @@ -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