2024-03-02 07:30:57 +01:00
|
|
|
// Copyright 2024 The Ebitengine Authors
|
|
|
|
//
|
|
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
|
|
// you may not use this file except in compliance with the License.
|
|
|
|
// You may obtain a copy of the License at
|
|
|
|
//
|
|
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
|
|
//
|
|
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
|
|
// See the License for the specific language governing permissions and
|
|
|
|
// limitations under the License.
|
|
|
|
|
|
|
|
package textinput
|
|
|
|
|
|
|
|
import (
|
2024-08-26 16:26:58 +02:00
|
|
|
"sync"
|
2024-03-02 07:30:57 +01:00
|
|
|
"unsafe"
|
|
|
|
|
|
|
|
"golang.org/x/sys/windows"
|
|
|
|
|
|
|
|
"github.com/hajimehoshi/ebiten/v2/internal/microsoftgdk"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2/internal/ui"
|
|
|
|
)
|
|
|
|
|
|
|
|
type textInput struct {
|
|
|
|
session *session
|
|
|
|
|
|
|
|
origWndProc uintptr
|
|
|
|
wndProcCallback uintptr
|
|
|
|
window windows.HWND
|
2024-08-26 16:26:58 +02:00
|
|
|
immContext uintptr
|
2024-03-02 07:30:57 +01:00
|
|
|
|
|
|
|
highSurrogate uint16
|
2024-08-26 16:26:58 +02:00
|
|
|
|
|
|
|
initOnce sync.Once
|
|
|
|
|
|
|
|
err error
|
2024-03-02 07:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
var theTextInput textInput
|
|
|
|
|
|
|
|
func (t *textInput) Start(x, y int) (chan State, func()) {
|
|
|
|
if microsoftgdk.IsXbox() {
|
|
|
|
return nil, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
var session *session
|
|
|
|
var err error
|
|
|
|
ui.Get().RunOnMainThread(func() {
|
|
|
|
t.end()
|
|
|
|
err = t.start(x, y)
|
|
|
|
session = newSession()
|
|
|
|
t.session = session
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
session.ch <- State{Error: err}
|
|
|
|
session.end()
|
|
|
|
}
|
2024-08-26 16:26:58 +02:00
|
|
|
return session.ch, func() {
|
|
|
|
ui.Get().RunOnMainThread(func() {
|
|
|
|
// Disable IME again.
|
|
|
|
if t.immContext != 0 {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
c, err := _ImmAssociateContext(t.window, 0)
|
|
|
|
if err != nil {
|
|
|
|
t.err = err
|
|
|
|
return
|
|
|
|
}
|
|
|
|
t.immContext = c
|
|
|
|
t.end()
|
|
|
|
})
|
|
|
|
}
|
2024-03-02 07:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// start must be called from the main thread.
|
|
|
|
func (t *textInput) start(x, y int) error {
|
2024-08-26 16:26:58 +02:00
|
|
|
if t.err != nil {
|
|
|
|
return t.err
|
|
|
|
}
|
|
|
|
|
2024-03-02 07:30:57 +01:00
|
|
|
if t.window == 0 {
|
|
|
|
t.window = _GetActiveWindow()
|
|
|
|
}
|
|
|
|
if t.origWndProc == 0 {
|
|
|
|
if t.wndProcCallback == 0 {
|
|
|
|
t.wndProcCallback = windows.NewCallback(t.wndProc)
|
|
|
|
}
|
|
|
|
// Note that a Win32API GetActiveWindow doesn't work on Xbox.
|
|
|
|
h, err := _SetWindowLongPtrW(t.window, _GWL_WNDPROC, t.wndProcCallback)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
t.origWndProc = h
|
|
|
|
}
|
|
|
|
|
2024-08-26 16:26:58 +02:00
|
|
|
// By default, IME was disabled by setting 0 as the IMM context.
|
|
|
|
// Restore the context once.
|
|
|
|
var err error
|
|
|
|
t.initOnce.Do(func() {
|
|
|
|
err = ui.Get().RestoreIMMContextOnMainThread()
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
if t.immContext != 0 {
|
|
|
|
if _, err := _ImmAssociateContext(t.window, t.immContext); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
t.immContext = 0
|
|
|
|
}
|
2024-03-02 07:30:57 +01:00
|
|
|
h := _ImmGetContext(t.window)
|
|
|
|
if err := _ImmSetCandidateWindow(h, &_CANDIDATEFORM{
|
|
|
|
dwIndex: 0,
|
|
|
|
dwStyle: _CFS_CANDIDATEPOS,
|
|
|
|
ptCurrentPos: _POINT{
|
|
|
|
x: int32(x),
|
|
|
|
y: int32(y),
|
|
|
|
},
|
|
|
|
}); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if err := _ImmReleaseContext(t.window, h); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (t *textInput) wndProc(hWnd uintptr, uMsg uint32, wParam, lParam uintptr) uintptr {
|
|
|
|
if t.session == nil {
|
|
|
|
return _CallWindowProcW(t.origWndProc, hWnd, uMsg, wParam, lParam)
|
|
|
|
}
|
|
|
|
|
|
|
|
switch uMsg {
|
|
|
|
case _WM_IME_SETCONTEXT:
|
|
|
|
// Draw preedit text by an application side.
|
|
|
|
if lParam&_ISC_SHOWUICOMPOSITIONWINDOW != 0 {
|
|
|
|
lParam &^= _ISC_SHOWUICOMPOSITIONWINDOW
|
|
|
|
}
|
|
|
|
case _WM_IME_COMPOSITION:
|
|
|
|
if lParam&(_GCS_RESULTSTR|_GCS_COMPSTR) != 0 {
|
|
|
|
if lParam&_GCS_RESULTSTR != 0 {
|
|
|
|
if err := t.commit(); err != nil {
|
|
|
|
t.session.ch <- State{Error: err}
|
|
|
|
t.end()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if lParam&_GCS_COMPSTR != 0 {
|
|
|
|
if err := t.update(); err != nil {
|
|
|
|
t.session.ch <- State{Error: err}
|
|
|
|
t.end()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
case _WM_CHAR, _WM_SYSCHAR:
|
|
|
|
if wParam >= 0xd800 && wParam <= 0xdbff {
|
|
|
|
t.highSurrogate = uint16(wParam)
|
|
|
|
} else {
|
|
|
|
var c rune
|
|
|
|
if wParam >= 0xdc00 && wParam <= 0xdfff {
|
|
|
|
if t.highSurrogate != 0 {
|
|
|
|
c += (rune(t.highSurrogate) - 0xd800) << 10
|
|
|
|
c += (rune(wParam) & 0xffff) - 0xdc00
|
|
|
|
c += 0x10000
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
c = rune(wParam) & 0xffff
|
|
|
|
}
|
|
|
|
t.highSurrogate = 0
|
|
|
|
if c >= 0x20 {
|
|
|
|
str := string(c)
|
|
|
|
t.send(str, 0, len(str), true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
case _WM_UNICHAR:
|
|
|
|
if wParam == _UNICODE_NOCHAR {
|
|
|
|
// WM_UNICHAR is not sent by Windows, but is sent by some third-party input method engine.
|
|
|
|
// Returning TRUE here announces support for this message.
|
|
|
|
return 1
|
|
|
|
}
|
|
|
|
if r := rune(wParam); r >= 0x20 {
|
|
|
|
str := string(r)
|
|
|
|
t.send(str, 0, len(str), true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return _CallWindowProcW(t.origWndProc, hWnd, uMsg, wParam, lParam)
|
|
|
|
}
|
|
|
|
|
|
|
|
// send must be called from the main thread.
|
|
|
|
func (t *textInput) send(text string, startInBytes, endInBytes int, committed bool) {
|
|
|
|
if t.session != nil {
|
|
|
|
t.session.trySend(State{
|
|
|
|
Text: text,
|
|
|
|
CompositionSelectionStartInBytes: startInBytes,
|
|
|
|
CompositionSelectionEndInBytes: endInBytes,
|
|
|
|
Committed: committed,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if committed {
|
|
|
|
t.end()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// end must be called from the main thread.
|
|
|
|
func (t *textInput) end() {
|
2024-08-26 16:26:58 +02:00
|
|
|
if t.session == nil {
|
|
|
|
return
|
2024-03-02 07:30:57 +01:00
|
|
|
}
|
2024-08-26 16:26:58 +02:00
|
|
|
|
|
|
|
t.session.end()
|
|
|
|
t.session = nil
|
2024-03-02 07:30:57 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// update must be called from the main thread.
|
|
|
|
func (t *textInput) update() (ferr error) {
|
2024-08-26 16:26:58 +02:00
|
|
|
if t.err != nil {
|
|
|
|
return t.err
|
|
|
|
}
|
|
|
|
|
2024-03-02 07:30:57 +01:00
|
|
|
hIMC := _ImmGetContext(t.window)
|
|
|
|
defer func() {
|
|
|
|
if err := _ImmReleaseContext(t.window, hIMC); err != nil && ferr != nil {
|
|
|
|
ferr = err
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
bufferLen, err := _ImmGetCompositionStringW(hIMC, _GCS_COMPSTR, nil, 0)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if bufferLen == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
buffer16 := make([]uint16, bufferLen/uint32(unsafe.Sizeof(uint16(0))))
|
|
|
|
if _, err := _ImmGetCompositionStringW(hIMC, _GCS_COMPSTR, unsafe.Pointer(&buffer16[0]), bufferLen); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
attrLen, err := _ImmGetCompositionStringW(hIMC, _GCS_COMPATTR, nil, 0)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
attr := make([]byte, attrLen)
|
|
|
|
if _, err := _ImmGetCompositionStringW(hIMC, _GCS_COMPATTR, unsafe.Pointer(&attr[0]), attrLen); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
clauseLen, err := _ImmGetCompositionStringW(hIMC, _GCS_COMPCLAUSE, nil, 0)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
clause := make([]uint32, clauseLen/uint32(unsafe.Sizeof(uint32(0))))
|
|
|
|
if _, err := _ImmGetCompositionStringW(hIMC, _GCS_COMPCLAUSE, unsafe.Pointer(&clause[0]), clauseLen); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
var start16 int
|
|
|
|
var end16 int
|
|
|
|
if len(clause) > 0 {
|
|
|
|
for i, c := range clause[:len(clause)-1] {
|
|
|
|
if int(c) == len(attr) {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
if attr[c] == _ATTR_TARGET_CONVERTED || attr[c] == _ATTR_TARGET_NOTCONVERTED {
|
|
|
|
start16 = int(c)
|
|
|
|
end16 = int(clause[i+1])
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
text := windows.UTF16ToString(buffer16)
|
|
|
|
t.send(text, convertUTF16CountToByteCount(text, start16), convertUTF16CountToByteCount(text, end16), false)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// commit must be called from the main thread.
|
|
|
|
func (t *textInput) commit() (ferr error) {
|
2024-08-26 16:26:58 +02:00
|
|
|
if t.err != nil {
|
|
|
|
return t.err
|
|
|
|
}
|
|
|
|
|
2024-03-02 07:30:57 +01:00
|
|
|
hIMC := _ImmGetContext(t.window)
|
|
|
|
defer func() {
|
|
|
|
if err := _ImmReleaseContext(t.window, hIMC); err != nil && ferr != nil {
|
|
|
|
ferr = err
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
bufferLen, err := _ImmGetCompositionStringW(hIMC, _GCS_RESULTSTR, nil, 0)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
if bufferLen == 0 {
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
buffer16 := make([]uint16, bufferLen/uint32(unsafe.Sizeof(uint16(0))))
|
|
|
|
if _, err := _ImmGetCompositionStringW(hIMC, _GCS_RESULTSTR, unsafe.Pointer(&buffer16[0]), bufferLen); err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
text := windows.UTF16ToString(buffer16)
|
|
|
|
t.send(text, 0, len(text), true)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|