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 { type Context struct {
playerFactory *playerFactory 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 sampleRate int
err error err error
ready bool ready bool
readyOnce sync.Once
playingPlayers map[*playerImpl]struct{} playingPlayers map[*playerImpl]struct{}
@ -100,7 +94,6 @@ func NewContext(sampleRate int) *Context {
sampleRate: sampleRate, sampleRate: sampleRate,
playerFactory: newPlayerFactory(sampleRate), playerFactory: newPlayerFactory(sampleRate),
playingPlayers: map[*playerImpl]struct{}{}, playingPlayers: map[*playerImpl]struct{}{},
inited: make(chan struct{}),
semaphore: make(chan struct{}, 1), semaphore: make(chan struct{}, 1),
} }
theContext = c theContext = c
@ -128,10 +121,6 @@ func NewContext(sampleRate int) *Context {
}) })
h.AppendHookOnBeforeUpdate(func() error { h.AppendHookOnBeforeUpdate(func() error {
c.initedOnce.Do(func() {
close(c.inited)
})
var err error var err error
theContextLock.Lock() theContextLock.Lock()
if theContext != nil { if theContext != nil {
@ -142,6 +131,19 @@ func NewContext(sampleRate int) *Context {
return err 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 { if err := c.updatePlayers(); err != nil {
return err return err
} }
@ -287,28 +289,7 @@ func (c *Context) updatePlayers() error {
func (c *Context) IsReady() bool { func (c *Context) IsReady() bool {
c.m.Lock() c.m.Lock()
defer c.m.Unlock() defer c.m.Unlock()
return c.ready
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
} }
// SampleRate returns the sample rate. // SampleRate returns the sample rate.
@ -316,18 +297,6 @@ func (c *Context) SampleRate() int {
return c.sampleRate 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. // Player is an audio player which has one stream.
// //
// Even when all references to a Player object is gone, // 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 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. // contextProxy is a proxy between otoContext and context.
type contextProxy struct { type contextProxy struct {
otoContext *oto.Context
} }
// NewPlayer implements context. // NewPlayer implements context.
func (c *contextProxy) NewPlayer(r io.Reader) player { 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 p.adjustedPosition = 0
return return
} }
if !p.context.IsReady() {
p.adjustedPosition = 0
return
}
samples := (p.stream.position() - int64(p.player.BufferedSize())) / bytesPerSampleInt16 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); int axisMask = getAxisMask(inputDevice);
Ebitenmobileview.onGamepadAdded(deviceId, inputDevice.getName(), gamepad.axes.size(), gamepad.hats.size()/2, descriptor, vendorId, productId, buttonMask, axisMask); 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: // The implementation is copied from SDL:

View File

@ -347,7 +347,8 @@ func (p *Player) draw(screen *ebiten.Image) {
// Compose the current time text. // Compose the current time text.
m := (c / time.Minute) % 100 m := (c / time.Minute) % 100
s := (c / time.Second) % 60 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 // Draw buttons
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}

View File

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

View File

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

View File

@ -32,7 +32,7 @@ import (
"github.com/hajimehoshi/ebiten/v2/vector" "github.com/hajimehoshi/ebiten/v2/vector"
) )
var fontFace = text.NewStdFace(bitmapfont.FaceEA) var fontFace = text.NewGoXFace(bitmapfont.FaceEA)
const ( const (
screenWidth = 640 screenWidth = 640
@ -40,16 +40,9 @@ const (
) )
type TextField struct { type TextField struct {
bounds image.Rectangle bounds image.Rectangle
multilines bool multilines bool
text string field textinput.Field
selectionStart int
selectionEnd int
focused bool
ch chan textinput.State
end func()
state textinput.State
} }
func NewTextField(bounds image.Rectangle, multilines bool) *TextField { 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 { func (t *TextField) SetSelectionStartByCursorPosition(x, y int) bool {
t.cleanUp()
idx, ok := t.textIndexByCursorPosition(x, y) idx, ok := t.textIndexByCursorPosition(x, y)
if !ok { if !ok {
return false return false
} }
t.selectionStart = idx t.field.SetSelection(idx, idx)
t.selectionEnd = idx
return true return true
} }
@ -95,20 +86,21 @@ func (t *TextField) textIndexByCursorPosition(x, y int) (int, bool) {
var nlCount int var nlCount int
var lineStart int var lineStart int
var prevAdvance float64 var prevAdvance float64
for i, r := range t.text { txt := t.field.Text()
for i, r := range txt {
var x0, x1 int var x0, x1 int
currentAdvance := text.Advance(t.text[lineStart:i], fontFace) currentAdvance := text.Advance(txt[lineStart:i], fontFace)
if lineStart < i { if lineStart < i {
x0 = int((prevAdvance + currentAdvance) / 2) x0 = int((prevAdvance + currentAdvance) / 2)
} }
if r == '\n' { if r == '\n' {
x1 = int(math.MaxInt32) x1 = int(math.MaxInt32)
} else if i < len(t.text) { } else if i < len(txt) {
nextI := i + 1 nextI := i + 1
for !utf8.ValidString(t.text[i:nextI]) { for !utf8.ValidString(txt[i:nextI]) {
nextI++ nextI++
} }
nextAdvance := text.Advance(t.text[lineStart:nextI], fontFace) nextAdvance := text.Advance(txt[lineStart:nextI], fontFace)
x1 = int((currentAdvance + nextAdvance) / 2) x1 = int((currentAdvance + nextAdvance) / 2)
} else { } else {
x1 = int(currentAdvance) 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() { func (t *TextField) Focus() {
t.focused = true t.field.Focus()
} }
func (t *TextField) Blur() { func (t *TextField) Blur() {
t.focused = false t.field.Blur()
} }
func (t *TextField) cleanUp() { func (t *TextField) Update() error {
if t.ch != nil { if !t.field.IsFocused() {
select { return nil
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
} }
var processed bool x, y := t.bounds.Min.X, t.bounds.Min.Y
cx, cy := t.cursorPos()
// Text inputting can happen multiple times in one tick (1/60[s] by default). px, py := textFieldPadding()
// Handle all of them. x += cx + px
for { y += cy + py + int(fontFace.Metrics().HAscent)
if t.ch == nil { handled, err := t.field.HandleInput(x, y)
x, y := t.bounds.Min.X, t.bounds.Min.Y if err != nil {
cx, cy := t.cursorPos() return err
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
} }
if handled {
if processed { return nil
return
} }
switch { switch {
case inpututil.IsKeyJustPressed(ebiten.KeyEnter): case inpututil.IsKeyJustPressed(ebiten.KeyEnter):
if t.multilines { if t.multilines {
t.text = t.text[:t.selectionStart] + "\n" + t.text[t.selectionEnd:] text := t.field.Text()
t.selectionStart += 1 selectionStart, selectionEnd := t.field.Selection()
t.selectionEnd = t.selectionStart text = text[:selectionStart] + "\n" + text[selectionEnd:]
selectionStart += len("\n")
selectionEnd = selectionStart
t.field.SetTextAndSelection(text, selectionStart, selectionEnd)
} }
case inpututil.IsKeyJustPressed(ebiten.KeyBackspace): 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. // TODO: Remove a grapheme instead of a code point.
_, l := utf8.DecodeLastRuneInString(t.text[:t.selectionStart]) _, l := utf8.DecodeLastRuneInString(text[:selectionStart])
t.text = t.text[:t.selectionStart-l] + t.text[t.selectionEnd:] text = text[:selectionStart-l] + text[selectionEnd:]
t.selectionStart -= l selectionStart -= l
} }
t.selectionEnd = t.selectionStart selectionEnd = selectionStart
t.field.SetTextAndSelection(text, selectionStart, selectionEnd)
case inpututil.IsKeyJustPressed(ebiten.KeyLeft): 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. // TODO: Remove a grapheme instead of a code point.
_, l := utf8.DecodeLastRuneInString(t.text[:t.selectionStart]) _, l := utf8.DecodeLastRuneInString(text[:selectionStart])
t.selectionStart -= l selectionStart -= l
} }
t.selectionEnd = t.selectionStart t.field.SetTextAndSelection(text, selectionStart, selectionStart)
case inpututil.IsKeyJustPressed(ebiten.KeyRight): 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. // TODO: Remove a grapheme instead of a code point.
_, l := utf8.DecodeRuneInString(t.text[t.selectionEnd:]) _, l := utf8.DecodeRuneInString(text[selectionEnd:])
t.selectionEnd += l selectionEnd += l
} }
t.selectionStart = t.selectionEnd t.field.SetTextAndSelection(text, selectionEnd, selectionEnd)
} }
if !t.multilines { if !t.multilines {
orig := t.text orig := t.field.Text()
new := strings.ReplaceAll(orig, "\n", "") new := strings.ReplaceAll(orig, "\n", "")
if new != orig { if new != orig {
t.selectionStart -= strings.Count(orig[:t.selectionStart], "\n") selectionStart, selectionEnd := t.field.Selection()
t.selectionEnd -= strings.Count(orig[:t.selectionEnd], "\n") 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) { func (t *TextField) cursorPos() (int, int) {
var nlCount int var nlCount int
lastNLPos := -1 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' { if r == '\n' {
nlCount++ nlCount++
lastNLPos = i lastNLPos = i
} }
} }
txt := t.text[lastNLPos+1 : t.selectionStart] txt = txt[lastNLPos+1:]
if t.state.Text != "" {
txt += t.state.Text[:t.state.CompositionSelectionStartInBytes]
}
x := int(text.Advance(txt, fontFace)) x := int(text.Advance(txt, fontFace))
y := nlCount * int(fontFace.Metrics().HLineGap+fontFace.Metrics().HAscent+fontFace.Metrics().HDescent) y := nlCount * int(fontFace.Metrics().HLineGap+fontFace.Metrics().HAscent+fontFace.Metrics().HDescent)
return x, y return x, y
@ -282,13 +228,14 @@ func (t *TextField) cursorPos() (int, int) {
func (t *TextField) Draw(screen *ebiten.Image) { 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) 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 var clr color.Color = color.Black
if t.focused { if t.field.IsFocused() {
clr = color.RGBA{0, 0, 0xff, 0xff} 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) 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() 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 x, y := t.bounds.Min.X, t.bounds.Min.Y
cx, cy := t.cursorPos() cx, cy := t.cursorPos()
x += px + cx 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) 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 tx := t.bounds.Min.X + px
ty := t.bounds.Min.Y + py ty := t.bounds.Min.Y + py
op := &text.DrawOptions{} op := &text.DrawOptions{}
op.GeoM.Translate(float64(tx), float64(ty)) op.GeoM.Translate(float64(tx), float64(ty))
op.ColorScale.ScaleWithColor(color.Black) op.ColorScale.ScaleWithColor(color.Black)
op.LineSpacing = fontFace.Metrics().HLineGap + fontFace.Metrics().HAscent + fontFace.Metrics().HDescent 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 const textFieldHeight = 24
@ -348,7 +290,9 @@ func (g *Game) Update() error {
} }
for _, tf := range g.textFields { for _, tf := range g.textFields {
tf.Update() if err := tf.Update(); err != nil {
return err
}
} }
x, y := ebiten.CursorPosition() 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 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 { type State struct {
// Text represents the current inputting text. // Text represents the current inputting text.
Text string Text string
@ -37,14 +39,19 @@ type State struct {
// Committed reports whether the current Text is the settled text. // Committed reports whether the current Text is the settled text.
Committed bool Committed bool
// Error is an error that happens during text inputting.
Error error
} }
// Start starts text inputting. // Start starts text inputting.
// Start returns a channel to send the state repeatedly, and a function to end the 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. // Start returns nil and nil if the current environment doesn't support this package.
func Start(x, y int) (states chan State, close func()) { 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)) 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()) { func (t *textInput) Start(x, y int) (chan State, func()) {
var session *session var session *session
ui.Get().RunOnMainThread(func() { ui.Get().RunOnMainThread(func() {
if t.session != nil { t.end()
t.session.end()
t.session = nil
}
C.start(C.int(x), C.int(y)) C.start(C.int(x), C.int(y))
session = newSession() session = newSession()
t.session = session t.session = session

View File

@ -12,7 +12,7 @@
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
//go:build (!darwin && !js) || ios //go:build (!darwin && !js && !windows) || ios
package textinput 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", "NumpadSubtract": "KPSubtract",
"NumpadEnter": "KPEnter", "NumpadEnter": "KPEnter",
"NumpadEqual": "KPEqual", "NumpadEqual": "KPEqual",
"IntlBackslash": "World1",
} }
// https://developer.android.com/reference/android/view/KeyEvent // 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{ androidKeyToUIKeyName = map[int]string{
55: "Comma", 55: "Comma",
56: "Period", 56: "Period",
@ -204,16 +212,16 @@ func init() {
0x35: "Backquote", 0x35: "Backquote",
// These three keys are: // These three keys are:
// - US backslash-pipe key (above return), // - US backslash-pipe key, and
// - non-US backslash key (next to left shift; on German layout this is the <>| key), and
// - non-US hashmark key (bottom left of return; on German layout, this is the #' key). // - 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. // On US layout configurations, they all map to the same characters - the backslash.
// //
// See also: https://www.w3.org/TR/uievents-code/#keyboard-102 // See also: https://www.w3.org/TR/uievents-code/#keyboard-102
0x31: "Backslash", // UIKeyboardHIDUsageKeyboardBackslash 0x31: "Backslash", // UIKeyboardHIDUsageKeyboardBackslash
0x64: "Backslash", // UIKeyboardHIDUsageKeyboardNonUSBackslash
0x32: "Backslash", // UIKeyboardHIDUsageKeyboardNonUSPound 0x32: "Backslash", // UIKeyboardHIDUsageKeyboardNonUSPound
0x64: "IntlBackslash", // UIKeyboardHIDUsageKeyboardNonUSBackslash
0x2A: "Backspace", 0x2A: "Backspace",
0x2F: "BracketLeft", 0x2F: "BracketLeft",
0x30: "BracketRight", 0x30: "BracketRight",
@ -326,6 +334,7 @@ func init() {
"NumpadEqual": "NumpadEqual", "NumpadEqual": "NumpadEqual",
"MetaLeft": "MetaLeft", "MetaLeft": "MetaLeft",
"MetaRight": "MetaRight", "MetaRight": "MetaRight",
"IntlBackslash": "IntlBackslash",
} }
const ( const (

8
go.mod
View File

@ -3,11 +3,11 @@ module github.com/hajimehoshi/ebiten/v2
go 1.18 go 1.18
require ( 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/hideconsole v1.0.0
github.com/ebitengine/oto/v3 v3.2.0-alpha.4 github.com/ebitengine/oto/v3 v3.2.0-alpha.4
github.com/ebitengine/purego v0.7.0-alpha.1 github.com/ebitengine/purego v0.7.0-alpha.3
github.com/go-text/typesetting v0.1.1-0.20231231232151-8d81c02dc157 github.com/go-text/typesetting v0.1.1-0.20240317203452-bc341f663203
github.com/hajimehoshi/bitmapfont/v3 v3.0.0 github.com/hajimehoshi/bitmapfont/v3 v3.0.0
github.com/hajimehoshi/go-mp3 v0.3.4 github.com/hajimehoshi/go-mp3 v0.3.4
github.com/jakecoffman/cp v1.2.1 github.com/jakecoffman/cp v1.2.1
@ -16,7 +16,7 @@ require (
github.com/kisielk/errcheck v1.6.3 github.com/kisielk/errcheck v1.6.3
golang.org/x/image v0.15.0 golang.org/x/image v0.15.0
golang.org/x/sync v0.6.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/text v0.14.0
golang.org/x/tools v0.18.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-20240318151619-0eadfb33c201 h1:QlcxUcnQjv62kMxsh0NAQpwE0P454ec2i82DxmthMeI=
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/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 h1:5J4U0kXF+pv/DhiXt5/lTz0eO5ogJ1iXb8Yj1yReDqE=
github.com/ebitengine/hideconsole v1.0.0/go.mod h1:hTTBTvVYWKBuxPr7peweneWdkUwEuHuB3C1R/ielR1A= 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 h1:aaUdcbEDUV1oErHDv/Cd0IAjQaQPChZuvO8Cn/kQHE8=
github.com/ebitengine/oto/v3 v3.2.0-alpha.4/go.mod h1:JtMbxJHZBDXfS8BmVYwzWk9Z6r7jsjwsHzOuZrEkfs4= 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.3 h1:9hH1aneqLaM3sM+PMUgRJVsMe2SqfVjZtV3DEzxBDJU=
github.com/ebitengine/purego v0.7.0-alpha.1/go.mod h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ= github.com/ebitengine/purego v0.7.0-alpha.3/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.20240317203452-bc341f663203 h1:dDnsjfTamLS74H+chc/GvLDFboY9BlK0Ztg5ibOzZ34=
github.com/go-text/typesetting v0.1.1-0.20231231232151-8d81c02dc157/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= 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-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= 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 h1:r2+6gYK38nfztS/et50gHAswb9hXgxXECYgE8Nczmi4=
github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA= github.com/hajimehoshi/bitmapfont/v3 v3.0.0/go.mod h1:+CxxG+uMmgU4mI2poq944i3uZ6UYFfAkj9V6WqmuvZA=
github.com/hajimehoshi/go-mp3 v0.3.4 h1:NUP7pBYH8OguP4diaTZ9wJbUbk3tC0KlfzsEpWmYj68= 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-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-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.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.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 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= 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. // 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. // When the image i is disposed, DrawTrianglesShader does nothing.
func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader *Shader, options *DrawTrianglesShaderOptions) { func (i *Image) DrawTrianglesShader(vertices []Vertex, indices []uint16, shader *Shader, options *DrawTrianglesShaderOptions) {
i.copyCheck() 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, // 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). // 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. // When the image i is disposed, DrawRectShader does nothing.
func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawRectShaderOptions) { func (i *Image) DrawRectShader(width, height int, shader *Shader, options *DrawRectShaderOptions) {
i.copyCheck() 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. // 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. // If the image is disposed, Set does nothing.
func (i *Image) Set(x, y int, clr color.Color) { func (i *Image) Set(x, y int, clr color.Color) {
i.copyCheck() i.copyCheck()
@ -1028,6 +1037,9 @@ func (i *Image) Deallocate() {
// //
// WritePixels also works on a sub-image. // 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. // When the image is disposed, WritePixels does nothing.
func (i *Image) WritePixels(pixels []byte) { func (i *Image) WritePixels(pixels []byte) {
i.copyCheck() 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 := g.add(name, sdlID)
gp.native = &nativeGamepadImpl{ gp.native = &nativeGamepadImpl{
androidDeviceID: androidDeviceID, androidDeviceID: androidDeviceID,
axesReady: make([]bool, axisCount),
axes: make([]float64, axisCount), axes: make([]float64, axisCount),
buttons: make([]bool, gamepaddb.SDLControllerButtonMax+1), buttons: make([]bool, gamepaddb.SDLControllerButtonMax+1),
hats: make([]int, hatCount), hats: make([]int, hatCount),
@ -108,6 +109,13 @@ func (g *Gamepad) updateAndroidGamepadAxis(axis int, value float64) {
return return
} }
n.axes[axis] = value 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) { func (g *Gamepad) updateAndroidGamepadButton(button Button, pressed bool) {

View File

@ -243,6 +243,7 @@ type nativeGamepad interface {
axisCount() int axisCount() int
buttonCount() int buttonCount() int
hatCount() int hatCount() int
isAxisReady(axis int) bool
axisValue(axis int) float64 axisValue(axis int) float64
buttonValue(button int) float64 buttonValue(button int) float64
isButtonPressed(button int) bool isButtonPressed(button int) bool
@ -296,6 +297,14 @@ func (g *Gamepad) HatCount() int {
return g.native.hatCount() 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. // Axis is concurrent-safe.
func (g *Gamepad) Axis(axis int) float64 { func (g *Gamepad) Axis(axis int) float64 {
g.m.Lock() g.m.Lock()
@ -356,8 +365,13 @@ func (g *Gamepad) IsStandardButtonAvailable(button gamepaddb.StandardButton) boo
// StandardAxisValue is concurrent-safe. // StandardAxisValue is concurrent-safe.
func (g *Gamepad) StandardAxisValue(axis gamepaddb.StandardAxis) float64 { func (g *Gamepad) StandardAxisValue(axis gamepaddb.StandardAxis) float64 {
if gamepaddb.HasStandardLayoutMapping(g.sdlID) { 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 { if m := g.native.standardAxisInOwnMapping(axis); m != nil {
return m.Value()*2 - 1 return m.Value()*2 - 1
} }
@ -367,8 +381,13 @@ func (g *Gamepad) StandardAxisValue(axis gamepaddb.StandardAxis) float64 {
// StandardButtonValue is concurrent-safe. // StandardButtonValue is concurrent-safe.
func (g *Gamepad) StandardButtonValue(button gamepaddb.StandardButton) float64 { func (g *Gamepad) StandardButtonValue(button gamepaddb.StandardButton) float64 {
if gamepaddb.HasStandardLayoutMapping(g.sdlID) { 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 { if m := g.native.standardButtonInOwnMapping(button); m != nil {
return m.Value() return m.Value()
} }
@ -378,8 +397,13 @@ func (g *Gamepad) StandardButtonValue(button gamepaddb.StandardButton) float64 {
// IsStandardButtonPressed is concurrent-safe. // IsStandardButtonPressed is concurrent-safe.
func (g *Gamepad) IsStandardButtonPressed(button gamepaddb.StandardButton) bool { func (g *Gamepad) IsStandardButtonPressed(button gamepaddb.StandardButton) bool {
if gamepaddb.HasStandardLayoutMapping(g.sdlID) { 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 { if m := g.native.standardButtonInOwnMapping(button); m != nil {
return m.Pressed() return m.Pressed()
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -120,19 +120,19 @@ const (
type mapping struct { type mapping struct {
Type mappingType Type mappingType
Index int Index int
AxisScale int AxisScale float64
AxisOffset int AxisOffset float64
HatState int HatState int
} }
var ( var (
gamepadNames = map[string]string{} gamepadNames = map[string]string{}
gamepadButtonMappings = map[string]map[StandardButton]*mapping{} gamepadButtonMappings = map[string]map[StandardButton]mapping{}
gamepadAxisMappings = map[string]map[StandardAxis]*mapping{} gamepadAxisMappings = map[string]map[StandardAxis]mapping{}
mappingsM sync.RWMutex 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) line = strings.TrimSpace(line)
if len(line) == 0 { if len(line) == 0 {
return "", "", nil, nil, nil 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 b, ok := toStandardGamepadButton(tks[0]); ok {
if buttons == nil { if buttons == nil {
buttons = map[StandardButton]*mapping{} buttons = map[StandardButton]mapping{}
} }
buttons[b] = gb buttons[b] = gb
continue continue
@ -200,7 +200,7 @@ func parseLine(line string, platform platform) (id string, name string, buttons
if a, ok := toStandardGamepadAxis(tks[0]); ok { if a, ok := toStandardGamepadAxis(tks[0]); ok {
if axes == nil { if axes == nil {
axes = map[StandardAxis]*mapping{} axes = map[StandardAxis]mapping{}
} }
axes[a] = gb axes[a] = gb
continue continue
@ -213,7 +213,7 @@ func parseLine(line string, platform platform) (id string, name string, buttons
return tokens[0], tokens[1], buttons, axes, nil return tokens[0], tokens[1], buttons, axes, nil
} }
func parseMappingElement(str string) (*mapping, error) { func parseMappingElement(str string) (mapping, error) {
switch { switch {
case str[0] == 'a' || strings.HasPrefix(str, "+a") || strings.HasPrefix(str, "-a"): case str[0] == 'a' || strings.HasPrefix(str, "+a") || strings.HasPrefix(str, "-a"):
var tilda bool var tilda bool
@ -222,8 +222,8 @@ func parseMappingElement(str string) (*mapping, error) {
tilda = true tilda = true
} }
min := -1 min := -1.0
max := 1 max := 1.0
numstr := str[1:] numstr := str[1:]
if str[0] == '+' { if str[0] == '+' {
@ -259,10 +259,10 @@ func parseMappingElement(str string) (*mapping, error) {
index, err := strconv.Atoi(numstr) index, err := strconv.Atoi(numstr)
if err != nil { if err != nil {
return nil, err return mapping{}, err
} }
return &mapping{ return mapping{
Type: mappingTypeAxis, Type: mappingTypeAxis,
Index: index, Index: index,
AxisScale: scale, AxisScale: scale,
@ -272,9 +272,9 @@ func parseMappingElement(str string) (*mapping, error) {
case str[0] == 'b': case str[0] == 'b':
index, err := strconv.Atoi(str[1:]) index, err := strconv.Atoi(str[1:])
if err != nil { if err != nil {
return nil, err return mapping{}, err
} }
return &mapping{ return mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: index, Index: index,
}, nil }, nil
@ -282,24 +282,24 @@ func parseMappingElement(str string) (*mapping, error) {
case str[0] == 'h': case str[0] == 'h':
tokens := strings.Split(str[1:], ".") tokens := strings.Split(str[1:], ".")
if len(tokens) < 2 { 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]) index, err := strconv.Atoi(tokens[0])
if err != nil { if err != nil {
return nil, err return mapping{}, err
} }
hat, err := strconv.Atoi(tokens[1]) hat, err := strconv.Atoi(tokens[1])
if err != nil { if err != nil {
return nil, err return mapping{}, err
} }
return &mapping{ return mapping{
Type: mappingTypeHat, Type: mappingTypeHat,
Index: index, Index: index,
HatState: hat, HatState: hat,
}, nil }, 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) { 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 { if m, ok := gamepadButtonMappings[id]; ok {
return m return m
} }
@ -370,7 +370,7 @@ func buttonMappings(id string) map[StandardButton]*mapping {
return nil return nil
} }
func axisMappings(id string) map[StandardAxis]*mapping { func axisMappings(id string) map[StandardAxis]mapping {
if m, ok := gamepadAxisMappings[id]; ok { if m, ok := gamepadAxisMappings[id]; ok {
return m return m
} }
@ -390,6 +390,7 @@ func HasStandardLayoutMapping(id string) bool {
} }
type GamepadState interface { type GamepadState interface {
IsAxisReady(index int) bool
Axis(index int) float64 Axis(index int) float64
Button(index int) bool Button(index int) bool
Hat(index int) int Hat(index int) int
@ -410,10 +411,11 @@ func HasStandardAxis(id string, axis StandardAxis) bool {
if mappings == nil { if mappings == nil {
return false 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() mappingsM.RLock()
defer mappingsM.RUnlock() defer mappingsM.RUnlock()
@ -422,14 +424,17 @@ func AxisValue(id string, axis StandardAxis, state GamepadState) float64 {
return 0 return 0
} }
mapping := mappings[axis] mapping, ok := mappings[axis]
if mapping == nil { if !ok {
return 0 return 0
} }
switch mapping.Type { switch mapping.Type {
case mappingTypeAxis: 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 { if v > 1 {
return 1 return 1
} else if v < -1 { } else if v < -1 {
@ -461,30 +466,34 @@ func HasStandardButton(id string, button StandardButton) bool {
if mappings == nil { if mappings == nil {
return false 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() mappingsM.RLock()
defer mappingsM.RUnlock() 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) mappings := buttonMappings(id)
if mappings == nil { if mappings == nil {
return 0 return 0
} }
mapping := mappings[button] mapping, ok := mappings[button]
if mapping == nil { if !ok {
return 0 return 0
} }
switch mapping.Type { switch mapping.Type {
case mappingTypeAxis: 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 { if v > 1 {
v = 1 v = 1
} else if 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. // Note: should be used with >, not >=, comparisons.
const ButtonPressedThreshold = 30.0 / 255.0 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() mappingsM.RLock()
defer mappingsM.RUnlock() defer mappingsM.RUnlock()
@ -522,14 +531,14 @@ func IsButtonPressed(id string, button StandardButton, state GamepadState) bool
return false return false
} }
mapping := mappings[button] mapping, ok := mappings[button]
if mapping == nil { if !ok {
return false return false
} }
switch mapping.Type { switch mapping.Type {
case mappingTypeAxis: case mappingTypeAxis:
v := buttonValue(id, button, state) v := standardButtonValue(id, button, state)
return v > ButtonPressedThreshold return v > ButtonPressedThreshold
case mappingTypeButton: case mappingTypeButton:
return state.Button(mapping.Index) return state.Button(mapping.Index)
@ -554,8 +563,8 @@ func Update(mappingData []byte) error {
type parsedLine struct { type parsedLine struct {
id string id string
name string name string
buttons map[StandardButton]*mapping buttons map[StandardButton]mapping
axes map[StandardAxis]*mapping axes map[StandardAxis]mapping
} }
var lines []parsedLine var lines []parsedLine
@ -609,44 +618,44 @@ func addAndroidDefaultMappings(id string) bool {
return false return false
} }
gamepadButtonMappings[id] = map[StandardButton]*mapping{} gamepadButtonMappings[id] = map[StandardButton]mapping{}
gamepadAxisMappings[id] = map[StandardAxis]*mapping{} gamepadAxisMappings[id] = map[StandardAxis]mapping{}
// For mappings, see mobile/ebitenmobileview/input_android.go. // For mappings, see mobile/ebitenmobileview/input_android.go.
if buttonMask&(1<<SDLControllerButtonA) != 0 { if buttonMask&(1<<SDLControllerButtonA) != 0 {
gamepadButtonMappings[id][StandardButtonRightBottom] = &mapping{ gamepadButtonMappings[id][StandardButtonRightBottom] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonA, Index: SDLControllerButtonA,
} }
} }
if buttonMask&(1<<SDLControllerButtonB) != 0 { if buttonMask&(1<<SDLControllerButtonB) != 0 {
gamepadButtonMappings[id][StandardButtonRightRight] = &mapping{ gamepadButtonMappings[id][StandardButtonRightRight] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonB, Index: SDLControllerButtonB,
} }
} else { } else {
// Use the back button as "B" for easy UI navigation with TV remotes. // Use the back button as "B" for easy UI navigation with TV remotes.
gamepadButtonMappings[id][StandardButtonRightRight] = &mapping{ gamepadButtonMappings[id][StandardButtonRightRight] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonBack, Index: SDLControllerButtonBack,
} }
buttonMask &^= uint16(1) << SDLControllerButtonBack buttonMask &^= uint16(1) << SDLControllerButtonBack
} }
if buttonMask&(1<<SDLControllerButtonX) != 0 { if buttonMask&(1<<SDLControllerButtonX) != 0 {
gamepadButtonMappings[id][StandardButtonRightLeft] = &mapping{ gamepadButtonMappings[id][StandardButtonRightLeft] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonX, Index: SDLControllerButtonX,
} }
} }
if buttonMask&(1<<SDLControllerButtonY) != 0 { if buttonMask&(1<<SDLControllerButtonY) != 0 {
gamepadButtonMappings[id][StandardButtonRightTop] = &mapping{ gamepadButtonMappings[id][StandardButtonRightTop] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonY, Index: SDLControllerButtonY,
} }
} }
if buttonMask&(1<<SDLControllerButtonBack) != 0 { if buttonMask&(1<<SDLControllerButtonBack) != 0 {
gamepadButtonMappings[id][StandardButtonCenterLeft] = &mapping{ gamepadButtonMappings[id][StandardButtonCenterLeft] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonBack, Index: SDLControllerButtonBack,
} }
@ -654,69 +663,69 @@ func addAndroidDefaultMappings(id string) bool {
if buttonMask&(1<<SDLControllerButtonGuide) != 0 { if buttonMask&(1<<SDLControllerButtonGuide) != 0 {
// TODO: If SDKVersion >= 30, add this code: // TODO: If SDKVersion >= 30, add this code:
// //
// gamepadButtonMappings[id][StandardButtonCenterCenter] = &mapping{ // gamepadButtonMappings[id][StandardButtonCenterCenter] = mapping{
// Type: mappingTypeButton, // Type: mappingTypeButton,
// Index: SDLControllerButtonGuide, // Index: SDLControllerButtonGuide,
// } // }
} }
if buttonMask&(1<<SDLControllerButtonStart) != 0 { if buttonMask&(1<<SDLControllerButtonStart) != 0 {
gamepadButtonMappings[id][StandardButtonCenterRight] = &mapping{ gamepadButtonMappings[id][StandardButtonCenterRight] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonStart, Index: SDLControllerButtonStart,
} }
} }
if buttonMask&(1<<SDLControllerButtonLeftStick) != 0 { if buttonMask&(1<<SDLControllerButtonLeftStick) != 0 {
gamepadButtonMappings[id][StandardButtonLeftStick] = &mapping{ gamepadButtonMappings[id][StandardButtonLeftStick] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonLeftStick, Index: SDLControllerButtonLeftStick,
} }
} }
if buttonMask&(1<<SDLControllerButtonRightStick) != 0 { if buttonMask&(1<<SDLControllerButtonRightStick) != 0 {
gamepadButtonMappings[id][StandardButtonRightStick] = &mapping{ gamepadButtonMappings[id][StandardButtonRightStick] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonRightStick, Index: SDLControllerButtonRightStick,
} }
} }
if buttonMask&(1<<SDLControllerButtonLeftShoulder) != 0 { if buttonMask&(1<<SDLControllerButtonLeftShoulder) != 0 {
gamepadButtonMappings[id][StandardButtonFrontTopLeft] = &mapping{ gamepadButtonMappings[id][StandardButtonFrontTopLeft] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonLeftShoulder, Index: SDLControllerButtonLeftShoulder,
} }
} }
if buttonMask&(1<<SDLControllerButtonRightShoulder) != 0 { if buttonMask&(1<<SDLControllerButtonRightShoulder) != 0 {
gamepadButtonMappings[id][StandardButtonFrontTopRight] = &mapping{ gamepadButtonMappings[id][StandardButtonFrontTopRight] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonRightShoulder, Index: SDLControllerButtonRightShoulder,
} }
} }
if buttonMask&(1<<SDLControllerButtonDpadUp) != 0 { if buttonMask&(1<<SDLControllerButtonDpadUp) != 0 {
gamepadButtonMappings[id][StandardButtonLeftTop] = &mapping{ gamepadButtonMappings[id][StandardButtonLeftTop] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonDpadUp, Index: SDLControllerButtonDpadUp,
} }
} }
if buttonMask&(1<<SDLControllerButtonDpadDown) != 0 { if buttonMask&(1<<SDLControllerButtonDpadDown) != 0 {
gamepadButtonMappings[id][StandardButtonLeftBottom] = &mapping{ gamepadButtonMappings[id][StandardButtonLeftBottom] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonDpadDown, Index: SDLControllerButtonDpadDown,
} }
} }
if buttonMask&(1<<SDLControllerButtonDpadLeft) != 0 { if buttonMask&(1<<SDLControllerButtonDpadLeft) != 0 {
gamepadButtonMappings[id][StandardButtonLeftLeft] = &mapping{ gamepadButtonMappings[id][StandardButtonLeftLeft] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonDpadLeft, Index: SDLControllerButtonDpadLeft,
} }
} }
if buttonMask&(1<<SDLControllerButtonDpadRight) != 0 { if buttonMask&(1<<SDLControllerButtonDpadRight) != 0 {
gamepadButtonMappings[id][StandardButtonLeftRight] = &mapping{ gamepadButtonMappings[id][StandardButtonLeftRight] = mapping{
Type: mappingTypeButton, Type: mappingTypeButton,
Index: SDLControllerButtonDpadRight, Index: SDLControllerButtonDpadRight,
} }
} }
if axisMask&(1<<SDLControllerAxisLeftX) != 0 { if axisMask&(1<<SDLControllerAxisLeftX) != 0 {
gamepadAxisMappings[id][StandardAxisLeftStickHorizontal] = &mapping{ gamepadAxisMappings[id][StandardAxisLeftStickHorizontal] = mapping{
Type: mappingTypeAxis, Type: mappingTypeAxis,
Index: SDLControllerAxisLeftX, Index: SDLControllerAxisLeftX,
AxisScale: 1, AxisScale: 1,
@ -724,7 +733,7 @@ func addAndroidDefaultMappings(id string) bool {
} }
} }
if axisMask&(1<<SDLControllerAxisLeftY) != 0 { if axisMask&(1<<SDLControllerAxisLeftY) != 0 {
gamepadAxisMappings[id][StandardAxisLeftStickVertical] = &mapping{ gamepadAxisMappings[id][StandardAxisLeftStickVertical] = mapping{
Type: mappingTypeAxis, Type: mappingTypeAxis,
Index: SDLControllerAxisLeftY, Index: SDLControllerAxisLeftY,
AxisScale: 1, AxisScale: 1,
@ -732,7 +741,7 @@ func addAndroidDefaultMappings(id string) bool {
} }
} }
if axisMask&(1<<SDLControllerAxisRightX) != 0 { if axisMask&(1<<SDLControllerAxisRightX) != 0 {
gamepadAxisMappings[id][StandardAxisRightStickHorizontal] = &mapping{ gamepadAxisMappings[id][StandardAxisRightStickHorizontal] = mapping{
Type: mappingTypeAxis, Type: mappingTypeAxis,
Index: SDLControllerAxisRightX, Index: SDLControllerAxisRightX,
AxisScale: 1, AxisScale: 1,
@ -740,7 +749,7 @@ func addAndroidDefaultMappings(id string) bool {
} }
} }
if axisMask&(1<<SDLControllerAxisRightY) != 0 { if axisMask&(1<<SDLControllerAxisRightY) != 0 {
gamepadAxisMappings[id][StandardAxisRightStickVertical] = &mapping{ gamepadAxisMappings[id][StandardAxisRightStickVertical] = mapping{
Type: mappingTypeAxis, Type: mappingTypeAxis,
Index: SDLControllerAxisRightY, Index: SDLControllerAxisRightY,
AxisScale: 1, AxisScale: 1,
@ -748,7 +757,7 @@ func addAndroidDefaultMappings(id string) bool {
} }
} }
if axisMask&(1<<SDLControllerAxisTriggerLeft) != 0 { if axisMask&(1<<SDLControllerAxisTriggerLeft) != 0 {
gamepadButtonMappings[id][StandardButtonFrontBottomLeft] = &mapping{ gamepadButtonMappings[id][StandardButtonFrontBottomLeft] = mapping{
Type: mappingTypeAxis, Type: mappingTypeAxis,
Index: SDLControllerAxisTriggerLeft, Index: SDLControllerAxisTriggerLeft,
AxisScale: 1, AxisScale: 1,
@ -756,7 +765,7 @@ func addAndroidDefaultMappings(id string) bool {
} }
} }
if axisMask&(1<<SDLControllerAxisTriggerRight) != 0 { if axisMask&(1<<SDLControllerAxisTriggerRight) != 0 {
gamepadButtonMappings[id][StandardButtonFrontBottomRight] = &mapping{ gamepadButtonMappings[id][StandardButtonFrontBottomRight] = mapping{
Type: mappingTypeAxis, Type: mappingTypeAxis,
Index: SDLControllerAxisTriggerRight, Index: SDLControllerAxisTriggerRight,
AxisScale: 1, 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 return nil, nil, nil, false
} }
stmts = append(stmts, ss...) 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] lhst := ts[0]
rhs, ts, ss, ok := cs.parseExpr(block, fname, e.Y, markLocalVariableUsed) 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. // Process the expression as a regular function call.
var t shaderir.Type var finalType shaderir.Type
switch callee.BuiltinFunc { switch callee.BuiltinFunc {
case shaderir.BoolF: case shaderir.BoolF:
if err := checkArgsForBoolBuiltinFunc(args, argts); err != nil { if err := checkArgsForBoolBuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false return nil, nil, nil, false
} }
t = shaderir.Type{Main: shaderir.Bool} finalType = shaderir.Type{Main: shaderir.Bool}
case shaderir.IntF: case shaderir.IntF:
if err := checkArgsForIntBuiltinFunc(args, argts); err != nil { if err := checkArgsForIntBuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false return nil, nil, nil, false
} }
t = shaderir.Type{Main: shaderir.Int} finalType = shaderir.Type{Main: shaderir.Int}
case shaderir.FloatF: case shaderir.FloatF:
if err := checkArgsForFloatBuiltinFunc(args, argts); err != nil { if err := checkArgsForFloatBuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false return nil, nil, nil, false
} }
t = shaderir.Type{Main: shaderir.Float} finalType = shaderir.Type{Main: shaderir.Float}
case shaderir.Vec2F: case shaderir.Vec2F:
if err := checkArgsForVec2BuiltinFunc(args, argts); err != nil { if err := checkArgsForVec2BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) 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) args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float} argts[i] = shaderir.Type{Main: shaderir.Float}
} }
t = shaderir.Type{Main: shaderir.Vec2} finalType = shaderir.Type{Main: shaderir.Vec2}
case shaderir.Vec3F: case shaderir.Vec3F:
if err := checkArgsForVec3BuiltinFunc(args, argts); err != nil { if err := checkArgsForVec3BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) 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) args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float} argts[i] = shaderir.Type{Main: shaderir.Float}
} }
t = shaderir.Type{Main: shaderir.Vec3} finalType = shaderir.Type{Main: shaderir.Vec3}
case shaderir.Vec4F: case shaderir.Vec4F:
if err := checkArgsForVec4BuiltinFunc(args, argts); err != nil { if err := checkArgsForVec4BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) 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) args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float} argts[i] = shaderir.Type{Main: shaderir.Float}
} }
t = shaderir.Type{Main: shaderir.Vec4} finalType = shaderir.Type{Main: shaderir.Vec4}
case shaderir.IVec2F: case shaderir.IVec2F:
if err := checkArgsForIVec2BuiltinFunc(args, argts); err != nil { if err := checkArgsForIVec2BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false return nil, nil, nil, false
} }
t = shaderir.Type{Main: shaderir.IVec2} finalType = shaderir.Type{Main: shaderir.IVec2}
case shaderir.IVec3F: case shaderir.IVec3F:
if err := checkArgsForIVec3BuiltinFunc(args, argts); err != nil { if err := checkArgsForIVec3BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false return nil, nil, nil, false
} }
t = shaderir.Type{Main: shaderir.IVec3} finalType = shaderir.Type{Main: shaderir.IVec3}
case shaderir.IVec4F: case shaderir.IVec4F:
if err := checkArgsForIVec4BuiltinFunc(args, argts); err != nil { if err := checkArgsForIVec4BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) cs.addError(e.Pos(), err.Error())
return nil, nil, nil, false return nil, nil, nil, false
} }
t = shaderir.Type{Main: shaderir.IVec4} finalType = shaderir.Type{Main: shaderir.IVec4}
case shaderir.Mat2F: case shaderir.Mat2F:
if err := checkArgsForMat2BuiltinFunc(args, argts); err != nil { if err := checkArgsForMat2BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) 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) args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float} argts[i] = shaderir.Type{Main: shaderir.Float}
} }
t = shaderir.Type{Main: shaderir.Mat2} finalType = shaderir.Type{Main: shaderir.Mat2}
case shaderir.Mat3F: case shaderir.Mat3F:
if err := checkArgsForMat3BuiltinFunc(args, argts); err != nil { if err := checkArgsForMat3BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) 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) args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float} argts[i] = shaderir.Type{Main: shaderir.Float}
} }
t = shaderir.Type{Main: shaderir.Mat3} finalType = shaderir.Type{Main: shaderir.Mat3}
case shaderir.Mat4F: case shaderir.Mat4F:
if err := checkArgsForMat4BuiltinFunc(args, argts); err != nil { if err := checkArgsForMat4BuiltinFunc(args, argts); err != nil {
cs.addError(e.Pos(), err.Error()) 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) args[i].Const = gconstant.ToFloat(args[i].Const)
argts[i] = shaderir.Type{Main: shaderir.Float} argts[i] = shaderir.Type{Main: shaderir.Float}
} }
t = shaderir.Type{Main: shaderir.Mat4} finalType = shaderir.Type{Main: shaderir.Mat4}
case shaderir.TexelAt: case shaderir.TexelAt:
if len(args) != 2 { if len(args) != 2 {
cs.addError(e.Pos(), fmt.Sprintf("number of %s's arguments must be 2 but %d", callee.BuiltinFunc, len(args))) 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)) 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 return nil, nil, nil, false
} }
t = shaderir.Type{Main: shaderir.Vec4} finalType = shaderir.Type{Main: shaderir.Vec4}
case shaderir.DiscardF: case shaderir.DiscardF:
if len(args) != 0 { if len(args) != 0 {
cs.addError(e.Pos(), fmt.Sprintf("number of %s's arguments must be 0 but %d", callee.BuiltinFunc, len(args))) 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 { switch callee.BuiltinFunc {
case shaderir.Clamp: case shaderir.Clamp:
if kind, allConsts := resolveConstKind(args, argts); allConsts { if kind, _ := resolveConstKind(args, argts); kind != gconstant.Unknown {
switch kind { 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: case gconstant.Int:
for i, arg := range args { 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) v := gconstant.ToInt(arg.Const)
if v.Kind() == gconstant.Unknown { if v.Kind() == gconstant.Unknown {
cs.addError(e.Pos(), fmt.Sprintf("cannot convert %s to type int", arg.Const.String())) 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: case gconstant.Float:
for i, arg := range args { 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) v := gconstant.ToFloat(arg.Const)
if v.Kind() == gconstant.Unknown { if v.Kind() == gconstant.Unknown {
cs.addError(e.Pos(), fmt.Sprintf("cannot convert %s to type float", arg.Const.String())) 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 { switch callee.BuiltinFunc {
case shaderir.Smoothstep: case shaderir.Smoothstep:
t = argts[2] finalType = argts[2]
default: 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: 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 { switch callee.BuiltinFunc {
case shaderir.Min, shaderir.Max: case shaderir.Min, shaderir.Max:
if kind, allConsts := resolveConstKind(args, argts); allConsts { if kind, _ := resolveConstKind(args, argts); kind != gconstant.Unknown {
switch kind { 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: case gconstant.Int:
for i, arg := range args { 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) v := gconstant.ToInt(arg.Const)
if v.Kind() == gconstant.Unknown { if v.Kind() == gconstant.Unknown {
cs.addError(e.Pos(), fmt.Sprintf("cannot convert %s to type int", arg.Const.String())) 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: case gconstant.Float:
for i, arg := range args { 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) v := gconstant.ToFloat(arg.Const)
if v.Kind() == gconstant.Unknown { if v.Kind() == gconstant.Unknown {
cs.addError(e.Pos(), fmt.Sprintf("cannot convert %s to type float", arg.Const.String())) 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 { switch callee.BuiltinFunc {
case shaderir.Distance, shaderir.Dot: case shaderir.Distance, shaderir.Dot:
t = shaderir.Type{Main: shaderir.Float} finalType = shaderir.Type{Main: shaderir.Float}
case shaderir.Step: case shaderir.Step:
t = argts[1] finalType = argts[1]
default: default:
t = argts[0] finalType = argts[0]
} }
default: default:
@ -718,9 +744,9 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
} }
} }
if callee.BuiltinFunc == shaderir.Length { if callee.BuiltinFunc == shaderir.Length {
t = shaderir.Type{Main: shaderir.Float} finalType = shaderir.Type{Main: shaderir.Float}
} else { } else {
t = argts[0] finalType = argts[0]
} }
} }
return []shaderir.Expr{ return []shaderir.Expr{
@ -728,7 +754,7 @@ func (cs *compileState) parseExpr(block *block, fname string, expr ast.Expr, mar
Type: shaderir.Call, Type: shaderir.Call,
Exprs: append([]shaderir.Expr{callee}, args...), Exprs: append([]shaderir.Expr{callee}, args...),
}, },
}, []shaderir.Type{t}, stmts, true }, []shaderir.Type{finalType}, stmts, true
} }
if callee.Type != shaderir.FunctionExpr { 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 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)) cs.addError(e.Pos(), fmt.Sprintf("unexpected swizzling: %s", e.Sel.Name))
return nil, nil, nil, false 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)) cs.addError(e.Pos(), fmt.Sprintf("multiple-value context is not available at a unary operator: %s", e.X))
return nil, nil, nil, false 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 { if exprs[0].Const != nil {
v := gconstant.UnaryOp(e.Op, exprs[0].Const, 0) 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") cs.addError(e.Pos(), "multiple-value context is not available at an index expression")
return nil, nil, nil, false return nil, nil, nil, false
} }
if len(ts) == 0 {
cs.addError(e.Pos(), "unexpected index expression")
return nil, nil, nil, false
}
x := exprs[0] x := exprs[0]
t := ts[0] t := ts[0]
@ -1169,8 +1203,24 @@ func resolveConstKind(exprs []shaderir.Expr, ts []shaderir.Type) (kind gconstant
panic("not reached") panic("not reached")
} }
allConsts = true
for _, expr := range exprs { for _, expr := range exprs {
if expr.Const == nil { 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 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. // Prefer floats over integers for non-typed constant values.
// For example, max(1.0, 1) should return a float value. // For example, max(1.0, 1) should return a float value.
if kind == gconstant.Unknown { for _, expr := range exprs {
for _, expr := range exprs { if expr.Const.Kind() == gconstant.Float {
if expr.Const.Kind() == gconstant.Float { return gconstant.Float, true
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 ret = out[0].typ
out = nil 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, 1); _ = a", err: false},
{stmt: "a := {{.Func}}(1.0, 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}}(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}}(int(1), int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec2(1)); _ = a", err: true}, {stmt: "a := {{.Func}}(1, vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec3(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, 1); _ = a", err: false},
{stmt: "a := {{.Func}}(1.0, 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}}(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}}(int(1), int(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec2(1)); _ = a", err: true}, {stmt: "a := {{.Func}}(1, vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec3(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.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}}(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.0); var _ int = a", err: false},
{stmt: "a := {{.Func}}(int(1), 1.1); _ = 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)); var _ int = a", err: false},
{stmt: "a := {{.Func}}(1.0, 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.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}}(int(1), float(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(float(1), int(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, vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec3(1)); _ = a", err: true}, {stmt: "a := {{.Func}}(1, vec3(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, vec4(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, ivec3(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(1, ivec4(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); _ = 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), vec2(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(vec2(1), vec3(1)); _ = a", err: true}, {stmt: "a := {{.Func}}(vec2(1), vec3(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec2(1), vec4(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); _ = 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), vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec3(1), vec3(1)); _ = a", err: false}, {stmt: "a := {{.Func}}(vec3(1), vec3(1)); _ = a", err: false},
{stmt: "a := {{.Func}}(vec3(1), vec4(1)); _ = a", err: true}, {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); _ = 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), vec2(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec4(1), vec3(1)); _ = a", err: true}, {stmt: "a := {{.Func}}(vec4(1), vec3(1)); _ = a", err: true},
{stmt: "a := {{.Func}}(vec4(1), vec4(1)); _ = a", err: false}, {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.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(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.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.0, 1); var _ float = a", err: false},
{stmt: "a := clamp(1, 1, 1.0); 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") 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]; uniform vec2 U0[4];
vec2[2] F0(void); void F0(out vec2 l0[2]);
vec2[2] F1(void); void F1(out vec2 l0[2]);
vec2[2] F0(void) { void F0(out vec2 l0[2]) {
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);
vec2 l1[2]; vec2 l1[2];
l1[0] = vec2(0); l1[0] = vec2(0);
l1[1] = vec2(0); l1[1] = vec2(0);
(l0)[0] = vec2(1.0); l0[0] = l1[0];
l1[0] = l0[0]; l0[1] = l1[1];
l1[1] = l0[1]; return;
(l1)[1] = vec2(2.0); }
return l1;
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) { void F0(thread array<float2, 3>& l0) {
array<float2, 2> l0 = {}; array<float2, 2> l1 = {};
array<float2, 3> l1 = {}; array<float2, 3> l2 = {};
{ {
array<float2, 2> l1 = {}; array<float2, 2> l2 = {};
l1 = l0; 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) { void F0(out vec2 l0[3]) {
vec2 l0[2]; vec2 l1[2];
l0[0] = vec2(0);
l0[1] = vec2(0);
vec2 l1[3];
l1[0] = vec2(0); l1[0] = vec2(0);
l1[1] = 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]; vec2 l2[2];
l1[0] = vec2(0); l2[0] = vec2(0);
l1[1] = vec2(0); l2[1] = vec2(0);
l1[0] = l0[0]; l2[0] = l1[0];
l1[1] = l0[1]; 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); void F0(out float l0[1]);
int[1] F1(void); void F1(out int l0[1]);
float[1] F0(void) { void F0(out float l0[1]) {
float l0[1]; float l1[1];
l0[0] = float(0); l1[0] = float(0);
(l0)[0] = 1.0; (l1)[0] = 1.0;
return l0; l0[0] = l1[0];
return;
} }
int[1] F1(void) { void F1(out int l0[1]) {
int l0[1]; int l1[1];
l0[0] = 0; l1[0] = 0;
(l0)[0] = 1; (l1)[0] = 1;
return l0; l0[0] = l1[0];
return;
} }

View File

@ -285,6 +285,10 @@ func (c *context) screenScaleAndOffsets() (scale, offsetX, offsetY float64) {
return return
} }
func (u *UserInterface) LogicalPositionToClientPosition(x, y float64) (float64, float64) { func (u *UserInterface) LogicalPositionToClientPositionInNativePixels(x, y float64) (float64, float64) {
return u.context.logicalPositionToClientPosition(x, y, u.Monitor().DeviceScaleFactor()) 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) { if !math.IsNaN(cx) && !math.IsNaN(cy) {
cx2, cy2 := u.context.logicalPositionToClientPosition(cx, cy, s) cx2, cy2 := u.context.logicalPositionToClientPosition(cx, cy, s)
cx2 = dipToGLFWPixel(cx2, m) cx2 = dipToGLFWPixel(cx2, s)
cy2 = dipToGLFWPixel(cy2, m) cy2 = dipToGLFWPixel(cy2, s)
if err := u.window.SetCursorPos(cx2, cy2); err != nil { if err := u.window.SetCursorPos(cx2, cy2); err != nil {
return err return err
} }
@ -106,8 +106,8 @@ func (u *UserInterface) updateInputStateImpl() error {
if err != nil { if err != nil {
return err return err
} }
cx2 = dipFromGLFWPixel(cx2, m) cx2 = dipFromGLFWPixel(cx2, s)
cy2 = dipFromGLFWPixel(cy2, m) cy2 = dipFromGLFWPixel(cy2, s)
cx, cy = u.context.clientPositionToLogicalPosition(cx2, cy2, s) cx, cy = u.context.clientPositionToLogicalPosition(cx2, cy2, s)
} }

View File

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

View File

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

View File

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

View File

@ -52,7 +52,8 @@ func (m *Monitor) DeviceScaleFactor() float64 {
func (m *Monitor) sizeInDIP() (float64, float64) { func (m *Monitor) sizeInDIP() (float64, float64) {
w, h := m.boundsInGLFWPixels.Dx(), m.boundsInGLFWPixels.Dy() 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 { type monitors struct {

View File

@ -129,3 +129,7 @@ func deviceScaleFactorImpl() float64 {
} }
return s 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 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. // NOTE: On macOS, GLFW exposes the device independent coordinate system.
// Thus, the conversion functions are unnecessary, // Thus, the conversion functions are unnecessary,
// however we still need the deviceScaleFactor internally // however we still need the deviceScaleFactor internally
@ -214,7 +214,7 @@ func dipFromGLFWPixel(x float64, monitor *Monitor) float64 {
return x return x
} }
func dipToGLFWPixel(x float64, monitor *Monitor) float64 { func dipToGLFWPixel(x float64, scale float64) float64 {
return x return x
} }

View File

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

View File

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

View File

@ -117,7 +117,7 @@ var iosKeyToUIKey = map[int]ui.Key{
97: ui.KeyNumpad9, 97: ui.KeyNumpad9,
98: ui.KeyNumpad0, 98: ui.KeyNumpad0,
99: ui.KeyNumpadDecimal, 99: ui.KeyNumpadDecimal,
100: ui.KeyBackslash, 100: ui.KeyIntlBackslash,
103: ui.KeyNumpadEqual, 103: ui.KeyNumpadEqual,
104: ui.KeyF13, 104: ui.KeyF13,
105: ui.KeyF14, 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" "github.com/hajimehoshi/ebiten/v2/vector"
) )
var _ Face = (*StdFace)(nil) var _ Face = (*GoXFace)(nil)
type stdFaceGlyphImageCacheKey struct { type goXFaceGlyphImageCacheKey struct {
rune rune rune rune
xoffset fixed.Int26_6 xoffset fixed.Int26_6
// yoffset is always the same if the rune is the same, so this doesn't have to be a key. // 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). // GoXFace 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. // GoXFace 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. // GoXFace must not be copied by value.
type StdFace struct { type GoXFace struct {
f *faceWithCache f *faceWithCache
glyphImageCache glyphImageCache[stdFaceGlyphImageCacheKey] glyphImageCache glyphImageCache[goXFaceGlyphImageCacheKey]
addr *StdFace addr *GoXFace
} }
// NewStdFace creates a new StdFace from a semi-standard font.Face. // NewGoXFace creates a new GoXFace from a semi-standard font.Face.
func NewStdFace(face font.Face) *StdFace { func NewGoXFace(face font.Face) *GoXFace {
s := &StdFace{ s := &GoXFace{
f: &faceWithCache{ f: &faceWithCache{
f: face, f: face,
}, },
@ -56,14 +56,14 @@ func NewStdFace(face font.Face) *StdFace {
return s return s
} }
func (s *StdFace) copyCheck() { func (s *GoXFace) copyCheck() {
if s.addr != s { 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. // Metrics implements Face.
func (s *StdFace) Metrics() Metrics { func (s *GoXFace) Metrics() Metrics {
s.copyCheck() s.copyCheck()
m := s.f.Metrics() m := s.f.Metrics()
@ -77,24 +77,24 @@ func (s *StdFace) Metrics() Metrics {
// UnsafeInternal returns its internal font.Face. // UnsafeInternal returns its internal font.Face.
// //
// This is unsafe since this might make internal cache states out of sync. // 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() s.copyCheck()
return s.f.f return s.f.f
} }
// advance implements Face. // 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)) return fixed26_6ToFloat64(font.MeasureString(s.f, text))
} }
// hasGlyph implements Face. // hasGlyph implements Face.
func (s *StdFace) hasGlyph(r rune) bool { func (s *GoXFace) hasGlyph(r rune) bool {
_, ok := s.f.GlyphAdvance(r) _, ok := s.f.GlyphAdvance(r)
return ok return ok
} }
// appendGlyphsForLine implements Face. // 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() s.copyCheck()
origin := fixed.Point26_6{ origin := fixed.Point26_6{
@ -129,8 +129,8 @@ func (s *StdFace) appendGlyphsForLine(glyphs []Glyph, line string, indexOffset i
return glyphs return glyphs
} }
func (s *StdFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int, int, fixed.Int26_6) { func (s *GoXFace) glyphImage(r rune, origin fixed.Point26_6) (*ebiten.Image, int, int, fixed.Int26_6) {
// Assume that StdFace's direction is always horizontal. // Assume that GoXFace's direction is always horizontal.
origin.X = adjustGranularity(origin.X, s) origin.X = adjustGranularity(origin.X, s)
origin.Y &^= ((1 << 6) - 1) 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), X: (origin.X + b.Min.X) & ((1 << 6) - 1),
Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1), Y: (origin.Y + b.Min.Y) & ((1 << 6) - 1),
} }
key := stdFaceGlyphImageCacheKey{ key := goXFaceGlyphImageCacheKey{
rune: r, rune: r,
xoffset: subpixelOffset.X, 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 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() w, h := (glyphBounds.Max.X - glyphBounds.Min.X).Ceil(), (glyphBounds.Max.Y - glyphBounds.Min.Y).Ceil()
if w == 0 || h == 0 { if w == 0 || h == 0 {
return nil return nil
@ -179,14 +179,14 @@ func (s *StdFace) glyphImageImpl(r rune, subpixelOffset fixed.Point26_6, glyphBo
} }
// direction implements Face. // direction implements Face.
func (s *StdFace) direction() Direction { func (s *GoXFace) direction() Direction {
return DirectionLeftToRight return DirectionLeftToRight
} }
// appendVectorPathForLine implements Face. // 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. // 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. // 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 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. // For vertical directions, the start and end are top and bottom respectively.
// //

View File

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

View File

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

View File

@ -27,7 +27,7 @@ import (
) )
// Face is an interface representing a font face. // 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 { type Face interface {
// Metrics returns the metrics for this Face. // Metrics returns the metrics for this Face.
Metrics() Metrics Metrics() Metrics
@ -59,15 +59,15 @@ type Metrics struct {
HDescent float64 HDescent float64
// VLineGap is the recommended amount of horizontal space between two lines of text in pixels. // 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 VLineGap float64
// VAscent is the distance in pixels from the top of a line to its baseline for vertical lines. // 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 VAscent float64
// VDescent is the distance in pixels from the top of a line to its baseline for vertical lines. // 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 VDescent float64
} }

View File

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