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.
This commit is contained in:
parent
0044b72943
commit
b4a366baa9
38
editor_ui.go
38
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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
107
uix/canvas_editable.go
Normal file
107
uix/canvas_editable.go
Normal file
|
@ -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
|
||||
|
||||
}
|
19
uix/draw_modes.go
Normal file
19
uix/draw_modes.go
Normal file
|
@ -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]
|
||||
}
|
Loading…
Reference in New Issue
Block a user