From 5434484b6ed39f46036779cc05e1ff71776ad0f2 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Thu, 16 Aug 2018 20:37:19 -0700 Subject: [PATCH] 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. --- Ideas.md | 30 ++++++ balance/numbers.go | 7 ++ editor_scene.go | 161 +++++-------------------------- editor_scene_debug.go | 11 +++ editor_ui.go | 24 +++-- events/events.go | 4 +- guitest_scene.go | 8 +- level/canvas.go | 216 ++++++++++++++++++++++++++++++++++++++++++ level/log.go | 9 ++ level/palette.go | 10 +- main_scene.go | 2 +- play_scene.go | 24 ++--- render/interface.go | 30 ++++++ render/point_test.go | 50 ++++++++++ render/sdl/canvas.go | 2 +- ui/button.go | 8 +- ui/check_button.go | 10 +- ui/checkbox.go | 4 +- ui/supervisor.go | 28 ++++-- ui/widget.go | 31 ++++-- 20 files changed, 478 insertions(+), 191 deletions(-) create mode 100644 balance/numbers.go create mode 100644 editor_scene_debug.go create mode 100644 level/canvas.go create mode 100644 level/log.go create mode 100644 render/point_test.go diff --git a/Ideas.md b/Ideas.md index b09cfd1..87961fe 100644 --- a/Ideas.md +++ b/Ideas.md @@ -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 diff --git a/balance/numbers.go b/balance/numbers.go new file mode 100644 index 0000000..085d3b3 --- /dev/null +++ b/balance/numbers.go @@ -0,0 +1,7 @@ +package balance + +// Numbers. +var ( + // Speed to scroll a canvas with arrow keys in Edit Mode. + CanvasScrollSpeed int32 = 8 +) diff --git a/editor_scene.go b/editor_scene.go index 3809a09..60ebdcd 100644 --- a/editor_scene.go +++ b/editor_scene.go @@ -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 diff --git a/editor_scene_debug.go b/editor_scene_debug.go new file mode 100644 index 0000000..8320a7e --- /dev/null +++ b/editor_scene_debug.go @@ -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 +} diff --git a/editor_ui.go b/editor_ui.go index a1ed9ea..01a0619 100644 --- a/editor_ui.go +++ b/editor_ui.go @@ -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: ", StatusFilenameText: "Filename: ", } + + // 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{ diff --git a/events/events.go b/events/events.go index a4dffbb..67c86ed 100644 --- a/events/events.go +++ b/events/events.go @@ -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 { diff --git a/guitest_scene.go b/guitest_scene.go index 90fce3e..8e67a03 100644 --- a/guitest_scene.go +++ b/guitest_scene.go @@ -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) }) diff --git a/level/canvas.go b/level/canvas.go new file mode 100644 index 0000000..00ce716 --- /dev/null +++ b/level/canvas.go @@ -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, + }) + } + } +} diff --git a/level/log.go b/level/log.go new file mode 100644 index 0000000..0fac67a --- /dev/null +++ b/level/log.go @@ -0,0 +1,9 @@ +package level + +import "github.com/kirsle/golog" + +var log *golog.Logger + +func init() { + log = golog.GetLogger("doodle") +} diff --git a/level/palette.go b/level/palette.go index 8c63e05..c74d9f5 100644 --- a/level/palette.go +++ b/level/palette.go @@ -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 } diff --git a/main_scene.go b/main_scene.go index ea23338..8717c6e 100644 --- a/main_scene.go +++ b/main_scene.go @@ -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() }) diff --git a/play_scene.go b/play_scene.go index 0b66ada..b89adf4 100644 --- a/play_scene.go +++ b/play_scene.go @@ -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 } diff --git a/render/interface.go b/render/interface.go index a977570..dbef992 100644 --- a/render/interface.go +++ b/render/interface.go @@ -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 ! diff --git a/render/point_test.go b/render/point_test.go new file mode 100644 index 0000000..770ce57 --- /dev/null +++ b/render/point_test.go @@ -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), + ) + } + } +} diff --git a/render/sdl/canvas.go b/render/sdl/canvas.go index 9536508..2272c49 100644 --- a/render/sdl/canvas.go +++ b/render/sdl/canvas.go @@ -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() } diff --git a/ui/button.go b/ui/button.go index e279705..a26f237 100644 --- a/ui/button.go +++ b/ui/button.go @@ -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) }) diff --git a/ui/check_button.go b/ui/check_button.go index 257eb79..dd56f58 100644 --- a/ui/check_button.go +++ b/ui/check_button.go @@ -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 { diff --git a/ui/checkbox.go b/ui/checkbox.go index 38130e8..59b26fd 100644 --- a/ui/checkbox.go +++ b/ui/checkbox.go @@ -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) }) diff --git a/ui/supervisor.go b/ui/supervisor.go index dfa028e..8351537 100644 --- a/ui/supervisor.go +++ b/ui/supervisor.go @@ -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) } } diff --git a/ui/widget.go b/ui/widget.go index 1db6afb..6f07e4a 100644 --- a/ui/widget.go +++ b/ui/widget.go @@ -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.