diff --git a/event/event.go b/event/event.go new file mode 100644 index 000000000..925042cbc --- /dev/null +++ b/event/event.go @@ -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 +} diff --git a/examples/drag/main.go b/examples/drag/main.go index 7bc919265..c63eb14b9 100644 --- a/examples/drag/main.go +++ b/examples/drag/main.go @@ -16,6 +16,7 @@ package main import ( "bytes" + "flag" "image" "image/color" _ "image/png" @@ -25,6 +26,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" "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/inpututil" ) @@ -38,6 +40,10 @@ const ( screenHeight = 480 ) +var ( + flagEvent = flag.Bool("event", false, "use HandleEvent") +) + // Sprite represents an image. type Sprite struct { image *ebiten.Image @@ -117,6 +123,21 @@ func (t *TouchStrokeSource) IsJustReleased() bool { 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. type Stroke struct { source StrokeSource @@ -160,9 +181,12 @@ func (s *Stroke) Sprite() *Sprite { } type Game struct { - touchIDs []ebiten.TouchID - strokes map[*Stroke]struct{} - sprites []*Sprite + touchIDs []ebiten.TouchID + tickStrokes map[*Stroke]struct{} + eventStroke *Stroke + sprites []*Sprite + + mouseEventStrokeSource MouseEventStrokeSource } var ( @@ -205,8 +229,8 @@ func NewGame() *Game { // Initialize the game. return &Game{ - strokes: map[*Stroke]struct{}{}, - sprites: sprites, + tickStrokes: map[*Stroke]struct{}{}, + sprites: sprites, } } @@ -234,11 +258,47 @@ func (g *Game) moveSpriteToFront(sprite *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 { + if *flagEvent { + return nil + } + if inpututil.IsMouseButtonJustPressed(ebiten.MouseButtonLeft) { if sp := g.spriteAt(ebiten.CursorPosition()); sp != nil { s := NewStroke(&MouseStrokeSource{}, sp) - g.strokes[s] = struct{}{} + g.tickStrokes[s] = struct{}{} g.moveSpriteToFront(sp) } } @@ -246,15 +306,15 @@ func (g *Game) Update() error { for _, id := range g.touchIDs { if sp := g.spriteAt(ebiten.TouchPosition(id)); sp != nil { s := NewStroke(&TouchStrokeSource{id}, sp) - g.strokes[s] = struct{}{} + g.tickStrokes[s] = struct{}{} g.moveSpriteToFront(sp) } } - for s := range g.strokes { + for s := range g.tickStrokes { s.Update() if !s.sprite.dragged { - delete(g.strokes, s) + delete(g.tickStrokes, s) } } return nil @@ -277,6 +337,7 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { } func main() { + flag.Parse() ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowTitle("Drag & Drop (Ebitengine Demo)") if err := ebiten.RunGame(NewGame()); err != nil { diff --git a/examples/paint/main.go b/examples/paint/main.go index de3b5df13..722268f7e 100644 --- a/examples/paint/main.go +++ b/examples/paint/main.go @@ -15,6 +15,7 @@ package main import ( + "flag" "fmt" "image" "image/color" @@ -24,6 +25,7 @@ import ( "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/colorm" "github.com/hajimehoshi/ebiten/v2/ebitenutil" + "github.com/hajimehoshi/ebiten/v2/event" ) const ( @@ -31,6 +33,10 @@ const ( screenHeight = 480 ) +var ( + flagEvent = flag.Bool("event", false, "use HandleEvent") +) + var ( brushImage *ebiten.Image ) @@ -70,6 +76,8 @@ type Game struct { count int canvasImage *ebiten.Image + + mousePressedByEvent bool } func NewGame() *Game { @@ -80,14 +88,34 @@ func NewGame() *Game { 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 { - drawn := false + var drawn bool // Paint the brush by mouse dragging mx, my := ebiten.CursorPosition() - if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { - g.paint(g.canvasImage, mx, my) - drawn = true + if !*flagEvent { + if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) { + g.paint(g.canvasImage, mx, my) + drawn = true + } } g.cursor = pos{ x: mx, @@ -142,6 +170,7 @@ func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { } func main() { + flag.Parse() ebiten.SetWindowSize(screenWidth, screenHeight) ebiten.SetWindowTitle("Paint (Ebitengine Demo)") if err := ebiten.RunGame(NewGame()); err != nil { diff --git a/gameforui.go b/gameforui.go index a6852a237..459588e63 100644 --- a/gameforui.go +++ b/gameforui.go @@ -114,6 +114,12 @@ func (g *gameForUI) NewScreenImage(width, height int) *ui.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) { if l, ok := g.game.(LayoutFer); ok { return l.LayoutF(outsideWidth, outsideHeight) diff --git a/internal/graphicscommand/commandqueue.go b/internal/graphicscommand/commandqueue.go index 5021fed05..3f8d422f5 100644 --- a/internal/graphicscommand/commandqueue.go +++ b/internal/graphicscommand/commandqueue.go @@ -192,6 +192,9 @@ func (q *commandQueue) Flush(graphicsDriver graphicsdriver.Graphics, endFrame bo } } } + /*if endFrame { + sync = true + }*/ logger := debug.SwitchLogger() diff --git a/internal/ui/context.go b/internal/ui/context.go index ff957b020..e3b41daeb 100644 --- a/internal/ui/context.go +++ b/internal/ui/context.go @@ -16,6 +16,7 @@ package ui import ( "math" + "sync" "github.com/hajimehoshi/ebiten/v2/internal/atlas" "github.com/hajimehoshi/ebiten/v2/internal/clock" @@ -33,6 +34,7 @@ type Game interface { NewOffscreenImage(width, height int) *Image NewScreenImage(width, height int) *Image Layout(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64) + HandleEvent(event any) UpdateInputState(fn func(*InputState)) Update() error DrawOffscreen() error @@ -40,7 +42,8 @@ type Game interface { } type context struct { - game Game + game Game + inputCh chan any updateCalled bool @@ -55,12 +58,34 @@ type context struct { isOffscreenModified bool skipCount int + + gameM sync.Mutex } func newContext(game Game) *context { - return &context{ - game: game, + c := &context{ + 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 { @@ -90,6 +115,18 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update 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") if err := atlas.BeginFrame(graphicsDriver); err != nil { @@ -101,13 +138,11 @@ func (c *context) updateFrameImpl(graphicsDriver graphicsdriver.Graphics, update err = err1 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). if w, h := c.layoutGame(outsideWidth, outsideHeight, deviceScaleFactor); w == 0 || h == 0 { return nil diff --git a/internal/ui/input_glfw.go b/internal/ui/input_glfw.go index feee4a7eb..29b9ee6a8 100644 --- a/internal/ui/input_glfw.go +++ b/internal/ui/input_glfw.go @@ -19,6 +19,7 @@ package ui import ( "math" + "github.com/hajimehoshi/ebiten/v2/event" "github.com/hajimehoshi/ebiten/v2/internal/gamepad" "github.com/hajimehoshi/ebiten/v2/internal/glfw" ) @@ -51,6 +52,35 @@ func (u *UserInterface) registerInputCallbacks() error { 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 } diff --git a/run.go b/run.go index 7427ecda5..0107c0b1b 100644 --- a/run.go +++ b/run.go @@ -93,6 +93,10 @@ type LayoutFer interface { LayoutF(outsideWidth, outsideHeight float64) (screenWidth, screenHeight float64) } +type EventHandler interface { + HandleEvent(event any) +} + // FinalScreen represents the final screen image. // FinalScreen implements a part of Image functions. type FinalScreen interface {