Compare commits

...

32 Commits

Author SHA1 Message Date
Mykhailo Lohachov
c73ee2630a
Merge b0a7fb36c7 into b7dd45c0e4 2024-03-21 22:34:24 +09:00
Hajime Hoshi
b7dd45c0e4 internal/gamepad: ignore the very first MotionEvent with 0 value for Android
On Android, MotionEvent with 0 values might come for axes when connecting
a gamepad, even though a user didn't touch any axes. This is problematic
especially for tirgger axes, where the default value should be -1.

This change fixes the issue by adding a new state `axesReady` to check
if an axis is really touched or not. If an axis is not touched yet,
a button value for a standard (trigger) button always returns 0.

This change also removes an old hack to initialize axis values for
triggers.

Closes #2598
2024-03-21 22:28:48 +09:00
Hajime Hoshi
4b1c0526a7 exp/textinput: add Field
Closes #2827
2024-03-20 23:19:32 +09:00
Hajime Hoshi
cd90f083bc text/v2: rename StdFace to GoXFace
Closes #2925
2024-03-20 02:42:31 +09:00
Hajime Hoshi
d15b12b4e5 all: update gomobile 2024-03-19 00:17:21 +09:00
Hajime Hoshi
f79f6dc55f all: update go-text 2024-03-18 12:01:08 +09:00
Hajime Hoshi
6bbfec1869 audio: refactoring: initialize the context at an update hook
Closes #2715
2024-03-16 22:42:04 +09:00
Hajime Hoshi
4a212181e7 examples/audio: show milliseconds
Updates #2901
2024-03-16 22:42:01 +09:00
Hajime Hoshi
9cd525a04e audio: bug fix: position adjustment should not start before ready
Updates #2901
2024-03-16 22:03:36 +09:00
Hajime Hoshi
9cc017412f audio: refactoring 2024-03-16 21:44:39 +09:00
Hajime Hoshi
9faa3f4601 internal/gamepaddb: refactoring 2024-03-16 17:57:52 +09:00
Hajime Hoshi
696938987d internal/gamepad: use locks for consistency
Perhaps Gamepad's m might not be needed, but let's use the lock for
consistency for the current situation.
2024-03-16 16:31:49 +09:00
Hajime Hoshi
209dc50f72 internal/gamepaddb: refactoring 2024-03-16 15:37:42 +09:00
Hajime Hoshi
047858aa59 internal/gamepaddb: rename functions 2024-03-16 15:16:29 +09:00
Hajime Hoshi
6cdabf09d1 ebiten: guarantee invalid color values are not clamped
Closes #2798
2024-03-13 12:03:44 +09:00
Hajime Hoshi
bb6430d3ba internal/shader: bug fix: unexpected crash for out of range
Closes #2926
2024-03-13 11:37:00 +09:00
Hajime Hoshi
7389f9ddb2 ebiten: add KeyIntlBackslash
Updates #2921
2024-03-12 12:49:02 +09:00
Hajime Hoshi
4c7ed56077 text/v2: add a comment 2024-03-11 23:24:38 +09:00
Hajime Hoshi
63e97c7064 internal/shader: bug fix: needed to resolve const and non-const types
Closes #2922
2024-03-10 19:49:19 +09:00
Hajime Hoshi
c9a973c6c1 internal/ui: bug fix: needed to focus the window at launch
Updates #2725
Closes #2924
2024-03-10 12:44:28 +09:00
Hajime Hoshi
9a7dcb1077 internal/shader: bug fix: failed to return an array in HLSL
Closes #2923
2024-03-10 11:59:50 +09:00
Hajime Hoshi
927e025982 internal/shader: bug fix: wrong type conversion for min, max, and clamp
Closes #2922
2024-03-10 11:30:06 +09:00
Hajime Hoshi
dc05f2014f exp/textinput: implement for Windows
Closes #2735
2024-03-09 23:05:19 +09:00
Hajime Hoshi
3eaa03e193 all: update dependencies 2024-03-07 10:03:44 +09:00
Hajime Hoshi
34cdb20276 all: update PureGo to v0.7.0-alpha.3 2024-03-04 00:02:55 +09:00
Hajime Hoshi
c0d9954b3e exp/textinput: use native pixels for a candidate window position 2024-03-03 23:35:34 +09:00
Hajime Hoshi
3e4c47eb70 internal/ui: refactoring 2024-03-03 23:27:02 +09:00
Hajime Hoshi
4d72f97e45 exp/textinput: add State.Error 2024-03-03 20:53:58 +09:00
Hajime Hoshi
0fa39182cb exp/textinput: refactoring 2024-03-02 17:17:27 +09:00
Hajime Hoshi
0a20670f3f all: upadte PureGo
Updates ebitengine/purego#217
2024-03-01 03:00:52 +09:00
Hajime Hoshi
c5ebf8670b Revert "all: update PureGo"
This reverts commit 200c6569c3.

Reason: this caused crashes on macOS
2024-03-01 01:19:13 +09:00
Hajime Hoshi
200c6569c3 all: update PureGo 2024-02-28 22:18:11 +09:00
63 changed files with 1520 additions and 495 deletions

View File

@ -60,15 +60,9 @@ const (
type Context struct {
playerFactory *playerFactory
// inited represents whether the audio device is initialized and available or not.
// On Android, audio loop cannot be started unless JVM is accessible. After updating one frame, JVM should exist.
inited chan struct{}
initedOnce sync.Once
sampleRate int
err error
ready bool
readyOnce sync.Once
playingPlayers map[*playerImpl]struct{}
@ -100,7 +94,6 @@ func NewContext(sampleRate int) *Context {
sampleRate: sampleRate,
playerFactory: newPlayerFactory(sampleRate),
playingPlayers: map[*playerImpl]struct{}{},
inited: make(chan struct{}),
semaphore: make(chan struct{}, 1),
}
theContext = c
@ -128,10 +121,6 @@ func NewContext(sampleRate int) *Context {
})
h.AppendHookOnBeforeUpdate(func() error {
c.initedOnce.Do(func() {
close(c.inited)
})
var err error
theContextLock.Lock()
if theContext != nil {
@ -142,6 +131,19 @@ func NewContext(sampleRate int) *Context {
return err
}
// Initialize the context here in the case when there is no player and
// the program waits for IsReady() to be true (#969, #970, #2715).
ready, err := c.playerFactory.initContextIfNeeded()
if err != nil {
return err
}
if ready != nil {
go func() {
<-ready
c.setReady()
}()
}
if err := c.updatePlayers(); err != nil {
return err
}
@ -287,28 +289,7 @@ func (c *Context) updatePlayers() error {
func (c *Context) IsReady() bool {
c.m.Lock()
defer c.m.Unlock()
if c.ready {
return true
}
if len(c.playingPlayers) != 0 {
return false
}
c.readyOnce.Do(func() {
// Create another goroutine since (*Player).Play can lock the context's mutex.
// TODO: Is this needed for reader players?
go func() {
// The audio context is never ready unless there is a player. This is
// problematic when a user tries to play audio after the context is ready.
// Play a dummy player to avoid the blocking (#969).
// Use a long enough buffer so that writing doesn't finish immediately (#970).
p := NewPlayerFromBytes(c, make([]byte, 16384))
p.Play()
}()
})
return false
return c.ready
}
// SampleRate returns the sample rate.
@ -316,18 +297,6 @@ func (c *Context) SampleRate() int {
return c.sampleRate
}
func (c *Context) acquireSemaphore() {
c.semaphore <- struct{}{}
}
func (c *Context) releaseSemaphore() {
<-c.semaphore
}
func (c *Context) waitUntilInited() {
<-c.inited
}
// Player is an audio player which has one stream.
//
// Even when all references to a Player object is gone,

View File

@ -30,20 +30,12 @@ func newContext(sampleRate int) (context, chan struct{}, error) {
return &contextProxy{ctx}, ready, err
}
// otoContext is an interface for *oto.Context.
type otoContext interface {
NewPlayer(io.Reader) *oto.Player
Suspend() error
Resume() error
Err() error
}
// contextProxy is a proxy between otoContext and context.
type contextProxy struct {
otoContext
*oto.Context
}
// NewPlayer implements context.
func (c *contextProxy) NewPlayer(r io.Reader) player {
return c.otoContext.NewPlayer(r)
return c.Context.NewPlayer(r)
}

View File

@ -356,6 +356,10 @@ func (p *playerImpl) updatePosition() {
p.adjustedPosition = 0
return
}
if !p.context.IsReady() {
p.adjustedPosition = 0
return
}
samples := (p.stream.position() - int64(p.player.BufferedSize())) / bytesPerSampleInt16

View File

@ -244,12 +244,6 @@ public class EbitenView extends ViewGroup implements InputManager.InputDeviceLis
int axisMask = getAxisMask(inputDevice);
Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), gamepad.axes.size(), gamepad.hats.size()/2, descriptor, vendorId, productId, buttonMask, axisMask);
// Initialize the trigger axes values explicitly, or the initial button values would be 0.5 instead of 0.
if (gamepad.axes.size() >= 6) {
Ebitenmobileview.onGamepadAxisChanged(deviceId, 4, -1);
Ebitenmobileview.onGamepadAxisChanged(deviceId, 5, -1);
}
}
// The implementation is copied from SDL:

View File

@ -347,7 +347,8 @@ func (p *Player) draw(screen *ebiten.Image) {
// Compose the current time text.
m := (c / time.Minute) % 100
s := (c / time.Second) % 60
currentTimeStr := fmt.Sprintf("%02d:%02d", m, s)
ms := (c / time.Millisecond) % 1000
currentTimeStr := fmt.Sprintf("%02d:%02d.%03d", m, s, ms)
// Draw buttons
op := &ebiten.DrawImageOptions{}

View File

@ -35,7 +35,7 @@ const (
)
var (
inconsolataFace = text.NewStdFace(inconsolata.Bold8x16)
inconsolataFace = text.NewGoXFace(inconsolata.Bold8x16)
)
// mode is a blend mode with description.

View File

@ -35,7 +35,7 @@ const (
screenHeight = 240
)
var fontFace = text.NewStdFace(bitmapfont.Face)
var fontFace = text.NewGoXFace(bitmapfont.Face)
var keyboardImage *ebiten.Image

View File

@ -32,7 +32,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/vector"
)
var fontFace = text.NewStdFace(bitmapfont.FaceEA)
var fontFace = text.NewGoXFace(bitmapfont.FaceEA)
const (
screenWidth = 640
@ -40,16 +40,9 @@ const (
)
type TextField struct {
bounds image.Rectangle
multilines bool
text string
selectionStart int
selectionEnd int
focused bool
ch chan textinput.State
end func()
state textinput.State
bounds image.Rectangle
multilines bool
field textinput.Field
}
func NewTextField(bounds image.Rectangle, multilines bool) *TextField {
@ -64,13 +57,11 @@ func (t *TextField) Contains(x, y int) bool {
}
func (t *TextField) SetSelectionStartByCursorPosition(x, y int) bool {
t.cleanUp()
idx, ok := t.textIndexByCursorPosition(x, y)
if !ok {
return false
}
t.selectionStart = idx
t.selectionEnd = idx
t.field.SetSelection(idx, idx)
return true
}
@ -95,20 +86,21 @@ func (t *TextField) textIndexByCursorPosition(x, y int) (int, bool) {
var nlCount int
var lineStart int
var prevAdvance float64
for i, r := range t.text {
txt := t.field.Text()
for i, r := range txt {
var x0, x1 int
currentAdvance := text.Advance(t.text[lineStart:i], fontFace)
currentAdvance := text.Advance(txt[lineStart:i], fontFace)
if lineStart < i {
x0 = int((prevAdvance + currentAdvance) / 2)
}
if r == '\n' {
x1 = int(math.MaxInt32)
} else if i < len(t.text) {
} else if i < len(txt) {
nextI := i + 1
for !utf8.ValidString(t.text[i:nextI]) {
for !utf8.ValidString(txt[i:nextI]) {
nextI++
}
nextAdvance := text.Advance(t.text[lineStart:nextI], fontFace)
nextAdvance := text.Advance(txt[lineStart:nextI], fontFace)
x1 = int((currentAdvance + nextAdvance) / 2)
} else {
x1 = int(currentAdvance)
@ -125,155 +117,109 @@ func (t *TextField) textIndexByCursorPosition(x, y int) (int, bool) {
}
}
return len(t.text), true
return len(txt), true
}
func (t *TextField) Focus() {
t.focused = true
t.field.Focus()
}
func (t *TextField) Blur() {
t.focused = false
t.field.Blur()
}
func (t *TextField) cleanUp() {
if t.ch != nil {
select {
case state, ok := <-t.ch:
if ok && state.Committed {
t.text = t.text[:t.selectionStart] + state.Text + t.text[t.selectionEnd:]
t.selectionStart += len(state.Text)
t.selectionEnd = t.selectionStart
t.state = textinput.State{}
}
t.state = state
default:
break
}
}
if t.end != nil {
t.end()
t.ch = nil
t.end = nil
t.state = textinput.State{}
}
}
func (t *TextField) Update() {
if !t.focused {
// If the text field still has a session, read the last state and process it just in case.
t.cleanUp()
return
func (t *TextField) Update() error {
if !t.field.IsFocused() {
return nil
}
var processed bool
// Text inputting can happen multiple times in one tick (1/60[s] by default).
// Handle all of them.
for {
if t.ch == nil {
x, y := t.bounds.Min.X, t.bounds.Min.Y
cx, cy := t.cursorPos()
px, py := textFieldPadding()
x += cx + px
y += cy + py + int(fontFace.Metrics().HAscent)
t.ch, t.end = textinput.Start(x, y)
// Start returns nil for non-supported envrionments.
if t.ch == nil {
return
}
}
readchar:
for {
select {
case state, ok := <-t.ch:
processed = true
if !ok {
t.ch = nil
t.end = nil
t.state = textinput.State{}
break readchar
}
if state.Committed {
t.text = t.text[:t.selectionStart] + state.Text + t.text[t.selectionEnd:]
t.selectionStart += len(state.Text)
t.selectionEnd = t.selectionStart
t.state = textinput.State{}
continue
}
t.state = state
default:
break readchar
}
}
if t.ch == nil {
continue
}
break
x, y := t.bounds.Min.X, t.bounds.Min.Y
cx, cy := t.cursorPos()
px, py := textFieldPadding()
x += cx + px
y += cy + py + int(fontFace.Metrics().HAscent)
handled, err := t.field.HandleInput(x, y)
if err != nil {
return err
}
if processed {
return
if handled {
return nil
}
switch {
case inpututil.IsKeyJustPressed(ebiten.KeyEnter):
if t.multilines {
t.text = t.text[:t.selectionStart] + "\n" + t.text[t.selectionEnd:]
t.selectionStart += 1
t.selectionEnd = t.selectionStart
text := t.field.Text()
selectionStart, selectionEnd := t.field.Selection()
text = text[:selectionStart] + "\n" + text[selectionEnd:]
selectionStart += len("\n")
selectionEnd = selectionStart
t.field.SetTextAndSelection(text, selectionStart, selectionEnd)
}
case inpututil.IsKeyJustPressed(ebiten.KeyBackspace):
if t.selectionStart > 0 {
text := t.field.Text()
selectionStart, selectionEnd := t.field.Selection()
if selectionStart != selectionEnd {
text = text[:selectionStart] + text[selectionEnd:]
} else if selectionStart > 0 {
// TODO: Remove a grapheme instead of a code point.
_, l := utf8.DecodeLastRuneInString(t.text[:t.selectionStart])
t.text = t.text[:t.selectionStart-l] + t.text[t.selectionEnd:]
t.selectionStart -= l
_, l := utf8.DecodeLastRuneInString(text[:selectionStart])
text = text[:selectionStart-l] + text[selectionEnd:]
selectionStart -= l
}
t.selectionEnd = t.selectionStart
selectionEnd = selectionStart
t.field.SetTextAndSelection(text, selectionStart, selectionEnd)
case inpututil.IsKeyJustPressed(ebiten.KeyLeft):
if t.selectionStart > 0 {
text := t.field.Text()
selectionStart, _ := t.field.Selection()
if selectionStart > 0 {
// TODO: Remove a grapheme instead of a code point.
_, l := utf8.DecodeLastRuneInString(t.text[:t.selectionStart])
t.selectionStart -= l
_, l := utf8.DecodeLastRuneInString(text[:selectionStart])
selectionStart -= l
}
t.selectionEnd = t.selectionStart
t.field.SetTextAndSelection(text, selectionStart, selectionStart)
case inpututil.IsKeyJustPressed(ebiten.KeyRight):
if t.selectionEnd < len(t.text) {
text := t.field.Text()
_, selectionEnd := t.field.Selection()
if selectionEnd < len(text) {
// TODO: Remove a grapheme instead of a code point.
_, l := utf8.DecodeRuneInString(t.text[t.selectionEnd:])
t.selectionEnd += l
_, l := utf8.DecodeRuneInString(text[selectionEnd:])
selectionEnd += l
}
t.selectionStart = t.selectionEnd
t.field.SetTextAndSelection(text, selectionEnd, selectionEnd)
}
if !t.multilines {
orig := t.text
orig := t.field.Text()
new := strings.ReplaceAll(orig, "\n", "")
if new != orig {
t.selectionStart -= strings.Count(orig[:t.selectionStart], "\n")
t.selectionEnd -= strings.Count(orig[:t.selectionEnd], "\n")
selectionStart, selectionEnd := t.field.Selection()
selectionStart -= strings.Count(orig[:selectionStart], "\n")
selectionEnd -= strings.Count(orig[:selectionEnd], "\n")
t.field.SetSelection(selectionStart, selectionEnd)
}
}
return nil
}
func (t *TextField) cursorPos() (int, int) {
var nlCount int
lastNLPos := -1
for i, r := range t.text[:t.selectionStart] {
txt := t.field.TextForRendering()
selectionStart, _ := t.field.Selection()
if s, _, ok := t.field.CompositionSelection(); ok {
selectionStart += s
}
txt = txt[:selectionStart]
for i, r := range txt {
if r == '\n' {
nlCount++
lastNLPos = i
}
}
txt := t.text[lastNLPos+1 : t.selectionStart]
if t.state.Text != "" {
txt += t.state.Text[:t.state.CompositionSelectionStartInBytes]
}
txt = txt[lastNLPos+1:]
x := int(text.Advance(txt, fontFace))
y := nlCount * int(fontFace.Metrics().HLineGap+fontFace.Metrics().HAscent+fontFace.Metrics().HDescent)
return x, y
@ -282,13 +228,14 @@ func (t *TextField) cursorPos() (int, int) {
func (t *TextField) Draw(screen *ebiten.Image) {
vector.DrawFilledRect(screen, float32(t.bounds.Min.X), float32(t.bounds.Min.Y), float32(t.bounds.Dx()), float32(t.bounds.Dy()), color.White, false)
var clr color.Color = color.Black
if t.focused {
if t.field.IsFocused() {
clr = color.RGBA{0, 0, 0xff, 0xff}
}
vector.StrokeRect(screen, float32(t.bounds.Min.X), float32(t.bounds.Min.Y), float32(t.bounds.Dx()), float32(t.bounds.Dy()), 1, clr, false)
px, py := textFieldPadding()
if t.focused && t.selectionStart >= 0 {
selectionStart, _ := t.field.Selection()
if t.field.IsFocused() && selectionStart >= 0 {
x, y := t.bounds.Min.X, t.bounds.Min.Y
cx, cy := t.cursorPos()
x += px + cx
@ -297,18 +244,13 @@ func (t *TextField) Draw(screen *ebiten.Image) {
vector.StrokeLine(screen, float32(x), float32(y), float32(x), float32(y+h), 1, color.Black, false)
}
shownText := t.text
if t.focused && t.state.Text != "" {
shownText = t.text[:t.selectionStart] + t.state.Text + t.text[t.selectionEnd:]
}
tx := t.bounds.Min.X + px
ty := t.bounds.Min.Y + py
op := &text.DrawOptions{}
op.GeoM.Translate(float64(tx), float64(ty))
op.ColorScale.ScaleWithColor(color.Black)
op.LineSpacing = fontFace.Metrics().HLineGap + fontFace.Metrics().HAscent + fontFace.Metrics().HDescent
text.Draw(screen, shownText, fontFace, op)
text.Draw(screen, t.field.TextForRendering(), fontFace, op)
}
const textFieldHeight = 24
@ -348,7 +290,9 @@ func (g *Game) Update() error {
}
for _, tf := range g.textFields {
tf.Update()
if err := tf.Update(); err != nil {
return err
}
}
x, y := ebiten.CursorPosition()

View 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
}

246
exp/textinput/field.go Normal file
View File

@ -0,0 +1,246 @@
// 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 (
"sync"
)
var (
theFocusedField *Field
theFocusedFieldM sync.Mutex
)
func focusField(f *Field) {
var origField *Field
defer func() {
if origField != nil {
origField.cleanUp()
}
}()
theFocusedFieldM.Lock()
defer theFocusedFieldM.Unlock()
if theFocusedField == f {
return
}
origField = theFocusedField
theFocusedField = f
}
func blurField(f *Field) {
var origField *Field
defer func() {
if origField != nil {
origField.cleanUp()
}
}()
theFocusedFieldM.Lock()
defer theFocusedFieldM.Unlock()
if theFocusedField != f {
return
}
origField = theFocusedField
theFocusedField = nil
}
func isFieldFocused(f *Field) bool {
theFocusedFieldM.Lock()
defer theFocusedFieldM.Unlock()
return theFocusedField == f
}
// Field is a region accepting text inputting with IME.
//
// Field is not focused by default. You have to call Focus when you start text inputting.
//
// Field is a wrapper of the low-level API like Start.
//
// For an actual usage, see the examples "textinput".
type Field struct {
text string
selectionStart int
selectionEnd int
ch chan State
end func()
state State
err error
}
// HandleInput updates the field state.
// HandleInput must be called every tick, i.e., every HandleInput, when Field is focused.
// HandleInput takes a position where an IME window is shown if needed.
//
// HandleInput returns whether the text inputting is handled or not.
// If HandleInput returns true, a Field user should not handle further input events.
//
// HandleInput returns an error when handling input causes an error.
func (f *Field) HandleInput(x, y int) (handled bool, err error) {
if f.err != nil {
return false, f.err
}
if !f.IsFocused() {
return false, nil
}
// Text inputting can happen multiple times in one tick (1/60[s] by default).
// Handle all of them.
for {
if f.ch == nil {
// TODO: On iOS Safari, Start doesn't work as expected (#2898).
// Handle a click event and focus the textarea there.
f.ch, f.end = Start(x, y)
// Start returns nil for non-supported envrionments.
if f.ch == nil {
return true, nil
}
}
readchar:
for {
select {
case state, ok := <-f.ch:
if state.Error != nil {
f.err = state.Error
return false, f.err
}
handled = true
if !ok {
f.ch = nil
f.end = nil
f.state = State{}
break readchar
}
if state.Committed {
f.text = f.text[:f.selectionStart] + state.Text + f.text[f.selectionEnd:]
f.selectionStart += len(state.Text)
f.selectionEnd = f.selectionStart
f.state = State{}
continue
}
f.state = state
default:
break readchar
}
}
if f.ch == nil {
continue
}
break
}
return
}
// Focus focuses the field.
// A Field has to be focused to start text inputting.
//
// There can be only one Field that is focused at the same time.
// When Focus is called and there is already a focused field, Focus removes the focus of that.
func (f *Field) Focus() {
focusField(f)
}
// Blur removes the focus from the field.
func (f *Field) Blur() {
blurField(f)
}
// IsFocused reports whether the field is focused or not.
func (f *Field) IsFocused() bool {
return isFieldFocused(f)
}
func (f *Field) cleanUp() {
if f.err != nil {
return
}
// If the text field still has a session, read the last state and process it just in case.
if f.ch != nil {
select {
case state, ok := <-f.ch:
if state.Error != nil {
f.err = state.Error
return
}
if ok && state.Committed {
f.text = f.text[:f.selectionStart] + state.Text + f.text[f.selectionEnd:]
f.selectionStart += len(state.Text)
f.selectionEnd = f.selectionStart
f.state = State{}
}
f.state = state
default:
break
}
}
if f.end != nil {
f.end()
f.ch = nil
f.end = nil
f.state = State{}
}
}
// Selection returns the current selection range in bytes.
func (f *Field) Selection() (start, end int) {
return f.selectionStart, f.selectionEnd
}
// CompositionSelection returns the current composition selection in bytes if a text is composited.
// If a text is not composited, this returns 0s and false.
// The returned values indicate relative positions in bytes where the current composition text's start is 0.
func (f *Field) CompositionSelection() (start, end int, ok bool) {
if f.IsFocused() && f.state.Text != "" {
return f.state.CompositionSelectionStartInBytes, f.state.CompositionSelectionEndInBytes, true
}
return 0, 0, false
}
// SetSelection sets the selection range.
func (f *Field) SetSelection(start, end int) {
f.cleanUp()
f.selectionStart = start
f.selectionEnd = end
}
// Text returns the current text.
// The returned value doesn't include compositing texts.
func (f *Field) Text() string {
return f.text
}
// TextForRendering returns the text for rendering.
// The returned value includes compositing texts.
func (f *Field) TextForRendering() string {
if f.IsFocused() && f.state.Text != "" {
return f.text[:f.selectionStart] + f.state.Text + f.text[f.selectionEnd:]
}
return f.text
}
// SetTextAndSelection sets the text and the selection range.
func (f *Field) SetTextAndSelection(text string, selectionStart, selectionEnd int) {
f.cleanUp()
f.text = text
f.selectionStart = selectionStart
f.selectionEnd = selectionEnd
}

View File

@ -25,6 +25,8 @@ import (
)
// State represents the current state of text inputting.
//
// State is the low-level API. For most use cases, Field is easier to use.
type State struct {
// Text represents the current inputting text.
Text string
@ -37,14 +39,19 @@ type State struct {
// Committed reports whether the current Text is the settled text.
Committed bool
// Error is an error that happens during text inputting.
Error error
}
// Start starts text inputting.
// Start returns a channel to send the state repeatedly, and a function to end the text inputting.
//
// Start is the low-leve API. For most use cases, Field is easier to use.
//
// Start returns nil and nil if the current environment doesn't support this package.
func Start(x, y int) (states chan State, close func()) {
cx, cy := ui.Get().LogicalPositionToClientPosition(float64(x), float64(y))
cx, cy := ui.Get().LogicalPositionToClientPositionInNativePixels(float64(x), float64(y))
return theTextInput.Start(int(cx), int(cy))
}

View File

@ -59,10 +59,7 @@ var theTextInput textInput
func (t *textInput) Start(x, y int) (chan State, func()) {
var session *session
ui.Get().RunOnMainThread(func() {
if t.session != nil {
t.session.end()
t.session = nil
}
t.end()
C.start(C.int(x), C.int(y))
session = newSession()
t.session = session

View File

@ -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

View 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
}

View File

@ -140,9 +140,17 @@ func init() {
"NumpadSubtract": "KPSubtract",
"NumpadEnter": "KPEnter",
"NumpadEqual": "KPEqual",
"IntlBackslash": "World1",
}
// https://developer.android.com/reference/android/view/KeyEvent
//
// Android doesn't distinguish these keys:
// - a US backslash key (HID: 0x31),
// - an international pound/tilde key (HID: 0x32), and
// - an international backslash key (HID: 0x64).
// These are mapped to the same key code KEYCODE_BACKSLASH (73).
// See https://source.android.com/docs/core/interaction/input/keyboard-devices
androidKeyToUIKeyName = map[int]string{
55: "Comma",
56: "Period",
@ -204,16 +212,16 @@ func init() {
0x35: "Backquote",
// These three keys are:
// - US backslash-pipe key (above return),
// - non-US backslash key (next to left shift; on German layout this is the <>| key), and
// - US backslash-pipe key, and
// - non-US hashmark key (bottom left of return; on German layout, this is the #' key).
// On US layout configurations, they all map to the same characters - the backslash.
//
// See also: https://www.w3.org/TR/uievents-code/#keyboard-102
0x31: "Backslash", // UIKeyboardHIDUsageKeyboardBackslash
0x64: "Backslash", // UIKeyboardHIDUsageKeyboardNonUSBackslash
0x32: "Backslash", // UIKeyboardHIDUsageKeyboardNonUSPound
0x64: "IntlBackslash", // UIKeyboardHIDUsageKeyboardNonUSBackslash
0x2A: "Backspace",
0x2F: "BracketLeft",
0x30: "BracketRight",
@ -326,6 +334,7 @@ func init() {
"NumpadEqual": "NumpadEqual",
"MetaLeft": "MetaLeft",
"MetaRight": "MetaRight",
"IntlBackslash": "IntlBackslash",
}
const (

8
go.mod
View File

@ -3,11 +3,11 @@ module github.com/hajimehoshi/ebiten/v2
go 1.18
require (
github.com/ebitengine/gomobile v0.0.0-20240223151600-9f1d75a9f41c
github.com/ebitengine/gomobile v0.0.0-20240318151619-0eadfb33c201
github.com/ebitengine/hideconsole v1.0.0
github.com/ebitengine/oto/v3 v3.2.0-alpha.4
github.com/ebitengine/purego v0.7.0-alpha.1
github.com/go-text/typesetting v0.1.1-0.20231231232151-8d81c02dc157
github.com/ebitengine/purego v0.7.0-alpha.3
github.com/go-text/typesetting v0.1.1-0.20240317203452-bc341f663203
github.com/hajimehoshi/bitmapfont/v3 v3.0.0
github.com/hajimehoshi/go-mp3 v0.3.4
github.com/jakecoffman/cp v1.2.1
@ -16,7 +16,7 @@ require (
github.com/kisielk/errcheck v1.6.3
golang.org/x/image v0.15.0
golang.org/x/sync v0.6.0
golang.org/x/sys v0.17.0
golang.org/x/sys v0.18.0
golang.org/x/text v0.14.0
golang.org/x/tools v0.18.0
)

18
go.sum
View File

@ -1,14 +1,14 @@
github.com/ebitengine/gomobile v0.0.0-20240223151600-9f1d75a9f41c h1:fIrdax248gvTVn/QL+U0taS0Fs9SVKKSpXxiibMs6p4=
github.com/ebitengine/gomobile v0.0.0-20240223151600-9f1d75a9f41c/go.mod h1:8SdR2+sMMmYsei+c0ZW+AmJx2W6dPeSixWWSKRkJppA=
github.com/ebitengine/gomobile v0.0.0-20240318151619-0eadfb33c201 h1:QlcxUcnQjv62kMxsh0NAQpwE0P454ec2i82DxmthMeI=
github.com/ebitengine/gomobile v0.0.0-20240318151619-0eadfb33c201/go.mod h1:8SdR2+sMMmYsei+c0ZW+AmJx2W6dPeSixWWSKRkJppA=
github.com/ebitengine/hideconsole v1.0.0 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A=
github.com/ebitengine/oto/v3 v3.2.0-alpha.4 h1:aaUdcbEDUV1oErHDv/Cd0IAjQaQPChZuvO8Cn/kQHE8=
github.com/ebitengine/oto/v3 v3.2.0-alpha.4/go.mod h1:JtMbxJHZBDXfS8BmVYwzWk9Z6r7jsjwsHzOuZrEkfs4=
github.com/ebitengine/purego v0.7.0-alpha.1 h1:Dlm9jM2kuzVQS89S1AALkDZQquow/Bbn3KVG68tFtWU=
github.com/ebitengine/purego v0.7.0-alpha.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/go-text/typesetting v0.1.1-0.20231231232151-8d81c02dc157 h1:gNNO+Nvt8Kkg+VWN7qKvV02f+0pxH9zdKDMTixfn17Q=
github.com/go-text/typesetting v0.1.1-0.20231231232151-8d81c02dc157/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI=
github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY=
github.com/ebitengine/purego v0.7.0-alpha.3 h1:9hH1aneqLaM3sM+PMUgRJVsMe2SqfVjZtV3DEzxBDJU=
github.com/ebitengine/purego v0.7.0-alpha.3/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
github.com/go-text/typesetting v0.1.1-0.20240317203452-bc341f663203 h1:dDnsjfTamLS74H+chc/GvLDFboY9BlK0Ztg5ibOzZ34=
github.com/go-text/typesetting v0.1.1-0.20240317203452-bc341f663203/go.mod h1:2+owI/sxa73XA581LAzVuEBZ3WEEV2pXeDswCH/3i1I=
github.com/go-text/typesetting-utils v0.0.0-20240317173224-1986cbe96c66 h1:GUrm65PQPlhFSKjLPGOZNPNxLCybjzjYBzjfoBGaDUY=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68=
@ -54,8 +54,8 @@ golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220712014510-0a85c31ab51e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=

View File

@ -587,6 +587,9 @@ var _ [len(DrawTrianglesShaderOptions{}.Images) - graphics.ShaderImageCount]stru
//
// If a specified uniform variable's length or type doesn't match with an expected one, DrawTrianglesShader panics.
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// When the image i is disposed, DrawTrianglesShader does nothing.
func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader *Shader, options *DrawTrianglesShaderOptions) {
i.copyCheck()
@ -742,6 +745,9 @@ var _ [len(DrawRectShaderOptions{}.Images)]struct{} = [graphics.ShaderImageCount
// If no source images are specified, imageSrc0Size returns a valid size only when the unit is pixels,
// but always returns 0 when the unit is texels (default).
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// When the image i is disposed, DrawRectShader does nothing.
func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawRectShaderOptions) {
i.copyCheck()
@ -954,6 +960,9 @@ func (i *Image) at(x, y int) (r, g, b, a byte) {
//
// Set implements the standard draw.Image's Set.
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// If the image is disposed, Set does nothing.
func (i *Image) Set(x, y int, clr color.Color) {
i.copyCheck()
@ -1028,6 +1037,9 @@ func (i *Image) Deallocate() {
//
// WritePixels also works on a sub-image.
//
// Even if a result is an invalid color as a premultiplied-alpha color, i.e. an alpha value exceeds other color values,
// the value is kept and is not clamped.
//
// When the image is disposed, WritePixels does nothing.
func (i *Image) WritePixels(pixels []byte) {
i.copyCheck()

View File

@ -4582,3 +4582,44 @@ func TestImageDrawImageAfterDeallocation(t *testing.T) {
}
}
}
// Issue #2798
func TestImageInvalidPremultipliedAlphaColor(t *testing.T) {
// This test checks the rendering result when Set and WritePixels use an invalid premultiplied alpha color.
// The result values are kept and not clamped.
const (
w = 16
h = 16
)
dst := ebiten.NewImage(w, h)
dst.Set(0, 0, color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x40})
dst.Set(0, 1, color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x00})
if got, want := dst.At(0, 0).(color.RGBA), (color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x40}); got != want {
t.Errorf("got: %v, want: %v", got, want)
}
if got, want := dst.At(0, 1).(color.RGBA), (color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x00}); got != want {
t.Errorf("got: %v, want: %v", got, want)
}
pix := make([]byte, 4*w*h)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
pix[4*(j*16+i)] = byte(i)
pix[4*(j*16+i)+1] = byte(j)
pix[4*(j*16+i)+2] = 0x80
pix[4*(j*16+i)+3] = byte(i - j)
}
}
dst.WritePixels(pix)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
got := dst.At(i, j)
want := color.RGBA{R: byte(i), G: byte(j), B: 0x80, A: byte(i - j)}
if got != want {
t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}

View File

@ -45,6 +45,7 @@ func (g *gamepads) addAndroidGamepad(androidDeviceID int, name, sdlID string, ax
gp := g.add(name, sdlID)
gp.native = &nativeGamepadImpl{
androidDeviceID: androidDeviceID,
axesReady: make([]bool, axisCount),
axes: make([]float64, axisCount),
buttons: make([]bool, gamepaddb.SDLControllerButtonMax+1),
hats: make([]int, hatCount),
@ -108,6 +109,13 @@ func (g *Gamepad) updateAndroidGamepadAxis(axis int, value float64) {
return
}
n.axes[axis] = value
// MotionEvent with 0 value can be sent when a gamepad is connected even though an axis is not touched (#2598).
// This is problematic when an axis is a trigger button where -1 should be the default value.
// When MotionEvent with non-0 value is sent, it seems fine to assume that the axis is actually touched and ready.
if value != 0 {
n.axesReady[axis] = true
}
}
func (g *Gamepad) updateAndroidGamepadButton(button Button, pressed bool) {

View File

@ -243,6 +243,7 @@ type nativeGamepad interface {
axisCount() int
buttonCount() int
hatCount() int
isAxisReady(axis int) bool
axisValue(axis int) float64
buttonValue(button int) float64
isButtonPressed(button int) bool
@ -296,6 +297,14 @@ func (g *Gamepad) HatCount() int {
return g.native.hatCount()
}
// IsAxisReady is concurrent-safe.
func (g *Gamepad) IsAxisReady(axis int) bool {
g.m.Lock()
defer g.m.Unlock()
return g.native.isAxisReady(axis)
}
// Axis is concurrent-safe.
func (g *Gamepad) Axis(axis int) float64 {
g.m.Lock()
@ -356,8 +365,13 @@ func (g *Gamepad) IsStandardButtonAvailable(button gamepaddb.StandardButton) boo
// StandardAxisValue is concurrent-safe.
func (g *Gamepad) StandardAxisValue(axis gamepaddb.StandardAxis) float64 {
if gamepaddb.HasStandardLayoutMapping(g.sdlID) {
return gamepaddb.AxisValue(g.sdlID, axis, g)
// StandardAxisValue invokes g.Axis, g.Button, or g.Hat so this cannot be locked.
return gamepaddb.StandardAxisValue(g.sdlID, axis, g)
}
g.m.Lock()
defer g.m.Unlock()
if m := g.native.standardAxisInOwnMapping(axis); m != nil {
return m.Value()*2 - 1
}
@ -367,8 +381,13 @@ func (g *Gamepad) StandardAxisValue(axis gamepaddb.StandardAxis) float64 {
// StandardButtonValue is concurrent-safe.
func (g *Gamepad) StandardButtonValue(button gamepaddb.StandardButton) float64 {
if gamepaddb.HasStandardLayoutMapping(g.sdlID) {
return gamepaddb.ButtonValue(g.sdlID, button, g)
// StandardButtonValue invokes g.Axis, g.Button, or g.Hat so this cannot be locked.
return gamepaddb.StandardButtonValue(g.sdlID, button, g)
}
g.m.Lock()
defer g.m.Unlock()
if m := g.native.standardButtonInOwnMapping(button); m != nil {
return m.Value()
}
@ -378,8 +397,13 @@ func (g *Gamepad) StandardButtonValue(button gamepaddb.StandardButton) float64 {
// IsStandardButtonPressed is concurrent-safe.
func (g *Gamepad) IsStandardButtonPressed(button gamepaddb.StandardButton) bool {
if gamepaddb.HasStandardLayoutMapping(g.sdlID) {
return gamepaddb.IsButtonPressed(g.sdlID, button, g)
// IsStandardButtonPressed invokes g.Axis, g.Button, or g.Hat so this cannot be locked.
return gamepaddb.IsStandardButtonPressed(g.sdlID, button, g)
}
g.m.Lock()
defer g.m.Unlock()
if m := g.native.standardButtonInOwnMapping(button); m != nil {
return m.Pressed()
}

View File

@ -37,9 +37,10 @@ func (*nativeGamepadsImpl) update(gamepads *gamepads) error {
type nativeGamepadImpl struct {
androidDeviceID int
axes []float64
buttons []bool
hats []int
axesReady []bool
axes []float64
buttons []bool
hats []int
}
func (*nativeGamepadImpl) update(gamepad *gamepads) error {
@ -71,6 +72,13 @@ func (g *nativeGamepadImpl) hatCount() int {
return len(g.hats)
}
func (g *nativeGamepadImpl) isAxisReady(axis int) bool {
if axis < 0 || axis >= len(g.axesReady) {
return false
}
return g.axesReady[axis]
}
func (g *nativeGamepadImpl) axisValue(axis int) float64 {
if axis < 0 || axis >= len(g.axes) {
return 0

View File

@ -399,6 +399,10 @@ func (g *nativeGamepadImpl) hatCount() int {
return len(g.hatValues)
}
func (g *nativeGamepadImpl) isAxisReady(axis int) bool {
return axis >= 0 && axis < g.axisCount()
}
func (g *nativeGamepadImpl) axisValue(axis int) float64 {
if axis < 0 || axis >= len(g.axisValues) {
return 0

View File

@ -719,6 +719,10 @@ func (g *nativeGamepadDesktop) hatCount() int {
return 1
}
func (g *nativeGamepadDesktop) isAxisReady(axis int) bool {
return axis >= 0 && axis < g.axisCount()
}
func (g *nativeGamepadDesktop) axisValue(axis int) float64 {
if g.usesDInput() {
if axis < 0 || axis >= len(g.dinputAxes) {

View File

@ -76,6 +76,10 @@ func (g *nativeGamepadImpl) hatCount() int {
return len(g.hats)
}
func (g *nativeGamepadImpl) isAxisReady(axis int) bool {
return axis >= 0 && axis < g.axisCount()
}
func (g *nativeGamepadImpl) axisValue(axis int) float64 {
if axis < 0 || axis >= len(g.axes) {
return 0

View File

@ -152,6 +152,10 @@ func (g *nativeGamepadImpl) hatCount() int {
return 0
}
func (g *nativeGamepadImpl) isAxisReady(axis int) bool {
return axis >= 0 && axis < g.axisCount()
}
func (g *nativeGamepadImpl) axisValue(axis int) float64 {
axes := g.value.Get("axes")
if axis < 0 || axis >= axes.Length() {

View File

@ -592,6 +592,10 @@ func (g *nativeGamepadImpl) hatCount() int {
return g.hatCount_
}
func (g *nativeGamepadImpl) isAxisReady(axis int) bool {
return axis >= 0 && axis < g.axisCount()
}
func (g *nativeGamepadImpl) axisValue(axis int) float64 {
if axis < 0 || axis >= g.axisCount_ {
return 0

View File

@ -146,6 +146,10 @@ func (g *nativeGamepadImpl) hatCount() int {
return 0
}
func (g *nativeGamepadImpl) isAxisReady(axis int) bool {
return axis >= 0 && axis < g.axisCount()
}
func (g *nativeGamepadImpl) axisValue(axis int) float64 {
if axis < 0 || axis >= len(g.axisValues) {
return 0

View File

@ -66,6 +66,10 @@ func (*nativeGamepadImpl) hatCount() int {
return 0
}
func (g *nativeGamepadImpl) isAxisReady(axis int) bool {
return false
}
func (*nativeGamepadImpl) axisValue(axis int) float64 {
return 0
}

View File

@ -190,6 +190,10 @@ func (n *nativeGamepadXbox) hatCount() int {
return 0
}
func (g *nativeGamepadXbox) isAxisReady(axis int) bool {
return axis >= 0 && axis < g.axisCount()
}
func (n *nativeGamepadXbox) axisValue(axis int) float64 {
switch gamepaddb.StandardAxis(axis) {
case gamepaddb.StandardAxisLeftStickHorizontal:

View File

@ -120,19 +120,19 @@ const (
type mapping struct {
Type mappingType
Index int
AxisScale int
AxisOffset int
AxisScale float64
AxisOffset float64
HatState int
}
var (
gamepadNames = map[string]string{}
gamepadButtonMappings = map[string]map[StandardButton]*mapping{}
gamepadAxisMappings = map[string]map[StandardAxis]*mapping{}
gamepadButtonMappings = map[string]map[StandardButton]mapping{}
gamepadAxisMappings = map[string]map[StandardAxis]mapping{}
mappingsM sync.RWMutex
)
func parseLine(line string, platform platform) (id string, name string, buttons map[StandardButton]*mapping, axes map[StandardAxis]*mapping, err error) {
func parseLine(line string, platform platform) (id string, name string, buttons map[StandardButton]mapping, axes map[StandardAxis]mapping, err error) {
line = strings.TrimSpace(line)
if len(line) == 0 {
return "", "", nil, nil, nil
@ -192,7 +192,7 @@ func parseLine(line string, platform platform) (id string, name string, buttons
if b, ok := toStandardGamepadButton(tks[0]); ok {
if buttons == nil {
buttons = map[StandardButton]*mapping{}
buttons = map[StandardButton]mapping{}
}
buttons[b] = gb
continue
@ -200,7 +200,7 @@ func parseLine(line string, platform platform) (id string, name string, buttons
if a, ok := toStandardGamepadAxis(tks[0]); ok {
if axes == nil {
axes = map[StandardAxis]*mapping{}
axes = map[StandardAxis]mapping{}
}
axes[a] = gb
continue
@ -213,7 +213,7 @@ func parseLine(line string, platform platform) (id string, name string, buttons
return tokens[0], tokens[1], buttons, axes, nil
}
func parseMappingElement(str string) (*mapping, error) {
func parseMappingElement(str string) (mapping, error) {
switch {
case str[0] == 'a' || strings.HasPrefix(str, "+a") || strings.HasPrefix(str, "-a"):
var tilda bool
@ -222,8 +222,8 @@ func parseMappingElement(str string) (*mapping, error) {
tilda = true
}
min := -1
max := 1
min := -1.0
max := 1.0
numstr := str[1:]
if str[0] == '+' {
@ -259,10 +259,10 @@ func parseMappingElement(str string) (*mapping, error) {
index, err := strconv.Atoi(numstr)
if err != nil {
return nil, err
return mapping{}, err
}
return &mapping{
return mapping{
Type: mappingTypeAxis,
Index: index,
AxisScale: scale,
@ -272,9 +272,9 @@ func parseMappingElement(str string) (*mapping, error) {
case str[0] == 'b':
index, err := strconv.Atoi(str[1:])
if err != nil {
return nil, err
return mapping{}, err
}
return &mapping{
return mapping{
Type: mappingTypeButton,
Index: index,
}, nil
@ -282,24 +282,24 @@ func parseMappingElement(str string) (*mapping, error) {
case str[0] == 'h':
tokens := strings.Split(str[1:], ".")
if len(tokens) < 2 {
return nil, fmt.Errorf("gamepaddb: unexpected hat: %s", str)
return mapping{}, fmt.Errorf("gamepaddb: unexpected hat: %s", str)
}
index, err := strconv.Atoi(tokens[0])
if err != nil {
return nil, err
return mapping{}, err
}
hat, err := strconv.Atoi(tokens[1])
if err != nil {
return nil, err
return mapping{}, err
}
return &mapping{
return mapping{
Type: mappingTypeHat,
Index: index,
HatState: hat,
}, nil
}
return nil, fmt.Errorf("gamepaddb: unepxected mapping: %s", str)
return mapping{}, fmt.Errorf("gamepaddb: unepxected mapping: %s", str)
}
func toStandardGamepadButton(str string) (StandardButton, bool) {
@ -358,7 +358,7 @@ func toStandardGamepadAxis(str string) (StandardAxis, bool) {
}
}
func buttonMappings(id string) map[StandardButton]*mapping {
func buttonMappings(id string) map[StandardButton]mapping {
if m, ok := gamepadButtonMappings[id]; ok {
return m
}
@ -370,7 +370,7 @@ func buttonMappings(id string) map[StandardButton]*mapping {
return nil
}
func axisMappings(id string) map[StandardAxis]*mapping {
func axisMappings(id string) map[StandardAxis]mapping {
if m, ok := gamepadAxisMappings[id]; ok {
return m
}
@ -390,6 +390,7 @@ func HasStandardLayoutMapping(id string) bool {
}
type GamepadState interface {
IsAxisReady(index int) bool
Axis(index int) float64
Button(index int) bool
Hat(index int) int
@ -410,10 +411,11 @@ func HasStandardAxis(id string, axis StandardAxis) bool {
if mappings == nil {
return false
}
return mappings[axis] != nil
_, ok := mappings[axis]
return ok
}
func AxisValue(id string, axis StandardAxis, state GamepadState) float64 {
func StandardAxisValue(id string, axis StandardAxis, state GamepadState) float64 {
mappingsM.RLock()
defer mappingsM.RUnlock()
@ -422,14 +424,17 @@ func AxisValue(id string, axis StandardAxis, state GamepadState) float64 {
return 0
}
mapping := mappings[axis]
if mapping == nil {
mapping, ok := mappings[axis]
if !ok {
return 0
}
switch mapping.Type {
case mappingTypeAxis:
v := state.Axis(mapping.Index)*float64(mapping.AxisScale) + float64(mapping.AxisOffset)
if !state.IsAxisReady(mapping.Index) {
return 0
}
v := state.Axis(mapping.Index)*mapping.AxisScale + mapping.AxisOffset
if v > 1 {
return 1
} else if v < -1 {
@ -461,30 +466,34 @@ func HasStandardButton(id string, button StandardButton) bool {
if mappings == nil {
return false
}
return mappings[button] != nil
_, ok := mappings[button]
return ok
}
func ButtonValue(id string, button StandardButton, state GamepadState) float64 {
func StandardButtonValue(id string, button StandardButton, state GamepadState) float64 {
mappingsM.RLock()
defer mappingsM.RUnlock()
return buttonValue(id, button, state)
return standardButtonValue(id, button, state)
}
func buttonValue(id string, button StandardButton, state GamepadState) float64 {
func standardButtonValue(id string, button StandardButton, state GamepadState) float64 {
mappings := buttonMappings(id)
if mappings == nil {
return 0
}
mapping := mappings[button]
if mapping == nil {
mapping, ok := mappings[button]
if !ok {
return 0
}
switch mapping.Type {
case mappingTypeAxis:
v := state.Axis(mapping.Index)*float64(mapping.AxisScale) + float64(mapping.AxisOffset)
if !state.IsAxisReady(mapping.Index) {
return 0
}
v := state.Axis(mapping.Index)*mapping.AxisScale + mapping.AxisOffset
if v > 1 {
v = 1
} else if v < -1 {
@ -513,7 +522,7 @@ func buttonValue(id string, button StandardButton, state GamepadState) float64 {
// Note: should be used with >, not >=, comparisons.
const ButtonPressedThreshold = 30.0 / 255.0
func IsButtonPressed(id string, button StandardButton, state GamepadState) bool {
func IsStandardButtonPressed(id string, button StandardButton, state GamepadState) bool {
mappingsM.RLock()
defer mappingsM.RUnlock()
@ -522,14 +531,14 @@ func IsButtonPressed(id string, button StandardButton, state GamepadState) bool
return false
}
mapping := mappings[button]
if mapping == nil {
mapping, ok := mappings[button]
if !ok {
return false
}
switch mapping.Type {
case mappingTypeAxis:
v := buttonValue(id, button, state)
v := standardButtonValue(id, button, state)
return v > ButtonPressedThreshold
case mappingTypeButton:
return state.Button(mapping.Index)
@ -554,8 +563,8 @@ func Update(mappingData []byte) error {
type parsedLine struct {
id string
name string
buttons map[StandardButton]*mapping
axes map[StandardAxis]*mapping
buttons map[StandardButton]mapping
axes map[StandardAxis]mapping
}
var lines []parsedLine
@ -609,44 +618,44 @@ func addAndroidDefaultMappings(id string) bool {
return false
}
gamepadButtonMappings[id] = map[StandardButton]*mapping{}
gamepadAxisMappings[id] = map[StandardAxis]*mapping{}
gamepadButtonMappings[id] = map[StandardButton]mapping{}
gamepadAxisMappings[id] = map[StandardAxis]mapping{}
// For mappings, see mobile/ebitenmobileview/input_android.go.
if buttonMask&(1<<SDLControllerButtonA) != 0 {
gamepadButtonMappings[id][StandardButtonRightBottom] = &mapping{
gamepadButtonMappings[id][StandardButtonRightBottom] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonA,
}
}
if buttonMask&(1<<SDLControllerButtonB) != 0 {
gamepadButtonMappings[id][StandardButtonRightRight] = &mapping{
gamepadButtonMappings[id][StandardButtonRightRight] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonB,
}
} else {
// Use the back button as "B" for easy UI navigation with TV remotes.
gamepadButtonMappings[id][StandardButtonRightRight] = &mapping{
gamepadButtonMappings[id][StandardButtonRightRight] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonBack,
}
buttonMask &^= uint16(1) << SDLControllerButtonBack
}
if buttonMask&(1<<SDLControllerButtonX) != 0 {
gamepadButtonMappings[id][StandardButtonRightLeft] = &mapping{
gamepadButtonMappings[id][StandardButtonRightLeft] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonX,
}
}
if buttonMask&(1<<SDLControllerButtonY) != 0 {
gamepadButtonMappings[id][StandardButtonRightTop] = &mapping{
gamepadButtonMappings[id][StandardButtonRightTop] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonY,
}
}
if buttonMask&(1<<SDLControllerButtonBack) != 0 {
gamepadButtonMappings[id][StandardButtonCenterLeft] = &mapping{
gamepadButtonMappings[id][StandardButtonCenterLeft] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonBack,
}
@ -654,69 +663,69 @@ func addAndroidDefaultMappings(id string) bool {
if buttonMask&(1<<SDLControllerButtonGuide) != 0 {
// TODO: If SDKVersion >= 30, add this code:
//
// gamepadButtonMappings[id][StandardButtonCenterCenter] = &mapping{
// gamepadButtonMappings[id][StandardButtonCenterCenter] = mapping{
// Type: mappingTypeButton,
// Index: SDLControllerButtonGuide,
// }
}
if buttonMask&(1<<SDLControllerButtonStart) != 0 {
gamepadButtonMappings[id][StandardButtonCenterRight] = &mapping{
gamepadButtonMappings[id][StandardButtonCenterRight] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonStart,
}
}
if buttonMask&(1<<SDLControllerButtonLeftStick) != 0 {
gamepadButtonMappings[id][StandardButtonLeftStick] = &mapping{
gamepadButtonMappings[id][StandardButtonLeftStick] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonLeftStick,
}
}
if buttonMask&(1<<SDLControllerButtonRightStick) != 0 {
gamepadButtonMappings[id][StandardButtonRightStick] = &mapping{
gamepadButtonMappings[id][StandardButtonRightStick] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonRightStick,
}
}
if buttonMask&(1<<SDLControllerButtonLeftShoulder) != 0 {
gamepadButtonMappings[id][StandardButtonFrontTopLeft] = &mapping{
gamepadButtonMappings[id][StandardButtonFrontTopLeft] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonLeftShoulder,
}
}
if buttonMask&(1<<SDLControllerButtonRightShoulder) != 0 {
gamepadButtonMappings[id][StandardButtonFrontTopRight] = &mapping{
gamepadButtonMappings[id][StandardButtonFrontTopRight] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonRightShoulder,
}
}
if buttonMask&(1<<SDLControllerButtonDpadUp) != 0 {
gamepadButtonMappings[id][StandardButtonLeftTop] = &mapping{
gamepadButtonMappings[id][StandardButtonLeftTop] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonDpadUp,
}
}
if buttonMask&(1<<SDLControllerButtonDpadDown) != 0 {
gamepadButtonMappings[id][StandardButtonLeftBottom] = &mapping{
gamepadButtonMappings[id][StandardButtonLeftBottom] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonDpadDown,
}
}
if buttonMask&(1<<SDLControllerButtonDpadLeft) != 0 {
gamepadButtonMappings[id][StandardButtonLeftLeft] = &mapping{
gamepadButtonMappings[id][StandardButtonLeftLeft] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonDpadLeft,
}
}
if buttonMask&(1<<SDLControllerButtonDpadRight) != 0 {
gamepadButtonMappings[id][StandardButtonLeftRight] = &mapping{
gamepadButtonMappings[id][StandardButtonLeftRight] = mapping{
Type: mappingTypeButton,
Index: SDLControllerButtonDpadRight,
}
}
if axisMask&(1<<SDLControllerAxisLeftX) != 0 {
gamepadAxisMappings[id][StandardAxisLeftStickHorizontal] = &mapping{
gamepadAxisMappings[id][StandardAxisLeftStickHorizontal] = mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisLeftX,
AxisScale: 1,
@ -724,7 +733,7 @@ func addAndroidDefaultMappings(id string) bool {
}
}
if axisMask&(1<<SDLControllerAxisLeftY) != 0 {
gamepadAxisMappings[id][StandardAxisLeftStickVertical] = &mapping{
gamepadAxisMappings[id][StandardAxisLeftStickVertical] = mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisLeftY,
AxisScale: 1,
@ -732,7 +741,7 @@ func addAndroidDefaultMappings(id string) bool {
}
}
if axisMask&(1<<SDLControllerAxisRightX) != 0 {
gamepadAxisMappings[id][StandardAxisRightStickHorizontal] = &mapping{
gamepadAxisMappings[id][StandardAxisRightStickHorizontal] = mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisRightX,
AxisScale: 1,
@ -740,7 +749,7 @@ func addAndroidDefaultMappings(id string) bool {
}
}
if axisMask&(1<<SDLControllerAxisRightY) != 0 {
gamepadAxisMappings[id][StandardAxisRightStickVertical] = &mapping{
gamepadAxisMappings[id][StandardAxisRightStickVertical] = mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisRightY,
AxisScale: 1,
@ -748,7 +757,7 @@ func addAndroidDefaultMappings(id string) bool {
}
}
if axisMask&(1<<SDLControllerAxisTriggerLeft) != 0 {
gamepadButtonMappings[id][StandardButtonFrontBottomLeft] = &mapping{
gamepadButtonMappings[id][StandardButtonFrontBottomLeft] = mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisTriggerLeft,
AxisScale: 1,
@ -756,7 +765,7 @@ func addAndroidDefaultMappings(id string) bool {
}
}
if axisMask&(1<<SDLControllerAxisTriggerRight) != 0 {
gamepadButtonMappings[id][StandardButtonFrontBottomRight] = &mapping{
gamepadButtonMappings[id][StandardButtonFrontBottomRight] = mapping{
Type: mappingTypeAxis,
Index: SDLControllerAxisTriggerRight,
AxisScale: 1,

View File

@ -73,6 +73,10 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
return nil, nil, nil, false
}
stmts = append(stmts, ss...)
if len(ts) == 0 {
cs.addError(e.Pos(), fmt.Sprintf("unexpected binary operator: %s", e.X))
return nil, nil, nil, false
}
lhst := ts[0]
rhs, ts, ss, ok := cs.parseExpr(block, fname, e.Y, markLocalVariableUsed)
@ -283,26 +287,26 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
}
// Process the expression as a regular function call.
var t shaderir.Type
var finalType shaderir.Type
switch callee.BuiltinFunc {
case shaderir.BoolF:
if err := checkArgsForBoolBuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false
}
t = shaderir.Type{Main: shaderir.Bool}
finalType = shaderir.Type{Main: shaderir.Bool}
case shaderir.IntF:
if err := checkArgsForIntBuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false
}
t = shaderir.Type{Main: shaderir.Int}
finalType = shaderir.Type{Main: shaderir.Int}
case shaderir.FloatF:
if err := checkArgsForFloatBuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false
}
t = shaderir.Type{Main: shaderir.Float}
finalType = shaderir.Type{Main: shaderir.Float}
case shaderir.Vec2F:
if err := checkArgsForVec2BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
@ -315,7 +319,7 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float}
}
t = shaderir.Type{Main: shaderir.Vec2}
finalType = shaderir.Type{Main: shaderir.Vec2}
case shaderir.Vec3F:
if err := checkArgsForVec3BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
@ -328,7 +332,7 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float}
}
t = shaderir.Type{Main: shaderir.Vec3}
finalType = shaderir.Type{Main: shaderir.Vec3}
case shaderir.Vec4F:
if err := checkArgsForVec4BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
@ -341,25 +345,25 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float}
}
t = shaderir.Type{Main: shaderir.Vec4}
finalType = shaderir.Type{Main: shaderir.Vec4}
case shaderir.IVec2F:
if err := checkArgsForIVec2BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false
}
t = shaderir.Type{Main: shaderir.IVec2}
finalType = shaderir.Type{Main: shaderir.IVec2}
case shaderir.IVec3F:
if err := checkArgsForIVec3BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false
}
t = shaderir.Type{Main: shaderir.IVec3}
finalType = shaderir.Type{Main: shaderir.IVec3}
case shaderir.IVec4F:
if err := checkArgsForIVec4BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false
}
t = shaderir.Type{Main: shaderir.IVec4}
finalType = shaderir.Type{Main: shaderir.IVec4}
case shaderir.Mat2F:
if err := checkArgsForMat2BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
@ -372,7 +376,7 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float}
}
t = shaderir.Type{Main: shaderir.Mat2}
finalType = shaderir.Type{Main: shaderir.Mat2}
case shaderir.Mat3F:
if err := checkArgsForMat3BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
@ -385,7 +389,7 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float}
}
t = shaderir.Type{Main: shaderir.Mat3}
finalType = shaderir.Type{Main: shaderir.Mat3}
case shaderir.Mat4F:
if err := checkArgsForMat4BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error())
@ -398,7 +402,7 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float}
}
t = shaderir.Type{Main: shaderir.Mat4}
finalType = shaderir.Type{Main: shaderir.Mat4}
case shaderir.TexelAt:
if len(args) != 2 {
cs.addError(e.Pos(), fmt.Sprintf("number of %s's arguments must be 2 but %d", callee.BuiltinFunc, len(args)))
@ -412,7 +416,7 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
cs.addError(e.Pos(), fmt.Sprintf("cannot use %s as vec2 value in argument to %s", argts[1].String(), callee.BuiltinFunc))
return nil, nil, nil, false
}
t = shaderir.Type{Main: shaderir.Vec4}
finalType = shaderir.Type{Main: shaderir.Vec4}
case shaderir.DiscardF:
if len(args) != 0 {
cs.addError(e.Pos(), fmt.Sprintf("number of %s's arguments must be 0 but %d", callee.BuiltinFunc, len(args)))
@ -435,13 +439,17 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
}
switch callee.BuiltinFunc {
case shaderir.Clamp:
if kind, allConsts := resolveConstKind(args, argts); allConsts {
if kind, _ := resolveConstKind(args, argts); kind != gconstant.Unknown {
switch kind {
case gconstant.Unknown:
cs.addError(e.Pos(), fmt.Sprintf("%s's arguments don't match: %s, %s, and %s", callee.BuiltinFunc, argts[0].String(), argts[1].String(), argts[2].String()))
return nil, nil, nil, false
case gconstant.Int:
for i, arg := range args {
if arg.Const == nil {
if argts[i].Main != shaderir.Int {
cs.addError(e.Pos(), fmt.Sprintf("%s's arguments don't match: %s, %s, and %s", callee.BuiltinFunc, argts[0].String(), argts[1].String(), argts[2].String()))
return nil, nil, nil, false
}
continue
}
v := gconstant.ToInt(arg.Const)
if v.Kind() == gconstant.Unknown {
cs.addError(e.Pos(), fmt.Sprintf("cannot convert %s to type int", arg.Const.String()))
@ -452,6 +460,13 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
}
case gconstant.Float:
for i, arg := range args {
if arg.Const == nil {
if argts[i].Main != shaderir.Float {
cs.addError(e.Pos(), fmt.Sprintf("%s's arguments don't match: %s, %s, and %s", callee.BuiltinFunc, argts[0].String(), argts[1].String(), argts[2].String()))
return nil, nil, nil, false
}
continue
}
v := gconstant.ToFloat(arg.Const)
if v.Kind() == gconstant.Unknown {
cs.addError(e.Pos(), fmt.Sprintf("cannot convert %s to type float", arg.Const.String()))
@ -551,9 +566,9 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
switch callee.BuiltinFunc {
case shaderir.Smoothstep:
t = argts[2]
finalType = argts[2]
default:
t = argts[0]
finalType = argts[0]
}
case shaderir.Atan2, shaderir.Pow, shaderir.Mod, shaderir.Min, shaderir.Max, shaderir.Step, shaderir.Distance, shaderir.Dot, shaderir.Cross, shaderir.Reflect:
@ -565,13 +580,17 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
switch callee.BuiltinFunc {
case shaderir.Min, shaderir.Max:
if kind, allConsts := resolveConstKind(args, argts); allConsts {
if kind, _ := resolveConstKind(args, argts); kind != gconstant.Unknown {
switch kind {
case gconstant.Unknown:
cs.addError(e.Pos(), fmt.Sprintf("%s's arguments don't match: %s and %s", callee.BuiltinFunc, argts[0].String(), argts[1].String()))
return nil, nil, nil, false
case gconstant.Int:
for i, arg := range args {
if arg.Const == nil {
if argts[i].Main != shaderir.Int {
cs.addError(e.Pos(), fmt.Sprintf("%s's arguments don't match: %s and %s", callee.BuiltinFunc, argts[0].String(), argts[1].String()))
return nil, nil, nil, false
}
continue
}
v := gconstant.ToInt(arg.Const)
if v.Kind() == gconstant.Unknown {
cs.addError(e.Pos(), fmt.Sprintf("cannot convert %s to type int", arg.Const.String()))
@ -582,6 +601,13 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
}
case gconstant.Float:
for i, arg := range args {
if arg.Const == nil {
if argts[i].Main != shaderir.Float {
cs.addError(e.Pos(), fmt.Sprintf("%s's arguments don't match: %s and %s", callee.BuiltinFunc, argts[0].String(), argts[1].String()))
return nil, nil, nil, false
}
continue
}
v := gconstant.ToFloat(arg.Const)
if v.Kind() == gconstant.Unknown {
cs.addError(e.Pos(), fmt.Sprintf("cannot convert %s to type float", arg.Const.String()))
@ -669,11 +695,11 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
}
switch callee.BuiltinFunc {
case shaderir.Distance, shaderir.Dot:
t = shaderir.Type{Main: shaderir.Float}
finalType = shaderir.Type{Main: shaderir.Float}
case shaderir.Step:
t = argts[1]
finalType = argts[1]
default:
t = argts[0]
finalType = argts[0]
}
default:
@ -718,9 +744,9 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
}
}
if callee.BuiltinFunc == shaderir.Length {
t = shaderir.Type{Main: shaderir.Float}
finalType = shaderir.Type{Main: shaderir.Float}
} else {
t = argts[0]
finalType = argts[0]
}
}
return []shaderir.Expr{
@ -728,7 +754,7 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
Type: shaderir.Call,
Exprs: append([]shaderir.Expr{callee}, args...),
},
}, []shaderir.Type{t}, stmts, true
}, []shaderir.Type{finalType}, stmts, true
}
if callee.Type != shaderir.FunctionExpr {
@ -906,7 +932,7 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
return nil, nil, nil, false
}
if !isValidSwizzling(e.Sel.Name, types[0]) {
if len(types) == 0 || !isValidSwizzling(e.Sel.Name, types[0]) {
cs.addError(e.Pos(), fmt.Sprintf("unexpected swizzling: %s", e.Sel.Name))
return nil, nil, nil, false
}
@ -962,6 +988,10 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
cs.addError(e.Pos(), fmt.Sprintf("multiple-value context is not available at a unary operator: %s", e.X))
return nil, nil, nil, false
}
if len(ts) == 0 {
cs.addError(e.Pos(), fmt.Sprintf("unexpected unary operator: %s", e.X))
return nil, nil, nil, false
}
if exprs[0].Const != nil {
v := gconstant.UnaryOp(e.Op, exprs[0].Const, 0)
@ -1109,6 +1139,10 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
cs.addError(e.Pos(), "multiple-value context is not available at an index expression")
return nil, nil, nil, false
}
if len(ts) == 0 {
cs.addError(e.Pos(), "unexpected index expression")
return nil, nil, nil, false
}
x := exprs[0]
t := ts[0]
@ -1169,8 +1203,24 @@ func resolveConstKind(exprs []shaderir.Expr, ts []shaderir.Type) (kind gconstant
panic("not reached")
}
allConsts = true
for _, expr := range exprs {
if expr.Const == nil {
allConsts = false
}
}
if !allConsts {
for _, t := range ts {
if t.Main == shaderir.None {
continue
}
if t.Main == shaderir.Float {
return gconstant.Float, false
}
if t.Main == shaderir.Int {
return gconstant.Int, false
}
return gconstant.Unknown, false
}
}
@ -1199,13 +1249,15 @@ func resolveConstKind(exprs []shaderir.Expr, ts []shaderir.Type) (kind gconstant
}
}
if kind != gconstant.Unknown {
return kind, true
}
// Prefer floats over integers for non-typed constant values.
// For example, max(1.0, 1) should return a float value.
if kind == gconstant.Unknown {
for _, expr := range exprs {
if expr.Const.Kind() == gconstant.Float {
return gconstant.Float, true
}
for _, expr := range exprs {
if expr.Const.Kind() == gconstant.Float {
return gconstant.Float, true
}
}

View File

@ -740,7 +740,9 @@ func (cs *compileState) parseFuncParams(block *block, fname string, d *ast.FuncD
}
}
if len(out) == 1 && out[0].name == "" {
// If there is only one returning value, it is treated as a returning value.
// An array cannot be a returning value, especially for HLSL (#2923).
if len(out) == 1 && out[0].name == "" && out[0].typ.Main != shaderir.Array {
ret = out[0].typ
out = nil
}

View File

@ -2345,6 +2345,18 @@ func TestSyntaxBuiltinFuncDoubleArgsType(t *testing.T) {
{stmt: "a := {{.Func}}(1, 1); _ = a", err: false},
{stmt: "a := {{.Func}}(1.0, 1); _ = a", err: false},
{stmt: "a := {{.Func}}(1, 1.0); _ = a", err: false},
{stmt: "a := {{.Func}}(int(1), 1); _ = a", err: true},
{stmt: "a := {{.Func}}(int(1), 1.0); _ = a", err: true},
{stmt: "a := {{.Func}}(int(1), 1.1); _ = a", err: true},
{stmt: "a := {{.Func}}(float(1), 1); _ = a", err: false},
{stmt: "a := {{.Func}}(float(1), 1.0); _ = a", err: false},
{stmt: "a := {{.Func}}(float(1), 1.1); _ = a", err: false},
{stmt: "a := {{.Func}}(1, int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1.0, int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1.1, int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, float(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(1.0, float(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(1.1, float(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(int(1), int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec3(1)); _ = a", err: true},
@ -2405,6 +2417,18 @@ func TestSyntaxBuiltinFuncDoubleArgsType2(t *testing.T) {
{stmt: "a := {{.Func}}(1, 1); _ = a", err: false},
{stmt: "a := {{.Func}}(1.0, 1); _ = a", err: false},
{stmt: "a := {{.Func}}(1, 1.0); _ = a", err: false},
{stmt: "a := {{.Func}}(int(1), 1); _ = a", err: true},
{stmt: "a := {{.Func}}(int(1), 1.0); _ = a", err: true},
{stmt: "a := {{.Func}}(int(1), 1.1); _ = a", err: true},
{stmt: "a := {{.Func}}(float(1), 1); _ = a", err: false},
{stmt: "a := {{.Func}}(float(1), 1.0); _ = a", err: false},
{stmt: "a := {{.Func}}(float(1), 1.1); _ = a", err: false},
{stmt: "a := {{.Func}}(1, int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1.0, int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1.1, int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, float(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(1.0, float(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(1.1, float(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(int(1), int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec3(1)); _ = a", err: true},
@ -2465,13 +2489,25 @@ func TestSyntaxBuiltinFuncArgsMinMax(t *testing.T) {
{stmt: "a := {{.Func}}(1, 1.0); var _ float = a", err: false},
{stmt: "a := {{.Func}}(1.1, 1); _ = a", err: false},
{stmt: "a := {{.Func}}(1, 1.1); _ = a", err: false},
{stmt: "a := {{.Func}}(int(1), int(1)); var _ int = a", err: false},
{stmt: "a := {{.Func}}(int(1), 1); var _ int = a", err: false},
{stmt: "a := {{.Func}}(int(1), 1.0); var _ int = a", err: false},
{stmt: "a := {{.Func}}(int(1), 1.1); _ = a", err: true},
{stmt: "a := {{.Func}}(float(1), 1); _ = a", err: false},
{stmt: "a := {{.Func}}(float(1), 1.0); _ = a", err: false},
{stmt: "a := {{.Func}}(float(1), 1.1); _ = a", err: false},
{stmt: "a := {{.Func}}(1, int(1)); var _ int = a", err: false},
{stmt: "a := {{.Func}}(1.0, int(1)); var _ int = a", err: false},
{stmt: "a := {{.Func}}(1.1, int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, float(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(1.0, float(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(1.1, float(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(int(1), int(1)); var _ int = a", err: false},
{stmt: "a := {{.Func}}(int(1), float(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(float(1), int(1)); _ = a", err: true},
{stmt: "x := 1.1; a := {{.Func}}(int(x), 1); _ = a", err: false},
{stmt: "x := 1; a := {{.Func}}(float(x), 1.1); _ = a", err: false},
{stmt: "x := 1.1; a := {{.Func}}(1, int(x)); _ = a", err: false},
{stmt: "x := 1; a := {{.Func}}(1.1, float(x)); _ = a", err: false},
{stmt: "a := {{.Func}}(1, vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec3(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec4(1)); _ = a", err: true},
@ -2479,14 +2515,20 @@ func TestSyntaxBuiltinFuncArgsMinMax(t *testing.T) {
{stmt: "a := {{.Func}}(1, ivec3(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, ivec4(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec2(1), 1); _ = a", err: false}, // The second argument can be a scalar.
{stmt: "a := {{.Func}}(vec2(1), 1.0); _ = a", err: false},
{stmt: "a := {{.Func}}(vec2(1), 1.1); _ = a", err: false},
{stmt: "a := {{.Func}}(vec2(1), vec2(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(vec2(1), vec3(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec2(1), vec4(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec3(1), 1); _ = a", err: false}, // The second argument can be a scalar.
{stmt: "a := {{.Func}}(vec3(1), 1.0); _ = a", err: false},
{stmt: "a := {{.Func}}(vec3(1), 1.1); _ = a", err: false},
{stmt: "a := {{.Func}}(vec3(1), vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec3(1), vec3(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(vec3(1), vec4(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec4(1), 1); _ = a", err: false}, // The second argument can be a scalar.
{stmt: "a := {{.Func}}(vec4(1), 1.0); _ = a", err: false},
{stmt: "a := {{.Func}}(vec4(1), 1.1); _ = a", err: false},
{stmt: "a := {{.Func}}(vec4(1), vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec4(1), vec3(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec4(1), vec4(1)); _ = a", err: false},
@ -2661,6 +2703,17 @@ func TestSyntaxBuiltinFuncClampType(t *testing.T) {
{stmt: "a := clamp(int(1), 1, 1.0); var _ int = a", err: false},
{stmt: "a := clamp(int(1), 1.1, 1); _ = a", err: true},
{stmt: "a := clamp(int(1), 1, 1.1); _ = a", err: true},
{stmt: "a := clamp(float(1), 1, 1); var _ float = a", err: false},
{stmt: "a := clamp(float(1), 1.0, 1); var _ float = a", err: false},
{stmt: "a := clamp(float(1), 1, 1.0); var _ float = a", err: false},
{stmt: "a := clamp(float(1), 1.1, 1); _ = a", err: false},
{stmt: "a := clamp(float(1), 1, 1.1); _ = a", err: false},
{stmt: "x := 1.1; a := clamp(int(x), 1, 1); _ = a", err: false},
{stmt: "x := 1; a := clamp(float(x), 1.1, 1.1); _ = a", err: false},
{stmt: "x := 1.1; a := clamp(1, int(x), 1); _ = a", err: false},
{stmt: "x := 1; a := clamp(1.1, float(x), 1.1); _ = a", err: false},
{stmt: "x := 1.1; a := clamp(1, 1, int(x)); _ = a", err: false},
{stmt: "x := 1; a := clamp(1.1, 1.1, float(x)); _ = a", err: false},
{stmt: "a := clamp(1.0, 1, 1); var _ float = a", err: false},
{stmt: "a := clamp(1, 1.0, 1); var _ float = a", err: false},
{stmt: "a := clamp(1, 1, 1.0); var _ float = a", err: false},
@ -4165,3 +4218,51 @@ func Bar() (int, int) {
t.Error("compileToIR must return an error but did not")
}
}
// Issue #2926
func TestSyntaxNonTypeExpression(t *testing.T) {
if _, err := compileToIR([]byte(`package main
func Foo() {
}
func Bar() float {
return +Foo
}
`)); err == nil {
t.Error("compileToIR must return an error but did not")
}
if _, err := compileToIR([]byte(`package main
func Foo() {
}
func Bar() float {
return Foo + 1.0
}
`)); err == nil {
t.Error("compileToIR must return an error but did not")
}
if _, err := compileToIR([]byte(`package main
func Foo() {
}
func Bar() float {
return Foo.x
}
`)); err == nil {
t.Error("compileToIR must return an error but did not")
}
if _, err := compileToIR([]byte(`package main
func Foo() {
}
func Bar() float {
return Foo[0]
}
`)); err == nil {
t.Error("compileToIR must return an error but did not")
}
}

View File

@ -1,25 +1,29 @@
uniform vec2 U0[4];
vec2[2] F0(void);
vec2[2] F1(void);
void F0(out vec2 l0[2]);
void F1(out vec2 l0[2]);
vec2[2] F0(void) {
vec2 l0[2];
l0[0] = vec2(0);
l0[1] = vec2(0);
return l0;
}
vec2[2] F1(void) {
vec2 l0[2];
l0[0] = vec2(0);
l0[1] = vec2(0);
void F0(out vec2 l0[2]) {
vec2 l1[2];
l1[0] = vec2(0);
l1[1] = vec2(0);
(l0)[0] = vec2(1.0);
l1[0] = l0[0];
l1[1] = l0[1];
(l1)[1] = vec2(2.0);
return l1;
l0[0] = l1[0];
l0[1] = l1[1];
return;
}
void F1(out vec2 l0[2]) {
vec2 l1[2];
l1[0] = vec2(0);
l1[1] = vec2(0);
vec2 l2[2];
l2[0] = vec2(0);
l2[1] = vec2(0);
(l1)[0] = vec2(1.0);
l2[0] = l1[0];
l2[1] = l1[1];
(l2)[1] = vec2(2.0);
l0[0] = l2[0];
l0[1] = l2[1];
return;
}

View File

@ -1,11 +1,12 @@
array<float2, 3> F0(void);
void F0(thread array<float2, 3>& l0);
array<float2, 3> F0(void) {
array<float2, 2> l0 = {};
array<float2, 3> l1 = {};
void F0(thread array<float2, 3>& l0) {
array<float2, 2> l1 = {};
array<float2, 3> l2 = {};
{
array<float2, 2> l1 = {};
l1 = l0;
array<float2, 2> l2 = {};
l2 = l1;
}
return l1;
l0 = l2;
return;
}

View File

@ -1,19 +1,22 @@
vec2[3] F0(void);
void F0(out vec2 l0[3]);
vec2[3] F0(void) {
vec2 l0[2];
l0[0] = vec2(0);
l0[1] = vec2(0);
vec2 l1[3];
void F0(out vec2 l0[3]) {
vec2 l1[2];
l1[0] = vec2(0);
l1[1] = vec2(0);
l1[2] = vec2(0);
vec2 l2[3];
l2[0] = vec2(0);
l2[1] = vec2(0);
l2[2] = vec2(0);
{
vec2 l1[2];
l1[0] = vec2(0);
l1[1] = vec2(0);
l1[0] = l0[0];
l1[1] = l0[1];
vec2 l2[2];
l2[0] = vec2(0);
l2[1] = vec2(0);
l2[0] = l1[0];
l2[1] = l1[1];
}
return l1;
l0[0] = l2[0];
l0[1] = l2[1];
l0[2] = l2[2];
return;
}

View File

@ -1,16 +1,18 @@
float[1] F0(void);
int[1] F1(void);
void F0(out float l0[1]);
void F1(out int l0[1]);
float[1] F0(void) {
float l0[1];
l0[0] = float(0);
(l0)[0] = 1.0;
return l0;
void F0(out float l0[1]) {
float l1[1];
l1[0] = float(0);
(l1)[0] = 1.0;
l0[0] = l1[0];
return;
}
int[1] F1(void) {
int l0[1];
l0[0] = 0;
(l0)[0] = 1;
return l0;
void F1(out int l0[1]) {
int l1[1];
l1[0] = 0;
(l1)[0] = 1;
l0[0] = l1[0];
return;
}

View File

@ -285,6 +285,10 @@ func (c *context) screenScaleAndOffsets() (scale, offsetX, offsetY float64) {
return
}
func (u *UserInterface) LogicalPositionToClientPosition(x, y float64) (float64, float64) {
return u.context.logicalPositionToClientPosition(x, y, u.Monitor().DeviceScaleFactor())
func (u *UserInterface) LogicalPositionToClientPositionInNativePixels(x, y float64) (float64, float64) {
s := u.Monitor().DeviceScaleFactor()
x, y = u.context.logicalPositionToClientPosition(x, y, s)
x = dipToNativePixels(x, s)
y = dipToNativePixels(y, s)
return x, y
}

View File

@ -96,8 +96,8 @@ func (u *UserInterface) updateInputStateImpl() error {
if !math.IsNaN(cx) && !math.IsNaN(cy) {
cx2, cy2 := u.context.logicalPositionToClientPosition(cx, cy, s)
cx2 = dipToGLFWPixel(cx2, m)
cy2 = dipToGLFWPixel(cy2, m)
cx2 = dipToGLFWPixel(cx2, s)
cy2 = dipToGLFWPixel(cy2, s)
if err := u.window.SetCursorPos(cx2, cy2); err != nil {
return err
}
@ -106,8 +106,8 @@ func (u *UserInterface) updateInputStateImpl() error {
if err != nil {
return err
}
cx2 = dipFromGLFWPixel(cx2, m)
cy2 = dipFromGLFWPixel(cy2, m)
cx2 = dipFromGLFWPixel(cx2, s)
cy2 = dipFromGLFWPixel(cy2, s)
cx, cy = u.context.clientPositionToLogicalPosition(cx2, cy2, s)
}

View File

@ -106,6 +106,7 @@ const (
KeyF24
KeyHome
KeyInsert
KeyIntlBackslash
KeyMetaLeft
KeyMetaRight
KeyMinus
@ -315,6 +316,8 @@ func (k Key) String() string {
return "KeyHome"
case KeyInsert:
return "KeyInsert"
case KeyIntlBackslash:
return "KeyIntlBackslash"
case KeyMetaLeft:
return "KeyMetaLeft"
case KeyMetaRight:

View File

@ -89,6 +89,7 @@ var uiKeyToGLFWKey = map[Key]glfw.Key{
KeyHome: glfw.KeyHome,
KeyI: glfw.KeyI,
KeyInsert: glfw.KeyInsert,
KeyIntlBackslash: glfw.KeyWorld1,
KeyJ: glfw.KeyJ,
KeyK: glfw.KeyK,
KeyL: glfw.KeyL,

View File

@ -87,6 +87,7 @@ var uiKeyToJSCode = map[Key]js.Value{
KeyHome: js.ValueOf("Home"),
KeyI: js.ValueOf("KeyI"),
KeyInsert: js.ValueOf("Insert"),
KeyIntlBackslash: js.ValueOf("IntlBackslash"),
KeyJ: js.ValueOf("KeyJ"),
KeyK: js.ValueOf("KeyK"),
KeyL: js.ValueOf("KeyL"),

View File

@ -52,7 +52,8 @@ func (m *Monitor) DeviceScaleFactor() float64 {
func (m *Monitor) sizeInDIP() (float64, float64) {
w, h := m.boundsInGLFWPixels.Dx(), m.boundsInGLFWPixels.Dy()
return dipFromGLFWPixel(float64(w), m), dipFromGLFWPixel(float64(h), m)
s := m.DeviceScaleFactor()
return dipFromGLFWPixel(float64(w), s), dipFromGLFWPixel(float64(h), s)
}
type monitors struct {

View File

@ -129,3 +129,7 @@ func deviceScaleFactorImpl() float64 {
}
return s
}
func dipToNativePixels(x float64, scale float64) float64 {
return x * scale
}

View File

@ -206,7 +206,7 @@ func glfwMonitorSizeInGLFWPixels(m *glfw.Monitor) (int, int, error) {
return vm.Width, vm.Height, nil
}
func dipFromGLFWPixel(x float64, monitor *Monitor) float64 {
func dipFromGLFWPixel(x float64, scale float64) float64 {
// NOTE: On macOS, GLFW exposes the device independent coordinate system.
// Thus, the conversion functions are unnecessary,
// however we still need the deviceScaleFactor internally
@ -214,7 +214,7 @@ func dipFromGLFWPixel(x float64, monitor *Monitor) float64 {
return x
}
func dipToGLFWPixel(x float64, monitor *Monitor) float64 {
func dipToGLFWPixel(x float64, scale float64) float64 {
return x
}

View File

@ -321,13 +321,14 @@ func (u *UserInterface) setWindowMonitor(monitor *Monitor) error {
}
}
w := dipToGLFWPixel(float64(ww), monitor)
h := dipToGLFWPixel(float64(wh), monitor)
s := monitor.DeviceScaleFactor()
w := dipToGLFWPixel(float64(ww), s)
h := dipToGLFWPixel(float64(wh), s)
mx := monitor.boundsInGLFWPixels.Min.X
my := monitor.boundsInGLFWPixels.Min.Y
mw, mh := monitor.sizeInDIP()
mw = dipToGLFWPixel(mw, monitor)
mh = dipToGLFWPixel(mh, monitor)
mw = dipToGLFWPixel(mw, s)
mh = dipToGLFWPixel(mh, s)
px, py := InitialWindowPosition(int(mw), int(mh), int(w), int(h))
if err := u.window.SetPos(mx+px, my+py); err != nil {
return err
@ -843,8 +844,9 @@ func (u *UserInterface) createWindow() error {
monitor := u.getInitMonitor()
ww, wh := u.getInitWindowSizeInDIP()
width := int(dipToGLFWPixel(float64(ww), monitor))
height := int(dipToGLFWPixel(float64(wh), monitor))
s := monitor.DeviceScaleFactor()
width := int(dipToGLFWPixel(float64(ww), s))
height := int(dipToGLFWPixel(float64(wh), s))
window, err := glfw.CreateWindow(width, height, "", nil, nil)
if err != nil {
return err
@ -1240,8 +1242,9 @@ func (u *UserInterface) outsideSize() (float64, float64, error) {
if err != nil {
return 0, 0, err
}
w := dipFromGLFWPixel(float64(ww), m)
h := dipFromGLFWPixel(float64(wh), m)
s := m.DeviceScaleFactor()
w := dipFromGLFWPixel(float64(ww), s)
h := dipFromGLFWPixel(float64(wh), s)
return w, h, nil
}
@ -1318,6 +1321,9 @@ func (u *UserInterface) update() (float64, float64, error) {
if err = u.window.Show(); err != nil {
return
}
if err = u.window.Focus(); err != nil {
return
}
})
if err != nil {
return 0, 0, err
@ -1529,30 +1535,31 @@ func (u *UserInterface) updateWindowSizeLimits() error {
}
minw, minh, maxw, maxh := u.getWindowSizeLimitsInDIP()
s := m.DeviceScaleFactor()
if minw < 0 {
// Always set the minimum window width.
mw, err := u.minimumWindowWidth()
if err != nil {
return err
}
minw = int(dipToGLFWPixel(float64(mw), m))
minw = int(dipToGLFWPixel(float64(mw), s))
} else {
minw = int(dipToGLFWPixel(float64(minw), m))
minw = int(dipToGLFWPixel(float64(minw), s))
}
if minh < 0 {
minh = glfw.DontCare
} else {
minh = int(dipToGLFWPixel(float64(minh), m))
minh = int(dipToGLFWPixel(float64(minh), s))
}
if maxw < 0 {
maxw = glfw.DontCare
} else {
maxw = int(dipToGLFWPixel(float64(maxw), m))
maxw = int(dipToGLFWPixel(float64(maxw), s))
}
if maxh < 0 {
maxh = glfw.DontCare
} else {
maxh = int(dipToGLFWPixel(float64(maxh), m))
maxh = int(dipToGLFWPixel(float64(maxh), s))
}
if err := u.window.SetSizeLimits(minw, minh, maxw, maxh); err != nil {
return err
@ -1640,8 +1647,9 @@ func (u *UserInterface) setWindowSizeInDIP(width, height int, callSetSize bool)
if err != nil {
return err
}
newW := int(dipToGLFWPixel(float64(width), m))
newH := int(dipToGLFWPixel(float64(height), m))
s := m.DeviceScaleFactor()
newW := int(dipToGLFWPixel(float64(width), s))
newH := int(dipToGLFWPixel(float64(height), s))
if oldW != newW || oldH != newH {
// Just after SetSize, GetSize is not reliable especially on Linux/UNIX.
// Let's wait for FramebufferSize callback in any cases.
@ -1740,8 +1748,9 @@ func (u *UserInterface) setFullscreen(fullscreen bool) error {
if err != nil {
return err
}
ww := int(dipToGLFWPixel(float64(u.origWindowWidthInDIP), m))
wh := int(dipToGLFWPixel(float64(u.origWindowHeightInDIP), m))
s := m.DeviceScaleFactor()
ww := int(dipToGLFWPixel(float64(u.origWindowWidthInDIP), s))
wh := int(dipToGLFWPixel(float64(u.origWindowHeightInDIP), s))
if u.isNativeFullscreenAvailable() {
if err := u.setNativeFullscreen(false); err != nil {
return err
@ -2061,8 +2070,9 @@ func (u *UserInterface) setWindowPositionInDIP(x, y int, monitor *Monitor) error
mx := monitor.boundsInGLFWPixels.Min.X
my := monitor.boundsInGLFWPixels.Min.Y
xf := dipToGLFWPixel(float64(x), monitor)
yf := dipToGLFWPixel(float64(y), monitor)
s := monitor.DeviceScaleFactor()
xf := dipToGLFWPixel(float64(x), s)
yf := dipToGLFWPixel(float64(y), s)
if x, y := u.adjustWindowPosition(mx+int(xf), my+int(yf), monitor); f {
u.setOrigWindowPos(x, y)
} else {
@ -2124,3 +2134,7 @@ func IsScreenTransparentAvailable() bool {
func (u *UserInterface) RunOnMainThread(f func()) {
u.mainThread.Call(f)
}
func dipToNativePixels(x float64, scale float64) float64 {
return dipToGLFWPixel(x, scale)
}

View File

@ -92,3 +92,7 @@ func deviceScaleFactorImpl() float64 {
// TODO: Can this be called from non-main threads?
return float64(C.devicePixelRatio())
}
func dipToNativePixels(x float64, scale float64) float64 {
return x
}

View File

@ -811,3 +811,7 @@ func (u *UserInterface) updateIconIfNeeded() error {
func IsScreenTransparentAvailable() bool {
return true
}
func dipToNativePixels(x float64, scale float64) float64 {
return x
}

View File

@ -122,12 +122,12 @@ func glfwMonitorSizeInGLFWPixels(m *glfw.Monitor) (int, int, error) {
return physWidth, physHeight, nil
}
func dipFromGLFWPixel(x float64, monitor *Monitor) float64 {
return x / monitor.DeviceScaleFactor()
func dipFromGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x / deviceScaleFactor
}
func dipToGLFWPixel(x float64, monitor *Monitor) float64 {
return x * monitor.DeviceScaleFactor()
func dipToGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x * deviceScaleFactor
}
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) {

View File

@ -181,3 +181,7 @@ func (u *UserInterface) Monitor() *Monitor {
func IsScreenTransparentAvailable() bool {
return false
}
func dipToNativePixels(x float64, scale float64) float64 {
return x
}

View File

@ -174,3 +174,7 @@ func (u *UserInterface) Monitor() *Monitor {
func IsScreenTransparentAvailable() bool {
return false
}
func dipToNativePixels(x float64, scale float64) float64 {
return x
}

View File

@ -101,12 +101,12 @@ func glfwMonitorSizeInGLFWPixels(m *glfw.Monitor) (int, int, error) {
return vm.Width, vm.Height, nil
}
func dipFromGLFWPixel(x float64, monitor *Monitor) float64 {
return x / monitor.DeviceScaleFactor()
func dipFromGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x / deviceScaleFactor
}
func dipToGLFWPixel(x float64, monitor *Monitor) float64 {
return x * monitor.DeviceScaleFactor()
func dipToGLFWPixel(x float64, deviceScaleFactor float64) float64 {
return x * deviceScaleFactor
}
func (u *UserInterface) adjustWindowPosition(x, y int, monitor *Monitor) (int, int) {

View File

@ -322,8 +322,9 @@ func (w *glfwWindow) Position() (int, int) {
}
wx -= m.boundsInGLFWPixels.Min.X
wy -= m.boundsInGLFWPixels.Min.Y
xf := dipFromGLFWPixel(float64(wx), m)
yf := dipFromGLFWPixel(float64(wy), m)
s := m.DeviceScaleFactor()
xf := dipFromGLFWPixel(float64(wx), s)
yf := dipFromGLFWPixel(float64(wy), s)
x, y = int(xf), int(yf)
})
return x, y

View File

@ -113,6 +113,7 @@ const (
KeyF24 Key = Key(ui.KeyF24)
KeyHome Key = Key(ui.KeyHome)
KeyInsert Key = Key(ui.KeyInsert)
KeyIntlBackslash Key = Key(ui.KeyIntlBackslash)
KeyMetaLeft Key = Key(ui.KeyMetaLeft)
KeyMetaRight Key = Key(ui.KeyMetaRight)
KeyMinus Key = Key(ui.KeyMinus)
@ -365,6 +366,8 @@ func (k Key) isValid() bool {
return true
case KeyInsert:
return true
case KeyIntlBackslash:
return true
case KeyMeta:
return true
case KeyMetaLeft:
@ -618,6 +621,8 @@ func (k Key) String() string {
return "Home"
case KeyInsert:
return "Insert"
case KeyIntlBackslash:
return "IntlBackslash"
case KeyMeta:
return "Meta"
case KeyMetaLeft:
@ -892,6 +897,8 @@ func keyNameToKeyCode(name string) (Key, bool) {
return KeyHome, true
case "insert":
return KeyInsert, true
case "intlbackslash":
return KeyIntlBackslash, true
case "kp0":
return KeyKP0, true
case "kp1":

View File

@ -117,7 +117,7 @@ var iosKeyToUIKey = map[int]ui.Key{
97: ui.KeyNumpad9,
98: ui.KeyNumpad0,
99: ui.KeyNumpadDecimal,
100: ui.KeyBackslash,
100: ui.KeyIntlBackslash,
103: ui.KeyNumpadEqual,
104: ui.KeyF13,
105: ui.KeyF14,

View File

@ -2394,3 +2394,96 @@ func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
}
}
}
// Issue #2923
func TestShaderReturnArray(t *testing.T) {
const w, h = 16, 16
dst := ebiten.NewImage(w, h)
s, err := ebiten.NewShader([]byte(`//kage:unit pixels
package main
func foo() [4]float {
return [4]float{0.25, 0.5, 0.75, 1}
}
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
a := foo()
return vec4(a[0], a[1], a[2], a[3])
}
`))
if err != nil {
t.Fatal(err)
}
dst.DrawRectShader(w, h, s, nil)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
got := dst.At(i, j).(color.RGBA)
want := color.RGBA{R: 0x40, G: 0x80, B: 0xc0, A: 0xff}
if !sameColors(got, want, 2) {
t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}
// Issue #2798
func TestShaderInvalidPremultipliedAlphaColor(t *testing.T) {
// This test checks the rendering result when the shader returns an invalid premultiplied alpha color.
// The result values are kept and not clamped.
const w, h = 16, 16
dst := ebiten.NewImage(w, h)
s, err := ebiten.NewShader([]byte(`//kage:unit pixels
package main
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
return vec4(1, 0.75, 0.5, 0.25)
}
`))
if err != nil {
t.Fatal(err)
}
dst.DrawRectShader(w, h, s, nil)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
got := dst.At(i, j).(color.RGBA)
want := color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0x40}
if !sameColors(got, want, 2) {
t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
dst.Clear()
s, err = ebiten.NewShader([]byte(`//kage:unit pixels
package main
func Fragment(dstPos vec4, srcPos vec2, color vec4) vec4 {
return vec4(1, 0.75, 0.5, 0)
}
`))
if err != nil {
t.Fatal(err)
}
dst.DrawRectShader(w, h, s, nil)
for j := 0; j < h; j++ {
for i := 0; i < w; i++ {
got := dst.At(i, j).(color.RGBA)
want := color.RGBA{R: 0xff, G: 0xc0, B: 0x80, A: 0}
if !sameColors(got, want, 2) {
t.Errorf("dst.At(%d, %d): got: %v, want: %v", i, j, got, want)
}
}
}
}

View File

@ -25,29 +25,29 @@ import (
"github.com/hajimehoshi/ebiten/v2/vector"
)
var _ Face = (*StdFace)(nil)
var _ Face = (*GoXFace)(nil)
type stdFaceGlyphImageCacheKey struct {
type goXFaceGlyphImageCacheKey struct {
rune rune
xoffset fixed.Int26_6
// yoffset is always the same if the rune is the same, so this doesn't have to be a key.
}
// StdFace is a Face implementation for a semi-standard font.Face (golang.org/x/image/font).
// StdFace is useful to transit from existing codebase with text v1, or to use some bitmap fonts defined as font.Face.
// StdFace must not be copied by value.
type StdFace struct {
// GoXFace is a Face implementation for a semi-standard font.Face (golang.org/x/image/font).
// GoXFace is useful to transit from existing codebase with text v1, or to use some bitmap fonts defined as font.Face.
// GoXFace must not be copied by value.
type GoXFace struct {
f *faceWithCache
glyphImageCache glyphImageCache[stdFaceGlyphImageCacheKey]
glyphImageCache glyphImageCache[goXFaceGlyphImageCacheKey]
addr *StdFace
addr *GoXFace
}
// NewStdFace creates a new StdFace from a semi-standard font.Face.
func NewStdFace(face font.Face) *StdFace {
s := &StdFace{
// NewGoXFace creates a new GoXFace from a semi-standard font.Face.
func NewGoXFace(face font.Face) *GoXFace {
s := &GoXFace{
f: &faceWithCache{
f: face,
},
@ -56,14 +56,14 @@ func NewStdFace(face font.Face) *StdFace {
return s
}
func (s *StdFace) copyCheck() {
func (s *GoXFace) copyCheck() {
if s.addr != s {
panic("text: illegal use of non-zero StdFace copied by value")
panic("text: illegal use of non-zero GoXFace copied by value")
}
}
// Metrics implements Face.
func (s *StdFace) Metrics() Metrics {
func (s *GoXFace) Metrics() Metrics {
s.copyCheck()
m := s.f.Metrics()
@ -77,24 +77,24 @@ func (s *StdFace) Metrics() Metrics {
// UnsafeInternal returns its internal font.Face.
//
// This is unsafe since this might make internal cache states out of sync.
func (s *StdFace) UnsafeInternal() font.Face {
func (s *GoXFace) UnsafeInternal() font.Face {
s.copyCheck()
return s.f.f
}
// advance implements Face.
func (s *StdFace) advance(text string) float64 {
func (s *GoXFace) advance(text string) float64 {
return fixed26_6ToFloat64(font.MeasureString(s.f, text))
}
// hasGlyph implements Face.
func (s *StdFace) hasGlyph(r rune) bool {
func (s *GoXFace) hasGlyph(r rune) bool {
_, ok := s.f.GlyphAdvance(r)
return ok
}
// appendGlyphsForLine implements Face.
func (s *StdFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph {
func (s *GoXFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset int, originX, originY float64) []Glyph {
s.copyCheck()
origin := fixed.Point26_6{
@ -129,8 +129,8 @@ func (s *StdFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i
return glyphs
}
func (s *StdFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int, int, fixed.Int26_6) {
// Assume that StdFace's direction is always horizontal.
func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int, int, fixed.Int26_6) {
// Assume that GoXFace's direction is always horizontal.
origin.X = adjustGranularity(origin.X, s)
origin.Y &^= ((1 << 6) - 1)
@ -139,7 +139,7 @@ func (s *StdFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int
X: (origin.X + b.Min.X) & ((1 << 6) - 1),
Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1),
}
key := stdFaceGlyphImageCacheKey{
key := goXFaceGlyphImageCacheKey{
rune: r,
xoffset: subpixelOffset.X,
}
@ -151,7 +151,7 @@ func (s *StdFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int
return img, imgX, imgY, a
}
func (s *StdFace) glyphImageImpl(r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *ebiten.Image {
func (s *GoXFace) glyphImageImpl(r rune, subpixelOffset fixed.Point26_6, glyphBounds fixed.Rectangle26_6) *ebiten.Image {
w, h := (glyphBounds.Max.X - glyphBounds.Min.X).Ceil(), (glyphBounds.Max.Y - glyphBounds.Min.Y).Ceil()
if w == 0 || h == 0 {
return nil
@ -179,14 +179,14 @@ func (s *StdFace) glyphImageImpl(r rune, subpixelOffset fixed.Point26_6, glyphBo
}
// direction implements Face.
func (s *StdFace) direction() Direction {
func (s *GoXFace) direction() Direction {
return DirectionLeftToRight
}
// appendVectorPathForLine implements Face.
func (s *StdFace) appendVectorPathForLine(path *vector.Path, line string, originX, originY float64) {
func (s *GoXFace) appendVectorPathForLine(path *vector.Path, line string, originX, originY float64) {
}
// Metrics implements Face.
func (s *StdFace) private() {
func (s *GoXFace) private() {
}

View File

@ -89,7 +89,7 @@ type LayoutOptions struct {
//
// For horizontal directions, the start and end depends on the face.
// If the face is GoTextFace, the start and the end depend on the Direction property.
// If the face is StdFace, the start and the end are always left and right respectively.
// If the face is GoXFace, the start and the end are always left and right respectively.
//
// For vertical directions, the start and end are top and bottom respectively.
//

View File

@ -20,6 +20,7 @@ import (
var _ Face = (*LimitedFace)(nil)
// LimitedFace is a Face with glyph limitations.
type LimitedFace struct {
face Face
unicodeRanges unicodeRanges

View File

@ -26,7 +26,7 @@ import (
)
func TestMultiFace(t *testing.T) {
faces := []text.Face{text.NewStdFace(bitmapfont.Face)}
faces := []text.Face{text.NewGoXFace(bitmapfont.Face)}
f, err := text.NewMultiFace(faces...)
if err != nil {
t.Fatal(err)

View File

@ -27,7 +27,7 @@ import (
)
// Face is an interface representing a font face.
// The implementations are only faces defined in this package, like GoTextFace and StdFace.
// The implementations are only faces defined in this package, like GoTextFace and GoXFace.
type Face interface {
// Metrics returns the metrics for this Face.
Metrics() Metrics
@ -59,15 +59,15 @@ type Metrics struct {
HDescent float64
// VLineGap is the recommended amount of horizontal space between two lines of text in pixels.
// If the face is StdFace or the font doesn't support a vertical direction, VLineGap is 0.
// If the face is GoXFace or the font doesn't support a vertical direction, VLineGap is 0.
VLineGap float64
// VAscent is the distance in pixels from the top of a line to its baseline for vertical lines.
// If the face is StdFace or the font doesn't support a vertical direction, VAscent is 0.
// If the face is GoXFace or the font doesn't support a vertical direction, VAscent is 0.
VAscent float64
// VDescent is the distance in pixels from the top of a line to its baseline for vertical lines.
// If the face is StdFace or the font doesn't support a vertical direction, VDescent is 0.
// If the face is GoXFace or the font doesn't support a vertical direction, VDescent is 0.
VDescent float64
}

View File

@ -38,7 +38,7 @@ func TestGlyphIndex(t *testing.T) {
const sampleText = `The quick brown fox jumps
over the lazy dog.`
f := text.NewStdFace(bitmapfont.Face)
f := text.NewGoXFace(bitmapfont.Face)
got := sampleText
for _, g := range text.AppendGlyphs(nil, sampleText, f, nil) {
got = got[:g.StartIndexInBytes] + strings.Repeat(" ", g.EndIndexInBytes-g.StartIndexInBytes) + got[g.EndIndexInBytes:]
@ -55,7 +55,7 @@ func TestTextColor(t *testing.T) {
op := &text.DrawOptions{}
op.GeoM.Translate(0, 0)
op.ColorScale.ScaleWithColor(clr)
text.Draw(img, "Hello", text.NewStdFace(bitmapfont.Face), op)
text.Draw(img, "Hello", text.NewGoXFace(bitmapfont.Face), op)
w, h := img.Bounds().Dx(), img.Bounds().Dy()
allTransparent := true
@ -77,12 +77,12 @@ func TestTextColor(t *testing.T) {
}
}
const testStdFaceSize = 6
const testGoXFaceSize = 6
type testStdFace struct{}
type testGoXFace struct{}
func (f *testStdFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
dr = image.Rect(0, 0, testStdFaceSize, testStdFaceSize)
func (f *testGoXFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
dr = image.Rect(0, 0, testGoXFaceSize, testGoXFaceSize)
a := image.NewAlpha(dr)
switch r {
case 'a':
@ -99,56 +99,56 @@ func (f *testStdFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, ma
}
}
mask = a
advance = fixed.I(testStdFaceSize)
advance = fixed.I(testGoXFaceSize)
ok = true
return
}
func (f *testStdFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
bounds = fixed.R(0, 0, testStdFaceSize, testStdFaceSize)
advance = fixed.I(testStdFaceSize)
func (f *testGoXFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
bounds = fixed.R(0, 0, testGoXFaceSize, testGoXFaceSize)
advance = fixed.I(testGoXFaceSize)
ok = true
return
}
func (f *testStdFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
return fixed.I(testStdFaceSize), true
func (f *testGoXFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
return fixed.I(testGoXFaceSize), true
}
func (f *testStdFace) Kern(r0, r1 rune) fixed.Int26_6 {
func (f *testGoXFace) Kern(r0, r1 rune) fixed.Int26_6 {
if r1 == 'b' {
return fixed.I(-testStdFaceSize)
return fixed.I(-testGoXFaceSize)
}
return 0
}
func (f *testStdFace) Close() error {
func (f *testGoXFace) Close() error {
return nil
}
func (f *testStdFace) Metrics() font.Metrics {
func (f *testGoXFace) Metrics() font.Metrics {
return font.Metrics{
Height: fixed.I(testStdFaceSize),
Height: fixed.I(testGoXFaceSize),
Ascent: 0,
Descent: fixed.I(testStdFaceSize),
Descent: fixed.I(testGoXFaceSize),
XHeight: 0,
CapHeight: fixed.I(testStdFaceSize),
CapHeight: fixed.I(testGoXFaceSize),
CaretSlope: image.Pt(0, 1),
}
}
// Issue #1378
func TestNegativeKern(t *testing.T) {
f := text.NewStdFace(&testStdFace{})
dst := ebiten.NewImage(testStdFaceSize*2, testStdFaceSize)
f := text.NewGoXFace(&testGoXFace{})
dst := ebiten.NewImage(testGoXFaceSize*2, testGoXFaceSize)
// With testStdFace, 'b' is rendered at the previous position as 0xff.
// With testGoXFace, 'b' is rendered at the previous position as 0xff.
// 'a' is rendered at the current position as 0x80.
op := &text.DrawOptions{}
op.GeoM.Translate(0, 0)
text.Draw(dst, "ab", f, op)
for j := 0; j < testStdFaceSize; j++ {
for i := 0; i < testStdFaceSize; i++ {
for j := 0; j < testGoXFaceSize; j++ {
for i := 0; i < testGoXFaceSize; i++ {
got := dst.At(i, j)
want := color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
if got != want {
@ -159,10 +159,10 @@ func TestNegativeKern(t *testing.T) {
// The glyph 'a' should be treated correctly.
op = &text.DrawOptions{}
op.GeoM.Translate(testStdFaceSize, 0)
op.GeoM.Translate(testGoXFaceSize, 0)
text.Draw(dst, "a", f, op)
for j := 0; j < testStdFaceSize; j++ {
for i := testStdFaceSize; i < testStdFaceSize*2; i++ {
for j := 0; j < testGoXFaceSize; j++ {
for i := testGoXFaceSize; i < testGoXFaceSize*2; i++ {
got := dst.At(i, j)
want := color.RGBA{R: 0x80, G: 0x80, B: 0x80, A: 0x80}
if got != want {
@ -172,12 +172,12 @@ func TestNegativeKern(t *testing.T) {
}
}
type unhashableStdFace func()
type unhashableGoXFace func()
const unhashableStdFaceSize = 10
const unhashableGoXFaceSize = 10
func (u *unhashableStdFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
dr = image.Rect(0, 0, unhashableStdFaceSize, unhashableStdFaceSize)
func (u *unhashableGoXFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
dr = image.Rect(0, 0, unhashableGoXFaceSize, unhashableGoXFaceSize)
a := image.NewAlpha(dr)
for j := dr.Min.Y; j < dr.Max.Y; j++ {
for i := dr.Min.X; i < dr.Max.X; i++ {
@ -185,53 +185,53 @@ func (u *unhashableStdFace) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectang
}
}
mask = a
advance = fixed.I(unhashableStdFaceSize)
advance = fixed.I(unhashableGoXFaceSize)
ok = true
return
}
func (u *unhashableStdFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
bounds = fixed.R(0, 0, unhashableStdFaceSize, unhashableStdFaceSize)
advance = fixed.I(unhashableStdFaceSize)
func (u *unhashableGoXFace) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
bounds = fixed.R(0, 0, unhashableGoXFaceSize, unhashableGoXFaceSize)
advance = fixed.I(unhashableGoXFaceSize)
ok = true
return
}
func (u *unhashableStdFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
return fixed.I(unhashableStdFaceSize), true
func (u *unhashableGoXFace) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
return fixed.I(unhashableGoXFaceSize), true
}
func (u *unhashableStdFace) Kern(r0, r1 rune) fixed.Int26_6 {
func (u *unhashableGoXFace) Kern(r0, r1 rune) fixed.Int26_6 {
return 0
}
func (u *unhashableStdFace) Close() error {
func (u *unhashableGoXFace) Close() error {
return nil
}
func (u *unhashableStdFace) Metrics() font.Metrics {
func (u *unhashableGoXFace) Metrics() font.Metrics {
return font.Metrics{
Height: fixed.I(unhashableStdFaceSize),
Height: fixed.I(unhashableGoXFaceSize),
Ascent: 0,
Descent: fixed.I(unhashableStdFaceSize),
Descent: fixed.I(unhashableGoXFaceSize),
XHeight: 0,
CapHeight: fixed.I(unhashableStdFaceSize),
CapHeight: fixed.I(unhashableGoXFaceSize),
CaretSlope: image.Pt(0, 1),
}
}
// Issue #2669
func TestUnhashableFace(t *testing.T) {
var face unhashableStdFace
f := text.NewStdFace(&face)
dst := ebiten.NewImage(unhashableStdFaceSize*2, unhashableStdFaceSize*2)
var face unhashableGoXFace
f := text.NewGoXFace(&face)
dst := ebiten.NewImage(unhashableGoXFaceSize*2, unhashableGoXFaceSize*2)
text.Draw(dst, "a", f, nil)
for j := 0; j < unhashableStdFaceSize*2; j++ {
for i := 0; i < unhashableStdFaceSize*2; i++ {
for j := 0; j < unhashableGoXFaceSize*2; j++ {
for i := 0; i < unhashableGoXFaceSize*2; i++ {
got := dst.At(i, j)
var want color.RGBA
if i < unhashableStdFaceSize && j < unhashableStdFaceSize {
if i < unhashableGoXFaceSize && j < unhashableGoXFaceSize {
want = color.RGBA{R: 0xff, G: 0xff, B: 0xff, A: 0xff}
}
if got != want {