Abstract Drawing Canvas into Reusable Widget

The `level.Canvas` is a widget that holds onto its Palette and Grid and
has interactions to allow scrolling and editing the grid using the
swatches available on the palette.

Thus all of the logic in the Editor Mode for drawing directly onto the
root SDL surface are now handled inside a level.Canvas instance.

The `level.Canvas` widget has the following properties:
* Like any widget it has an X,Y position and a width/height.
* It has a Scroll position to control which slice of its drawing will be
  visible inside its bounding box.
* It supports levels having negative coordinates for their pixels. It
  doesn't care. The default Scroll position is (0,0) at the top left
  corner of the widget but you can scroll into the negatives and see the
  negative pixels.
* Keyboard keys will scroll the viewport inside the canvas.
* The canvas draws only the pixels that are visible inside its bounding
  box.

This feature will eventually pave the way toward:
* Doodads being dropped on top of your map, each Doodad being its own
  Canvas widget.
* Using drawings as button icons for the user interface, as the Canvas
  is a normal widget.
This commit is contained in:
Noah 2018-08-16 20:37:19 -07:00
parent 5956863996
commit 5434484b6e
20 changed files with 478 additions and 191 deletions

View File

@ -3,6 +3,7 @@
## Table of Contents ## Table of Contents
* [Major Milestones](#major-milestones) * [Major Milestones](#major-milestones)
* [Release Modes](#release-modes)
* [File Formats](#file-formats) * [File Formats](#file-formats)
* [Text Console](#text-console) * [Text Console](#text-console)
* [Doodads](#doodads) * [Doodads](#doodads)
@ -105,6 +106,35 @@ For creating Doodads in particular:
your window). This will use a Canvas widget in the UI toolkit as an abstraction your window). This will use a Canvas widget in the UI toolkit as an abstraction
layer. Small canvases will be useful for drawing doodads of a fixed size. layer. Small canvases will be useful for drawing doodads of a fixed size.
# Release Modes
## Shareware/Demo Version
This would be a free version with some limitations. Early public alpha releases
would be built with this release mode.
* Optional expiration date after which the game WILL NOT run.
* Can play the built-in maps and create your own custom maps.
* No support for Custom Doodads. The game will have the code to read Doodads from
disk dummied out/not compiled in, and any third-party map that embeds or
references custom Doodads will not be allowed to run.
* Custom maps created in a demo version will have some feature limitations:
* Infinite map sizes not allowed, only bounded ones with a fixed size.
* No custom wallpaper images, only built-in ones.
* No custom palette for new maps, only the default standard palette.
* No features for drawing doodad graphics (multiple frames, etc.)
As an end user, it means basically:
* You are limited to built-in doodads but you can make (and share) and play
other users' custom maps that only use the built-in doodads.
## Release Version
TBD.
Probably mostly DRM free. Will want some sort of account server early-on though.
# File Formats # File Formats
* The file formats should eventually have a **Protocol Buffers** binary * The file formats should eventually have a **Protocol Buffers** binary

7
balance/numbers.go Normal file
View File

@ -0,0 +1,7 @@
package balance
// Numbers.
var (
// Speed to scroll a canvas with arrow keys in Edit Mode.
CanvasScrollSpeed int32 = 8
)

View File

@ -1,12 +1,8 @@
package doodle package doodle
import ( import (
"fmt"
"image"
"image/png"
"io/ioutil" "io/ioutil"
"os" "os"
"time"
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/level"
@ -18,18 +14,16 @@ type EditorScene struct {
// Configuration for the scene initializer. // Configuration for the scene initializer.
OpenFile bool OpenFile bool
Filename string Filename string
Canvas level.Grid Canvas *level.Grid
UI *EditorUI UI *EditorUI
Palette *level.Palette // Full palette of swatches for this level // The canvas widget that contains the map we're working on.
Swatch *level.Swatch // actively selected painting swatch // XXX: in dev builds this is available at $ d.Scene.GetDrawing()
drawing *level.Canvas
// History of all the pixels placed by the user. // History of all the pixels placed by the user.
pixelHistory []*level.Pixel filename string // Last saved filename.
lastPixel *level.Pixel // last pixel placed while mouse down and dragging
canvas level.Grid
filename string // Last saved filename.
// Canvas size // Canvas size
width int32 width int32
@ -43,7 +37,11 @@ func (s *EditorScene) Name() string {
// Setup the editor scene. // Setup the editor scene.
func (s *EditorScene) Setup(d *Doodle) error { func (s *EditorScene) Setup(d *Doodle) error {
s.Palette = level.DefaultPalette() s.drawing = level.NewCanvas(true)
s.drawing.Palette = level.DefaultPalette()
if len(s.drawing.Palette.Swatches) > 0 {
s.drawing.SetSwatch(s.drawing.Palette.Swatches[0])
}
// Were we given configuration data? // Were we given configuration data?
if s.Filename != "" { if s.Filename != "" {
@ -59,28 +57,15 @@ func (s *EditorScene) Setup(d *Doodle) error {
} }
if s.Canvas != nil { if s.Canvas != nil {
log.Debug("EditorScene: Received Canvas from caller") log.Debug("EditorScene: Received Canvas from caller")
s.canvas = s.Canvas s.drawing.Load(s.drawing.Palette, s.Canvas)
s.Canvas = nil s.Canvas = nil
} }
// Select the first swatch in the palette.
if len(s.Palette.Swatches) > 0 {
s.Swatch = s.Palette.Swatches[0]
s.Palette.ActiveSwatch = s.Swatch.Name
}
// Initialize the user interface. It references the palette and such so it // Initialize the user interface. It references the palette and such so it
// must be initialized after those things. // must be initialized after those things.
s.UI = NewEditorUI(d, s) s.UI = NewEditorUI(d, s)
d.Flash("Editor Mode. Press 'P' to play this map.") d.Flash("Editor Mode. Press 'P' to play this map.")
if s.pixelHistory == nil {
s.pixelHistory = []*level.Pixel{}
}
if s.canvas == nil {
log.Debug("EditorScene: Setting default canvas to an empty grid")
s.canvas = level.Grid{}
}
s.width = d.width // TODO: canvas width = copy the window size s.width = d.width // TODO: canvas width = copy the window size
s.height = d.height s.height = d.height
return nil return nil
@ -89,99 +74,45 @@ func (s *EditorScene) Setup(d *Doodle) error {
// Loop the editor scene. // Loop the editor scene.
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error { func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
s.UI.Loop(ev) s.UI.Loop(ev)
s.drawing.Loop(ev)
// Taking a screenshot?
if ev.ScreenshotKey.Pressed() {
log.Info("Taking a screenshot")
s.Screenshot()
}
// Switching to Play Mode? // Switching to Play Mode?
if ev.KeyName.Read() == "p" { if ev.KeyName.Read() == "p" {
log.Info("Play Mode, Go!") log.Info("Play Mode, Go!")
d.Goto(&PlayScene{ d.Goto(&PlayScene{
Canvas: s.canvas, Canvas: s.drawing.Grid(),
}) })
return nil return nil
} }
// Clicking? Log all the pixels while doing so.
if ev.Button1.Now {
// log.Warn("Button1: %+v", ev.Button1)
lastPixel := s.lastPixel
pixel := &level.Pixel{
X: ev.CursorX.Now,
Y: ev.CursorY.Now,
Palette: s.Palette,
Swatch: s.Swatch,
}
// Append unique new pixels.
if len(s.pixelHistory) == 0 || s.pixelHistory[len(s.pixelHistory)-1] != pixel {
if lastPixel != nil {
// Draw the pixels in between.
if lastPixel != pixel {
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
dot := &level.Pixel{
X: point.X,
Y: point.Y,
Palette: lastPixel.Palette,
Swatch: lastPixel.Swatch,
}
s.canvas[dot] = nil
}
}
}
s.lastPixel = pixel
s.pixelHistory = append(s.pixelHistory, pixel)
// Save in the pixel canvas map.
s.canvas[pixel] = nil
}
} else {
s.lastPixel = nil
}
return nil return nil
} }
// Draw the current frame. // Draw the current frame.
func (s *EditorScene) Draw(d *Doodle) error { func (s *EditorScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with white. // Clear the canvas and fill it with magenta so it's clear if any spots are missed.
d.Engine.Clear(render.White) d.Engine.Clear(render.Magenta)
s.canvas.Draw(d.Engine)
s.UI.Present(d.Engine) s.UI.Present(d.Engine)
// TODO: move inside the UI. Just an approximate position for now.
s.drawing.MoveTo(render.NewPoint(0, 19))
s.drawing.Resize(render.NewRect(d.width-150, d.height-44))
s.drawing.Compute(d.Engine)
s.drawing.Present(d.Engine, s.drawing.Point())
return nil return nil
} }
// LoadLevel loads a level from disk. // LoadLevel loads a level from disk.
func (s *EditorScene) LoadLevel(filename string) error { func (s *EditorScene) LoadLevel(filename string) error {
s.filename = filename s.filename = filename
s.pixelHistory = []*level.Pixel{} return s.drawing.LoadFilename(filename)
s.canvas = level.Grid{}
m, err := level.LoadJSON(filename)
if err != nil {
return err
}
s.Palette = m.Palette
if len(s.Palette.Swatches) > 0 {
s.Swatch = m.Palette.Swatches[0]
}
for _, pixel := range m.Pixels {
s.pixelHistory = append(s.pixelHistory, pixel)
s.canvas[pixel] = nil
}
return nil
} }
// SaveLevel saves the level to disk. // SaveLevel saves the level to disk.
// TODO: move this into the Canvas?
func (s *EditorScene) SaveLevel(filename string) { func (s *EditorScene) SaveLevel(filename string) {
s.filename = filename s.filename = filename
@ -190,9 +121,9 @@ func (s *EditorScene) SaveLevel(filename string) {
m.Author = os.Getenv("USER") m.Author = os.Getenv("USER")
m.Width = s.width m.Width = s.width
m.Height = s.height m.Height = s.height
m.Palette = s.Palette m.Palette = s.drawing.Palette
for pixel := range s.canvas { for pixel := range *s.drawing.Grid() {
m.Pixels = append(m.Pixels, &level.Pixel{ m.Pixels = append(m.Pixels, &level.Pixel{
X: pixel.X, X: pixel.X,
Y: pixel.Y, Y: pixel.Y,
@ -213,48 +144,6 @@ func (s *EditorScene) SaveLevel(filename string) {
} }
} }
// Screenshot saves the level canvas to disk as a PNG image.
func (s *EditorScene) Screenshot() {
screenshot := image.NewRGBA(image.Rect(0, 0, int(s.width), int(s.height)))
// White-out the image.
for x := 0; x < int(s.width); x++ {
for y := 0; y < int(s.height); y++ {
screenshot.Set(x, y, image.White)
}
}
// Fill in the dots we drew.
for pixel := range s.canvas {
screenshot.Set(int(pixel.X), int(pixel.Y), image.Black)
}
// Create the screenshot directory.
if _, err := os.Stat("./screenshots"); os.IsNotExist(err) {
log.Info("Creating directory: ./screenshots")
err = os.Mkdir("./screenshots", 0755)
if err != nil {
log.Error("Can't create ./screenshots: %s", err)
return
}
}
filename := fmt.Sprintf("./screenshots/screenshot-%s.png",
time.Now().Format("2006-01-02T15-04-05"),
)
fh, err := os.Create(filename)
if err != nil {
log.Error(err.Error())
return
}
defer fh.Close()
if err := png.Encode(fh, screenshot); err != nil {
log.Error(err.Error())
return
}
}
// Destroy the scene. // Destroy the scene.
func (s *EditorScene) Destroy() error { func (s *EditorScene) Destroy() error {
return nil return nil

11
editor_scene_debug.go Normal file
View File

@ -0,0 +1,11 @@
package doodle
import "git.kirsle.net/apps/doodle/level"
// TODO: build flags to not include this in production builds.
// This adds accessors for private variables from the dev console.
// GetDrawing returns the level.Canvas
func (w *EditorScene) GetDrawing() *level.Canvas {
return w.drawing
}

View File

@ -18,6 +18,7 @@ type EditorUI struct {
StatusMouseText string StatusMouseText string
StatusPaletteText string StatusPaletteText string
StatusFilenameText string StatusFilenameText string
selectedSwatch string // name of selected swatch in palette
// Widgets // Widgets
Supervisor *ui.Supervisor Supervisor *ui.Supervisor
@ -36,6 +37,12 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
StatusPaletteText: "Swatch: <none>", StatusPaletteText: "Swatch: <none>",
StatusFilenameText: "Filename: <none>", StatusFilenameText: "Filename: <none>",
} }
// Select the first swatch of the palette.
if u.Scene.drawing.Palette.ActiveSwatch != nil {
u.selectedSwatch = u.Scene.drawing.Palette.ActiveSwatch.Name
}
u.MenuBar = u.SetupMenuBar(d) u.MenuBar = u.SetupMenuBar(d)
u.StatusBar = u.SetupStatusBar(d) u.StatusBar = u.SetupStatusBar(d)
u.Palette = u.SetupPalette(d) u.Palette = u.SetupPalette(d)
@ -51,7 +58,7 @@ func (u *EditorUI) Loop(ev *events.State) {
ev.CursorY.Now, ev.CursorY.Now,
) )
u.StatusPaletteText = fmt.Sprintf("Swatch: %s", u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
u.Scene.Swatch, u.Scene.drawing.Palette.ActiveSwatch,
) )
// Statusbar filename label. // Statusbar filename label.
@ -154,7 +161,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
BorderSize: 1, BorderSize: 1,
OutlineSize: 0, OutlineSize: 0,
}) })
w.Handle("MouseUp", btn.Click) w.Handle(ui.MouseUp, btn.Click)
u.Supervisor.Add(w) u.Supervisor.Add(w)
frame.Pack(w, ui.Pack{ frame.Pack(w, ui.Pack{
Anchor: ui.W, Anchor: ui.W,
@ -185,25 +192,26 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
// Handler function for the radio buttons being clicked. // Handler function for the radio buttons being clicked.
onClick := func(p render.Point) { onClick := func(p render.Point) {
name := u.Scene.Palette.ActiveSwatch name := u.selectedSwatch
swatch, ok := u.Scene.Palette.Get(name) swatch, ok := u.Scene.drawing.Palette.Get(name)
if !ok { if !ok {
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name) log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
return return
} }
u.Scene.Swatch = swatch log.Info("Set swatch: %s", swatch)
u.Scene.drawing.SetSwatch(swatch)
} }
// Draw the radio buttons for the palette. // Draw the radio buttons for the palette.
for _, swatch := range u.Scene.Palette.Swatches { for _, swatch := range u.Scene.drawing.Palette.Swatches {
label := ui.NewLabel(ui.Label{ label := ui.NewLabel(ui.Label{
Text: swatch.Name, Text: swatch.Name,
Font: balance.StatusFont, Font: balance.StatusFont,
}) })
label.Font.Color = swatch.Color.Darken(40) label.Font.Color = swatch.Color.Darken(40)
btn := ui.NewRadioButton("palette", &u.Scene.Palette.ActiveSwatch, swatch.Name, label) btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, label)
btn.Handle("MouseUp", onClick) btn.Handle(ui.Click, onClick)
u.Supervisor.Add(btn) u.Supervisor.Add(btn)
window.Pack(btn, ui.Pack{ window.Pack(btn, ui.Pack{

View File

@ -1,7 +1,9 @@
// Package events manages mouse and keyboard SDL events for Doodle. // Package events manages mouse and keyboard SDL events for Doodle.
package events package events
import "strings" import (
"strings"
)
// State keeps track of event states. // State keeps track of event states.
type State struct { type State struct {

View File

@ -83,7 +83,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Text: label, Text: label,
Font: balance.StatusFont, Font: balance.StatusFont,
})) }))
btn.Handle("Click", func(p render.Point) { btn.Handle(ui.Click, func(p render.Point) {
d.Flash("%s clicked", btn) d.Flash("%s clicked", btn)
}) })
s.Supervisor.Add(btn) s.Supervisor.Add(btn)
@ -134,7 +134,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Height: 20, Height: 20,
BorderStyle: ui.BorderRaised, BorderStyle: ui.BorderRaised,
}) })
btn.Handle("Click", func(p render.Point) { btn.Handle(ui.Click, func(p render.Point) {
d.Flash("%s clicked", btn) d.Flash("%s clicked", btn)
}) })
rowFrame.Pack(btn, ui.Pack{ rowFrame.Pack(btn, ui.Pack{
@ -209,7 +209,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Font: balance.StatusFont, Font: balance.StatusFont,
})) }))
button1.SetBackground(render.Blue) button1.SetBackground(render.Blue)
button1.Handle("Click", func(p render.Point) { button1.Handle(ui.Click, func(p render.Point) {
d.NewMap() d.NewMap()
}) })
@ -219,7 +219,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Text: "Load Map", Text: "Load Map",
Font: balance.StatusFont, Font: balance.StatusFont,
})) }))
button2.Handle("Click", func(p render.Point) { button2.Handle(ui.Click, func(p render.Point) {
d.Prompt("Map name>", func(name string) { d.Prompt("Map name>", func(name string) {
d.EditLevel(name) d.EditLevel(name)
}) })

216
level/canvas.go Normal file
View File

@ -0,0 +1,216 @@
package level
import (
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
)
// Canvas is a custom ui.Widget that manages a single drawing.
type Canvas struct {
ui.Frame
Palette *Palette
// Set to true to allow clicking to edit this canvas.
Editable bool
grid Grid
pixelHistory []*Pixel
lastPixel *Pixel
// We inherit the ui.Widget which manages the width and height.
Scroll render.Point // Scroll offset for which parts of canvas are visible.
}
// NewCanvas initializes a Canvas widget.
func NewCanvas(editable bool) *Canvas {
w := &Canvas{
Editable: editable,
Palette: NewPalette(),
grid: Grid{},
}
w.setup()
return w
}
// Load initializes the Canvas using an existing Palette and Grid.
func (w *Canvas) Load(p *Palette, g *Grid) {
w.Palette = p
w.grid = *g
}
// LoadFilename initializes the Canvas using a file on disk.
func (w *Canvas) LoadFilename(filename string) error {
w.grid = Grid{}
m, err := LoadJSON(filename)
if err != nil {
return err
}
for _, pixel := range m.Pixels {
w.grid[pixel] = nil
}
w.Palette = m.Palette
if len(w.Palette.Swatches) > 0 {
w.SetSwatch(w.Palette.Swatches[0])
}
return nil
}
// SetSwatch changes the currently selected swatch for editing.
func (w *Canvas) SetSwatch(s *Swatch) {
w.Palette.ActiveSwatch = s
}
// setup common configs between both initializers of the canvas.
func (w *Canvas) setup() {
w.SetBackground(render.White)
w.Handle(ui.MouseOver, func(p render.Point) {
w.SetBackground(render.Yellow)
})
w.Handle(ui.MouseOut, func(p render.Point) {
w.SetBackground(render.SkyBlue)
})
}
// Loop is called on the scene's event loop to handle mouse interaction with
// the canvas, i.e. to edit it.
func (w *Canvas) Loop(ev *events.State) error {
log.Info("my territory")
var (
P = w.Point()
_ = P
)
// Arrow keys to scroll the view.
scrollBy := render.Point{}
if ev.Right.Now {
scrollBy.X += balance.CanvasScrollSpeed
} else if ev.Left.Now {
scrollBy.X -= balance.CanvasScrollSpeed
}
if ev.Down.Now {
scrollBy.Y += balance.CanvasScrollSpeed
} else if ev.Up.Now {
scrollBy.Y -= balance.CanvasScrollSpeed
}
if !scrollBy.IsZero() {
w.ScrollBy(scrollBy)
}
// Only care if the cursor is over our space.
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
if !cursor.Inside(w.Rect()) {
return nil
}
// If no swatch is active, do nothing with mouse clicks.
if w.Palette.ActiveSwatch == nil {
return nil
}
// Clicking? Log all the pixels while doing so.
if ev.Button1.Now {
// log.Warn("Button1: %+v", ev.Button1)
lastPixel := w.lastPixel
pixel := &Pixel{
X: ev.CursorX.Now - P.X + w.Scroll.X,
Y: ev.CursorY.Now - P.Y + w.Scroll.Y,
Palette: w.Palette,
Swatch: w.Palette.ActiveSwatch,
}
// Append unique new pixels.
if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel {
if lastPixel != nil {
// Draw the pixels in between.
if lastPixel != pixel {
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
dot := &Pixel{
X: point.X,
Y: point.Y,
Palette: lastPixel.Palette,
Swatch: lastPixel.Swatch,
}
w.grid[dot] = nil
}
}
}
w.lastPixel = pixel
w.pixelHistory = append(w.pixelHistory, pixel)
// Save in the pixel canvas map.
w.grid[pixel] = nil
}
} else {
w.lastPixel = nil
}
return nil
}
// Viewport returns a rect containing the viewable drawing coordinates in this
// canvas. The X,Y values are the scroll offset (top left) and the W,H values
// are the scroll offset plus the width/height of the Canvas widget.
func (w *Canvas) Viewport() render.Rect {
var S = w.Size()
return render.Rect{
X: w.Scroll.X,
Y: w.Scroll.Y,
W: S.W - w.BoxThickness(2),
H: S.H - w.BoxThickness(2),
}
}
// Grid returns the underlying grid object.
func (w *Canvas) Grid() *Grid {
return &w.grid
}
// ScrollBy adjusts the viewport scroll position.
func (w *Canvas) ScrollBy(by render.Point) {
w.Scroll.Add(by)
}
// Compute the canvas.
func (w *Canvas) Compute(e render.Engine) {
}
// Present the canvas.
func (w *Canvas) Present(e render.Engine, p render.Point) {
var (
S = w.Size()
Viewport = w.Viewport()
)
w.MoveTo(p)
w.DrawBox(e, p)
e.DrawBox(w.Background(), render.Rect{
X: p.X + w.BoxThickness(1),
Y: p.Y + w.BoxThickness(1),
W: S.W - w.BoxThickness(2),
H: S.H - w.BoxThickness(2),
})
for pixel := range w.grid {
point := render.NewPoint(pixel.X, pixel.Y)
if point.Inside(Viewport) {
// This pixel is visible in the canvas, but offset it by the
// scroll height.
point.Add(render.Point{
X: -Viewport.X,
Y: -Viewport.Y,
})
color := pixel.Swatch.Color
e.DrawPoint(color, render.Point{
X: p.X + w.BoxThickness(1) + point.X,
Y: p.Y + w.BoxThickness(1) + point.Y,
})
}
}
}

9
level/log.go Normal file
View File

@ -0,0 +1,9 @@
package level
import "github.com/kirsle/golog"
var log *golog.Logger
func init() {
log = golog.GetLogger("doodle")
}

View File

@ -33,12 +33,20 @@ func DefaultPalette() *Palette {
} }
} }
// NewPalette initializes a blank palette.
func NewPalette() *Palette {
return &Palette{
Swatches: []*Swatch{},
byName: map[string]int{},
}
}
// Palette holds an index of colors used in a drawing. // Palette holds an index of colors used in a drawing.
type Palette struct { type Palette struct {
Swatches []*Swatch `json:"swatches"` Swatches []*Swatch `json:"swatches"`
// Private runtime values // Private runtime values
ActiveSwatch string `json:"-"` // name of the actively selected color ActiveSwatch *Swatch `json:"-"` // name of the actively selected color
byName map[string]int // Cache map of swatches by name byName map[string]int // Cache map of swatches by name
} }

View File

@ -29,7 +29,7 @@ func (s *MainScene) Setup(d *Doodle) error {
Text: "New Map", Text: "New Map",
Font: balance.StatusFont, Font: balance.StatusFont,
})) }))
button1.Handle("Click", func(p render.Point) { button1.Handle(ui.Click, func(p render.Point) {
d.NewMap() d.NewMap()
}) })

View File

@ -11,10 +11,10 @@ import (
type PlayScene struct { type PlayScene struct {
// Configuration attributes. // Configuration attributes.
Filename string Filename string
Canvas level.Grid Canvas *level.Grid
// Private variables. // Private variables.
canvas level.Grid canvas *level.Grid
// Canvas size // Canvas size
width int32 width int32
@ -46,7 +46,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
if s.canvas == nil { if s.canvas == nil {
log.Debug("PlayScene.Setup: no grid given, initializing empty grid") log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
s.canvas = level.Grid{} s.canvas = &level.Grid{}
} }
s.width = d.width // TODO: canvas width = copy the window size s.width = d.width // TODO: canvas width = copy the window size
@ -110,7 +110,7 @@ func (s *PlayScene) movePlayer(ev *events.State) {
// Apply gravity. // Apply gravity.
// var onFloor bool // var onFloor bool
info, ok := doodads.CollidesWithGrid(s.Player, &s.canvas, delta) info, ok := doodads.CollidesWithGrid(s.Player, s.canvas, delta)
if ok { if ok {
// Collision happened with world. // Collision happened with world.
} }
@ -128,16 +128,16 @@ func (s *PlayScene) movePlayer(ev *events.State) {
// LoadLevel loads a level from disk. // LoadLevel loads a level from disk.
func (s *PlayScene) LoadLevel(filename string) error { func (s *PlayScene) LoadLevel(filename string) error {
s.canvas = level.Grid{} s.canvas = &level.Grid{}
m, err := level.LoadJSON(filename) // m, err := level.LoadJSON(filename)
if err != nil { // if err != nil {
return err // return err
} // }
for _, pixel := range m.Pixels { // for _, pixel := range m.Pixels {
s.canvas[pixel] = nil // // *s.canvas[pixel] = nil
} // }
return nil return nil
} }

View File

@ -56,6 +56,28 @@ func (p Point) String() string {
return fmt.Sprintf("Point<%d,%d>", p.X, p.Y) return fmt.Sprintf("Point<%d,%d>", p.X, p.Y)
} }
// IsZero returns if the point is the zero value.
func (p Point) IsZero() bool {
return p.X == 0 && p.Y == 0
}
// Inside returns whether the Point falls inside the rect.
func (p Point) Inside(r Rect) bool {
var (
x1 = r.X
y1 = r.Y
x2 = r.X + r.W
y2 = r.Y + r.H
)
return p.X >= x1 && p.X <= x2 && p.Y >= y1 && p.Y <= y2
}
// Add (or subtract) the other point to your current point.
func (p *Point) Add(other Point) {
p.X += other.X
p.Y += other.Y
}
// Rect has a coordinate and a width and height. // Rect has a coordinate and a width and height.
type Rect struct { type Rect struct {
X int32 X int32
@ -79,6 +101,14 @@ func (r Rect) String() string {
) )
} }
// Point returns the rectangle's X,Y values as a Point.
func (r Rect) Point() Point {
return Point{
X: r.X,
Y: r.Y,
}
}
// Bigger returns if the given rect is larger than the current one. // Bigger returns if the given rect is larger than the current one.
func (r Rect) Bigger(other Rect) bool { func (r Rect) Bigger(other Rect) bool {
// TODO: don't know why this is ! // TODO: don't know why this is !

50
render/point_test.go Normal file
View File

@ -0,0 +1,50 @@
package render_test
import (
"strconv"
"testing"
"git.kirsle.net/apps/doodle/render"
)
func TestPointInside(t *testing.T) {
var p = render.Point{
X: 128,
Y: 256,
}
type testCase struct {
rect render.Rect
shouldPass bool
}
tests := []testCase{
testCase{
rect: render.Rect{
X: 0,
Y: 0,
W: 500,
H: 500,
},
shouldPass: true,
},
testCase{
rect: render.Rect{
X: 100,
Y: 80,
W: 40,
H: 60,
},
shouldPass: false,
},
}
for _, test := range tests {
if p.Inside(test.rect) != test.shouldPass {
t.Errorf("Failed: %s inside %s should %s",
p,
test.rect,
strconv.FormatBool(test.shouldPass),
)
}
}
}

View File

@ -9,7 +9,7 @@ import (
// Clear the canvas and set this color. // Clear the canvas and set this color.
func (r *Renderer) Clear(color render.Color) { func (r *Renderer) Clear(color render.Color) {
if color != r.lastColor { if color != r.lastColor {
r.renderer.SetDrawColor(color.Red, color.Blue, color.Green, color.Alpha) r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha)
} }
r.renderer.Clear() r.renderer.Clear()
} }

View File

@ -35,20 +35,20 @@ func NewButton(name string, child Widget) *Button {
Background: theme.ButtonBackgroundColor, Background: theme.ButtonBackgroundColor,
}) })
w.Handle("MouseOver", func(p render.Point) { w.Handle(MouseOver, func(p render.Point) {
w.hovering = true w.hovering = true
w.SetBackground(theme.ButtonHoverColor) w.SetBackground(theme.ButtonHoverColor)
}) })
w.Handle("MouseOut", func(p render.Point) { w.Handle(MouseOut, func(p render.Point) {
w.hovering = false w.hovering = false
w.SetBackground(theme.ButtonBackgroundColor) w.SetBackground(theme.ButtonBackgroundColor)
}) })
w.Handle("MouseDown", func(p render.Point) { w.Handle(MouseDown, func(p render.Point) {
w.clicked = true w.clicked = true
w.SetBorderStyle(BorderSunken) w.SetBorderStyle(BorderSunken)
}) })
w.Handle("MouseUp", func(p render.Point) { w.Handle(MouseUp, func(p render.Point) {
w.clicked = false w.clicked = false
w.SetBorderStyle(BorderRaised) w.SetBorderStyle(BorderRaised)
}) })

View File

@ -78,24 +78,24 @@ func (w *CheckButton) setup() {
Background: theme.ButtonBackgroundColor, Background: theme.ButtonBackgroundColor,
}) })
w.Handle("MouseOver", func(p render.Point) { w.Handle(MouseOver, func(p render.Point) {
w.hovering = true w.hovering = true
w.SetBackground(theme.ButtonHoverColor) w.SetBackground(theme.ButtonHoverColor)
}) })
w.Handle("MouseOut", func(p render.Point) { w.Handle(MouseOut, func(p render.Point) {
w.hovering = false w.hovering = false
w.SetBackground(theme.ButtonBackgroundColor) w.SetBackground(theme.ButtonBackgroundColor)
}) })
w.Handle("MouseDown", func(p render.Point) { w.Handle(MouseDown, func(p render.Point) {
w.clicked = true w.clicked = true
w.SetBorderStyle(BorderSunken) w.SetBorderStyle(BorderSunken)
}) })
w.Handle("MouseUp", func(p render.Point) { w.Handle(MouseUp, func(p render.Point) {
w.clicked = false w.clicked = false
}) })
w.Handle("MouseDown", func(p render.Point) { w.Handle(Click, func(p render.Point) {
var sunken bool var sunken bool
if w.BoolVar != nil { if w.BoolVar != nil {
if *w.BoolVar { if *w.BoolVar {

View File

@ -35,8 +35,8 @@ func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, c
w.Frame.Setup() w.Frame.Setup()
// Forward clicks on the child widget to the CheckButton. // Forward clicks on the child widget to the CheckButton.
for _, e := range []string{"MouseOver", "MouseOut", "MouseUp", "MouseDown"} { for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} {
func(e string) { func(e Event) {
w.child.Handle(e, func(p render.Point) { w.child.Handle(e, func(p render.Point) {
w.button.Event(e, p) w.button.Event(e, p)
}) })

View File

@ -7,6 +7,22 @@ import (
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
) )
// Event is a named event that the supervisor will send.
type Event int
// Events.
const (
NullEvent Event = iota
MouseOver
MouseOut
MouseDown
MouseUp
Click
KeyDown
KeyUp
KeyPress
)
// Supervisor keeps track of widgets of interest to notify them about // Supervisor keeps track of widgets of interest to notify them about
// interaction events such as mouse hovers and clicks in their general // interaction events such as mouse hovers and clicks in their general
// vicinity. // vicinity.
@ -49,30 +65,30 @@ func (s *Supervisor) Loop(ev *events.State) {
if XY.X >= P.X && XY.X <= P2.X && XY.Y >= P.Y && XY.Y <= P2.Y { if XY.X >= P.X && XY.X <= P2.X && XY.Y >= P.Y && XY.Y <= P2.Y {
// Cursor has intersected the widget. // Cursor has intersected the widget.
if _, ok := s.hovering[id]; !ok { if _, ok := s.hovering[id]; !ok {
w.Event("MouseOver", XY) w.Event(MouseOver, XY)
s.hovering[id] = nil s.hovering[id] = nil
} }
_, isClicked := s.clicked[id] _, isClicked := s.clicked[id]
if ev.Button1.Now { if ev.Button1.Now {
if !isClicked { if !isClicked {
w.Event("MouseDown", XY) w.Event(MouseDown, XY)
s.clicked[id] = nil s.clicked[id] = nil
} }
} else if isClicked { } else if isClicked {
w.Event("MouseUp", XY) w.Event(MouseUp, XY)
w.Event("Click", XY) w.Event(Click, XY)
delete(s.clicked, id) delete(s.clicked, id)
} }
} else { } else {
// Cursor is not intersecting the widget. // Cursor is not intersecting the widget.
if _, ok := s.hovering[id]; ok { if _, ok := s.hovering[id]; ok {
w.Event("MouseOut", XY) w.Event(MouseOut, XY)
delete(s.hovering, id) delete(s.hovering, id)
} }
if _, ok := s.clicked[id]; ok { if _, ok := s.clicked[id]; ok {
w.Event("MouseUp", XY) w.Event(MouseUp, XY)
delete(s.clicked, id) delete(s.clicked, id)
} }
} }

View File

@ -29,9 +29,10 @@ type Widget interface {
BoxSize() render.Rect // Return the full size including the border and outline. BoxSize() render.Rect // Return the full size including the border and outline.
Resize(render.Rect) Resize(render.Rect)
ResizeBy(render.Rect) ResizeBy(render.Rect)
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
Handle(string, func(render.Point)) Handle(Event, func(render.Point))
Event(string, render.Point) // called internally to trigger an event Event(Event, render.Point) // called internally to trigger an event
// Thickness of the padding + border + outline. // Thickness of the padding + border + outline.
BoxThickness(multiplier int32) int32 BoxThickness(multiplier int32) int32
@ -103,7 +104,7 @@ type BaseWidget struct {
borderSize int32 borderSize int32
outlineColor render.Color outlineColor render.Color
outlineSize int32 outlineSize int32
handlers map[string][]func(render.Point) handlers map[Event][]func(render.Point)
} }
// SetID sets a string name for your widget, helpful for debugging purposes. // SetID sets a string name for your widget, helpful for debugging purposes.
@ -170,6 +171,16 @@ func (w *BaseWidget) Configure(c Config) {
} }
} }
// Rect returns the widget's absolute rectangle, the combined Size and Point.
func (w *BaseWidget) Rect() render.Rect {
return render.Rect{
X: w.point.X,
Y: w.point.Y,
W: w.width,
H: w.height,
}
}
// Point returns the X,Y position of the widget on the window. // Point returns the X,Y position of the widget on the window.
func (w *BaseWidget) Point() render.Point { func (w *BaseWidget) Point() render.Point {
return w.point return w.point
@ -395,8 +406,8 @@ func (w *BaseWidget) SetOutlineSize(v int32) {
} }
// Event is called internally by Doodle to trigger an event. // Event is called internally by Doodle to trigger an event.
func (w *BaseWidget) Event(name string, p render.Point) { func (w *BaseWidget) Event(event Event, p render.Point) {
if handlers, ok := w.handlers[name]; ok { if handlers, ok := w.handlers[event]; ok {
for _, fn := range handlers { for _, fn := range handlers {
fn(p) fn(p)
} }
@ -404,16 +415,16 @@ func (w *BaseWidget) Event(name string, p render.Point) {
} }
// Handle an event in the widget. // Handle an event in the widget.
func (w *BaseWidget) Handle(name string, fn func(render.Point)) { func (w *BaseWidget) Handle(event Event, fn func(render.Point)) {
if w.handlers == nil { if w.handlers == nil {
w.handlers = map[string][]func(render.Point){} w.handlers = map[Event][]func(render.Point){}
} }
if _, ok := w.handlers[name]; !ok { if _, ok := w.handlers[event]; !ok {
w.handlers[name] = []func(render.Point){} w.handlers[event] = []func(render.Point){}
} }
w.handlers[name] = append(w.handlers[name], fn) w.handlers[event] = append(w.handlers[event], fn)
} }
// OnMouseOut should be overridden on widgets who want this event. // OnMouseOut should be overridden on widgets who want this event.