Skip to content
This repository has been archived by the owner on Aug 15, 2023. It is now read-only.

Commit

Permalink
Add mouse support (#107)
Browse files Browse the repository at this point in the history
  • Loading branch information
elgopher authored May 8, 2020
1 parent 18a90ec commit 918cdcc
Show file tree
Hide file tree
Showing 21 changed files with 1,972 additions and 30 deletions.
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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?

Expand All @@ -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!
Expand Down
48 changes: 48 additions & 0 deletions examples/mouse/position/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
48 changes: 48 additions & 0 deletions examples/mouse/pressed/main.go
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
35 changes: 35 additions & 0 deletions examples/mouse/scroll/main.go
Original file line number Diff line number Diff line change
@@ -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()
}
})
})
}
48 changes: 47 additions & 1 deletion glfw/glfw.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package glfw

import (
"log"
"sync"
"time"

gl33 "github.com/go-gl/gl/v3.3-core/gl"
Expand All @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -211,14 +246,25 @@ 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")
continue
}
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()
})
Expand Down
47 changes: 47 additions & 0 deletions glfw/glfw_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package glfw_test

import (
"os"
"sync"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -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()
})
}
2 changes: 1 addition & 1 deletion glfw/internal/keyboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
88 changes: 88 additions & 0 deletions glfw/internal/mouse.go
Original file line number Diff line number Diff line change
@@ -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
}
Loading

0 comments on commit 918cdcc

Please sign in to comment.