mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-11-10 04:57:26 +01:00
parent
3eaa03e193
commit
dc05f2014f
154
exp/textinput/api_windows.go
Normal file
154
exp/textinput/api_windows.go
Normal file
@ -0,0 +1,154 @@
|
||||
// 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 (
|
||||
"fmt"
|
||||
"runtime"
|
||||
"unsafe"
|
||||
|
||||
"golang.org/x/sys/windows"
|
||||
)
|
||||
|
||||
const (
|
||||
_ATTR_TARGET_CONVERTED = 0x01
|
||||
_ATTR_TARGET_NOTCONVERTED = 0x03
|
||||
|
||||
_CFS_CANDIDATEPOS = 0x0040
|
||||
|
||||
_GCS_COMPATTR = 0x0010
|
||||
_GCS_COMPCLAUSE = 0x0020
|
||||
_GCS_COMPSTR = 0x0008
|
||||
_GCS_RESULTSTR = 0x0800
|
||||
|
||||
_GWL_WNDPROC = -4
|
||||
|
||||
_ISC_SHOWUICOMPOSITIONWINDOW = 0x80000000
|
||||
|
||||
_UNICODE_NOCHAR = 0xffff
|
||||
|
||||
_WM_CHAR = 0x0102
|
||||
_WM_IME_COMPOSITION = 0x010F
|
||||
_WM_IME_SETCONTEXT = 0x0281
|
||||
_WM_SYSCHAR = 0x0106
|
||||
_WM_UNICHAR = 0x0109
|
||||
)
|
||||
|
||||
type (
|
||||
_HIMC uintptr
|
||||
)
|
||||
|
||||
type _CANDIDATEFORM struct {
|
||||
dwIndex uint32
|
||||
dwStyle uint32
|
||||
ptCurrentPos _POINT
|
||||
rcArea _RECT
|
||||
}
|
||||
|
||||
type _POINT struct {
|
||||
x int32
|
||||
y int32
|
||||
}
|
||||
|
||||
type _RECT struct {
|
||||
left int32
|
||||
top int32
|
||||
right int32
|
||||
bottom int32
|
||||
}
|
||||
|
||||
var (
|
||||
imm32 = windows.NewLazySystemDLL("imm32.dll")
|
||||
user32 = windows.NewLazySystemDLL("user32.dll")
|
||||
|
||||
procImmGetCompositionStringW = imm32.NewProc("ImmGetCompositionStringW")
|
||||
procImmGetContext = imm32.NewProc("ImmGetContext")
|
||||
procImmReleaseContext = imm32.NewProc("ImmReleaseContext")
|
||||
procImmSetCandidateWindow = imm32.NewProc("ImmSetCandidateWindow")
|
||||
|
||||
procCallWindowProcW = user32.NewProc("CallWindowProcW")
|
||||
procGetActiveWindow = user32.NewProc("GetActiveWindow")
|
||||
procSetWindowLongW = user32.NewProc("SetWindowLongW") // 32-Bit Windows version.
|
||||
procSetWindowLongPtrW = user32.NewProc("SetWindowLongPtrW") // 64-Bit Windows version.
|
||||
)
|
||||
|
||||
func _CallWindowProcW(lpPrevWndFunc uintptr, hWnd uintptr, msg uint32, wParam, lParam uintptr) uintptr {
|
||||
r, _, _ := procCallWindowProcW.Call(lpPrevWndFunc, hWnd, uintptr(msg), wParam, lParam)
|
||||
return r
|
||||
}
|
||||
|
||||
func _GetActiveWindow() windows.HWND {
|
||||
r, _, _ := procGetActiveWindow.Call()
|
||||
return windows.HWND(r)
|
||||
}
|
||||
|
||||
func _ImmGetCompositionStringW(unnamedParam1 _HIMC, unnamedParam2 uint32, lpBuf unsafe.Pointer, dwBufLen uint32) (uint32, error) {
|
||||
r, _, e := procImmGetCompositionStringW.Call(uintptr(unnamedParam1), uintptr(unnamedParam2), uintptr(lpBuf), uintptr(dwBufLen))
|
||||
runtime.KeepAlive(lpBuf)
|
||||
if r < 0 {
|
||||
return 0, fmt.Errorf("textinput: ImmGetCompositionStringW failed: %d", r)
|
||||
}
|
||||
if e != nil && e != windows.ERROR_SUCCESS {
|
||||
return 0, fmt.Errorf("textinput: ImmGetCompositionStringW failed: %w", e)
|
||||
}
|
||||
return uint32(r), nil
|
||||
}
|
||||
|
||||
func _ImmGetContext(unnamedParam1 windows.HWND) _HIMC {
|
||||
r, _, _ := procImmGetContext.Call(uintptr(unnamedParam1))
|
||||
return _HIMC(r)
|
||||
}
|
||||
|
||||
func _ImmReleaseContext(unnamedParam1 windows.HWND, unnamedParam2 _HIMC) error {
|
||||
r, _, e := procImmReleaseContext.Call(uintptr(unnamedParam1), uintptr(unnamedParam2))
|
||||
if int32(r) == 0 {
|
||||
if e != nil && e != windows.ERROR_SUCCESS {
|
||||
return fmt.Errorf("textinput: ImmReleaseContext failed: %w", e)
|
||||
}
|
||||
return fmt.Errorf("textinput: ImmReleaseContext returned 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _ImmSetCandidateWindow(unnamedParam1 _HIMC, lpCandidate *_CANDIDATEFORM) error {
|
||||
r, _, e := procImmSetCandidateWindow.Call(uintptr(unnamedParam1), uintptr(unsafe.Pointer(lpCandidate)))
|
||||
runtime.KeepAlive(lpCandidate)
|
||||
if int32(r) == 0 {
|
||||
if e != nil && e != windows.ERROR_SUCCESS {
|
||||
return fmt.Errorf("textinput: ImmSetCandidateWindow failed: %w", e)
|
||||
}
|
||||
return fmt.Errorf("textinput: ImmSetCandidateWindow returned 0")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func _SetWindowLongPtrW(hWnd windows.HWND, nIndex int32, dwNewLong uintptr) (uintptr, error) {
|
||||
var p *windows.LazyProc
|
||||
if procSetWindowLongPtrW.Find() == nil {
|
||||
// 64-Bit Windows.
|
||||
p = procSetWindowLongPtrW
|
||||
} else {
|
||||
// 32-Bit Windows.
|
||||
p = procSetWindowLongW
|
||||
}
|
||||
h, _, e := p.Call(uintptr(hWnd), uintptr(nIndex), dwNewLong)
|
||||
if h == 0 {
|
||||
if e != nil && e != windows.ERROR_SUCCESS {
|
||||
return 0, fmt.Errorf("textinput: SetWindowLongPtrW failed: %w", e)
|
||||
}
|
||||
return 0, fmt.Errorf("textinput: SetWindowLongPtrW returned 0")
|
||||
}
|
||||
return h, nil
|
||||
}
|
@ -12,7 +12,7 @@
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//go:build (!darwin && !js) || ios
|
||||
//go:build (!darwin && !js && !windows) || ios
|
||||
|
||||
package textinput
|
||||
|
||||
|
263
exp/textinput/textinput_windows.go
Normal file
263
exp/textinput/textinput_windows.go
Normal file
@ -0,0 +1,263 @@
|
||||
// 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 (
|
||||
"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
|
||||
|
||||
highSurrogate uint16
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
return session.ch, session.end
|
||||
}
|
||||
|
||||
// start must be called from the main thread.
|
||||
func (t *textInput) start(x, y int) error {
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
if t.session != nil {
|
||||
t.session.end()
|
||||
t.session = nil
|
||||
}
|
||||
}
|
||||
|
||||
// update must be called from the main thread.
|
||||
func (t *textInput) update() (ferr error) {
|
||||
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) {
|
||||
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
|
||||
}
|
Loading…
Reference in New Issue
Block a user