From 27e0b9cc7d07a3fa17a0889c913b9ccd151f8b75 Mon Sep 17 00:00:00 2001 From: Elias Naur Date: Fri, 11 Feb 2022 19:05:46 +0000 Subject: [PATCH] app,app/internal/windows: [Windows] implement IME support References: https://todo.sr.ht/~eliasnaur/gio/246 Signed-off-by: Elias Naur --- app/internal/windows/windows.go | 129 ++++++++++++++++++++++---------- app/os_windows.go | 52 ++++++++++++- 2 files changed, 142 insertions(+), 39 deletions(-) diff --git a/app/internal/windows/windows.go b/app/internal/windows/windows.go index cf28cb55..012895cb 100644 --- a/app/internal/windows/windows.go +++ b/app/internal/windows/windows.go @@ -9,6 +9,7 @@ import ( "fmt" "runtime" "time" + "unicode/utf16" "unsafe" syscall "golang.org/x/sys/windows" @@ -75,14 +76,26 @@ type MonitorInfo struct { const ( TRUE = 1 - CS_HREDRAW = 0x0002 - CS_VREDRAW = 0x0001 - CS_OWNDC = 0x0020 + CPS_CANCEL = 0x0004 + + CS_HREDRAW = 0x0002 + CS_INSERTCHAR = 0x2000 + CS_NOMOVECARET = 0x4000 + CS_VREDRAW = 0x0001 + CS_OWNDC = 0x0020 CW_USEDEFAULT = -2147483648 - GWL_STYLE = ^(uint32(16) - 1) // -16 - HWND_TOPMOST = ^(uint32(1) - 1) // -1 + GWL_STYLE = ^(uint32(16) - 1) // -16 + + GCS_COMPSTR = 0x0008 + GCS_COMPREADSTR = 0x0001 + GCS_CURSORPOS = 0x0080 + GCS_DELTASTART = 0x0100 + GCS_RESULTREADSTR = 0x0200 + GCS_RESULTSTR = 0x0800 + + HWND_TOPMOST = ^(uint32(1) - 1) // -1 HTCLIENT = 1 @@ -102,10 +115,14 @@ const ( MONITOR_DEFAULTTOPRIMARY = 1 + NI_COMPOSITIONSTR = 0x0015 + SIZE_MAXIMIZED = 2 SIZE_MINIMIZED = 1 SIZE_RESTORED = 0 + SCS_SETSTR = GCS_COMPREADSTR | GCS_COMPSTR + SW_SHOWDEFAULT = 10 SW_SHOWMINIMIZED = 2 SW_SHOWMAXIMIZED = 3 @@ -170,44 +187,46 @@ const ( UNICODE_NOCHAR = 65535 - WM_CANCELMODE = 0x001F - WM_CHAR = 0x0102 - WM_CREATE = 0x0001 - WM_DPICHANGED = 0x02E0 - WM_DESTROY = 0x0002 - WM_ERASEBKGND = 0x0014 - WM_KEYDOWN = 0x0100 - WM_KEYUP = 0x0101 - 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_CLOSE = 0x0010 - WM_QUIT = 0x0012 - WM_SETCURSOR = 0x0020 - WM_SETFOCUS = 0x0007 - WM_KILLFOCUS = 0x0008 - 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_GETMINMAXINFO = 0x0024 - 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_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 WS_MAXIMIZE = 0x01000000 WS_ICONIC = 0x20000000 WS_VISIBLE = 0x10000000 - WS_OVERLAPPED = 0x00000000 WS_OVERLAPPEDWINDOW = WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX @@ -313,6 +332,12 @@ var ( gdi32 = syscall.NewLazySystemDLL("gdi32") _GetDeviceCaps = gdi32.NewProc("GetDeviceCaps") + + imm32 = syscall.NewLazySystemDLL("imm32") + _ImmGetContext = imm32.NewProc("ImmGetContext") + _ImmGetCompositionString = imm32.NewProc("ImmGetCompositionStringW") + _ImmNotifyIME = imm32.NewProc("ImmNotifyIME") + _ImmReleaseContext = imm32.NewProc("ImmReleaseContext") ) func AdjustWindowRectEx(r *Rect, dwStyle uint32, bMenu int, dwExStyle uint32) { @@ -487,6 +512,34 @@ func GetWindowLong(hwnd syscall.Handle) (style uintptr) { return } +func ImmGetContext(hwnd syscall.Handle) syscall.Handle { + h, _, _ := _ImmGetContext.Call(uintptr(hwnd)) + return syscall.Handle(h) +} + +func ImmReleaseContext(hwnd, imc syscall.Handle) { + _ImmReleaseContext.Call(uintptr(hwnd), uintptr(imc)) +} + +func ImmNotifyIME(imc syscall.Handle, action, index, value int) { + _ImmNotifyIME.Call(uintptr(imc), uintptr(action), uintptr(index), uintptr(value)) +} + +func ImmGetCompositionString(imc syscall.Handle, key int) string { + size, _, _ := _ImmGetCompositionString.Call(uintptr(imc), uintptr(key), 0, 0) + if int32(size) <= 0 { + return "" + } + u16 := make([]uint16, size/unsafe.Sizeof(uint16(0))) + _ImmGetCompositionString.Call(uintptr(imc), uintptr(key), uintptr(unsafe.Pointer(&u16[0])), size) + return string(utf16.Decode(u16)) +} + +func ImmGetCompositionValue(imc syscall.Handle, key int) int { + val, _, _ := _ImmGetCompositionString.Call(uintptr(imc), uintptr(key), 0, 0) + return int(int32(val)) +} + 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_windows.go b/app/os_windows.go index badb9950..7ed7f71e 100644 --- a/app/os_windows.go +++ b/app/os_windows.go @@ -13,6 +13,7 @@ import ( "sync" "time" "unicode" + "unicode/utf8" "unsafe" syscall "golang.org/x/sys/windows" @@ -349,6 +350,48 @@ func windowProc(hwnd syscall.Handle, msg uint32, wParam, lParam uintptr) uintptr } case _WM_WAKEUP: w.w.Event(wakeupEvent{}) + case windows.WM_IME_COMPOSITION: + imc := windows.ImmGetContext(w.hwnd) + if imc == 0 { + return windows.TRUE + } + defer windows.ImmReleaseContext(w.hwnd, imc) + state := w.w.EditorState() + rng := state.compose + if rng.Start == -1 { + rng = state.Selection + } + if rng.Start > rng.End { + rng.Start, rng.End = rng.End, rng.Start + } + var replacement string + switch { + case lParam&windows.GCS_RESULTSTR != 0: + replacement = windows.ImmGetCompositionString(imc, windows.GCS_RESULTSTR) + case lParam&windows.GCS_COMPSTR != 0: + replacement = windows.ImmGetCompositionString(imc, windows.GCS_COMPSTR) + } + end := rng.Start + utf8.RuneCountInString(replacement) + w.w.EditorReplace(rng, replacement) + state = w.w.EditorState() + comp := key.Range{ + Start: rng.Start, + End: end, + } + if lParam&windows.GCS_DELTASTART != 0 { + start := windows.ImmGetCompositionValue(imc, windows.GCS_DELTASTART) + comp.Start = state.RunesIndex(state.UTF16Index(comp.Start) + start) + } + w.w.SetComposingRegion(comp) + if lParam&windows.GCS_CURSORPOS != 0 { + rel := windows.ImmGetCompositionValue(imc, windows.GCS_CURSORPOS) + pos := state.RunesIndex(state.UTF16Index(rng.Start) + rel) + w.w.SetEditorSelection(key.Range{Start: pos, End: pos}) + } + return windows.TRUE + case windows.WM_IME_ENDCOMPOSITION: + w.w.SetComposingRegion(key.Range{Start: -1, End: -1}) + return windows.TRUE } return windows.DefWindowProc(hwnd, msg, wParam, lParam) @@ -451,7 +494,14 @@ loop: return nil } -func (w *window) EditorStateChanged(old, new editorState) {} +func (w *window) EditorStateChanged(old, new editorState) { + imc := windows.ImmGetContext(w.hwnd) + if imc == 0 { + return + } + defer windows.ImmReleaseContext(w.hwnd, imc) + windows.ImmNotifyIME(imc, windows.NI_COMPOSITIONSTR, windows.CPS_CANCEL, 0) +} func (w *window) SetAnimating(anim bool) { w.animating = anim