From b4a366baa95ca0c5e1bde21de1028d44a73364f3 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Sat, 20 Oct 2018 17:08:20 -0700 Subject: [PATCH] Introduce Drawing Tools Concept, Pencil and Actor The uix.Canvas widget now maintains a selected Tool which configures how the mouse interacts with the (editable) Canvas widget. The default Tool is the PencilTool and implements the old behavior: it draws pixels when clicked and dragged based on your currently selected Color Swatch. This tool automatically becomes active when you toggle the Palette tab in the editor mode. A new Tool is the ActorTool which becomes active when you select the Doodads tab. In the ActorTool you can't draw pixels on the level, but when you mouse over a Doodad instance (Actor) in your level, you may pick it up and drag it someplace else. Left-click an Actor to pick it up and drag it somewhere else. Right-click to delete it completely. You can also delete an Actor by dragging it OFF of the Canvas, like back onto the palette drawer or onto the menu bar. --- editor_ui.go | 38 ++++++++++++--- editor_ui_doodad.go | 29 ++++++----- guitest_scene.go | 10 ---- level/actors.go | 10 ++++ render/sdl/events.go | 30 +++++++----- uix/canvas.go | 61 ++++++----------------- uix/canvas_editable.go | 107 +++++++++++++++++++++++++++++++++++++++++ uix/draw_modes.go | 19 ++++++++ 8 files changed, 216 insertions(+), 88 deletions(-) create mode 100644 uix/canvas_editable.go create mode 100644 uix/draw_modes.go diff --git a/editor_ui.go b/editor_ui.go index 6b5e962..0e0d6b5 100644 --- a/editor_ui.go +++ b/editor_ui.go @@ -6,9 +6,11 @@ import ( "strconv" "git.kirsle.net/apps/doodle/balance" + "git.kirsle.net/apps/doodle/doodads" "git.kirsle.net/apps/doodle/enum" "git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/ui" "git.kirsle.net/apps/doodle/uix" @@ -167,8 +169,8 @@ func (u *EditorUI) Loop(ev *events.State) error { ev.CursorY.Now, debugWorldIndex, ) - u.StatusPaletteText = fmt.Sprintf("Swatch: %s", - u.Canvas.Palette.ActiveSwatch, + u.StatusPaletteText = fmt.Sprintf("%s Tool", + u.Canvas.Tool, ) u.StatusScrollText = fmt.Sprintf("Scroll: %s Viewport: %s", u.Canvas.Scroll, @@ -245,6 +247,27 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas { drawing.SetSwatch(drawing.Palette.Swatches[0]) } + // Handle the Canvas deleting our actors in edit mode. + drawing.OnDeleteActors = func(actors []*level.Actor) { + if u.Scene.Level != nil { + for _, actor := range actors { + u.Scene.Level.Actors.Remove(actor) + } + drawing.InstallActors(u.Scene.Level.Actors) + } + } + + // A drag event initiated inside the Canvas. This happens in the ActorTool + // mode when you click an existing Doodad and it "pops" out of the canvas + // and onto the cursor to be repositioned. + drawing.OnDragStart = func(filename string) { + doodad, err := doodads.LoadJSON(userdir.DoodadPath(filename)) + if err != nil { + log.Error("drawing.OnDragStart: %s", err.Error()) + } + u.startDragActor(doodad) + } + // Set up the drop handler for draggable doodads. // NOTE: The drag event begins at editor_ui_doodad.go when configuring the // Doodad Palette buttons. @@ -415,10 +438,6 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { // Frame that holds the tab buttons in Level Edit mode. tabFrame := ui.NewFrame("Palette Tabs") - if u.Scene.DrawingType != enum.LevelDrawing { - // Don't show the tab bar except in Level Edit mode. - tabFrame.Hide() - } for _, name := range []string{"Palette", "Doodads"} { if u.paletteTab == "" { u.paletteTab = name @@ -429,9 +448,11 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { })) tab.Handle(ui.Click, func(p render.Point) { if u.paletteTab == "Palette" { + u.Canvas.Tool = uix.PencilTool u.PaletteTab.Show() u.DoodadTab.Hide() } else { + u.Canvas.Tool = uix.ActorTool u.PaletteTab.Hide() u.DoodadTab.Show() } @@ -450,6 +471,11 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { PadY: 4, }) + // Only show the tab frame in Level drawing mode! + if u.Scene.DrawingType != enum.LevelDrawing { + tabFrame.Hide() + } + // Doodad frame. { frame, err := u.setupDoodadFrame(d.Engine, window) diff --git a/editor_ui_doodad.go b/editor_ui_doodad.go index d06f7a6..433641c 100644 --- a/editor_ui_doodad.go +++ b/editor_ui_doodad.go @@ -21,6 +21,22 @@ type DraggableActor struct { doodad *doodads.Doodad } +// startDragActor begins the drag event for a Doodad onto a level. +func (u *EditorUI) startDragActor(doodad *doodads.Doodad) { + u.Supervisor.DragStart() + + // Create the canvas to render on the mouse cursor. + drawing := uix.NewCanvas(doodad.Layers[0].Chunker.Size, false) + drawing.LoadDoodad(doodad) + drawing.Resize(doodad.Rect()) + drawing.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO: invisible becomes white + drawing.MaskColor = balance.DragColor // blueprint effect + u.DraggableActor = &DraggableActor{ + canvas: drawing, + doodad: doodad, + } +} + // setupDoodadFrame configures the Doodad Palette tab for Edit Mode. // This is a subroutine of editor_ui.go#SetupPalette() // @@ -84,18 +100,7 @@ func (u *EditorUI) setupDoodadFrame(e render.Engine, window *ui.Window) (*ui.Fra // NOTE: The drag target is the EditorUI.Canvas in // editor_ui.go#SetupCanvas() btn.Handle(ui.MouseDown, func(e render.Point) { - u.Supervisor.DragStart() - - // Create the canvas to render on the mouse cursor. - drawing := uix.NewCanvas(doodad.Layers[0].Chunker.Size, false) - drawing.LoadDoodad(doodad) - drawing.Resize(doodad.Rect()) - drawing.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO: invisible becomes white - drawing.MaskColor = balance.DragColor // blueprint effect - u.DraggableActor = &DraggableActor{ - canvas: drawing, - doodad: doodad, - } + u.startDragActor(doodad) }) u.Supervisor.Add(btn) diff --git a/guitest_scene.go b/guitest_scene.go index cd1722d..1b349eb 100644 --- a/guitest_scene.go +++ b/guitest_scene.go @@ -176,16 +176,6 @@ func (s *GUITestScene) Setup(d *Doodle) error { }) cb.Supervise(s.Supervisor) - // Put an image in. - img, err := ui.OpenImage(d.Engine, "exit.bmp") - if err != nil { - log.Error(err.Error()) - } - frame.Pack(img, ui.Pack{ - Anchor: ui.NE, - Padding: 4, - }) - frame.Pack(ui.NewLabel(ui.Label{ Text: "Like Tk!", Font: render.Text{ diff --git a/level/actors.go b/level/actors.go index 8256864..332c044 100644 --- a/level/actors.go +++ b/level/actors.go @@ -24,6 +24,16 @@ func (m ActorMap) Add(a *Actor) { m[a.id] = a } +// Remove an Actor from the map. The ID must be set at the very least, so to +// remove by ID just create an Actor{id: x} +func (m ActorMap) Remove(a *Actor) bool { + if _, ok := m[a.id]; ok { + delete(m, a.id) + return true + } + return false +} + // Actor is an instance of a Doodad in the level. type Actor struct { id string // NOTE: read only, use ID() to access. diff --git a/render/sdl/events.go b/render/sdl/events.go index ea54187..5a8bad3 100644 --- a/render/sdl/events.go +++ b/render/sdl/events.go @@ -49,23 +49,27 @@ func (r *Renderer) Poll() (*events.State, error) { s.CursorY.Push(t.Y) // Is a mouse button pressed down? - if t.Button == 1 { - var eventName string - if t.State == 1 && s.Button1.Now == false { - eventName = "DOWN" - } else if t.State == 0 && s.Button1.Now == true { - eventName = "UP" - } + checkDown := func(number uint8, target *events.BoolTick) bool { + if t.Button == number { + var eventName string + if t.State == 1 && target.Now == false { + eventName = "DOWN" + } else if t.State == 0 && target.Now == true { + eventName = "UP" + } - if eventName != "" { - s.Button1.Push(eventName == "DOWN") - - // Return the event immediately. - return s, nil + if eventName != "" { + target.Push(eventName == "DOWN") + } + return true } + return false } - // s.Button2.Push(t.Button == 3 && t.State == 1) + if checkDown(1, s.Button1) || checkDown(3, s.Button2) { + // Return the event immediately. + return s, nil + } case *sdl.MouseWheelEvent: if DebugMouseEvents { log.Debug("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d", diff --git a/uix/canvas.go b/uix/canvas.go index 476283c..58c5062 100644 --- a/uix/canvas.go +++ b/uix/canvas.go @@ -23,6 +23,9 @@ type Canvas struct { Editable bool // Clicking will edit pixels of this canvas. Scrollable bool // Cursor keys will scroll the viewport of this canvas. + // Selected draw tool/mode, default Pencil, for editable canvases. + Tool Tool + // MaskColor will force every pixel to render as this color regardless of // the palette index of that pixel. Otherwise pixels behave the same and // the palette does work as normal. Set to render.Invisible (zero value) @@ -36,6 +39,12 @@ type Canvas struct { actor *level.Actor // if this canvas IS an actor actors []*Actor + // When the Canvas wants to delete Actors, but ultimately it is upstream + // that controls the actors. Upstream should delete them and then reinstall + // the actor list from scratch. + OnDeleteActors func([]*level.Actor) + OnDragStart func(filename string) + // Tracking pixels while editing. TODO: get rid of pixelHistory? pixelHistory []*level.Pixel lastPixel *level.Pixel @@ -151,10 +160,6 @@ func (w *Canvas) setup() { // 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 { - // Get the absolute position of the canvas on screen to accurately match - // it up to mouse clicks. - var P = ui.AbsolutePosition(w) - if w.Scrollable { // Arrow keys to scroll the view. scrollBy := render.Point{} @@ -173,51 +178,13 @@ func (w *Canvas) Loop(ev *events.State) error { } } - // Only care if the cursor is over our space. - cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now) - if !cursor.Inside(ui.AbsoluteRect(w)) { - 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 { - lastPixel := w.lastPixel - cursor := render.Point{ - X: ev.CursorX.Now - P.X - w.Scroll.X, - Y: ev.CursorY.Now - P.Y - w.Scroll.Y, + // If the canvas is editable, only care if it's over our space. + if w.Editable { + cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now) + if cursor.Inside(ui.AbsoluteRect(w)) { + return w.loopEditable(ev) } - pixel := &level.Pixel{ - X: cursor.X, - Y: cursor.Y, - 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) { - w.chunks.Set(point, lastPixel.Swatch) - } - } - } - - w.lastPixel = pixel - w.pixelHistory = append(w.pixelHistory, pixel) - - // Save in the pixel canvas map. - w.chunks.Set(cursor, pixel.Swatch) - } - } else { - w.lastPixel = nil } - return nil } diff --git a/uix/canvas_editable.go b/uix/canvas_editable.go new file mode 100644 index 0000000..184c5e2 --- /dev/null +++ b/uix/canvas_editable.go @@ -0,0 +1,107 @@ +package uix + +import ( + "git.kirsle.net/apps/doodle/events" + "git.kirsle.net/apps/doodle/level" + "git.kirsle.net/apps/doodle/render" + "git.kirsle.net/apps/doodle/ui" +) + +// loopEditable handles the Loop() part for editable canvases. +func (w *Canvas) loopEditable(ev *events.State) error { + // Get the absolute position of the canvas on screen to accurately match + // it up to mouse clicks. + var ( + P = ui.AbsolutePosition(w) + cursor = render.Point{ + X: ev.CursorX.Now - P.X - w.Scroll.X, + Y: ev.CursorY.Now - P.Y - w.Scroll.Y, + } + ) + + switch w.Tool { + case PencilTool: + // 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 { + lastPixel := w.lastPixel + pixel := &level.Pixel{ + X: cursor.X, + Y: cursor.Y, + 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) { + w.chunks.Set(point, lastPixel.Swatch) + } + } + } + + w.lastPixel = pixel + w.pixelHistory = append(w.pixelHistory, pixel) + + // Save in the pixel canvas map. + w.chunks.Set(cursor, pixel.Swatch) + } + } else { + w.lastPixel = nil + } + case ActorTool: + // See if any of the actors are below the mouse cursor. + var WP = w.WorldIndexAt(cursor) + _ = WP + + var deleteActors = []*level.Actor{} + for _, actor := range w.actors { + box := render.Rect{ + X: actor.Actor.Point.X - P.X - w.Scroll.X, + Y: actor.Actor.Point.Y - P.Y - w.Scroll.Y, + W: actor.Canvas.Size().W, + H: actor.Canvas.Size().H, + } + + if WP.Inside(box) { + actor.Canvas.Configure(ui.Config{ + BorderSize: 1, + BorderColor: render.RGBA(255, 153, 0, 255), + BorderStyle: ui.BorderSolid, + Background: render.White, // TODO: cuz the border draws a bgcolor + }) + + // Check for a mouse down event to begin dragging this + // canvas around. + if ev.Button1.Read() { + // Pop this canvas out for the drag/drop. + if w.OnDragStart != nil { + deleteActors = append(deleteActors, actor.Actor) + w.OnDragStart(actor.Actor.Filename) + } + break + } else if ev.Button2.Read() { + // Right click to delete an actor. + deleteActors = append(deleteActors, actor.Actor) + } + } else { + actor.Canvas.SetBorderSize(0) + actor.Canvas.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO + } + } + + // Change in actor count? + if len(deleteActors) > 0 && w.OnDeleteActors != nil { + w.OnDeleteActors(deleteActors) + } + } + + return nil + +} diff --git a/uix/draw_modes.go b/uix/draw_modes.go new file mode 100644 index 0000000..2168b1f --- /dev/null +++ b/uix/draw_modes.go @@ -0,0 +1,19 @@ +package uix + +// Tool is a draw mode for an editable Canvas. +type Tool int + +// Draw modes for editable Canvas. +const ( + PencilTool Tool = iota // draw pixels where the mouse clicks + ActorTool // drag and move actors +) + +var toolNames = []string{ + "Pencil", + "Doodad", // readable name for ActorTool +} + +func (t Tool) String() string { + return toolNames[t] +}