WIP: implement HandleEvent

Updates #1704
This commit is contained in:
Hajime Hoshi 2023-10-18 01:03:06 +09:00
parent 4676fd26f0
commit ff51c4a2c7
8 changed files with 217 additions and 21 deletions

28
event/event.go Normal file
View File

@ -0,0 +1,28 @@
// Copyright 2023 The Ebitengine Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package event
type MouseMoveEvent struct {
X float64
Y float64
}
type MouseDownEvent struct {
// TODO
}
type MouseUpEvent struct {
// TODO
}

View File

@ -16,6 +16,7 @@ package main
import ( import (
"bytes" "bytes"
"flag"
"image" "image"
"image/color" "image/color"
_ "image/png" _ "image/png"
@ -25,6 +26,7 @@ import (
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/event"
"github.com/hajimehoshi/ebiten/v2/examples/resources/images" "github.com/hajimehoshi/ebiten/v2/examples/resources/images"
"github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/inpututil"
) )
@ -38,6 +40,10 @@ const (
screenHeight = 480 screenHeight = 480
) )
var (
flagEvent = flag.Bool("event", false, "use HandleEvent")
)
// Sprite represents an image. // Sprite represents an image.
type Sprite struct { type Sprite struct {
image *ebiten.Image image *ebiten.Image
@ -117,6 +123,21 @@ func (t *TouchStrokeSource) IsJustReleased() bool {
return inpututil.IsTouchJustReleased(t.ID) return inpututil.IsTouchJustReleased(t.ID)
} }
type MouseEventStrokeSource struct {
pressed bool
x float64
y float64
isJustReleased bool
}
func (m *MouseEventStrokeSource) Position() (int, int) {
return int(m.x), int(m.y)
}
func (m *MouseEventStrokeSource) IsJustReleased() bool {
return m.isJustReleased
}
// Stroke manages the current drag state by mouse. // Stroke manages the current drag state by mouse.
type Stroke struct { type Stroke struct {
source StrokeSource source StrokeSource
@ -160,9 +181,12 @@ func (s *Stroke) Sprite() *Sprite {
} }
type Game struct { type Game struct {
touchIDs []ebiten.TouchID touchIDs []ebiten.TouchID
strokes map[*Stroke]struct{} tickStrokes map[*Stroke]struct{}
sprites []*Sprite eventStroke *Stroke
sprites []*Sprite
mouseEventStrokeSource MouseEventStrokeSource
} }
var ( var (
@ -205,8 +229,8 @@ func NewGame() *Game {
// Initialize the game. // Initialize the game.
return &Game{ return &Game{
strokes: map[*Stroke]struct{}{}, tickStrokes: map[*Stroke]struct{}{},
sprites: sprites, sprites: sprites,
} }
} }
@ -234,11 +258,47 @@ func (g *Game) moveSpriteToFront(sprite *Sprite) {
g.sprites = append(g.sprites, sprite) g.sprites = append(g.sprites, sprite)
} }
func (g *Game) HandleEvent(e any) {
if !*flagEvent {
return
}
switch e := e.(type) {
case event.MouseDownEvent:
g.mouseEventStrokeSource.pressed = true
g.mouseEventStrokeSource.isJustReleased = false
case event.MouseMoveEvent:
if g.mouseEventStrokeSource.pressed {
g.mouseEventStrokeSource.x = e.X
g.mouseEventStrokeSource.y = e.Y
if g.eventStroke == nil {
if sp := g.spriteAt(int(e.X), int(e.Y)); sp != nil {
g.eventStroke = NewStroke(&g.mouseEventStrokeSource, sp)
}
}
}
case event.MouseUpEvent:
g.mouseEventStrokeSource.pressed = false
g.mouseEventStrokeSource.isJustReleased = true
}
if g.eventStroke != nil {
g.eventStroke.Update()
if !g.eventStroke.sprite.dragged {
g.eventStroke = nil
}
}
}
func (g *Game) Update() error { func (g *Game) Update() error {
if *flagEvent {
return nil
}
if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) {
if sp := g.spriteAt(ebiten.CursorPosition()); sp != nil { if sp := g.spriteAt(ebiten.CursorPosition()); sp != nil {
s := NewStroke(&MouseStrokeSource{}, sp) s := NewStroke(&MouseStrokeSource{}, sp)
g.strokes[s] = struct{}{} g.tickStrokes[s] = struct{}{}
g.moveSpriteToFront(sp) g.moveSpriteToFront(sp)
} }
} }
@ -246,15 +306,15 @@ func (g *Game) Update() error {
for _, id := range g.touchIDs { for _, id := range g.touchIDs {
if sp := g.spriteAt(ebiten.TouchPosition(id)); sp != nil { if sp := g.spriteAt(ebiten.TouchPosition(id)); sp != nil {
s := NewStroke(&TouchStrokeSource{id}, sp) s := NewStroke(&TouchStrokeSource{id}, sp)
g.strokes[s] = struct{}{} g.tickStrokes[s] = struct{}{}
g.moveSpriteToFront(sp) g.moveSpriteToFront(sp)
} }
} }
for s := range g.strokes { for s := range g.tickStrokes {
s.Update() s.Update()
if !s.sprite.dragged { if !s.sprite.dragged {
delete(g.strokes, s) delete(g.tickStrokes, s)
} }
} }
return nil return nil
@ -277,6 +337,7 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
} }
func main() { func main() {
flag.Parse()
ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Drag & Drop (Ebitengine Demo)") ebiten.SetWindowTitle("Drag & Drop (Ebitengine Demo)")
if err := ebiten.RunGame(NewGame()); err != nil { if err := ebiten.RunGame(NewGame()); err != nil {

View File

@ -15,6 +15,7 @@
package main package main
import ( import (
"flag"
"fmt" "fmt"
"image" "image"
"image/color" "image/color"
@ -24,6 +25,7 @@ import (
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/colorm" "github.com/hajimehoshi/ebiten/v2/colorm"
"github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/ebitenutil"
"github.com/hajimehoshi/ebiten/v2/event"
) )
const ( const (
@ -31,6 +33,10 @@ const (
screenHeight = 480 screenHeight = 480
) )
var (
flagEvent = flag.Bool("event", false, "use HandleEvent")
)
var ( var (
brushImage *ebiten.Image brushImage *ebiten.Image
) )
@ -70,6 +76,8 @@ type Game struct {
count int count int
canvasImage *ebiten.Image canvasImage *ebiten.Image
mousePressedByEvent bool
} }
func NewGame() *Game { func NewGame() *Game {
@ -80,14 +88,34 @@ func NewGame() *Game {
return g return g
} }
func (g *Game) HandleEvent(e any) {
if !*flagEvent {
return
}
switch e := e.(type) {
case event.MouseDownEvent:
g.mousePressedByEvent = true
case event.MouseMoveEvent:
if g.mousePressedByEvent {
g.paint(g.canvasImage, int(e.X), int(e.Y))
g.count++
}
case event.MouseUpEvent:
g.mousePressedByEvent = false
}
}
func (g *Game) Update() error { func (g *Game) Update() error {
drawn := false var drawn bool
// Paint the brush by mouse dragging // Paint the brush by mouse dragging
mx, my := ebiten.CursorPosition() mx, my := ebiten.CursorPosition()
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { if !*flagEvent {
g.paint(g.canvasImage, mx, my) if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
drawn = true g.paint(g.canvasImage, mx, my)
drawn = true
}
} }
g.cursor = pos{ g.cursor = pos{
x: mx, x: mx,
@ -142,6 +170,7 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
} }
func main() { func main() {
flag.Parse()
ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowSize(screenWidth, screenHeight)
ebiten.SetWindowTitle("Paint (Ebitengine Demo)") ebiten.SetWindowTitle("Paint (Ebitengine Demo)")
if err := ebiten.RunGame(NewGame()); err != nil { if err := ebiten.RunGame(NewGame()); err != nil {

View File

@ -114,6 +114,12 @@ func (g *gameForUI) NewScreenImage(width, height int) *ui.Image {
return g.screen.image return g.screen.image
} }
func (g *gameForUI) HandleEvent(event any) {
if h, ok := g.game.(EventHandler); ok {
h.HandleEvent(event)
}
}
func (g *gameForUI) Layout(outsideWidth, outsideHeight float64) (float64, float64) { func (g *gameForUI) Layout(outsideWidth, outsideHeight float64) (float64, float64) {
if l, ok := g.game.(LayoutFer); ok { if l, ok := g.game.(LayoutFer); ok {
return l.LayoutF(outsideWidth, outsideHeight) return l.LayoutF(outsideWidth, outsideHeight)

View File

@ -192,6 +192,9 @@ func (q *commandQueue) Flush(graphicsDriver graphicsdriver.Graphics, endFrame bo
} }
} }
} }
/*if endFrame {
sync = true
}*/
logger := debug.SwitchLogger() logger := debug.SwitchLogger()

View File

@ -16,6 +16,7 @@ package ui
import ( import (
"math" "math"
"sync"
"github.com/hajimehoshi/ebiten/v2/internal/atlas" "github.com/hajimehoshi/ebiten/v2/internal/atlas"
"github.com/hajimehoshi/ebiten/v2/internal/clock" "github.com/hajimehoshi/ebiten/v2/internal/clock"
@ -33,6 +34,7 @@ type Game interface {
NewOffscreenImage(width, height int) *Image NewOffscreenImage(width, height int) *Image
NewScreenImage(width, height int) *Image NewScreenImage(width, height int) *Image
Layout(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64) Layout(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64)
HandleEvent(event any)
UpdateInputState(fn func(*InputState)) UpdateInputState(fn func(*InputState))
Update() error Update() error
DrawOffscreen() error DrawOffscreen() error
@ -40,7 +42,8 @@ type Game interface {
} }
type context struct { type context struct {
game Game game Game
inputCh chan any
updateCalled bool updateCalled bool
@ -55,12 +58,34 @@ type context struct {
isOffscreenModified bool isOffscreenModified bool
skipCount int skipCount int
gameM sync.Mutex
} }
func newContext(game Game) *context { func newContext(game Game) *context {
return &context{ c := &context{
game: game, game: game,
inputCh: make(chan any, 128),
} }
// Create an independent goroutine from Update/Draw not to cause deadlock at (*UserInterface).readPixels.
go func() {
// TODO: Abort this loop when the game terminates.
for e := range c.inputCh {
e := e
func() {
c.gameM.Lock()
defer c.gameM.Unlock()
c.game.HandleEvent(e)
}()
}
}()
return c
}
func (c *context) sendInputEvent(event any) {
c.inputCh <- event
} }
func (c *context) updateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, swapBuffersForGL func()) error { func (c *context) updateFrame(graphicsDriver graphicsdriver.Graphics, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, swapBuffersForGL func()) error {
@ -90,6 +115,18 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
return nil return nil
} }
if err := c.updateGameInFrame(graphicsDriver, updateCount, outsideWidth, outsideHeight, deviceScaleFactor, ui, forceDraw, swapBuffersForGL); err != nil {
return err
}
if err := atlas.SwapBuffers(graphicsDriver, swapBuffersForGL); err != nil {
return err
}
return nil
}
func (c *context) updateGameInFrame(graphicsDriver graphicsdriver.Graphics, updateCount int, outsideWidth, outsideHeight float64, deviceScaleFactor float64, ui *UserInterface, forceDraw bool, swapBuffersForGL func()) (err error) {
debug.Logf("----\n") debug.Logf("----\n")
if err := atlas.BeginFrame(graphicsDriver); err != nil { if err := atlas.BeginFrame(graphicsDriver); err != nil {
@ -101,13 +138,11 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update
err = err1 err = err1
return return
} }
if err1 := atlas.SwapBuffers(graphicsDriver, swapBuffersForGL); err1 != nil && err == nil {
err = err1
return
}
}() }()
c.gameM.Lock()
defer c.gameM.Unlock()
// ForceUpdate can be invoked even if the context is not initialized yet (#1591). // ForceUpdate can be invoked even if the context is not initialized yet (#1591).
if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 { if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 {
return nil return nil

View File

@ -19,6 +19,7 @@ package ui
import ( import (
"math" "math"
"github.com/hajimehoshi/ebiten/v2/event"
"github.com/hajimehoshi/ebiten/v2/internal/gamepad" "github.com/hajimehoshi/ebiten/v2/internal/gamepad"
"github.com/hajimehoshi/ebiten/v2/internal/glfw" "github.com/hajimehoshi/ebiten/v2/internal/glfw"
) )
@ -51,6 +52,35 @@ func (u *UserInterface) registerInputCallbacks() error {
return err return err
} }
// For HandleEvent
if _, err := u.window.SetCursorPosCallback(func(w *glfw.Window, xpos float64, ypos float64) {
m, err := u.currentMonitor()
if err != nil {
u.setError(err)
return
}
x := dipFromGLFWPixel(xpos, m)
y := dipFromGLFWPixel(ypos, m)
x, y = u.context.clientPositionToLogicalPosition(x, y, m.deviceScaleFactor())
u.context.sendInputEvent(event.MouseMoveEvent{
X: x,
Y: y,
})
}); err != nil {
return err
}
if _, err := u.window.SetMouseButtonCallback(func(w *glfw.Window, button glfw.MouseButton, action glfw.Action, mods glfw.ModifierKey) {
// TODO: Use button and mods
switch action {
case glfw.Release:
u.context.sendInputEvent(event.MouseUpEvent{})
case glfw.Press:
u.context.sendInputEvent(event.MouseDownEvent{})
}
}); err != nil {
return err
}
return nil return nil
} }

4
run.go
View File

@ -93,6 +93,10 @@ type LayoutFer interface {
LayoutF(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64) LayoutF(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64)
} }
type EventHandler interface {
HandleEvent(event any)
}
// FinalScreen represents the final screen image. // FinalScreen represents the final screen image.
// FinalScreen implements a part of Image functions. // FinalScreen implements a part of Image functions.
type FinalScreen interface { type FinalScreen interface {