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
* [Major Milestones](#major-milestones)
* [Release Modes](#release-modes)
* [File Formats](#file-formats)
* [Text Console](#text-console)
* [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
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
* 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
import (
"fmt"
"image"
"image/png"
"io/ioutil"
"os"
"time"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
@ -18,18 +14,16 @@ type EditorScene struct {
// Configuration for the scene initializer.
OpenFile bool
Filename string
Canvas level.Grid
Canvas *level.Grid
UI *EditorUI
Palette *level.Palette // Full palette of swatches for this level
Swatch *level.Swatch // actively selected painting swatch
// The canvas widget that contains the map we're working on.
// XXX: in dev builds this is available at $ d.Scene.GetDrawing()
drawing *level.Canvas
// History of all the pixels placed by the user.
pixelHistory []*level.Pixel
lastPixel *level.Pixel // last pixel placed while mouse down and dragging
canvas level.Grid
filename string // Last saved filename.
filename string // Last saved filename.
// Canvas size
width int32
@ -43,7 +37,11 @@ func (s *EditorScene) Name() string {
// Setup the editor scene.
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?
if s.Filename != "" {
@ -59,28 +57,15 @@ func (s *EditorScene) Setup(d *Doodle) error {
}
if s.Canvas != nil {
log.Debug("EditorScene: Received Canvas from caller")
s.canvas = s.Canvas
s.drawing.Load(s.drawing.Palette, s.Canvas)
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
// must be initialized after those things.
s.UI = NewEditorUI(d, s)
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.height = d.height
return nil
@ -89,99 +74,45 @@ func (s *EditorScene) Setup(d *Doodle) error {
// Loop the editor scene.
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
s.UI.Loop(ev)
// Taking a screenshot?
if ev.ScreenshotKey.Pressed() {
log.Info("Taking a screenshot")
s.Screenshot()
}
s.drawing.Loop(ev)
// Switching to Play Mode?
if ev.KeyName.Read() == "p" {
log.Info("Play Mode, Go!")
d.Goto(&PlayScene{
Canvas: s.canvas,
Canvas: s.drawing.Grid(),
})
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
}
// Draw the current frame.
func (s *EditorScene) Draw(d *Doodle) error {
// Clear the canvas and fill it with white.
d.Engine.Clear(render.White)
// Clear the canvas and fill it with magenta so it's clear if any spots are missed.
d.Engine.Clear(render.Magenta)
s.canvas.Draw(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
}
// LoadLevel loads a level from disk.
func (s *EditorScene) LoadLevel(filename string) error {
s.filename = filename
s.pixelHistory = []*level.Pixel{}
s.canvas = level.Grid{}
return s.drawing.LoadFilename(filename)
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.
// TODO: move this into the Canvas?
func (s *EditorScene) SaveLevel(filename string) {
s.filename = filename
@ -190,9 +121,9 @@ func (s *EditorScene) SaveLevel(filename string) {
m.Author = os.Getenv("USER")
m.Width = s.width
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{
X: pixel.X,
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.
func (s *EditorScene) Destroy() error {
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
StatusPaletteText string
StatusFilenameText string
selectedSwatch string // name of selected swatch in palette
// Widgets
Supervisor *ui.Supervisor
@ -36,6 +37,12 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
StatusPaletteText: "Swatch: <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.StatusBar = u.SetupStatusBar(d)
u.Palette = u.SetupPalette(d)
@ -51,7 +58,7 @@ func (u *EditorUI) Loop(ev *events.State) {
ev.CursorY.Now,
)
u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
u.Scene.Swatch,
u.Scene.drawing.Palette.ActiveSwatch,
)
// Statusbar filename label.
@ -154,7 +161,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
BorderSize: 1,
OutlineSize: 0,
})
w.Handle("MouseUp", btn.Click)
w.Handle(ui.MouseUp, btn.Click)
u.Supervisor.Add(w)
frame.Pack(w, ui.Pack{
Anchor: ui.W,
@ -185,25 +192,26 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
// Handler function for the radio buttons being clicked.
onClick := func(p render.Point) {
name := u.Scene.Palette.ActiveSwatch
swatch, ok := u.Scene.Palette.Get(name)
name := u.selectedSwatch
swatch, ok := u.Scene.drawing.Palette.Get(name)
if !ok {
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
return
}
u.Scene.Swatch = swatch
log.Info("Set swatch: %s", swatch)
u.Scene.drawing.SetSwatch(swatch)
}
// 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{
Text: swatch.Name,
Font: balance.StatusFont,
})
label.Font.Color = swatch.Color.Darken(40)
btn := ui.NewRadioButton("palette", &u.Scene.Palette.ActiveSwatch, swatch.Name, label)
btn.Handle("MouseUp", onClick)
btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, label)
btn.Handle(ui.Click, onClick)
u.Supervisor.Add(btn)
window.Pack(btn, ui.Pack{

View File

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

View File

@ -83,7 +83,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Text: label,
Font: balance.StatusFont,
}))
btn.Handle("Click", func(p render.Point) {
btn.Handle(ui.Click, func(p render.Point) {
d.Flash("%s clicked", btn)
})
s.Supervisor.Add(btn)
@ -134,7 +134,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Height: 20,
BorderStyle: ui.BorderRaised,
})
btn.Handle("Click", func(p render.Point) {
btn.Handle(ui.Click, func(p render.Point) {
d.Flash("%s clicked", btn)
})
rowFrame.Pack(btn, ui.Pack{
@ -209,7 +209,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Font: balance.StatusFont,
}))
button1.SetBackground(render.Blue)
button1.Handle("Click", func(p render.Point) {
button1.Handle(ui.Click, func(p render.Point) {
d.NewMap()
})
@ -219,7 +219,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
Text: "Load Map",
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.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.
type Palette struct {
Swatches []*Swatch `json:"swatches"`
// 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
}

View File

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

View File

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

View File

@ -56,6 +56,28 @@ func (p Point) String() string {
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.
type Rect struct {
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.
func (r Rect) Bigger(other Rect) bool {
// 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.
func (r *Renderer) Clear(color render.Color) {
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()
}

View File

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

View File

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

View File

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

View File

@ -7,6 +7,22 @@ import (
"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
// interaction events such as mouse hovers and clicks in their general
// 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 {
// Cursor has intersected the widget.
if _, ok := s.hovering[id]; !ok {
w.Event("MouseOver", XY)
w.Event(MouseOver, XY)
s.hovering[id] = nil
}
_, isClicked := s.clicked[id]
if ev.Button1.Now {
if !isClicked {
w.Event("MouseDown", XY)
w.Event(MouseDown, XY)
s.clicked[id] = nil
}
} else if isClicked {
w.Event("MouseUp", XY)
w.Event("Click", XY)
w.Event(MouseUp, XY)
w.Event(Click, XY)
delete(s.clicked, id)
}
} else {
// Cursor is not intersecting the widget.
if _, ok := s.hovering[id]; ok {
w.Event("MouseOut", XY)
w.Event(MouseOut, XY)
delete(s.hovering, id)
}
if _, ok := s.clicked[id]; ok {
w.Event("MouseUp", XY)
w.Event(MouseUp, XY)
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.
Resize(render.Rect)
ResizeBy(render.Rect)
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
Handle(string, func(render.Point))
Event(string, render.Point) // called internally to trigger an event
Handle(Event, func(render.Point))
Event(Event, render.Point) // called internally to trigger an event
// Thickness of the padding + border + outline.
BoxThickness(multiplier int32) int32
@ -103,7 +104,7 @@ type BaseWidget struct {
borderSize int32
outlineColor render.Color
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.
@ -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.
func (w *BaseWidget) Point() render.Point {
return w.point
@ -395,8 +406,8 @@ func (w *BaseWidget) SetOutlineSize(v int32) {
}
// Event is called internally by Doodle to trigger an event.
func (w *BaseWidget) Event(name string, p render.Point) {
if handlers, ok := w.handlers[name]; ok {
func (w *BaseWidget) Event(event Event, p render.Point) {
if handlers, ok := w.handlers[event]; ok {
for _, fn := range handlers {
fn(p)
}
@ -404,16 +415,16 @@ func (w *BaseWidget) Event(name string, p render.Point) {
}
// 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 {
w.handlers = map[string][]func(render.Point){}
w.handlers = map[Event][]func(render.Point){}
}
if _, ok := w.handlers[name]; !ok {
w.handlers[name] = []func(render.Point){}
if _, ok := w.handlers[event]; !ok {
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.