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.