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:
Elias Naur
2022-02-13 16:28:30 +01:00
parent cffa748c6a
commit 31f55232bf
13 changed files with 295 additions and 100 deletions
+34 -6
View File
@@ -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
View File
@@ -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)
}
+73 -34
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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) {
+17
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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