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.pull/1/head
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
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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