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

View File

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

View File

@ -7,8 +7,6 @@ import (
"image/color" "image/color"
"regexp" "regexp"
"strconv" "strconv"
"github.com/vmihailenco/msgpack"
) )
var ( var (
@ -165,52 +163,6 @@ func (c *Color) UnmarshalJSON(b []byte) error {
return nil 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). // IsZero returns if the color is all zeroes (invisible).
func (c Color) IsZero() bool { func (c Color) IsZero() bool {
return c.Red+c.Green+c.Blue+c.Alpha == 0 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" "fmt"
"image" "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 // Engine is the interface for the rendering engine, keeping SDL-specific stuff
@ -13,7 +13,7 @@ type Engine interface {
Setup() error Setup() error
// Poll for events like keypresses and mouse clicks. // Poll for events like keypresses and mouse clicks.
Poll() (*events.State, error) Poll() (*event.State, error)
GetTicks() uint32 GetTicks() uint32
WindowSize() (w, h int) WindowSize() (w, h int)

View File

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

View File

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

View File

@ -2,10 +2,8 @@ package sdl
import ( import (
"fmt" "fmt"
"strings"
"sync" "sync"
"git.kirsle.net/apps/doodle/lib/events"
"git.kirsle.net/apps/doodle/lib/render" "git.kirsle.net/apps/doodle/lib/render"
"github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/sdl"
"github.com/veandco/go-sdl2/ttf" "github.com/veandco/go-sdl2/ttf"
@ -77,20 +75,6 @@ func LoadFont(filename string, size int) (*ttf.Font, error) {
return font, nil 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 // ComputeTextRect computes and returns a Rect for how large the text would
// appear if rendered. // appear if rendered.
func (r *Renderer) ComputeTextRect(text render.Text) (render.Rect, error) { func (r *Renderer) ComputeTextRect(text render.Text) (render.Rect, error) {