diff --git a/README.md b/README.md index 6bc4279c..6f788d65 100644 --- a/README.md +++ b/README.md @@ -9,8 +9,8 @@ Create Pixel Art games in Golang with fun and ease. ## What you can do with Pixiq? + draw images on a screen in real time using your favourite [Go programming language](https://golang.org/) -+ manipulate every single pixel directly or with the use of tools -+ handle user input (_only keyboard supported at the moment_) ++ manipulate every single pixel directly or with the use of tools (_blend and clear supported at the moment_) ++ handle user input (_keyboard and mouse supported at the moment_) ## What is Pixel Art? @@ -22,8 +22,8 @@ Create Pixel Art games in Golang with fun and ease. ## Installation + [Go 1.14+](https://golang.org/dl/) -+ Ubuntu/Debian: `sudo apt-get install libgl1-mesa-dev xorg-dev` -+ CentOS/Fedora: `sudo yum install libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel libXi-devel` ++ Ubuntu/Debian: `sudo apt-get install libgl1-mesa-dev xorg-dev gcc` ++ CentOS/Fedora: `sudo yum install libX11-devel libXcursor-devel libXrandr-devel libXinerama-devel mesa-libGL-devel libXi-devel gcc` + MacOS: `xcode-select --install` ## Hello world! diff --git a/examples/mouse/position/main.go b/examples/mouse/position/main.go new file mode 100644 index 00000000..f77ca145 --- /dev/null +++ b/examples/mouse/position/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "log" + + "github.com/jacekolszak/pixiq/clear" + "github.com/jacekolszak/pixiq/colornames" + "github.com/jacekolszak/pixiq/glfw" + "github.com/jacekolszak/pixiq/image" + "github.com/jacekolszak/pixiq/loop" + "github.com/jacekolszak/pixiq/mouse" +) + +func main() { + glfw.RunOrDie(func(openGL *glfw.OpenGL) { + window, err := openGL.OpenWindow(80, 20, glfw.Title("Move mouse left and right"), glfw.Zoom(7)) + if err != nil { + log.Panicf("OpenWindow failed: %v", err) + } + // Create mouse instance for window. + mouseState := mouse.New(window) + x := 40 + clearTool := clear.New() + loop.Run(window, func(frame *loop.Frame) { + screen := frame.Screen() + clearTool.Clear(screen) + // Poll mouse events + mouseState.Update() + x += mouseState.PositionChange().X() + if x < 0 { + x = 0 + } + if x >= screen.Width() { + x = screen.Width() - 1 + } + drawVerticalLine(screen, x) + if window.ShouldClose() { + frame.StopLoopEventually() + } + }) + }) +} + +func drawVerticalLine(screen image.Selection, x int) { + for y := 0; y < screen.Height(); y++ { + screen.SetColor(x, y, colornames.Azure) + } +} diff --git a/examples/mouse/pressed/main.go b/examples/mouse/pressed/main.go new file mode 100644 index 00000000..e2baa772 --- /dev/null +++ b/examples/mouse/pressed/main.go @@ -0,0 +1,48 @@ +package main + +import ( + "log" + + "github.com/jacekolszak/pixiq/colornames" + "github.com/jacekolszak/pixiq/glfw" + "github.com/jacekolszak/pixiq/image" + "github.com/jacekolszak/pixiq/loop" + "github.com/jacekolszak/pixiq/mouse" +) + +func main() { + glfw.RunOrDie(func(openGL *glfw.OpenGL) { + window, err := openGL.OpenWindow(80, 40, glfw.Title("Use left and right mouse buttons to draw"), glfw.Zoom(20)) + if err != nil { + log.Panicf("OpenWindow failed: %v", err) + } + // Create mouse instance for window. + mouseState := mouse.New(window) + loop.Run(window, func(frame *loop.Frame) { + screen := frame.Screen() + // Poll mouse events + mouseState.Update() + // Get cursor position + pos := mouseState.Position() + // Pressed returns true if given key is currently pressed. + if mouseState.Pressed(mouse.Left) { + // pos.X() and pos.Y() returns position in pixel dimensions + drawSquare(screen, pos.X(), pos.Y(), colornames.White) + } + if mouseState.Pressed(mouse.Right) { + drawSquare(screen, pos.X(), pos.Y(), colornames.Black) + } + if window.ShouldClose() { + frame.StopLoopEventually() + } + }) + }) +} + +func drawSquare(screen image.Selection, x int, y int, color image.Color) { + for xOff := -1; xOff <= 1; xOff++ { + for yOff := -1; yOff <= 1; yOff++ { + screen.SetColor(x+xOff, y+yOff, color) + } + } +} diff --git a/examples/mouse/scroll/main.go b/examples/mouse/scroll/main.go new file mode 100644 index 00000000..cef50865 --- /dev/null +++ b/examples/mouse/scroll/main.go @@ -0,0 +1,35 @@ +package main + +import ( + "log" + + "github.com/jacekolszak/pixiq/colornames" + "github.com/jacekolszak/pixiq/glfw" + "github.com/jacekolszak/pixiq/loop" + "github.com/jacekolszak/pixiq/mouse" +) + +func main() { + glfw.RunOrDie(func(openGL *glfw.OpenGL) { + window, err := openGL.OpenWindow(80, 40, glfw.Title("Move mouse wheel in all possible directions"), glfw.Zoom(20)) + if err != nil { + log.Panicf("OpenWindow failed: %v", err) + } + x := 40 + y := 20 + // Create mouse instance for window. + mouseState := mouse.New(window) + loop.Run(window, func(frame *loop.Frame) { + screen := frame.Screen() + // Poll mouse events + mouseState.Update() + scroll := mouseState.Scroll() + x += int(scroll.X()) + y += int(scroll.Y()) + screen.SetColor(x, y, colornames.Azure) + if window.ShouldClose() { + frame.StopLoopEventually() + } + }) + }) +} diff --git a/glfw/glfw.go b/glfw/glfw.go index 77b33b56..c8c90121 100644 --- a/glfw/glfw.go +++ b/glfw/glfw.go @@ -7,6 +7,7 @@ package glfw import ( "log" + "sync" "time" gl33 "github.com/go-gl/gl/v3.3-core/gl" @@ -16,6 +17,7 @@ import ( "github.com/jacekolszak/pixiq/glfw/internal" "github.com/jacekolszak/pixiq/image" "github.com/jacekolszak/pixiq/keyboard" + "github.com/jacekolszak/pixiq/mouse" ) // NewOpenGL creates OpenGL instance. @@ -184,6 +186,39 @@ func (g *OpenGL) NewImage(width, height int) *image.Image { return image.New(acceleratedImage) } +// mouseWindow implements mouse.Window +type mouseWindow struct { + mutex sync.Mutex + glfwWindow *glfw.Window + mainThreadLoop *MainThreadLoop + zoom int + width, height int +} + +func (m *mouseWindow) CursorPosition() (float64, float64) { + var x, y float64 + m.mainThreadLoop.Execute(func() { + x, y = m.glfwWindow.GetCursorPos() + }) + return x, y +} + +// Size() is thread-safe +func (m *mouseWindow) Size() (int, int) { + m.mutex.Lock() + defer m.mutex.Unlock() + if m.width == 0 { + m.mainThreadLoop.Execute(func() { + m.width, m.height = m.glfwWindow.GetSize() + }) + } + return m.width, m.height +} + +func (m *mouseWindow) Zoom() int { + return m.zoom +} + // OpenWindow creates and shows Window. func (g *OpenGL) OpenWindow(width, height int, options ...WindowOption) (*Window, error) { if width < 1 { @@ -211,7 +246,6 @@ func (g *OpenGL) OpenWindow(width, height int, options ...WindowOption) (*Window if err != nil { return } - win.glfwWindow.SetKeyCallback(win.keyboardEvents.OnKeyCallback) for _, option := range options { if option == nil { log.Println("nil option given when opening the window") @@ -219,6 +253,18 @@ func (g *OpenGL) OpenWindow(width, height int, options ...WindowOption) (*Window } option(win) } + win.mouseWindow = &mouseWindow{ + glfwWindow: win.glfwWindow, + mainThreadLoop: g.mainThreadLoop, + zoom: win.zoom, + } + win.mouseEvents = internal.NewMouseEvents( + // FIXME: EventBuffer size should be configurable + mouse.NewEventBuffer(32), + win.mouseWindow) + win.glfwWindow.SetKeyCallback(win.keyboardEvents.OnKeyCallback) + win.glfwWindow.SetMouseButtonCallback(win.mouseEvents.OnMouseButtonCallback) + win.glfwWindow.SetScrollCallback(win.mouseEvents.OnScrollCallback) win.glfwWindow.SetSize(win.requestedWidth*win.zoom, win.requestedHeight*win.zoom) win.glfwWindow.Show() }) diff --git a/glfw/glfw_test.go b/glfw/glfw_test.go index 21aea4e1..86d34118 100644 --- a/glfw/glfw_test.go +++ b/glfw/glfw_test.go @@ -2,6 +2,7 @@ package glfw_test import ( "os" + "sync" "testing" "github.com/stretchr/testify/assert" @@ -293,3 +294,49 @@ func TestOpenGL_OpenWindow(t *testing.T) { } }) } + +func TestWindow_Width(t *testing.T) { + t.Run("concurrent Width() calls should return the same value", func(t *testing.T) { + openGL, err := glfw.NewOpenGL(mainThreadLoop) + require.NoError(t, err) + defer openGL.Destroy() + // when + win, err := openGL.OpenWindow(640, 360) + require.NoError(t, err) + defer win.Close() + // then + var wg sync.WaitGroup + goroutines := 100 + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { + go func() { + assert.Equal(t, win.Width(), 640) + wg.Done() + }() + } + wg.Wait() + }) +} + +func TestWindow_Height(t *testing.T) { + t.Run("concurrent Height() calls should return the same value", func(t *testing.T) { + openGL, err := glfw.NewOpenGL(mainThreadLoop) + require.NoError(t, err) + defer openGL.Destroy() + // when + win, err := openGL.OpenWindow(640, 360) + require.NoError(t, err) + defer win.Close() + // then + var wg sync.WaitGroup + goroutines := 100 + wg.Add(goroutines) + for i := 0; i < goroutines; i++ { + go func() { + assert.Equal(t, win.Height(), 360) + wg.Done() + }() + } + wg.Wait() + }) +} diff --git a/glfw/internal/keyboard.go b/glfw/internal/keyboard.go index 05bea688..f0c3ad43 100644 --- a/glfw/internal/keyboard.go +++ b/glfw/internal/keyboard.go @@ -12,7 +12,7 @@ type KeyboardEvents struct { buffer *keyboard.EventBuffer } -// NewKeyboardEvents creates *KeyboardEvents of given size. +// NewKeyboardEvents creates *KeyboardEvents using given buffer func NewKeyboardEvents(buffer *keyboard.EventBuffer) *KeyboardEvents { if buffer == nil { panic("nil buffer") diff --git a/glfw/internal/mouse.go b/glfw/internal/mouse.go new file mode 100644 index 00000000..d48d49e9 --- /dev/null +++ b/glfw/internal/mouse.go @@ -0,0 +1,88 @@ +package internal + +import ( + "github.com/go-gl/glfw/v3.3/glfw" + + "github.com/jacekolszak/pixiq/mouse" +) + +// MouseEvents maps GLFW events to mouse.Event. Mapped events can be +// polled using mouse.EventSource interface. +type MouseEvents struct { + buffer *mouse.EventBuffer + window Window + lastPosX, lastPosY float64 +} + +// NewMouseEvents creates *MouseEvents using given buffer and window. Based on the +// information returned by Window mouse move events are generated. +func NewMouseEvents(buffer *mouse.EventBuffer, window Window) *MouseEvents { + if buffer == nil { + panic("nil buffer") + } + if window == nil { + panic("nil window") + } + return &MouseEvents{buffer: buffer, window: window} +} + +var mouseButtonMapping = map[glfw.MouseButton]mouse.Button{ + glfw.MouseButtonLeft: mouse.Left, + glfw.MouseButtonRight: mouse.Right, + glfw.MouseButtonMiddle: mouse.Middle, + glfw.MouseButton4: mouse.Button4, + glfw.MouseButton5: mouse.Button5, + glfw.MouseButton6: mouse.Button6, + glfw.MouseButton7: mouse.Button7, + glfw.MouseButton8: mouse.Button8, +} + +// OnMouseButtonCallback passes GLFW mouse event +func (e *MouseEvents) OnMouseButtonCallback(_ *glfw.Window, button glfw.MouseButton, action glfw.Action, _ glfw.ModifierKey) { + btn, ok := mouseButtonMapping[button] + if !ok { + return + } + switch action { + case glfw.Press: + e.buffer.Add(mouse.NewPressedEvent(btn)) + case glfw.Release: + e.buffer.Add(mouse.NewReleasedEvent(btn)) + } +} + +// OnScrollCallback passes GLFW mouse event +func (e *MouseEvents) OnScrollCallback(_ *glfw.Window, xoff float64, yoff float64) { + e.buffer.Add(mouse.NewScrolledEvent(-xoff, -yoff)) +} + +// Window is an abstraction for getting information about cursor position, size and zoom. +// It is needed for generating mouse move events +type Window interface { + CursorPosition() (float64, float64) + Size() (int, int) + Zoom() int +} + +// Poll return next mapped event +func (e *MouseEvents) Poll() (mouse.Event, bool) { + event, ok := e.buffer.Poll() + if ok { + return event, ok + } + // generate move event, because GLFW does not provide move events for Linux + // and Windows when cursor is outside window. + realX, realY := e.window.CursorPosition() + if e.lastPosX != realX || e.lastPosY != realY { + w, h := e.window.Size() + zoom := float64(e.window.Zoom()) + insideWindow := true + if int(realX) >= w || int(realY) >= h || realX < 0 || realY < 0 { + insideWindow = false + } + e.lastPosX = realX + e.lastPosY = realY + return mouse.NewMovedEvent(int(realX/zoom), int(realY/zoom), realX, realY, insideWindow), true + } + return mouse.EmptyEvent, false +} diff --git a/glfw/internal/mouse_test.go b/glfw/internal/mouse_test.go new file mode 100644 index 00000000..1be00b03 --- /dev/null +++ b/glfw/internal/mouse_test.go @@ -0,0 +1,308 @@ +package internal_test + +import ( + "testing" + + "github.com/go-gl/glfw/v3.3/glfw" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/jacekolszak/pixiq/glfw/internal" + "github.com/jacekolszak/pixiq/mouse" +) + +func TestNewMouseEvents(t *testing.T) { + t.Run("should create MouseEvents when buffer is given", func(t *testing.T) { + buffer := mouse.NewEventBuffer(1) + // expect + assert.NotNil(t, internal.NewMouseEvents(buffer, &fakeWindow{})) + }) + t.Run("should panic for nil buffer", func(t *testing.T) { + assert.Panics(t, func() { + assert.NotNil(t, internal.NewMouseEvents(nil, &fakeWindow{})) + }) + }) + t.Run("should panic for nil window", func(t *testing.T) { + buffer := mouse.NewEventBuffer(1) + assert.Panics(t, func() { + assert.NotNil(t, internal.NewMouseEvents(buffer, nil)) + }) + }) +} + +func TestMouseEvents_Poll(t *testing.T) { + t.Run("should return EmptyEvent when there are no events", func(t *testing.T) { + buffer := mouse.NewEventBuffer(1) + events := internal.NewMouseEvents(buffer, &fakeWindow{}) + // when + event, ok := events.Poll() + // then + require.False(t, ok) + assert.Equal(t, mouse.EmptyEvent, event) + }) + + t.Run("should map mouse button event", func(t *testing.T) { + tests := map[string]struct { + button glfw.MouseButton + action glfw.Action + expectedEvent mouse.Event + }{ + "press left": { + button: glfw.MouseButtonLeft, + action: glfw.Press, + expectedEvent: mouse.NewPressedEvent(mouse.Left), + }, + "press right": { + button: glfw.MouseButtonRight, + action: glfw.Press, + expectedEvent: mouse.NewPressedEvent(mouse.Right), + }, + "press 1": { + button: glfw.MouseButton1, + action: glfw.Press, + expectedEvent: mouse.NewPressedEvent(mouse.Left), + }, + "release left": { + button: glfw.MouseButtonLeft, + action: glfw.Release, + expectedEvent: mouse.NewReleasedEvent(mouse.Left), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + buffer := mouse.NewEventBuffer(1) + events := internal.NewMouseEvents(buffer, &fakeWindow{}) + // when + events.OnMouseButtonCallback(nil, test.button, test.action, 0) + event, ok := events.Poll() + // then + require.True(t, ok) + assert.Equal(t, test.expectedEvent, event) + }) + } + }) + t.Run("should map scroll event", func(t *testing.T) { + tests := map[string]struct { + x, y float64 + expectedEvent mouse.Event + }{ + "0,0": { + expectedEvent: mouse.NewScrolledEvent(0, 0), + }, + "1,2": { + x: 1, + y: 2, + expectedEvent: mouse.NewScrolledEvent(-1, -2), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + buffer := mouse.NewEventBuffer(1) + events := internal.NewMouseEvents(buffer, &fakeWindow{}) + // when + events.OnScrollCallback(nil, test.x, test.y) + event, ok := events.Poll() + // then + require.True(t, ok) + assert.Equal(t, test.expectedEvent, event) + }) + } + }) + + t.Run("should return two button events", func(t *testing.T) { + buffer := mouse.NewEventBuffer(2) + events := internal.NewMouseEvents(buffer, &fakeWindow{}) + events.OnMouseButtonCallback(nil, glfw.MouseButtonLeft, glfw.Press, 0) + events.OnMouseButtonCallback(nil, glfw.MouseButtonRight, glfw.Release, 0) + // when + event, ok := events.Poll() + // then + require.True(t, ok) + assert.Equal(t, mouse.NewPressedEvent(mouse.Left), event) + // and + event, ok = events.Poll() + require.True(t, ok) + assert.Equal(t, mouse.NewReleasedEvent(mouse.Right), event) + // and + assertNoMoreMouseEvents(t, events) + }) + + t.Run("should generate MoveEvent", func(t *testing.T) { + tests := map[string]struct { + window internal.Window + expectedEvent mouse.Event + }{ + "1,2": { + window: &fakeWindow{ + posX: 1, + posY: 2, + width: 2, + height: 3, + zoom: 1, + }, + expectedEvent: mouse.NewMovedEvent(1, 2, 1, 2, true), + }, + "zoom 2": { + window: &fakeWindow{ + posX: 2.0, + posY: 4.0, + width: 3, + height: 5, + zoom: 2, + }, + expectedEvent: mouse.NewMovedEvent(1, 2, 2.0, 4.0, true), + }, + "outside window, x == width": { + window: &fakeWindow{ + posX: 1, + posY: 0, + width: 1, + height: 1, + zoom: 1, + }, + expectedEvent: mouse.NewMovedEvent(1, 0, 1, 0, false), + }, + "outside window, x >= width": { + window: &fakeWindow{ + posX: 2, + posY: 0, + width: 1, + height: 1, + zoom: 1, + }, + expectedEvent: mouse.NewMovedEvent(2, 0, 2, 0, false), + }, + "outside window, x < width": { + window: &fakeWindow{ + posX: -1, + posY: 0, + width: 1, + height: 1, + zoom: 1, + }, + expectedEvent: mouse.NewMovedEvent(-1, 0, -1, 0, false), + }, + "outside window, y == height": { + window: &fakeWindow{ + posX: 0, + posY: 1, + width: 1, + height: 1, + zoom: 1, + }, + expectedEvent: mouse.NewMovedEvent(0, 1, 0, 1, false), + }, + "outside window, y >= height": { + window: &fakeWindow{ + posX: 0, + posY: 2, + width: 1, + height: 1, + zoom: 1, + }, + expectedEvent: mouse.NewMovedEvent(0, 2, 0, 2, false), + }, + "outside window, y < 0": { + window: &fakeWindow{ + posX: 0, + posY: -1, + width: 1, + height: 1, + zoom: 1, + }, + expectedEvent: mouse.NewMovedEvent(0, -1, 0, -1, false), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + buffer := mouse.NewEventBuffer(1) + events := internal.NewMouseEvents(buffer, test.window) + // when + event, ok := events.Poll() + // then + assert.True(t, ok) + assert.Equal(t, test.expectedEvent, event) + }) + } + + }) + + t.Run("same position should not generate MoveEvent", func(t *testing.T) { + buffer := mouse.NewEventBuffer(1) + window := &fakeWindow{ + posX: 1, + posY: 1, + width: 1, + height: 1, + zoom: 1, + } + events := internal.NewMouseEvents(buffer, window) + _, _ = events.Poll() + event, ok := events.Poll() + // then + assert.False(t, ok) + assert.Equal(t, mouse.EmptyEvent, event) + }) + + t.Run("when position changes should generate MoveEvent", func(t *testing.T) { + tests := map[string]struct { + newPosX, newPosY float64 + expectedEvent mouse.Event + }{ + "inside window": { + newPosX: 2, + newPosY: 3, + expectedEvent: mouse.NewMovedEvent(2, 3, 2, 3, true), + }, + "outside window": { + newPosX: 10, + newPosY: 20, + expectedEvent: mouse.NewMovedEvent(10, 20, 10, 20, false), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + buffer := mouse.NewEventBuffer(1) + window := &fakeWindow{ + posX: 1, + posY: 1, + width: 6, + height: 6, + zoom: 1, + } + events := internal.NewMouseEvents(buffer, window) + _, _ = events.Poll() + window.posX = test.newPosX + window.posY = test.newPosY + event, ok := events.Poll() + // then + assert.True(t, ok) + assert.Equal(t, test.expectedEvent, event) + }) + } + }) +} + +func assertNoMoreMouseEvents(t *testing.T, events *internal.MouseEvents) { + event, ok := events.Poll() + require.False(t, ok) + assert.Equal(t, mouse.EmptyEvent, event) +} + +type fakeWindow struct { + posX, posY float64 + width, height int + zoom int +} + +func (f *fakeWindow) CursorPosition() (float64, float64) { + return f.posX, f.posY +} + +func (f *fakeWindow) Size() (int, int) { + return f.width, f.height +} + +func (f *fakeWindow) Zoom() int { + return f.zoom +} diff --git a/glfw/window.go b/glfw/window.go index 6c2ecfd0..96155c67 100644 --- a/glfw/window.go +++ b/glfw/window.go @@ -7,6 +7,7 @@ import ( "github.com/jacekolszak/pixiq/glfw/internal" "github.com/jacekolszak/pixiq/image" "github.com/jacekolszak/pixiq/keyboard" + "github.com/jacekolszak/pixiq/mouse" ) // Window is an implementation of loop.Screen and keyboard.EventSource @@ -15,6 +16,7 @@ type Window struct { mainThreadLoop *MainThreadLoop screenPolygon *screenPolygon keyboardEvents *internal.KeyboardEvents + mouseEvents *internal.MouseEvents requestedWidth int requestedHeight int zoom int @@ -23,6 +25,13 @@ type Window struct { api gl.API context *gl.Context program *gl.Program + mouseWindow *mouseWindow +} + +// PollMouseEvent retrieves and removes next mouse Event. If there are no more +// events false is returned. It implements mouse.EventSource method. +func (w *Window) PollMouseEvent() (mouse.Event, bool) { + return w.mouseEvents.Poll() } // Draw draws a screen image in the window @@ -68,10 +77,7 @@ func (w *Window) ShouldClose() bool { // than requested width used when window was open due to platform limitation. // If zooming is used the width is multiplied by zoom. func (w *Window) Width() int { - var width int - w.mainThreadLoop.Execute(func() { - width, _ = w.glfwWindow.GetSize() - }) + width, _ := w.mouseWindow.Size() return width } @@ -79,10 +85,7 @@ func (w *Window) Width() int { // than requested height used when window was open due to platform limitation. // If zooming is used the height is multiplied by zoom. func (w *Window) Height() int { - var height int - w.mainThreadLoop.Execute(func() { - _, height = w.glfwWindow.GetSize() - }) + _, height := w.mouseWindow.Size() return height } @@ -92,9 +95,9 @@ func (w *Window) Zoom() int { return w.zoom } -// Poll retrieves and removes next keyboard Event. If there are no more +// PollKeyboardEvent retrieves and removes next keyboard Event. If there are no more // events false is returned. It implements keyboard.EventSource method. -func (w *Window) Poll() (keyboard.Event, bool) { +func (w *Window) PollKeyboardEvent() (keyboard.Event, bool) { var ( event keyboard.Event ok bool diff --git a/glfw/window_bench_test.go b/glfw/window_bench_test.go index 301f22fc..11400a07 100644 --- a/glfw/window_bench_test.go +++ b/glfw/window_bench_test.go @@ -21,3 +21,20 @@ func BenchmarkWindow_Draw(b *testing.B) { win.Draw() } } + +func BenchmarkWindow_PollMouseEvent(b *testing.B) { + openGL, err := glfw.NewOpenGL(mainThreadLoop) + require.NoError(b, err) + defer openGL.Destroy() + win, err := openGL.OpenWindow(640, 360) + if err != nil { + panic(err) + } + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < 3; j++ { + win.PollMouseEvent() + } + } +} diff --git a/glfw/window_test.go b/glfw/window_test.go index 1729f2e5..72037e7f 100644 --- a/glfw/window_test.go +++ b/glfw/window_test.go @@ -12,6 +12,7 @@ import ( "github.com/jacekolszak/pixiq/glfw" "github.com/jacekolszak/pixiq/image" "github.com/jacekolszak/pixiq/keyboard" + "github.com/jacekolszak/pixiq/mouse" ) func TestWindow_DrawIntoBackBuffer(t *testing.T) { @@ -198,7 +199,7 @@ func framebufferPixels(context gl2.API, x, y, width, height int32) []image.Color return frameBuffer } -func TestWindow_Poll(t *testing.T) { +func TestWindow_PollKeyboardEvent(t *testing.T) { t.Run("should return EmptyEvent and false when there is no keyboard events", func(t *testing.T) { openGL, err := glfw.NewOpenGL(mainThreadLoop) require.NoError(t, err) @@ -207,13 +208,30 @@ func TestWindow_Poll(t *testing.T) { require.NoError(t, err) defer win.Close() // when - event, ok := win.Poll() + event, ok := win.PollKeyboardEvent() // then assert.Equal(t, keyboard.EmptyEvent, event) assert.False(t, ok) }) } +func TestWindow_PollMouseEvent(t *testing.T) { + t.Run("should return EmptyEvent and false when there is no mouse events", func(t *testing.T) { + openGL, err := glfw.NewOpenGL(mainThreadLoop) + require.NoError(t, err) + defer openGL.Destroy() + win, err := openGL.OpenWindow(1, 1) + require.NoError(t, err) + defer win.Close() + // when + _, _ = win.PollMouseEvent() // mouse.MoveEvent is always returned first + event, ok := win.PollMouseEvent() + // then + assert.Equal(t, mouse.EmptyEvent, event) + assert.False(t, ok) + }) +} + func TestWindow_Zoom(t *testing.T) { t.Run("should return specified zoom for window", func(t *testing.T) { tests := map[string]struct { diff --git a/keyboard/keyboard.go b/keyboard/keyboard.go index 5bac020f..a2ef61e8 100644 --- a/keyboard/keyboard.go +++ b/keyboard/keyboard.go @@ -19,12 +19,12 @@ import ( ) // EventSource is a source of keyboard Events. On each Update() Keyboard polls -// the EventSource by executing Poll method multiple times - until Poll() +// the EventSource by executing PollKeyboardEvent method multiple times - until PollKeyboardEvent() // returns false. In other words Keyboard#Update drains the EventSource. type EventSource interface { - // Poll retrieves and removes next keyboard Event. If there are no more + // PollKeyboardEvent retrieves and removes next keyboard Event. If there are no more // events false is returned. - Poll() (Event, bool) + PollKeyboardEvent() (Event, bool) } func newKey(token token) Key { @@ -118,6 +118,8 @@ func NewReleasedEvent(key Key) Event { } // Event describes what happened with the key. Whether it was pressed or released. +// +// Event can be constructed using NewXXXEvent function type Event struct { typ eventType key Key @@ -132,7 +134,9 @@ const ( released eventType = 2 ) -// New creates Keyboard instance. +// New creates Keyboard instance. It will consume all events from EventSource each +// time Update method is called. For this reason you can't have two Keyboard instances +// for the same EventSource. func New(source EventSource) *Keyboard { if source == nil { panic("nil EventSource") @@ -148,7 +152,7 @@ func New(source EventSource) *Keyboard { // Keyboard provides a read-only information about the current state of the // keyboard, such as what keys are currently pressed. Please note that // updating the Keyboard state retrieves and removes events from EventSource. -// Therefore only Keyboard instance can be created for one EventSource. +// Therefore only one Keyboard instance can be created for specific EventSource. type Keyboard struct { source EventSource pressed map[Key]struct{} @@ -162,7 +166,7 @@ func (k *Keyboard) Update() { k.clearJustPressed() k.clearJustReleased() for { - event, ok := k.source.Poll() + event, ok := k.source.PollKeyboardEvent() if !ok { return } diff --git a/keyboard/keyboard_bench_test.go b/keyboard/keyboard_bench_test.go index 42b3bc10..83ded549 100644 --- a/keyboard/keyboard_bench_test.go +++ b/keyboard/keyboard_bench_test.go @@ -9,9 +9,10 @@ import ( func BenchmarkKeyboard_Update(b *testing.B) { var ( event = keyboard.NewPressedEvent(keyboard.A) - source = &cyclicEventsSoure{event: event} + source = &cyclicEventsSource{event: event} keys = keyboard.New(source) ) + b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { keys.Update() // should be 0 alloc/op @@ -21,22 +22,23 @@ func BenchmarkKeyboard_Update(b *testing.B) { func BenchmarkKeyboard_PressedKeys(b *testing.B) { var ( event = keyboard.NewPressedEvent(keyboard.A) - source = &cyclicEventsSoure{event: event} + source = &cyclicEventsSource{event: event} keys = keyboard.New(source) ) keys.Update() + b.ReportAllocs() b.ResetTimer() for i := 0; i < b.N; i++ { keys.PressedKeys() // should be at most 1 alloc/op } } -type cyclicEventsSoure struct { +type cyclicEventsSource struct { hasEvent bool event keyboard.Event } -func (f *cyclicEventsSoure) Poll() (keyboard.Event, bool) { +func (f *cyclicEventsSource) PollKeyboardEvent() (keyboard.Event, bool) { f.hasEvent = !f.hasEvent if f.hasEvent { return f.event, true diff --git a/keyboard/keyboard_test.go b/keyboard/keyboard_test.go index b50ba6cc..c956c8fa 100644 --- a/keyboard/keyboard_test.go +++ b/keyboard/keyboard_test.go @@ -146,7 +146,7 @@ func TestKeyboard_Pressed(t *testing.T) { }) } -func TestPressedKeys(t *testing.T) { +func TestKeyboard_PressedKeys(t *testing.T) { var ( aPressed = keyboard.NewPressedEvent(keyboard.A) aReleased = keyboard.NewReleasedEvent(keyboard.A) @@ -593,7 +593,7 @@ type fakeEventSource struct { events []keyboard.Event } -func (f *fakeEventSource) Poll() (keyboard.Event, bool) { +func (f *fakeEventSource) PollKeyboardEvent() (keyboard.Event, bool) { if len(f.events) > 0 { event := f.events[0] f.events = f.events[1:] diff --git a/mouse/event_buffer.go b/mouse/event_buffer.go new file mode 100644 index 00000000..5e526775 --- /dev/null +++ b/mouse/event_buffer.go @@ -0,0 +1,50 @@ +package mouse + +// EventBuffer is a capped collection of accumulated events which can +// be used by libraries or in unit tests as a fake implementation of EventSource. +// The order of added events is preserved. +// EventBuffer is an EventSource and can be directly consumed by Mouse. +type EventBuffer struct { + circularBuffer []Event + writeIndex int + readIndex int + readAfterWrite bool +} + +// NewEventBuffer creates EventBuffer of given size. The minimum size of buffer is 1. +// Size smaller than 1 is constrained to 1. +func NewEventBuffer(size int) *EventBuffer { + if size < 1 { + size = 1 + } + return &EventBuffer{circularBuffer: make([]Event, size)} +} + +// Add adds event to the buffer. If there is not enough space the oldest event +// will be replaced. +func (q *EventBuffer) Add(event Event) { + if len(q.circularBuffer) == q.writeIndex { + q.writeIndex = 0 + q.readAfterWrite = true + } + if q.readAfterWrite && q.readIndex == q.writeIndex { + q.readIndex++ + } + q.circularBuffer[q.writeIndex] = event + q.writeIndex++ +} + +// Poll retrieves and removes event from the buffer. If there are no available +// events EmptyEvent and false is returned. +func (q *EventBuffer) Poll() (Event, bool) { + if q.writeIndex == q.readIndex && !q.readAfterWrite { + return EmptyEvent, false + } + if len(q.circularBuffer) == q.readIndex { + q.readIndex = 0 + q.readAfterWrite = false + } + event := q.circularBuffer[q.readIndex] + q.readIndex++ + return event, true +} diff --git a/mouse/event_buffer_bench_test.go b/mouse/event_buffer_bench_test.go new file mode 100644 index 00000000..56d941b4 --- /dev/null +++ b/mouse/event_buffer_bench_test.go @@ -0,0 +1,26 @@ +package mouse_test + +import ( + "testing" + + "github.com/jacekolszak/pixiq/mouse" +) + +// Should be 0 allocs/op +func BenchmarkMouseEvents(b *testing.B) { + const size = 8 + events := mouse.NewEventBuffer(size) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + for j := 0; j < size*2; j++ { + events.Add(mouse.NewPressedEvent(mouse.Left)) + } + for { + _, ok := events.Poll() + if !ok { + break + } + } + } +} diff --git a/mouse/event_buffer_test.go b/mouse/event_buffer_test.go new file mode 100644 index 00000000..d4114247 --- /dev/null +++ b/mouse/event_buffer_test.go @@ -0,0 +1,113 @@ +package mouse_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/jacekolszak/pixiq/mouse" +) + +func TestNewEventBuffer(t *testing.T) { + t.Run("should create EventBuffer", func(t *testing.T) { + sizes := []int{-1, 1, 1, 16} + for _, size := range sizes { + buffer := mouse.NewEventBuffer(size) + assert.NotNil(t, buffer) + } + }) +} + +func TestEventBuffer_Poll(t *testing.T) { + t.Run("should return EmptyEvent and false for empty EventBuffer", func(t *testing.T) { + buffer := mouse.NewEventBuffer(1) + // when + event, ok := buffer.Poll() + // then + assert.False(t, ok) + assert.Equal(t, mouse.EmptyEvent, event) + }) +} + +func TestEventBuffer_Add(t *testing.T) { + event1 := mouse.NewPressedEvent(mouse.Left) + event2 := mouse.NewScrolledEvent(1, 2) + event3 := mouse.NewMovedEvent(1, 2, 1, 2, true) + + t.Run("should add events to EventBuffer with enough space", func(t *testing.T) { + tests := map[string][]mouse.Event{ + "one event": {event1}, + "two events": {event1, event2}, + "three events": {event1, event2, event3}, + } + for name, events := range tests { + t.Run(name, func(t *testing.T) { + buffer := mouse.NewEventBuffer(3) + // when + for _, event := range events { + buffer.Add(event) + } + // then + for _, event := range events { + actualEvent, found := buffer.Poll() + assert.True(t, found) + assert.Equal(t, event, actualEvent) + } + // and + actualEvent, found := buffer.Poll() + assert.False(t, found) + assert.Equal(t, mouse.EmptyEvent, actualEvent) + }) + } + }) + t.Run("should override old events when EventBuffer has not enough space", func(t *testing.T) { + tests := map[string]struct { + buffer *mouse.EventBuffer + events []mouse.Event + expectedEvents []mouse.Event + }{ + "size 1": { + buffer: mouse.NewEventBuffer(1), + events: []mouse.Event{event1, event2}, + expectedEvents: []mouse.Event{event2}, + }, + "size 2": { + buffer: mouse.NewEventBuffer(2), + events: []mouse.Event{event1, event2, event3}, + expectedEvents: []mouse.Event{event2, event3}, + }, + "already added and polled": { + buffer: prepare(mouse.NewEventBuffer(2), func(q *mouse.EventBuffer) { + q.Add(event1) + q.Poll() + }), + events: []mouse.Event{event2, event3}, + expectedEvents: []mouse.Event{event2, event3}, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + events := test.events + // when + for _, event := range events { + test.buffer.Add(event) + } + // then + for _, event := range test.expectedEvents { + actualEvent, found := test.buffer.Poll() + assert.True(t, found) + assert.Equal(t, event, actualEvent) + } + // and + actualEvent, found := test.buffer.Poll() + assert.False(t, found) + assert.Equal(t, mouse.EmptyEvent, actualEvent) + }) + } + }) +} + +func prepare(b *mouse.EventBuffer, f func(q *mouse.EventBuffer)) *mouse.EventBuffer { + f(b) + return b +} diff --git a/mouse/mouse.go b/mouse/mouse.go new file mode 100644 index 00000000..71f99e58 --- /dev/null +++ b/mouse/mouse.go @@ -0,0 +1,352 @@ +package mouse + +// EventSource is a source of mouse Events. On each Update() Mouse polls +// the EventSource by executing PollMouseEvent method multiple times - until PollMouseEvent() +// returns false. In other words Mouse#Update drains the EventSource. +type EventSource interface { + // PollMouseEvent retrieves and removes next mouse Event. If there are no more + // events false is returned. + PollMouseEvent() (Event, bool) +} + +// New creates Mouse instance. It will consume all events from EventSource each +// time Update method is called. For this reason you can't have two Mouse instances +// for the same EventSource. +func New(source EventSource) *Mouse { + if source == nil { + panic("nil EventSource") + } + return &Mouse{ + source: source, + pressed: map[Button]struct{}{}, + justPressed: make(map[Button]bool), + justReleased: make(map[Button]bool), + position: Position{ + insideWindow: true, + }, + } +} + +// Mouse provides a read-only information about the current state of the +// mouse, such as what buttons are currently pressed. Please note that +// updating the Mouse state retrieves and removes events from EventSource. +// Therefore only one Mouse instance can be created for specific EventSource. +type Mouse struct { + source EventSource + pressed map[Button]struct{} + justPressed map[Button]bool + justReleased map[Button]bool + position Position + positionChange PositionChange + scroll Scroll +} + +// Update updates the state of the mouse by polling events queued since last +// time the function was executed. +func (m *Mouse) Update() { + m.clearJustPressed() + m.clearJustReleased() + lastPosition := m.position + m.scroll = Scroll{} + defer func() { + if lastPosition != m.position { + windowLeft := false + if !m.position.insideWindow && lastPosition.insideWindow { + windowLeft = true + } + windowEntered := false + if m.position.insideWindow && !lastPosition.insideWindow { + windowEntered = true + } + m.positionChange = PositionChange{ + x: m.position.x - lastPosition.x, + y: m.position.y - lastPosition.y, + realX: m.position.realX - lastPosition.realX, + realY: m.position.realY - lastPosition.realY, + windowLeft: windowLeft, + windowEntered: windowEntered, + } + } else { + m.positionChange = PositionChange{} + } + }() + for { + event, ok := m.source.PollMouseEvent() + if !ok { + return + } + switch event.typ { + case pressed: + m.pressed[event.button] = struct{}{} + m.justPressed[event.button] = true + case released: + delete(m.pressed, event.button) + m.justReleased[event.button] = true + case moved: + m.position = event.position + case scrolled: + m.scroll = Scroll{ + x: event.scrollX + m.scroll.x, + y: event.scrollY + m.scroll.y, + } + } + } +} + +func (m *Mouse) clearJustPressed() { + for button := range m.justPressed { + delete(m.justPressed, button) + } +} + +func (m *Mouse) clearJustReleased() { + for button := range m.justReleased { + delete(m.justReleased, button) + } +} + +// Pressed returns true if given mouse button is currently pressed. +// If between two last mouse.Update calls the key was pressed and released +// then the this method returns false. +func (m *Mouse) Pressed(button Button) bool { + _, found := m.pressed[button] + return found +} + +// PressedButtons returns a slice of all currently pressed buttons. It may be empty +// aka nil. This function can be used to get a button mapping for a given action +// in the game. +// If between two last mouse.Update calls the button was pressed and released +// then the button is not returned. +func (m *Mouse) PressedButtons() []Button { + var pressedButtons []Button + for button := range m.pressed { + pressedButtons = append(pressedButtons, button) + } + return pressedButtons +} + +// JustPressed returns true if the button was pressed between two last mouse.Update +// calls. If it was pressed and released at the same time between these calls +// this method return true. +func (m *Mouse) JustPressed(button Button) bool { + return m.justPressed[button] +} + +// JustReleased returns true if the button was released between two last mouse.Update +// calls. If it was released and pressed at the same time between these calls +// this method return true. +func (m *Mouse) JustReleased(button Button) bool { + return m.justReleased[button] +} + +// Position returns current mouse position +func (m *Mouse) Position() Position { + return m.position +} + +// PositionChange returns information about how the mouse position has changed between +// the last two Mouse.Update calls +func (m *Mouse) PositionChange() PositionChange { + return m.positionChange +} + +// PositionChanged returns true whether mouse position has changed between +// the last two Mouse.Update calls +func (m *Mouse) PositionChanged() bool { + return m.positionChange != PositionChange{} +} + +// Scroll returns information about cumulative scroll between two +// last Mouse.Update calls. +func (m *Mouse) Scroll() Scroll { + return m.scroll +} + +// Position contains information about current mouse position +type Position struct { + x, y int + realX, realY float64 + insideWindow bool +} + +// X returns the pixel x-coordinate where mouse is pointing to. +func (p Position) X() int { + return p.x +} + +// Y returns the pixel y-coordinate where mouse is pointing to. +func (p Position) Y() int { + return p.y +} + +// RealX returns the pixel x-coordinate where mouse is pointing to in a display +// resolution. If OS supports sub-pixel precision for mouse position then fractional +// number is returned. +func (p Position) RealX() float64 { + return p.realX +} + +// RealY returns the pixel y-coordinate where mouse is pointing to in a display +// resolution. If OS supports sub-pixel precision for mouse position then fractional +// number is returned. +func (p Position) RealY() float64 { + return p.realY +} + +// InsideWindow returns true if mouse is pointing to a pixel inside window. +func (p Position) InsideWindow() bool { + return p.insideWindow +} + +// PositionChange returns information about how the mouse position has changed between +// the last two Mouse.Update calls +type PositionChange struct { + x, y int + realX, realY float64 + windowEntered bool + windowLeft bool +} + +// X returns the pixel x-coordinate difference. +func (p PositionChange) X() int { + return p.x +} + +// Y returns the pixel y-coordinate difference. +func (p PositionChange) Y() int { + return p.y +} + +// RealX returns the pixel x-coordinate difference in a display resolution. +// If OS supports sub-pixel precision for mouse position then fractional +// number is returned. +func (p PositionChange) RealX() float64 { + return p.realX +} + +// RealY returns the pixel y-coordinate difference in a display resolution. +// If OS supports sub-pixel precision for mouse position then fractional +// number is returned. +func (p PositionChange) RealY() float64 { + return p.realY +} + +// WindowEntered returns true if mouse just entered the window (entered the window +// between last two Mouse.Update calls). +func (p PositionChange) WindowEntered() bool { + return p.windowEntered +} + +// WindowLeft returns true if mouse just left the window (left the window +// between last two Mouse.Update calls). +func (p PositionChange) WindowLeft() bool { + return p.windowLeft +} + +// Scroll provides information about cumulative scroll between two +// last Mouse.Update calls. +type Scroll struct { + x, y float64 +} + +// X can be negative (scrolled left) or positive (scrolled right). +func (s Scroll) X() float64 { + return s.x +} + +// Y can be negative (scrolled down) or positive (scrolled up). +func (s Scroll) Y() float64 { + return s.y +} + +// EmptyEvent should be returned by EventSource when it does not have more events. +var EmptyEvent = Event{} + +// Event describes what happened with the mouse. +// +// Event can be constructed using NewXXXEvent function. +type Event struct { + typ eventType + // Pressed/Released + button Button + // Moved + position Position + // Scroll + scrollX, scrollY float64 +} + +type eventType byte + +const ( + pressed eventType = iota + released + moved + scrolled +) + +// Button is a mouse button which was pressed or released. +type Button int + +const ( + // Left is a left mouse button + Left Button = 1 + // Right is a right mouse button + Right Button = 2 + // Middle is a middle mouse button + Middle Button = 3 + // Button4 is 4th mouse button + Button4 Button = 4 + // Button5 is 5th mouse button + Button5 Button = 5 + // Button6 is 6th mouse button + Button6 Button = 6 + // Button7 is 7th mouse button + Button7 Button = 7 + // Button8 is 8th mouse button + Button8 Button = 8 +) + +// NewReleasedEvent returns new instance of Event when button was released. +func NewReleasedEvent(button Button) Event { + return Event{ + typ: released, + button: button, + } +} + +// NewPressedEvent returns new instance of Event when button was pressed. +func NewPressedEvent(button Button) Event { + return Event{ + typ: pressed, + button: button, + } +} + +// NewScrolledEvent returns new instance of Event when mouse was scrolled. +func NewScrolledEvent(x, y float64) Event { + return Event{ + typ: scrolled, + scrollX: x, + scrollY: y, + } +} + +// NewMovedEvent returns new instance of Event when mouse was moved. +// +// realPosX and realPosY are the cursor position in real pixel coordinates. For +// systems supporting subpixel coordinates these might be fractional numbers. +// posX and posY should be virtual pixel coorindates taking into account current zoom. +// For zoom=2 and realPosX=2, posX should be 1. +func NewMovedEvent(posX, posY int, realPosX, realPosY float64, insideWindow bool) Event { + return Event{ + typ: moved, + position: Position{ + x: posX, + y: posY, + realX: realPosX, + realY: realPosY, + insideWindow: insideWindow, + }, + } +} diff --git a/mouse/mouse_bench_test.go b/mouse/mouse_bench_test.go new file mode 100644 index 00000000..82eaf087 --- /dev/null +++ b/mouse/mouse_bench_test.go @@ -0,0 +1,47 @@ +package mouse_test + +import ( + "testing" + + "github.com/jacekolszak/pixiq/mouse" +) + +func BenchmarkMouse_Update(b *testing.B) { + var ( + event = mouse.NewPressedEvent(mouse.Left) + source = &cyclicEventsSource{event: event} + mouseState = mouse.New(source) + ) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + mouseState.Update() // should be 0 alloc/op + } +} + +func BenchmarkMouse_PressedButtons(b *testing.B) { + var ( + event = mouse.NewPressedEvent(mouse.Left) + source = &cyclicEventsSource{event: event} + mouseState = mouse.New(source) + ) + mouseState.Update() + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + mouseState.PressedButtons() // should be at most 1 alloc/op + } +} + +type cyclicEventsSource struct { + hasEvent bool + event mouse.Event +} + +func (f *cyclicEventsSource) PollMouseEvent() (mouse.Event, bool) { + f.hasEvent = !f.hasEvent + if f.hasEvent { + return f.event, true + } + return mouse.EmptyEvent, false +} diff --git a/mouse/mouse_test.go b/mouse/mouse_test.go new file mode 100644 index 00000000..7ec83478 --- /dev/null +++ b/mouse/mouse_test.go @@ -0,0 +1,690 @@ +package mouse_test + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/jacekolszak/pixiq/mouse" +) + +func TestNew(t *testing.T) { + t.Run("should panic when source is nil", func(t *testing.T) { + assert.Panics(t, func() { + mouse.New(nil) + }) + }) + t.Run("should create a mouse instance", func(t *testing.T) { + source := &fakeEventSource{} + // when + mouseState := mouse.New(source) + // then + assert.NotNil(t, mouseState) + }) +} + +func TestMouse_Pressed(t *testing.T) { + t.Run("before Update was called, Pressed returns false for all buttons", func(t *testing.T) { + tests := []mouse.Button{mouse.Left, mouse.Right, mouse.Middle, mouse.Button4, mouse.Button5, mouse.Button6, mouse.Button7, mouse.Button8} + for _, button := range tests { + testName := fmt.Sprintf("for button: %v", button) + t.Run(testName, func(t *testing.T) { + var ( + event = mouse.NewPressedEvent(mouse.Left) + source = newFakeEventSource(event) + mouseState = mouse.New(source) + ) + // when + pressed := mouseState.Pressed(button) + // then + assert.False(t, pressed) + }) + } + }) + t.Run("after Update was called", func(t *testing.T) { + var ( + leftPressed = mouse.NewPressedEvent(mouse.Left) + leftReleased = mouse.NewReleasedEvent(mouse.Left) + rightPressed = mouse.NewPressedEvent(mouse.Right) + rightReleased = mouse.NewReleasedEvent(mouse.Right) + ) + tests := map[string]struct { + source mouse.EventSource + expectedPressed []mouse.Button + expectedNotPressed []mouse.Button + }{ + "one PressedEvent for Left": { + source: newFakeEventSource(leftPressed), + expectedPressed: []mouse.Button{mouse.Left}, + expectedNotPressed: []mouse.Button{mouse.Right}, + }, + "two PressedEvents for Right and Left": { + source: newFakeEventSource(rightPressed, leftPressed), + expectedPressed: []mouse.Button{mouse.Left, mouse.Right}, + }, + "two PressedEvents for Left and Right": { + source: newFakeEventSource(leftPressed, rightPressed), + expectedPressed: []mouse.Button{mouse.Left, mouse.Right}, + }, + "one PressedEvent; one ReleasedEvent for Left": { + source: newFakeEventSource(leftPressed, leftReleased), + expectedNotPressed: []mouse.Button{mouse.Left}, + }, + "one PressedEvent for Left; one ReleasedEvent for Right": { + source: newFakeEventSource(leftPressed, rightReleased), + expectedPressed: []mouse.Button{mouse.Left}, + expectedNotPressed: []mouse.Button{mouse.Right}, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mouseState := mouse.New(test.source) + // when + mouseState.Update() + // then + for _, expectedPressedButton := range test.expectedPressed { + assert.True(t, mouseState.Pressed(expectedPressedButton)) + } + for _, expectedNotPressedButton := range test.expectedNotPressed { + assert.False(t, mouseState.Pressed(expectedNotPressedButton)) + } + }) + } + }) +} + +func TestMouse_PressedButtons(t *testing.T) { + var ( + leftPressed = mouse.NewPressedEvent(mouse.Left) + leftReleased = mouse.NewReleasedEvent(mouse.Left) + rightPressed = mouse.NewPressedEvent(mouse.Right) + ) + t.Run("before Update pressed buttons are empty", func(t *testing.T) { + source := newFakeEventSource(leftPressed) + mouseState := mouse.New(source) + // when + pressed := mouseState.PressedButtons() + // then + assert.Empty(t, pressed) + }) + t.Run("after Update", func(t *testing.T) { + tests := map[string]struct { + source mouse.EventSource + expectedPressed []mouse.Button + }{ + "one PressedEvent for Left": { + source: newFakeEventSource(leftPressed), + expectedPressed: []mouse.Button{mouse.Left}, + }, + "one PressedEvent for Right": { + source: newFakeEventSource(rightPressed), + expectedPressed: []mouse.Button{mouse.Right}, + }, + "one PressedEvent for Left, one ReleaseEvent for Left": { + source: newFakeEventSource(leftPressed, leftReleased), + expectedPressed: nil, + }, + "Left pressed, then released and pressed again": { + source: newFakeEventSource(leftPressed, leftReleased, leftPressed), + expectedPressed: []mouse.Button{mouse.Left}, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mouseState := mouse.New(test.source) + mouseState.Update() + // when + pressed := mouseState.PressedButtons() + // then + assert.Equal(t, test.expectedPressed, pressed) + }) + } + }) + t.Run("after second update", func(t *testing.T) { + t.Run("when Left was pressed before first update", func(t *testing.T) { + source := newFakeEventSource(leftPressed) + mouseState := mouse.New(source) + mouseState.Update() + mouseState.Update() + // when + pressed := mouseState.PressedButtons() + // then + assert.Equal(t, []mouse.Button{mouse.Left}, pressed) + }) + t.Run("when Left was pressed before first update, then released before second one", func(t *testing.T) { + source := newFakeEventSource(leftPressed) + mouseState := mouse.New(source) + mouseState.Update() + source.events = append(source.events, leftReleased) + mouseState.Update() + // when + pressed := mouseState.PressedButtons() + // then + assert.Empty(t, pressed) + }) + }) +} + +func TestJustPressed(t *testing.T) { + var ( + leftPressed = mouse.NewPressedEvent(mouse.Left) + leftReleased = mouse.NewReleasedEvent(mouse.Left) + rightPressed = mouse.NewPressedEvent(mouse.Right) + ) + + t.Run("before update should return false", func(t *testing.T) { + tests := map[string]struct { + source mouse.EventSource + }{ + "for no events": { + source: newFakeEventSource(), + }, + "when Left was pressed": { + source: newFakeEventSource(leftPressed), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mouseState := mouse.New(test.source) + // when + justPressed := mouseState.JustPressed(mouse.Left) + // then + assert.False(t, justPressed) + }) + } + }) + + t.Run("after first update", func(t *testing.T) { + tests := map[string]struct { + source mouse.EventSource + expectedJustPressed bool + }{ + "for no events": { + source: newFakeEventSource(), + expectedJustPressed: false, + }, + "when Left has been pressed": { + source: newFakeEventSource(leftPressed), + expectedJustPressed: true, + }, + "when Right has been pressed": { + source: newFakeEventSource(rightPressed), + expectedJustPressed: false, + }, + "when Left has been released": { + source: newFakeEventSource(leftReleased), + expectedJustPressed: false, + }, + "when Left has been pressed and released": { + source: newFakeEventSource(leftPressed, leftReleased), + expectedJustPressed: true, + }, + "when Left has been released and pressed": { + source: newFakeEventSource(leftReleased, leftPressed), + expectedJustPressed: true, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mouseState := mouse.New(test.source) + mouseState.Update() + // when + justPressed := mouseState.JustPressed(mouse.Left) + // then + assert.Equal(t, test.expectedJustPressed, justPressed) + }) + } + }) + + t.Run("should return false after second update", func(t *testing.T) { + source := newFakeEventSource(leftPressed) + mouseState := mouse.New(source) + mouseState.Update() + mouseState.Update() + // when + pressed := mouseState.JustPressed(mouse.Left) + // then + assert.False(t, pressed) + }) +} + +func TestJustReleased(t *testing.T) { + var ( + leftReleased = mouse.NewReleasedEvent(mouse.Left) + leftPressed = mouse.NewPressedEvent(mouse.Left) + rightReleased = mouse.NewReleasedEvent(mouse.Right) + ) + + t.Run("before update should return false", func(t *testing.T) { + tests := map[string]struct { + source mouse.EventSource + }{ + "for no events": { + source: newFakeEventSource(), + }, + "when Left was released": { + source: newFakeEventSource(leftReleased), + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mouseState := mouse.New(test.source) + // when + justReleased := mouseState.JustReleased(mouse.Left) + // then + assert.False(t, justReleased) + }) + } + }) + + t.Run("after first update", func(t *testing.T) { + tests := map[string]struct { + source mouse.EventSource + expectedJustReleased bool + }{ + "for no events": { + source: newFakeEventSource(), + expectedJustReleased: false, + }, + "when Left was released": { + source: newFakeEventSource(leftReleased), + expectedJustReleased: true, + }, + "when Right was released": { + source: newFakeEventSource(rightReleased), + expectedJustReleased: false, + }, + "when Left was pressed": { + source: newFakeEventSource(leftPressed), + expectedJustReleased: false, + }, + "when Left was released and pressed": { + source: newFakeEventSource(leftReleased, leftPressed), + expectedJustReleased: true, + }, + "when Left was pressed and released": { + source: newFakeEventSource(leftPressed, leftReleased), + expectedJustReleased: true, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mouseState := mouse.New(test.source) + mouseState.Update() + // when + justReleased := mouseState.JustReleased(mouse.Left) + // then + assert.Equal(t, test.expectedJustReleased, justReleased) + }) + } + }) + + t.Run("should return false after second update", func(t *testing.T) { + source := newFakeEventSource(leftReleased) + mouseState := mouse.New(source) + mouseState.Update() + mouseState.Update() + // when + released := mouseState.JustReleased(mouse.Left) + // then + assert.False(t, released) + }) +} + +func TestMouse_Update(t *testing.T) { + tests := map[string]int{ + "1 event": 1, + "2 events": 2, + "1000 events": 1000, + } + for name, numberOfEvents := range tests { + t.Run(name, func(t *testing.T) { + t.Run("should drain EventSource", func(t *testing.T) { + source := newFakeEventSourceWith(numberOfEvents) + mouseState := mouse.New(source) + // when + mouseState.Update() + // then + assert.Empty(t, source.events) + }) + + t.Run("should drain EventSource after second Update()", func(t *testing.T) { + source := newFakeEventSourceWith(numberOfEvents) + mouseState := mouse.New(source) + mouseState.Update() + // when + mouseState.Update() + // then + assert.Empty(t, source.events) + }) + }) + } +} + +type expectedPosition struct { + x, y int + realX, realY float64 + insideWindow bool +} + +type expectedPositionChange struct { + x, y int + realX, realY float64 + windowEntered, windowLeft bool + hasChanged bool +} + +func TestMouse_Position(t *testing.T) { + t.Run("before Update was called, Position returns 0, 0", func(t *testing.T) { + source := newFakeEventSource() + mouseState := mouse.New(source) + // when + position := mouseState.Position() + // then + assert.Equal(t, 0, position.X()) + assert.Equal(t, 0, position.Y()) + assert.Equal(t, 0.0, position.RealX()) + assert.Equal(t, 0.0, position.RealY()) + assert.True(t, position.InsideWindow()) + }) + t.Run("after Update was called", func(t *testing.T) { + // when + tests := map[string]struct { + source mouse.EventSource + expectedPosition expectedPosition + }{ + "no events": { + source: newFakeEventSource(), + expectedPosition: expectedPosition{ + insideWindow: true, + }, + }, + "one event": { + source: newFakeEventSource( + mouse.NewMovedEvent(0, 0, 0.0, 0.0, true)), + expectedPosition: expectedPosition{ + insideWindow: true, + }, + }, + "one event with non default values": { + source: newFakeEventSource( + mouse.NewMovedEvent(1, 2, 1.0, 2.0, false)), + expectedPosition: expectedPosition{ + x: 1, + y: 2, + realX: 1.0, + realY: 2.0, + insideWindow: false, + }, + }, + "one event with zoom": { + source: newFakeEventSource( + mouse.NewMovedEvent(1, 2, 4, 8, true)), + expectedPosition: expectedPosition{ + x: 1, + y: 2, + realX: 4.0, + realY: 8.0, + insideWindow: true, + }, + }, + "one event with subpixels": { + source: newFakeEventSource( + mouse.NewMovedEvent(1, 2, 1.5, 2.3, true)), + expectedPosition: expectedPosition{ + x: 1, + y: 2, + realX: 1.5, + realY: 2.3, + insideWindow: true, + }, + }, + "two events": { + source: newFakeEventSource( + mouse.NewMovedEvent(2, 3, 2.0, 3.0, true), + mouse.NewMovedEvent(1, 2, 1.0, 2.0, false), + ), + expectedPosition: expectedPosition{ + x: 1, + y: 2, + realX: 1.0, + realY: 2.0, + insideWindow: false, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mouseState := mouse.New(test.source) + mouseState.Update() + // when + position := mouseState.Position() + // then + assert.Equal(t, test.expectedPosition.x, position.X()) + assert.Equal(t, test.expectedPosition.y, position.Y()) + // and + assert.Equal(t, test.expectedPosition.realX, position.RealX()) + assert.Equal(t, test.expectedPosition.realY, position.RealY()) + // and + assert.Equal(t, test.expectedPosition.insideWindow, position.InsideWindow()) + }) + } + }) + t.Run("should return position change", func(t *testing.T) { + // when + tests := map[string]struct { + source mouse.EventSource + expectedChange expectedPositionChange + }{ + "no events": { + source: newFakeEventSource(), + }, + "one event": { + source: newFakeEventSource( + mouse.NewMovedEvent(1, 2, 1.0, 2.0, false), + ), + expectedChange: expectedPositionChange{ + x: 1, + y: 2, + realX: 1.0, + realY: 2.0, + windowLeft: true, + hasChanged: true, + }, + }, + "two events": { + source: newFakeEventSource( + mouse.NewMovedEvent(1, 2, 1.0, 2.0, false), + mouse.NewMovedEvent(2, 3, 2.0, 3.0, false), + ), + expectedChange: expectedPositionChange{ + x: 2, + y: 3, + realX: 2.0, + realY: 3.0, + windowLeft: true, + hasChanged: true, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mouseState := mouse.New(test.source) + mouseState.Update() + // when + change := mouseState.PositionChange() + // then + assert.Equal(t, test.expectedChange.hasChanged, mouseState.PositionChanged()) + // and + assert.Equal(t, test.expectedChange.x, change.X()) + assert.Equal(t, test.expectedChange.y, change.Y()) + // and + assert.Equal(t, test.expectedChange.realX, change.RealX()) + assert.Equal(t, test.expectedChange.realY, change.RealY()) + // and + assert.Equal(t, test.expectedChange.windowEntered, change.WindowEntered()) + assert.Equal(t, test.expectedChange.windowLeft, change.WindowLeft()) + }) + } + }) + t.Run("should return position change after second update", func(t *testing.T) { + // when + tests := map[string]struct { + newEvents []mouse.Event + expectedChange expectedPositionChange + }{ + "no new events": {}, + "one new event": { + newEvents: []mouse.Event{ + mouse.NewMovedEvent(2, 4, 2.5, 4.3, false), + }, + expectedChange: expectedPositionChange{ + x: 1, + y: 2, + realX: 1.5, + realY: 2.3, + hasChanged: true, + }, + }, + "one new event inside window": { + newEvents: []mouse.Event{ + mouse.NewMovedEvent(1, 0, 1.0, 0, true), + }, + expectedChange: expectedPositionChange{ + x: 0, + y: -2, + realX: 0.0, + realY: -2.0, + windowEntered: true, + hasChanged: true, + }, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + source := newFakeEventSource( + mouse.NewMovedEvent(1, 2, 1.0, 2.0, false), + ) + mouseState := mouse.New(source) + mouseState.Update() + source.events = test.newEvents + mouseState.Update() + // when + change := mouseState.PositionChange() + // then + assert.Equal(t, test.expectedChange.hasChanged, mouseState.PositionChanged()) + // and + assert.Equal(t, test.expectedChange.x, change.X()) + assert.Equal(t, test.expectedChange.y, change.Y()) + // and + assert.Equal(t, test.expectedChange.realX, change.RealX()) + assert.Equal(t, test.expectedChange.realY, change.RealY()) + // and + assert.Equal(t, test.expectedChange.windowEntered, change.WindowEntered()) + assert.Equal(t, test.expectedChange.windowLeft, change.WindowLeft()) + }) + } + }) +} + +func TestMouse_Scroll(t *testing.T) { + t.Run("before Update was called, Scroll returns 0, 0", func(t *testing.T) { + source := newFakeEventSource() + mouseState := mouse.New(source) + // when + scroll := mouseState.Scroll() + // then + assert.Equal(t, 0.0, scroll.X()) + assert.Equal(t, 0.0, scroll.Y()) + }) + t.Run("after Update was called", func(t *testing.T) { + tests := map[string]struct { + source mouse.EventSource + expectedX, expectedY float64 + }{ + "no events": { + source: newFakeEventSource(), + }, + "one event": { + source: newFakeEventSource(mouse.NewScrolledEvent(1.1, 2.2)), + expectedX: 1.1, + expectedY: 2.2, + }, + "two events": { + source: newFakeEventSource(mouse.NewScrolledEvent(1.1, 2.2), mouse.NewScrolledEvent(0.3, 0.5)), + expectedX: 1.4, + expectedY: 2.7, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + mouseState := mouse.New(test.source) + mouseState.Update() + // when + scroll := mouseState.Scroll() + // then + assert.InDelta(t, test.expectedX, scroll.X(), 0.000000001) + assert.InDelta(t, test.expectedY, scroll.Y(), 0.000000001) + }) + } + }) + t.Run("after second Update was called", func(t *testing.T) { + tests := map[string]struct { + newEvents []mouse.Event + expectedX, expectedY float64 + }{ + "no new events": { + expectedX: 0.0, + expectedY: 0.0, + }, + "one event": { + newEvents: []mouse.Event{mouse.NewScrolledEvent(0.1, 0.2)}, + expectedX: 0.1, + expectedY: 0.2, + }, + } + for name, test := range tests { + t.Run(name, func(t *testing.T) { + source := newFakeEventSource(mouse.NewScrolledEvent(1.1, 2.2)) + mouseState := mouse.New(source) + mouseState.Update() + source.events = test.newEvents + mouseState.Update() + // when + scroll := mouseState.Scroll() + // then + assert.InDelta(t, test.expectedX, scroll.X(), 0.000000001) + assert.InDelta(t, test.expectedY, scroll.Y(), 0.000000001) + }) + } + }) +} + +func newFakeEventSource(events ...mouse.Event) *fakeEventSource { + source := &fakeEventSource{} + source.events = []mouse.Event{} + source.events = append(source.events, events...) + return source +} + +type fakeEventSource struct { + events []mouse.Event +} + +func (f *fakeEventSource) PollMouseEvent() (mouse.Event, bool) { + if len(f.events) > 0 { + event := f.events[0] + f.events = f.events[1:] + return event, true + } + return mouse.EmptyEvent, false +} + +func newFakeEventSourceWith(numberOfEvents int) *fakeEventSource { + source := newFakeEventSource() + for i := 0; i < numberOfEvents; i++ { + source.events = append(source.events, mouse.NewPressedEvent(mouse.Left)) + } + return source +}