mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2025-10-05 07:57:10 +08:00
242 lines
7.3 KiB
Go
242 lines
7.3 KiB
Go
// Copyright 2023 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.
|
|
|
|
//go:build !ios
|
|
|
|
package textinput
|
|
|
|
// #cgo CFLAGS: -x objective-c
|
|
// #cgo LDFLAGS: -framework Cocoa
|
|
//
|
|
// #include <stdint.h>
|
|
// #include <Cocoa/Cocoa.h>
|
|
import "C"
|
|
|
|
import (
|
|
"image"
|
|
|
|
"github.com/ebitengine/purego/objc"
|
|
|
|
"github.com/hajimehoshi/ebiten/v2/internal/ui"
|
|
)
|
|
|
|
//export ebitengine_textinput_hasMarkedText
|
|
func ebitengine_textinput_hasMarkedText() C.int64_t {
|
|
_, _, _, state, ok := currentState()
|
|
if !ok {
|
|
return 0
|
|
}
|
|
if len(state.Text) > 0 {
|
|
return 1
|
|
}
|
|
return 0
|
|
}
|
|
|
|
//export ebitengine_textinput_markedRange
|
|
func ebitengine_textinput_markedRange(start, length *C.int64_t) {
|
|
*start = -1
|
|
*length = 0
|
|
|
|
text, startInBytes, _, state, ok := currentState()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
if len(state.Text) == 0 {
|
|
return
|
|
}
|
|
|
|
startInUTF16 := convertByteCountToUTF16Count(text, startInBytes)
|
|
markedTextLenInUTF16 := convertByteCountToUTF16Count(state.Text, len(state.Text))
|
|
*start = C.int64_t(startInUTF16)
|
|
*length = C.int64_t(markedTextLenInUTF16)
|
|
}
|
|
|
|
//export ebitengine_textinput_selectedRange
|
|
func ebitengine_textinput_selectedRange(start, length *C.int64_t) {
|
|
*start = -1
|
|
*length = 0
|
|
|
|
text, startInBytes, endInBytes, _, ok := currentState()
|
|
if !ok {
|
|
return
|
|
}
|
|
|
|
startInUTF16 := convertByteCountToUTF16Count(text, startInBytes)
|
|
endInUTF16 := convertByteCountToUTF16Count(text, endInBytes)
|
|
*start = C.int64_t(startInUTF16)
|
|
*length = C.int64_t(endInUTF16 - startInUTF16)
|
|
}
|
|
|
|
//export ebitengine_textinput_unmarkText
|
|
func ebitengine_textinput_unmarkText() {
|
|
}
|
|
|
|
//export ebitengine_textinput_setMarkedText
|
|
func ebitengine_textinput_setMarkedText(text *C.char, selectionStart, selectionLen, replaceStart, replaceLen C.int64_t) {
|
|
// selectionStart's origin is the beginning of the inserted text.
|
|
// replaceStart's origin is also the beginning of the inserted text (= the marked text in the current implementation).
|
|
// As the text argument already represents the complete marked text, it seems fine to ignore the replaceStart and replaceLen arguments.
|
|
//
|
|
// https://developer.apple.com/documentation/appkit/nstextinputclient/setmarkedtext(_:selectedrange:replacementrange:)?language=objc
|
|
|
|
t := C.GoString(text)
|
|
startInBytes := convertUTF16CountToByteCount(t, int(selectionStart))
|
|
endInBytes := convertUTF16CountToByteCount(t, int(selectionStart+selectionLen))
|
|
theTextInput.update(t, startInBytes, endInBytes, 0, 0, false)
|
|
}
|
|
|
|
//export ebitengine_textinput_insertText
|
|
func ebitengine_textinput_insertText(text *C.char, replaceStart, replaceLen C.int64_t) {
|
|
// replaceStart's origin is the beginning of the current text.
|
|
//
|
|
// https://developer.apple.com/documentation/appkit/nstextinputclient/inserttext(_:replacementrange:)?language=objc
|
|
|
|
t := C.GoString(text)
|
|
var delStartInBytes, delEndInBytes int
|
|
if replaceStart >= 0 {
|
|
if text, _, _, _, ok := currentState(); ok {
|
|
delStartInBytes = convertUTF16CountToByteCount(text, int(replaceStart))
|
|
delEndInBytes = convertUTF16CountToByteCount(text, int(replaceStart+replaceLen))
|
|
}
|
|
}
|
|
theTextInput.update(t, 0, len(t), delStartInBytes, delEndInBytes, true)
|
|
}
|
|
|
|
//export ebitengine_textinput_firstRectForCharacterRange
|
|
func ebitengine_textinput_firstRectForCharacterRange(self C.uintptr_t, crange C.NSRange, actualRange C.NSRangePointer) C.NSRect {
|
|
if actualRange != nil {
|
|
if text, startInBytes, _, _, ok := currentState(); ok {
|
|
s := C.NSUInteger(convertUTF16CountToByteCount(text, startInBytes))
|
|
actualRange.location = s
|
|
// 0 seems to work correctly.
|
|
// See https://developer.apple.com/documentation/appkit/nstextinputclient/firstrect(forcharacterrange:actualrange:)?language=objc
|
|
// > If the length of aRange is 0 (as it would be if there is nothing selected at the insertion point),
|
|
// > the rectangle coincides with the insertion point, and its width is 0.
|
|
actualRange.length = 0
|
|
}
|
|
}
|
|
|
|
window := objc.ID(self).Send(selWindow)
|
|
frame := objc.Send[C.NSRect](objc.ID(self), selFrame)
|
|
return objc.Send[C.NSRect](window, selConvertRectToScreen, frame)
|
|
}
|
|
|
|
type textInput struct {
|
|
// session must be accessed from the main thread.
|
|
session *session
|
|
}
|
|
|
|
var theTextInput textInput
|
|
|
|
func (t *textInput) Start(bounds image.Rectangle) (<-chan textInputState, func()) {
|
|
ui.Get().RunOnMainThread(func() {
|
|
t.start(bounds)
|
|
})
|
|
return t.session.ch, t.session.end
|
|
}
|
|
|
|
func (t *textInput) update(text string, startInBytes, endInBytes int, deleteStartInBytes, deleteEndInBytes int, committed bool) {
|
|
if t.session != nil {
|
|
t.session.trySend(textInputState{
|
|
Text: text,
|
|
CompositionSelectionStartInBytes: startInBytes,
|
|
CompositionSelectionEndInBytes: endInBytes,
|
|
DeleteStartInBytes: deleteStartInBytes,
|
|
DeleteEndInBytes: deleteEndInBytes,
|
|
Committed: committed,
|
|
})
|
|
}
|
|
if committed {
|
|
t.endIfNeeded()
|
|
}
|
|
}
|
|
|
|
//export ebitengine_textinput_end
|
|
func ebitengine_textinput_end() {
|
|
theTextInput.endIfNeeded()
|
|
}
|
|
|
|
func (t *textInput) endIfNeeded() {
|
|
if t.session == nil {
|
|
return
|
|
}
|
|
t.session.end()
|
|
t.session = nil
|
|
}
|
|
|
|
var (
|
|
selAddSubview = objc.RegisterName("addSubview:")
|
|
selAlloc = objc.RegisterName("alloc")
|
|
selContentView = objc.RegisterName("contentView")
|
|
selConvertRectToScreen = objc.RegisterName("convertRectToScreen:")
|
|
selFrame = objc.RegisterName("frame")
|
|
selInit = objc.RegisterName("init")
|
|
selMainWindow = objc.RegisterName("mainWindow")
|
|
selMakeFirstResponder = objc.RegisterName("makeFirstResponder:")
|
|
selSetFrame = objc.RegisterName("setFrame:")
|
|
selSharedApplication = objc.RegisterName("sharedApplication")
|
|
selWindow = objc.RegisterName("window")
|
|
|
|
idNSApplication = objc.ID(objc.GetClass("NSApplication"))
|
|
)
|
|
|
|
var theTextInputClient objc.ID
|
|
|
|
func getTextInputClient() objc.ID {
|
|
if theTextInputClient == 0 {
|
|
class := objc.ID(objc.GetClass("TextInputClient"))
|
|
theTextInputClient = class.Send(selAlloc).Send(selInit)
|
|
}
|
|
return theTextInputClient
|
|
}
|
|
|
|
type nsPoint struct {
|
|
x float64
|
|
y float64
|
|
}
|
|
|
|
type nsSize struct {
|
|
width float64
|
|
height float64
|
|
}
|
|
|
|
type nsRect struct {
|
|
origin nsPoint
|
|
size nsSize
|
|
}
|
|
|
|
func (t *textInput) start(bounds image.Rectangle) {
|
|
t.endIfNeeded()
|
|
|
|
tc := getTextInputClient()
|
|
window := idNSApplication.Send(selSharedApplication).Send(selMainWindow)
|
|
contentView := window.Send(selContentView)
|
|
contentView.Send(selAddSubview, tc)
|
|
window.Send(selMakeFirstResponder, tc)
|
|
|
|
r := objc.Send[nsRect](contentView, selFrame)
|
|
// The Y dirction is upward in the Cocoa coordinate system.
|
|
y := int(r.size.height) - bounds.Max.Y
|
|
// X is shifted a little bit, especially for the accent popup.
|
|
bounds = bounds.Add(image.Pt(6, 0))
|
|
tc.Send(selSetFrame, nsRect{
|
|
origin: nsPoint{float64(bounds.Min.X), float64(y)},
|
|
size: nsSize{float64(bounds.Dx()), float64(bounds.Dy())},
|
|
})
|
|
|
|
session := newSession()
|
|
t.session = session
|
|
}
|