mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-05 17:35:36 +00:00
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.content.Context;
|
||||||
import android.graphics.Canvas;
|
import android.graphics.Canvas;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
|
import android.graphics.Matrix;
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -37,14 +38,15 @@ import android.view.SurfaceHolder;
|
|||||||
import android.view.Window;
|
import android.view.Window;
|
||||||
import android.view.WindowInsetsController;
|
import android.view.WindowInsetsController;
|
||||||
import android.view.WindowManager;
|
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.CorrectionInfo;
|
||||||
import android.view.inputmethod.CompletionInfo;
|
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.ExtractedText;
|
||||||
import android.view.inputmethod.ExtractedTextRequest;
|
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.inputmethod.SurroundingText;
|
||||||
import android.view.accessibility.AccessibilityNodeProvider;
|
import android.view.accessibility.AccessibilityNodeProvider;
|
||||||
import android.view.accessibility.AccessibilityNodeInfo;
|
import android.view.accessibility.AccessibilityNodeInfo;
|
||||||
@@ -441,6 +443,31 @@ public final class GioView extends SurfaceView {
|
|||||||
imm.updateSelection(this, selStart, selEnd, compStart, compEnd);
|
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 long onCreateView(GioView view);
|
||||||
static private native void onDestroyView(long handle);
|
static private native void onDestroyView(long handle);
|
||||||
static private native void onStartView(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) {
|
/*@Override*/ public boolean requestCursorUpdates(int cursorUpdateMode) {
|
||||||
return false;
|
// We always provide cursor updates.
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*@Override*/ public void closeConnection() {
|
/*@Override*/ public void closeConnection() {
|
||||||
@@ -675,7 +703,7 @@ public final class GioView extends SurfaceView {
|
|||||||
private static class Snippet {
|
private static class Snippet {
|
||||||
String snippet;
|
String snippet;
|
||||||
// offset of snippet into the entire editor content. It is in runes because we won't require
|
// 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.
|
// ever see snippets.
|
||||||
int offset;
|
int offset;
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -78,7 +78,7 @@ func FuzzIME(f *testing.F) {
|
|||||||
if rng.End > len(runes) {
|
if rng.End > len(runes) {
|
||||||
rng.End = len(runes)
|
rng.End = len(runes)
|
||||||
}
|
}
|
||||||
state.Selection = rng
|
state.Selection.Range = rng
|
||||||
case cmdSnip:
|
case cmdSnip:
|
||||||
r.Queue(key.SnippetEvent(rng))
|
r.Queue(key.SnippetEvent(rng))
|
||||||
runes := []rune(e.Text())
|
runes := []rune(e.Text())
|
||||||
@@ -106,6 +106,8 @@ func FuzzIME(f *testing.F) {
|
|||||||
e.Layout(gtx, cache, text.Font{}, unit.Px(10), nil)
|
e.Layout(gtx, cache, text.Font{}, unit.Px(10), nil)
|
||||||
r.Frame(gtx.Ops)
|
r.Frame(gtx.Ops)
|
||||||
newState := r.EditorState()
|
newState := r.EditorState()
|
||||||
|
// We don't track caret position.
|
||||||
|
state.Selection.Caret = newState.Selection.Caret
|
||||||
if newState != state.EditorState {
|
if newState != state.EditorState {
|
||||||
t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState)
|
t.Errorf("IME state: %+v\neditor state: %+v", state.EditorState, newState)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,19 @@ import (
|
|||||||
syscall "golang.org/x/sys/windows"
|
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 {
|
type Rect struct {
|
||||||
Left, Top, Right, Bottom int32
|
Left, Top, Right, Bottom int32
|
||||||
}
|
}
|
||||||
@@ -95,6 +108,9 @@ const (
|
|||||||
GCS_RESULTREADSTR = 0x0200
|
GCS_RESULTREADSTR = 0x0200
|
||||||
GCS_RESULTSTR = 0x0800
|
GCS_RESULTSTR = 0x0800
|
||||||
|
|
||||||
|
CFS_POINT = 0x0002
|
||||||
|
CFS_CANDIDATEPOS = 0x0040
|
||||||
|
|
||||||
HWND_TOPMOST = ^(uint32(1) - 1) // -1
|
HWND_TOPMOST = ^(uint32(1) - 1) // -1
|
||||||
|
|
||||||
HTCLIENT = 1
|
HTCLIENT = 1
|
||||||
@@ -187,40 +203,41 @@ const (
|
|||||||
|
|
||||||
UNICODE_NOCHAR = 65535
|
UNICODE_NOCHAR = 65535
|
||||||
|
|
||||||
WM_CANCELMODE = 0x001F
|
WM_CANCELMODE = 0x001F
|
||||||
WM_CHAR = 0x0102
|
WM_CHAR = 0x0102
|
||||||
WM_CLOSE = 0x0010
|
WM_CLOSE = 0x0010
|
||||||
WM_CREATE = 0x0001
|
WM_CREATE = 0x0001
|
||||||
WM_DPICHANGED = 0x02E0
|
WM_DPICHANGED = 0x02E0
|
||||||
WM_DESTROY = 0x0002
|
WM_DESTROY = 0x0002
|
||||||
WM_ERASEBKGND = 0x0014
|
WM_ERASEBKGND = 0x0014
|
||||||
WM_GETMINMAXINFO = 0x0024
|
WM_GETMINMAXINFO = 0x0024
|
||||||
WM_IME_COMPOSITION = 0x010F
|
WM_IME_COMPOSITION = 0x010F
|
||||||
WM_IME_ENDCOMPOSITION = 0x010E
|
WM_IME_ENDCOMPOSITION = 0x010E
|
||||||
WM_KEYDOWN = 0x0100
|
WM_IME_STARTCOMPOSITION = 0x010D
|
||||||
WM_KEYUP = 0x0101
|
WM_KEYDOWN = 0x0100
|
||||||
WM_KILLFOCUS = 0x0008
|
WM_KEYUP = 0x0101
|
||||||
WM_LBUTTONDOWN = 0x0201
|
WM_KILLFOCUS = 0x0008
|
||||||
WM_LBUTTONUP = 0x0202
|
WM_LBUTTONDOWN = 0x0201
|
||||||
WM_MBUTTONDOWN = 0x0207
|
WM_LBUTTONUP = 0x0202
|
||||||
WM_MBUTTONUP = 0x0208
|
WM_MBUTTONDOWN = 0x0207
|
||||||
WM_MOUSEMOVE = 0x0200
|
WM_MBUTTONUP = 0x0208
|
||||||
WM_MOUSEWHEEL = 0x020A
|
WM_MOUSEMOVE = 0x0200
|
||||||
WM_MOUSEHWHEEL = 0x020E
|
WM_MOUSEWHEEL = 0x020A
|
||||||
WM_PAINT = 0x000F
|
WM_MOUSEHWHEEL = 0x020E
|
||||||
WM_QUIT = 0x0012
|
WM_PAINT = 0x000F
|
||||||
WM_SETCURSOR = 0x0020
|
WM_QUIT = 0x0012
|
||||||
WM_SETFOCUS = 0x0007
|
WM_SETCURSOR = 0x0020
|
||||||
WM_SHOWWINDOW = 0x0018
|
WM_SETFOCUS = 0x0007
|
||||||
WM_SIZE = 0x0005
|
WM_SHOWWINDOW = 0x0018
|
||||||
WM_SYSKEYDOWN = 0x0104
|
WM_SIZE = 0x0005
|
||||||
WM_SYSKEYUP = 0x0105
|
WM_SYSKEYDOWN = 0x0104
|
||||||
WM_RBUTTONDOWN = 0x0204
|
WM_SYSKEYUP = 0x0105
|
||||||
WM_RBUTTONUP = 0x0205
|
WM_RBUTTONDOWN = 0x0204
|
||||||
WM_TIMER = 0x0113
|
WM_RBUTTONUP = 0x0205
|
||||||
WM_UNICHAR = 0x0109
|
WM_TIMER = 0x0113
|
||||||
WM_USER = 0x0400
|
WM_UNICHAR = 0x0109
|
||||||
WM_WINDOWPOSCHANGED = 0x0047
|
WM_USER = 0x0400
|
||||||
|
WM_WINDOWPOSCHANGED = 0x0047
|
||||||
|
|
||||||
WS_CLIPCHILDREN = 0x00010000
|
WS_CLIPCHILDREN = 0x00010000
|
||||||
WS_CLIPSIBLINGS = 0x04000000
|
WS_CLIPSIBLINGS = 0x04000000
|
||||||
@@ -338,6 +355,8 @@ var (
|
|||||||
_ImmGetCompositionString = imm32.NewProc("ImmGetCompositionStringW")
|
_ImmGetCompositionString = imm32.NewProc("ImmGetCompositionStringW")
|
||||||
_ImmNotifyIME = imm32.NewProc("ImmNotifyIME")
|
_ImmNotifyIME = imm32.NewProc("ImmNotifyIME")
|
||||||
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
|
_ImmReleaseContext = imm32.NewProc("ImmReleaseContext")
|
||||||
|
_ImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
|
||||||
|
_ImmSetCompositionWindow = imm32.NewProc("ImmSetCompositionWindow")
|
||||||
)
|
)
|
||||||
|
|
||||||
func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) {
|
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))
|
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) {
|
func SetWindowLong(hwnd syscall.Handle, idx uint32, style uintptr) {
|
||||||
if runtime.GOARCH == "386" {
|
if runtime.GOARCH == "386" {
|
||||||
_SetWindowLong32.Call(uintptr(hwnd), uintptr(idx), style)
|
_SetWindowLong32.Call(uintptr(hwnd), uintptr(idx), style)
|
||||||
|
|||||||
+14
-2
@@ -123,6 +123,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"image/color"
|
"image/color"
|
||||||
|
"math"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -190,6 +191,7 @@ var gioView struct {
|
|||||||
isA11yActive C.jmethodID
|
isA11yActive C.jmethodID
|
||||||
restartInput C.jmethodID
|
restartInput C.jmethodID
|
||||||
updateSelection C.jmethodID
|
updateSelection C.jmethodID
|
||||||
|
updateCaret C.jmethodID
|
||||||
}
|
}
|
||||||
|
|
||||||
// ViewEvent is sent whenever the Window's underlying Android view
|
// 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.isA11yActive = getMethodID(env, class, "isA11yActive", "()Z")
|
||||||
m.restartInput = getMethodID(env, class, "restartInput", "()V")
|
m.restartInput = getMethodID(env, class, "restartInput", "()V")
|
||||||
m.updateSelection = getMethodID(env, class, "updateSelection", "()V")
|
m.updateSelection = getMethodID(env, class, "updateSelection", "()V")
|
||||||
|
m.updateCaret = getMethodID(env, class, "updateCaret", "(FFFFFFFFFF)V")
|
||||||
})
|
})
|
||||||
view = C.jni_NewGlobalRef(env, view)
|
view = C.jni_NewGlobalRef(env, view)
|
||||||
wopts := <-mainWindow.out
|
wopts := <-mainWindow.out
|
||||||
@@ -1101,10 +1104,19 @@ func (w *window) EditorStateChanged(old, new editorState) {
|
|||||||
callVoidMethod(env, w.view, gioView.restartInput)
|
callVoidMethod(env, w.view, gioView.restartInput)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if old.Selection != new.Selection {
|
if old.Selection.Range != new.Selection.Range {
|
||||||
w.callbacks.SetComposingRegion(key.Range{Start: -1, End: -1})
|
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"
|
import "C"
|
||||||
|
|
||||||
@@ -363,8 +373,13 @@ func (w *window) SetCursor(name pointer.CursorName) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *window) EditorStateChanged(old, new editorState) {
|
func (w *window) EditorStateChanged(old, new editorState) {
|
||||||
C.discardMarkedText(w.view)
|
if old.Selection.Range != new.Selection.Range || old.Snippet != new.Snippet {
|
||||||
w.w.SetComposingRegion(key.Range{Start: -1, End: -1})
|
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) {}
|
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()
|
state := w.w.EditorState()
|
||||||
rng := state.compose
|
rng := state.compose
|
||||||
if rng.Start == -1 {
|
if rng.Start == -1 {
|
||||||
rng = state.Selection
|
rng = state.Selection.Range
|
||||||
}
|
}
|
||||||
if replaceRange.location != C.NSNotFound {
|
if replaceRange.location != C.NSNotFound {
|
||||||
// replaceRange is relative to marked (or selected) text.
|
// 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()
|
state := w.w.EditorState()
|
||||||
rng := state.compose
|
rng := state.compose
|
||||||
if rng.Start == -1 {
|
if rng.Start == -1 {
|
||||||
rng = state.Selection
|
rng = state.Selection.Range
|
||||||
}
|
}
|
||||||
if crng.location != C.NSNotFound {
|
if crng.location != C.NSNotFound {
|
||||||
rng = key.Range{
|
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})
|
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() {
|
func (w *window) draw() {
|
||||||
w.scale = float32(C.getViewBackingScale(w.view))
|
w.scale = float32(C.getViewBackingScale(w.view))
|
||||||
wf, hf := float32(C.viewWidth(w.view)), float32(C.viewHeight(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);
|
gio_insertText((__bridge CFTypeRef)self, (__bridge CFTypeRef)str, replaceRange);
|
||||||
}
|
}
|
||||||
- (NSUInteger)characterIndexForPoint:(NSPoint)point {
|
- (NSUInteger)characterIndexForPoint:(NSPoint)p {
|
||||||
return NSNotFound;
|
return gio_characterIndexForPoint((__bridge CFTypeRef)self, p);
|
||||||
}
|
}
|
||||||
- (NSRect)firstRectForCharacterRange:(NSRange)range
|
- (NSRect)firstRectForCharacterRange:(NSRange)rng
|
||||||
actualRange:(NSRangePointer)actualRange {
|
actualRange:(NSRangePointer)actual {
|
||||||
return NSZeroRect;
|
NSRect r = gio_firstRectForCharacterRange((__bridge CFTypeRef)self, rng, actual);
|
||||||
|
r = [self convertRect:r toView:nil];
|
||||||
|
return [[self window] convertRectToScreen:r];
|
||||||
}
|
}
|
||||||
@end
|
@end
|
||||||
|
|
||||||
|
|||||||
+15
-2
@@ -349,6 +349,17 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
}
|
}
|
||||||
case _WM_WAKEUP:
|
case _WM_WAKEUP:
|
||||||
w.w.Event(wakeupEvent{})
|
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:
|
case windows.WM_IME_COMPOSITION:
|
||||||
imc := windows.ImmGetContext(w.hwnd)
|
imc := windows.ImmGetContext(w.hwnd)
|
||||||
if imc == 0 {
|
if imc == 0 {
|
||||||
@@ -358,7 +369,7 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr
|
|||||||
state := w.w.EditorState()
|
state := w.w.EditorState()
|
||||||
rng := state.compose
|
rng := state.compose
|
||||||
if rng.Start == -1 {
|
if rng.Start == -1 {
|
||||||
rng = state.Selection
|
rng = state.Selection.Range
|
||||||
}
|
}
|
||||||
if rng.Start > rng.End {
|
if rng.Start > rng.End {
|
||||||
rng.Start, rng.End = rng.End, rng.Start
|
rng.Start, rng.End = rng.End, rng.Start
|
||||||
@@ -499,7 +510,9 @@ func (w *window) EditorStateChanged(old, new editorState) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer windows.ImmReleaseContext(w.hwnd, imc)
|
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) {
|
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) {
|
func (c *callbacks) EditorInsert(text string) {
|
||||||
sel := c.w.imeState.Selection
|
sel := c.w.imeState.Selection.Range
|
||||||
c.EditorReplace(sel, text)
|
c.EditorReplace(sel, text)
|
||||||
start := sel.Start
|
start := sel.Start
|
||||||
if sel.End < 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) {
|
func (c *callbacks) SetEditorSelection(r key.Range) {
|
||||||
c.w.imeState.Selection = r
|
c.w.imeState.Selection.Range = r
|
||||||
c.Event(key.SelectionEvent(r))
|
c.Event(key.SelectionEvent(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -157,7 +157,7 @@ const (
|
|||||||
TypeSemanticSelectedLen = 2
|
TypeSemanticSelectedLen = 2
|
||||||
TypeSemanticDisabledLen = 2
|
TypeSemanticDisabledLen = 2
|
||||||
TypeSnippetLen = 1 + 4 + 4
|
TypeSnippetLen = 1 + 4 + 4
|
||||||
TypeSelectionLen = 1 + 4 + 4
|
TypeSelectionLen = 1 + 2*4 + 2*4 + 4 + 4
|
||||||
)
|
)
|
||||||
|
|
||||||
func (op *ClipOp) Decode(data []byte) {
|
func (op *ClipOp) Decode(data []byte) {
|
||||||
|
|||||||
@@ -12,8 +12,10 @@ package key
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gioui.org/f32"
|
||||||
"gioui.org/internal/ops"
|
"gioui.org/internal/ops"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/op"
|
"gioui.org/op"
|
||||||
@@ -45,6 +47,7 @@ type FocusOp struct {
|
|||||||
type SelectionOp struct {
|
type SelectionOp struct {
|
||||||
Tag event.Tag
|
Tag event.Tag
|
||||||
Range
|
Range
|
||||||
|
Caret
|
||||||
}
|
}
|
||||||
|
|
||||||
// SnippetOp updates the content snippet for an input handler.
|
// SnippetOp updates the content snippet for an input handler.
|
||||||
@@ -67,6 +70,16 @@ type Snippet struct {
|
|||||||
Text string
|
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.
|
// SelectionEvent is generated when an input method changes the selection.
|
||||||
type SelectionEvent Range
|
type SelectionEvent Range
|
||||||
|
|
||||||
@@ -229,6 +242,10 @@ func (s SelectionOp) Add(o *op.Ops) {
|
|||||||
bo := binary.LittleEndian
|
bo := binary.LittleEndian
|
||||||
bo.PutUint32(data[1:], uint32(s.Start))
|
bo.PutUint32(data[1:], uint32(s.Start))
|
||||||
bo.PutUint32(data[5:], uint32(s.End))
|
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() {}
|
func (EditEvent) ImplementsEvent() {}
|
||||||
|
|||||||
+11
-4
@@ -3,14 +3,19 @@
|
|||||||
package router
|
package router
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"gioui.org/f32"
|
||||||
"gioui.org/io/event"
|
"gioui.org/io/event"
|
||||||
"gioui.org/io/key"
|
"gioui.org/io/key"
|
||||||
)
|
)
|
||||||
|
|
||||||
// EditorState represents the state of an editor needed by input handlers.
|
// EditorState represents the state of an editor needed by input handlers.
|
||||||
type EditorState struct {
|
type EditorState struct {
|
||||||
Selection key.Range
|
Selection struct {
|
||||||
Snippet key.Snippet
|
Transform f32.Affine2D
|
||||||
|
key.Range
|
||||||
|
key.Caret
|
||||||
|
}
|
||||||
|
Snippet key.Snippet
|
||||||
}
|
}
|
||||||
|
|
||||||
type TextInputState uint8
|
type TextInputState uint8
|
||||||
@@ -143,9 +148,11 @@ func (k *keyCollector) inputOp(op key.InputOp) {
|
|||||||
h.hint = op.Hint
|
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 {
|
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"
|
"encoding/binary"
|
||||||
"image"
|
"image"
|
||||||
"io"
|
"io"
|
||||||
|
"math"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -335,8 +336,16 @@ func (q *Router) collect() {
|
|||||||
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
|
Start: int(int32(bo.Uint32(encOp.Data[1:]))),
|
||||||
End: int(int32(bo.Uint32(encOp.Data[5:]))),
|
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.
|
// Semantic ops.
|
||||||
case ops.TypeSemanticLabel:
|
case ops.TypeSemanticLabel:
|
||||||
|
|||||||
+60
-38
@@ -103,7 +103,10 @@ type Editor struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type imeState struct {
|
type imeState struct {
|
||||||
selection key.Range
|
selection struct {
|
||||||
|
rng key.Range
|
||||||
|
caret key.Caret
|
||||||
|
}
|
||||||
snippet key.Snippet
|
snippet key.Snippet
|
||||||
start, end int
|
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.processEvents(gtx)
|
||||||
e.makeValid()
|
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 {
|
if viewSize := gtx.Constraints.Constrain(e.dims.Size); viewSize != e.viewSize {
|
||||||
e.viewSize = viewSize
|
e.viewSize = viewSize
|
||||||
e.invalidate()
|
e.invalidate()
|
||||||
}
|
}
|
||||||
e.makeValid()
|
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
|
// 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 {
|
if !e.caret.on {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
carWidth := fixed.I(gtx.Px(unit.Dp(1)))
|
carWidth2 := gtx.Px(unit.Dp(1)) / 2
|
||||||
caretStart := e.closestPosition(combinedPos{runes: e.caret.start})
|
if carWidth2 < 1 {
|
||||||
carX := caretStart.x
|
carWidth2 = 1
|
||||||
carY := caretStart.y
|
}
|
||||||
|
caretPos, carAsc, carDesc := e.caretInfo()
|
||||||
carX -= carWidth / 2
|
|
||||||
carAsc, carDesc := -e.lines[caretStart.lineCol.Y].Bounds.Min.Y, e.lines[caretStart.lineCol.Y].Bounds.Max.Y
|
carRect := image.Rectangle{
|
||||||
carRect := image.Rectangle{
|
Min: caretPos.Sub(image.Pt(carWidth2, carAsc)),
|
||||||
Min: image.Point{X: carX.Ceil(), Y: carY - carAsc.Ceil()},
|
Max: caretPos.Add(image.Pt(carWidth2, carDesc)),
|
||||||
Max: image.Point{X: carX.Ceil() + carWidth.Ceil(), Y: carY + carDesc.Ceil()},
|
|
||||||
}
|
}
|
||||||
carRect = carRect.Add(image.Point{
|
|
||||||
X: -e.scrollOff.X,
|
|
||||||
Y: -e.scrollOff.Y,
|
|
||||||
})
|
|
||||||
cl := textPadding(e.lines)
|
cl := textPadding(e.lines)
|
||||||
// Account for caret width to each side.
|
// Account for caret width to each side.
|
||||||
whalf := (carWidth / 2).Ceil()
|
if cl.Max.X < carWidth2 {
|
||||||
if cl.Max.X < whalf {
|
cl.Max.X = carWidth2
|
||||||
cl.Max.X = whalf
|
|
||||||
}
|
}
|
||||||
if cl.Min.X > -whalf {
|
if cl.Min.X > -carWidth2 {
|
||||||
cl.Min.X = -whalf
|
cl.Min.X = -carWidth2
|
||||||
}
|
}
|
||||||
cl.Max = cl.Max.Add(e.viewSize)
|
cl.Max = cl.Max.Add(e.viewSize)
|
||||||
carRect = cl.Intersect(carRect)
|
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.
|
// TODO: copied from package math. Remove when Go 1.18 is minimum.
|
||||||
const (
|
const (
|
||||||
intSize = 32 << (^uint(0) >> 63) // 32 or 64
|
intSize = 32 << (^uint(0) >> 63) // 32 or 64
|
||||||
|
|||||||
Reference in New Issue
Block a user