render: Refactor Events System to Make Module Standalone

* Refactor the events used in lib/render/sdl to be more general-purpose
  to make librender a stand-alone library separate from Doodle.
This commit is contained in:
Noah 2019-12-22 14:11:01 -08:00
parent 857e5ec098
commit 5c803f6a88
9 changed files with 200 additions and 154 deletions

7
README.md Normal file
View File

@ -0,0 +1,7 @@
# Render: Go Graphics Library
Render is a graphics library written in Go which targets desktop applications on
Windows, MacOS and Linux as well as WebAssembly to run in the browser.
For desktop systems it uses SDL2 under the hood, and in WebAssembly it interacts
with an HTML Canvas element for drawing.

View File

@ -4,7 +4,7 @@ import (
"syscall/js"
"time"
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render/event"
)
// Engine implements a rendering engine targeting an HTML canvas for
@ -17,7 +17,7 @@ type Engine struct {
ticks uint32
// Private fields.
events *events.State
events *event.State
running bool
textures map[string]*Texture // cached texture PNG images
@ -34,7 +34,7 @@ func New(canvasID string) (*Engine, error) {
engine := &Engine{
canvas: canvas,
startTime: time.Now(),
events: events.New(),
events: event.NewState(),
width: canvas.ClientW(),
height: canvas.ClientH(),
queue: make(chan Event, 1024),

View File

@ -3,7 +3,7 @@ package canvas
import (
"syscall/js"
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render/event"
)
// EventClass to categorize JavaScript events.
@ -158,21 +158,21 @@ func (e *Engine) PollEvent() *Event {
}
// Poll for events.
func (e *Engine) Poll() (*events.State, error) {
func (e *Engine) Poll() (*event.State, error) {
s := e.events
for event := e.PollEvent(); event != nil; event = e.PollEvent() {
switch event.Class {
case WindowEvent:
s.Resized.Push(true)
s.WindowResized = true
case MouseEvent:
s.CursorX.Push(int32(event.X))
s.CursorY.Push(int32(event.Y))
s.CursorX = event.X
s.CursorY = event.Y
case ClickEvent:
s.CursorX.Push(int32(event.X))
s.CursorY.Push(int32(event.Y))
s.Button1.Push(event.LeftClick)
s.Button2.Push(event.RightClick)
s.CursorX = event.X
s.CursorY = event.Y
s.Button1 = event.LeftClick
s.Button2 = event.RightClick
case KeyEvent:
switch event.KeyName {
case "Escape":
@ -180,45 +180,34 @@ func (e *Engine) Poll() (*events.State, error) {
continue
}
if event.State {
s.EscapeKey.Push(true)
}
s.Escape = event.State
case "Enter":
if event.Repeat {
continue
}
if event.State {
s.EnterKey.Push(true)
}
s.Enter = event.State
case "F3":
if event.State {
s.KeyName.Push("F3")
}
s.SetKeyDown("F3", event.State)
case "ArrowUp":
s.Up.Push(event.State)
s.Up = event.State
case "ArrowLeft":
s.Left.Push(event.State)
s.Left = event.State
case "ArrowRight":
s.Right.Push(event.State)
s.Right = event.State
case "ArrowDown":
s.Down.Push(event.State)
s.Down = event.State
case "Shift":
s.ShiftActive.Push(event.State)
s.Shift = event.State
continue
case "Alt":
s.Alt = event.State
case "Control":
continue
s.Ctrl = event.State
case "Backspace":
if event.State {
s.KeyName.Push(`\b`)
}
s.SetKeyDown(`\b`, event.State)
default:
if event.State {
s.KeyName.Push(event.KeyName)
} else {
s.KeyName.Push("")
}
s.SetKeyDown(event.KeyName, event.State)
}
}
}

View File

@ -7,8 +7,6 @@ import (
"image/color"
"regexp"
"strconv"
"github.com/vmihailenco/msgpack"
)
var (
@ -165,52 +163,6 @@ func (c *Color) UnmarshalJSON(b []byte) error {
return nil
}
func (c Color) EncodeMsgpack(enc *msgpack.Encoder) error {
return enc.EncodeString(fmt.Sprintf(
`"#%02x%02x%02x"`,
c.Red, c.Green, c.Blue,
))
}
func (c Color) DecodeMsgpack(dec *msgpack.Decoder) error {
hex, err := dec.DecodeString()
if err != nil {
return fmt.Errorf("Color.DecodeMsgpack: %s", err)
}
parsed, err := HexColor(hex)
if err != nil {
return fmt.Errorf("Color.DecodeMsgpack: HexColor: %s", err)
}
c.Red = parsed.Red
c.Blue = parsed.Blue
c.Green = parsed.Green
c.Alpha = parsed.Alpha
return nil
}
// // MarshalMsgpack serializes the Color for msgpack.
// func (c Color) MarshalMsgpack() ([]byte, error) {
// data := []uint8{
// c.Red, c.Green, c.Blue, c.Alpha,
// }
// return msgpack.Marshal(data)
// }
//
// // UnmarshalMsgpack decodes a Color from msgpack format.
// func (c *Color) UnmarshalMsgpack(b []byte) error {
// var data []uint8
// if err := msgpack.Unmarshal(data, b); err != nil {
// return err
// }
// c.Red = 255
// c.Green = data[1]
// c.Blue = data[2]
// c.Alpha = data[3]
// return nil
// }
// IsZero returns if the color is all zeroes (invisible).
func (c Color) IsZero() bool {
return c.Red+c.Green+c.Blue+c.Alpha == 0

109
event/event.go Normal file
View File

@ -0,0 +1,109 @@
package event
import "strings"
// State holds the current state of key/mouse events.
type State struct {
// Mouse buttons.
Button1 bool // Left
Button2 bool // Middle
Button3 bool // Right
// Special keys
Escape bool
Enter bool
Shift bool
Ctrl bool
Alt bool
Up bool
Left bool
Right bool
Down bool
// Pressed keys.
keydown map[string]interface{}
// Cursor position
CursorX int
CursorY int
// Window resized
WindowResized bool
}
// NewState creates a new event.State.
func NewState() *State {
return &State{
keydown: map[string]interface{}{},
}
}
// SetKeyDown sets that a named key is pressed down.
func (s *State) SetKeyDown(name string, down bool) {
if down {
s.keydown[name] = nil
} else {
delete(s.keydown, name)
}
}
// KeyDown returns whether a named key is currently pressed.
func (s *State) KeyDown(name string) bool {
_, ok := s.keydown[name]
return ok
}
// KeysDown returns a list of all key names currently pressed down.
// Set shifted to True to return the key symbols correctly shifted
// (uppercase, or symbols on number keys, etc.)
func (s *State) KeysDown(shifted bool) []string {
var (
result = make([]string, len(s.keydown))
i = 0
)
for key := range s.keydown {
if shifted && s.Shift {
if symbol, ok := shiftMap[key]; ok {
result[i] = symbol
} else {
result[i] = strings.ToUpper(key)
}
} else {
result[i] = key
}
i++
}
return result
}
// ResetKeyDown clears all key-down states.
func (s *State) ResetKeyDown() {
s.keydown = map[string]interface{}{}
}
// shiftMap maps keys to their Shift versions.
var shiftMap = map[string]string{
"`": "~",
"1": "!",
"2": "@",
"3": "#",
"4": "$",
"5": "%",
"6": "^",
"7": "&",
"8": "*",
"9": "(",
"0": ")",
"-": "_",
"=": "+",
"[": "{",
"]": "}",
`\`: "|",
";": ":",
`'`: `"`,
",": "<",
".": ">",
"/": "?",
}

View File

@ -4,7 +4,7 @@ import (
"fmt"
"image"
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render/event"
)
// Engine is the interface for the rendering engine, keeping SDL-specific stuff
@ -13,7 +13,7 @@ type Engine interface {
Setup() error
// Poll for events like keypresses and mouse clicks.
Poll() (*events.State, error)
Poll() (*event.State, error)
GetTicks() uint32
WindowSize() (w, h int)

View File

@ -4,7 +4,7 @@ import (
"errors"
"fmt"
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render/event"
"github.com/veandco/go-sdl2/sdl"
)
@ -12,19 +12,17 @@ import (
var (
DebugWindowEvents = false
DebugMouseEvents = false
DebugClickEvents = false
DebugClickEvents = true
DebugKeyEvents = false
)
// Poll for events.
func (r *Renderer) Poll() (*events.State, error) {
func (r *Renderer) Poll() (*event.State, error) {
s := r.events
// helper function to push keyboard key names on keyDown events only.
pushKey := func(name string, state uint8) {
if state == 1 {
s.KeyName.Push(name)
}
s.SetKeyDown(name, state == 1)
}
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
@ -42,7 +40,10 @@ func (r *Renderer) Poll() (*events.State, error) {
)
}
}
s.Resized.Push(true)
if t.Event == sdl.WINDOWEVENT_RESIZED {
s.WindowResized = true
}
case *sdl.MouseMotionEvent:
if DebugMouseEvents {
fmt.Printf("[%d ms] tick:%d MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d",
@ -51,42 +52,50 @@ func (r *Renderer) Poll() (*events.State, error) {
}
// Push the cursor position.
s.CursorX.Push(t.X)
s.CursorY.Push(t.Y)
s.Button1.Push(t.State == 1)
s.CursorX = int(t.X)
s.CursorY = int(t.Y)
case *sdl.MouseButtonEvent:
if DebugClickEvents {
fmt.Printf("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d",
fmt.Printf("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d\n",
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.Button, t.State,
)
}
// Push the cursor position.
s.CursorX.Push(t.X)
s.CursorY.Push(t.Y)
s.CursorX = int(t.X)
s.CursorY = int(t.Y)
// Is a mouse button pressed down?
checkDown := func(number uint8, target *events.BoolTick) bool {
if t.Button == number {
var eventName string
if t.State == 1 && target.Now == false {
eventName = "DOWN"
} else if t.State == 0 && target.Now == true {
eventName = "UP"
}
if eventName != "" {
target.Push(eventName == "DOWN")
}
return true
}
return false
}
if checkDown(1, s.Button1) || checkDown(3, s.Button2) || checkDown(2, s.Button3) {
// Return the event immediately.
return s, nil
// Store the clicked state of the mouse button.
if t.Button == 1 {
s.Button1 = t.State == 1
} else if t.Button == 2 {
s.Button2 = t.State == 1
} else if t.Button == 3 {
s.Button3 = t.State == 1
}
//
// // Is a mouse button pressed down?
// checkDown := func(number uint8, target *events.BoolTick) bool {
// if t.Button == number {
// var eventName string
// if t.State == 1 && target.Now == false {
// eventName = "DOWN"
// } else if t.State == 0 && target.Now == true {
// eventName = "UP"
// }
//
// if eventName != "" {
// target.Push(eventName == "DOWN")
// }
// return true
// }
// return false
// }
//
// if checkDown(1, s.Button1) || checkDown(3, s.Button2) || checkDown(2, s.Button3) {
// // Return the event immediately.
// return s, nil
// }
case *sdl.MouseWheelEvent:
if DebugMouseEvents {
fmt.Printf("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d",
@ -105,12 +114,12 @@ func (r *Renderer) Poll() (*events.State, error) {
if t.Repeat == 1 {
continue
}
s.EscapeKey.Push(t.State == 1)
s.Escape = t.State == 1
case sdl.SCANCODE_RETURN:
if t.Repeat == 1 {
continue
}
s.EnterKey.Push(t.State == 1)
s.Enter = t.State == 1
case sdl.SCANCODE_F1:
pushKey("F1", t.State)
case sdl.SCANCODE_F2:
@ -136,33 +145,29 @@ func (r *Renderer) Poll() (*events.State, error) {
case sdl.SCANCODE_F12:
pushKey("F12", t.State)
case sdl.SCANCODE_UP:
s.Up.Push(t.State == 1)
s.Up = t.State == 1
case sdl.SCANCODE_LEFT:
s.Left.Push(t.State == 1)
s.Left = t.State == 1
case sdl.SCANCODE_RIGHT:
s.Right.Push(t.State == 1)
s.Right = t.State == 1
case sdl.SCANCODE_DOWN:
s.Down.Push(t.State == 1)
s.Down = t.State == 1
case sdl.SCANCODE_LSHIFT:
case sdl.SCANCODE_RSHIFT:
s.ShiftActive.Push(t.State == 1)
s.Shift = t.State == 1
case sdl.SCANCODE_LALT:
case sdl.SCANCODE_RALT:
continue
s.Alt = t.State == 1
case sdl.SCANCODE_LCTRL:
s.ControlActive.Push(t.State == 1)
s.Ctrl = t.State == 1
case sdl.SCANCODE_RCTRL:
s.ControlActive.Push(t.State == 1)
s.Ctrl = t.State == 1
case sdl.SCANCODE_BACKSPACE:
// Make it a key event with "\b" as the sequence.
if t.State == 1 || t.Repeat == 1 {
s.KeyName.Push(`\b`)
}
s.SetKeyDown(`\b`, t.State == 1 || t.Repeat == 1)
default:
// Push the string value of the key.
if t.State == 1 {
s.KeyName.Push(string(t.Keysym.Sym))
}
s.SetKeyDown(string(t.Keysym.Sym), t.State == 1)
}
}
}

View File

@ -5,8 +5,8 @@ import (
"fmt"
"time"
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render"
"git.kirsle.net/apps/doodle/lib/render/event"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
)
@ -20,7 +20,7 @@ type Renderer struct {
startTime time.Time
// Private fields.
events *events.State
events *event.State
window *sdl.Window
renderer *sdl.Renderer
running bool
@ -34,7 +34,7 @@ type Renderer struct {
// New creates the SDL renderer.
func New(title string, width, height int) *Renderer {
return &Renderer{
events: events.New(),
events: event.NewState(),
title: title,
width: int32(width),
height: int32(height),

View File

@ -2,10 +2,8 @@ package sdl
import (
"fmt"
"strings"
"sync"
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render"
"github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf"
@ -77,20 +75,6 @@ func LoadFont(filename string, size int) (*ttf.Font, error) {
return font, nil
}
// Keysym returns the current key pressed, taking into account the Shift
// key modifier.
func (r *Renderer) Keysym(ev *events.State) string {
if key := ev.KeyName.Read(); key != "" {
if ev.ShiftActive.Pressed() {
if symbol, ok := shiftMap[key]; ok {
return symbol
}
return strings.ToUpper(key)
}
}
return ""
}
// ComputeTextRect computes and returns a Rect for how large the text would
// appear if rendered.
func (r *Renderer) ComputeTextRect(text render.Text) (render.Rect, error) {