Noah Petherbridge
5654145fd8
Finally add a second option for Chunk MapAccessor implementation besides the MapAccessor. The RLEAccessor is basically a MapAccessor that will compress your drawing with Run Length Encoding (RLE) in the on-disk format in the ZIP file. This slashes the file sizes of most levels: * Shapeshifter: 21.8 MB -> 8.1 MB * Jungle: 10.4 MB -> 4.1 MB * Zoo: 2.8 MB -> 1.3 MB Implementation details: * The RLE binary format for Chunks is a stream of Uvarint pairs storing the palette index number and the number of pixels to repeat it (along the Y,X axis of the chunk). * Null colors are represented by a Uvarint that decodes to 0xFFFF or 65535 in decimal. * Gameplay logic currently limits maps to 256 colors. * The default for newly created chunks in-game will be RLE by default. * Its in-memory representation is still a MapAccessor (a map of absolute world coordinates to palette index). * The game can still open and play legacy MapAccessor maps. * On save in the editor, the game will upgrade/convert MapAccessor chunks over to RLEAccessors, improving on your level's file size with a simple re-save. Current Bugs * On every re-save to RLE, one pixel is lost in the bottom-right corner of each chunk. Each subsequent re-save loses one more pixel to the left, so what starts as a single pixel per chunk slowly evolves into a horizontal line. * Some pixels smear vertically as well. * Off-by-negative-one errors when some chunks Iter() their pixels but compute a relative coordinate of (-1,0)! Some mismatch between the stored world coords of a pixel inside the chunk vs. the chunk's assigned coordinate by the Chunker: certain combinations of chunk coord/abs coord. To Do * The `doodad touch` command should re-save existing levels to upgrade them.
649 lines
19 KiB
Go
649 lines
19 KiB
Go
package uix
|
|
|
|
import (
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/balance"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/drawtool"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/keybind"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/level"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
|
|
"git.kirsle.net/SketchyMaze/doodle/pkg/shmem"
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/render/event"
|
|
"git.kirsle.net/go/ui"
|
|
)
|
|
|
|
// Modified returns whether the canvas has been modified since it was last
|
|
// loaded. Methods like Load and LoadFile will set modified to false, and
|
|
// commitStroke sets it to true.
|
|
func (w *Canvas) Modified() bool {
|
|
return w.modified
|
|
}
|
|
|
|
// SetModified sets the modified bit on the canvas.
|
|
func (w *Canvas) SetModified(v bool) {
|
|
w.modified = v
|
|
}
|
|
|
|
// commitStroke is the common function that applies a stroke the user is
|
|
// actively drawing onto the canvas. This is for Edit Mode.
|
|
func (w *Canvas) commitStroke(tool drawtool.Tool, addHistory bool) {
|
|
if w.currentStroke == nil {
|
|
// nothing to commit
|
|
return
|
|
}
|
|
|
|
// Zoom the stroke coordinates (this modifies the pointer).
|
|
// Note: all the points on the stroke were mouse cursor coordinates on the screen.
|
|
w.currentStroke = w.ZoomStroke(w.currentStroke)
|
|
|
|
// Mark the canvas as modified.
|
|
w.modified = true
|
|
|
|
var (
|
|
deleting = w.currentStroke.Shape == drawtool.Eraser
|
|
dedupe = map[render.Point]interface{}{} // don't revisit the same point twice
|
|
|
|
// Helper functions to set pixels on the level while storing the original
|
|
// value of any pixel being replaced.
|
|
set = func(pt render.Point, sw *level.Swatch) {
|
|
// Take note of what pixel was originally here before we change it.
|
|
if swatch, err := w.chunks.Get(pt); err == nil {
|
|
if _, ok := dedupe[pt]; !ok {
|
|
w.currentStroke.OriginalPoints[pt] = swatch
|
|
dedupe[pt] = nil
|
|
}
|
|
}
|
|
|
|
if deleting {
|
|
w.chunks.Delete(pt)
|
|
} else if sw != nil {
|
|
w.chunks.Set(pt, sw)
|
|
} else {
|
|
panic("Canvas.commitStroke.set: current stroke has no level.Swatch in ExtraData")
|
|
}
|
|
}
|
|
|
|
// Rects: read existing pixels first, then write new pixels
|
|
readRect = func(rect render.Rect) {
|
|
for pt := range w.chunks.IterViewport(rect) {
|
|
point := pt.Point()
|
|
if _, ok := dedupe[point]; !ok {
|
|
w.currentStroke.OriginalPoints[pt.Point()] = pt.Swatch
|
|
dedupe[point] = nil
|
|
}
|
|
}
|
|
}
|
|
setRect = func(rect render.Rect, sw *level.Swatch) {
|
|
if deleting {
|
|
w.chunks.DeleteRect(rect)
|
|
} else if sw != nil {
|
|
w.chunks.SetRect(rect, sw)
|
|
} else {
|
|
panic("Canvas.commitStroke.setRect: current stroke has no level.Swatch in ExtraData")
|
|
}
|
|
}
|
|
)
|
|
|
|
var swatch *level.Swatch
|
|
if v, ok := w.currentStroke.ExtraData.(*level.Swatch); ok {
|
|
swatch = v
|
|
}
|
|
|
|
if w.currentStroke.Thickness > 0 {
|
|
// Eraser Tool only: record which pixels will be blown away by this.
|
|
// This is SLOW for thick (rect-based) lines, but eraser tool must have it.
|
|
if deleting {
|
|
for rect := range w.currentStroke.IterThickPoints() {
|
|
readRect(rect)
|
|
}
|
|
}
|
|
for rect := range w.currentStroke.IterThickPoints() {
|
|
setRect(rect, swatch)
|
|
}
|
|
} else {
|
|
for pt := range w.currentStroke.IterPoints() {
|
|
// note: set already records the original pixel if changing it.
|
|
set(pt, swatch)
|
|
}
|
|
}
|
|
|
|
// Add the stroke to level history.
|
|
if addHistory {
|
|
w.strokeToHistory(w.currentStroke)
|
|
}
|
|
|
|
w.RemoveStroke(w.currentStroke)
|
|
w.currentStroke = nil
|
|
|
|
w.lastPixel = nil
|
|
}
|
|
|
|
// Add a recently drawn stroke to the UndoHistory.
|
|
func (w *Canvas) strokeToHistory(stroke *drawtool.Stroke) {
|
|
if w.level != nil {
|
|
w.level.UndoHistory.AddStroke(stroke)
|
|
} else if w.doodad != nil {
|
|
if w.doodad.UndoHistory == nil {
|
|
// HACK: if UndoHistory was not initialized properly.
|
|
w.doodad.UndoHistory = drawtool.NewHistory(balance.UndoHistory)
|
|
}
|
|
w.doodad.UndoHistory.AddStroke(stroke)
|
|
}
|
|
}
|
|
|
|
// loopEditable handles the Loop() part for editable canvases.
|
|
func (w *Canvas) loopEditable(ev *event.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 - P.X - w.Scroll.X,
|
|
Y: ev.CursorY - P.Y - w.Scroll.Y,
|
|
}
|
|
)
|
|
|
|
// If the actual cursor is not over the actual Canvas UI element, don't
|
|
// pay any attention to clicks. I added this when I saw you were able to
|
|
// accidentally draw (with large brush size) when clicking on the Palette
|
|
// panel and not the drawing itself.
|
|
if !w.IsCursorOver() {
|
|
return nil
|
|
}
|
|
|
|
switch w.Tool {
|
|
case drawtool.PanTool:
|
|
// Pan tool = click to pan the level.
|
|
var delta render.Point
|
|
if keybind.LeftClick(ev) || keybind.MiddleClick(ev) {
|
|
if !w.scrollDragging {
|
|
w.scrollDragging = true
|
|
w.scrollStartAt = shmem.Cursor
|
|
w.scrollWasAt = w.Scroll
|
|
} else {
|
|
delta = shmem.Cursor.Compare(w.scrollStartAt)
|
|
w.Scroll = w.scrollWasAt
|
|
w.Scroll.Subtract(delta)
|
|
|
|
// TODO: if I don't call this, the user is able to (temporarily!)
|
|
// pan outside the level boundaries before it snaps-back when they
|
|
// release. But the normal middle-click to pan code doesn't let
|
|
// them do this.. investigate why later.
|
|
w.loopConstrainScroll()
|
|
}
|
|
} else {
|
|
if w.scrollDragging {
|
|
w.scrollDragging = false
|
|
}
|
|
}
|
|
|
|
// All the Pan tool to still interact with the Settings button on mouse-over
|
|
// of an actor. On touch devices it's difficult to access an actor's settings
|
|
// without accidentally dragging the actor, so the Pan Tool allows safe access.
|
|
// NOTE: code copied from Actor Tool but with delete and drag/drop hooks removed.
|
|
var WP = w.WorldIndexAt(cursor)
|
|
for _, actor := range w.actors {
|
|
|
|
// Compute the bounding box on screen where this doodad
|
|
// visually appears.
|
|
var scrollBias = render.Point{
|
|
X: w.Scroll.X,
|
|
Y: w.Scroll.Y,
|
|
}
|
|
if w.Zoom != 0 {
|
|
scrollBias.X = w.ZoomDivide(scrollBias.X)
|
|
scrollBias.Y = w.ZoomDivide(scrollBias.Y)
|
|
}
|
|
box := render.Rect{
|
|
X: actor.Actor.Point.X - scrollBias.X - w.ZoomDivide(P.X),
|
|
Y: actor.Actor.Point.Y - scrollBias.Y - w.ZoomDivide(P.Y),
|
|
W: actor.Canvas.Size().W,
|
|
H: actor.Canvas.Size().H,
|
|
}
|
|
|
|
// Mouse hover?
|
|
if WP.Inside(box) {
|
|
actor.Canvas.Configure(ui.Config{
|
|
BorderSize: 1,
|
|
BorderColor: render.RGBA(153, 153, 153, 255),
|
|
BorderStyle: ui.BorderSolid,
|
|
Background: render.White, // TODO: cuz the border draws a bgcolor
|
|
})
|
|
|
|
// Show doodad buttons.
|
|
actor.Canvas.ShowDoodadButtons = true
|
|
|
|
// Check for a mouse down event to begin dragging this
|
|
// canvas around.
|
|
if keybind.LeftClick(ev) && delta == render.Origin {
|
|
// Did they click onto the doodad buttons?
|
|
if shmem.Cursor.Inside(actor.Canvas.doodadButtonRect()) {
|
|
keybind.ClearLeftClick(ev)
|
|
if w.OnDoodadConfig != nil {
|
|
w.OnDoodadConfig(actor)
|
|
} else {
|
|
log.Error("OnDoodadConfig: handler not defined for parent canvas")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
break
|
|
}
|
|
} else {
|
|
actor.Canvas.SetBorderSize(0)
|
|
actor.Canvas.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO
|
|
actor.Canvas.ShowDoodadButtons = false
|
|
}
|
|
}
|
|
|
|
case drawtool.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 keybind.LeftClick(ev) {
|
|
// Initialize a new Stroke for this atomic drawing operation?
|
|
if w.currentStroke == nil {
|
|
w.currentStroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color)
|
|
w.currentStroke.Pattern = w.Palette.ActiveSwatch.Pattern
|
|
w.currentStroke.Thickness = w.BrushSize
|
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
|
w.AddStroke(w.currentStroke)
|
|
}
|
|
|
|
lastPixel := w.lastPixel
|
|
pixel := &level.Pixel{
|
|
X: cursor.X,
|
|
Y: cursor.Y,
|
|
Swatch: w.Palette.ActiveSwatch,
|
|
}
|
|
|
|
// If the user is holding the mouse down over one spot and not
|
|
// moving, don't do anything. The pixel has already been set and
|
|
// needless writes to the map cause needless cache rewrites etc.
|
|
if lastPixel != nil {
|
|
if pixel.X == lastPixel.X && pixel.Y == lastPixel.Y {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Append unique new pixels.
|
|
if lastPixel != nil || lastPixel != pixel {
|
|
// Draw the pixels in between.
|
|
if lastPixel != nil && lastPixel != pixel {
|
|
for point := range render.IterLine(lastPixel.Point(), pixel.Point()) {
|
|
w.currentStroke.AddPoint(point)
|
|
}
|
|
}
|
|
|
|
w.lastPixel = pixel
|
|
|
|
// Save the pixel in the current stroke.
|
|
w.currentStroke.AddPoint(render.Point{
|
|
X: cursor.X,
|
|
Y: cursor.Y,
|
|
})
|
|
}
|
|
} else {
|
|
w.commitStroke(w.Tool, true)
|
|
}
|
|
|
|
case drawtool.LineTool:
|
|
// 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 keybind.LeftClick(ev) {
|
|
// Initialize a new Stroke for this atomic drawing operation?
|
|
if w.currentStroke == nil {
|
|
w.currentStroke = drawtool.NewStroke(drawtool.Line, w.Palette.ActiveSwatch.Color)
|
|
w.currentStroke.Pattern = w.Palette.ActiveSwatch.Pattern
|
|
w.currentStroke.Thickness = w.BrushSize
|
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
|
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
|
w.AddStroke(w.currentStroke)
|
|
}
|
|
|
|
w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y)
|
|
} else {
|
|
w.commitStroke(w.Tool, true)
|
|
}
|
|
|
|
case drawtool.RectTool:
|
|
// 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 keybind.LeftClick(ev) {
|
|
// Initialize a new Stroke for this atomic drawing operation?
|
|
if w.currentStroke == nil {
|
|
w.currentStroke = drawtool.NewStroke(drawtool.Rectangle, w.Palette.ActiveSwatch.Color)
|
|
w.currentStroke.Pattern = w.Palette.ActiveSwatch.Pattern
|
|
w.currentStroke.Thickness = w.BrushSize
|
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
|
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
|
w.AddStroke(w.currentStroke)
|
|
}
|
|
|
|
w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y)
|
|
} else {
|
|
w.commitStroke(w.Tool, true)
|
|
}
|
|
|
|
case drawtool.EllipseTool:
|
|
if w.Palette.ActiveSwatch == nil {
|
|
return nil
|
|
}
|
|
|
|
if keybind.LeftClick(ev) {
|
|
if w.currentStroke == nil {
|
|
w.currentStroke = drawtool.NewStroke(drawtool.Ellipse, w.Palette.ActiveSwatch.Color)
|
|
w.currentStroke.Pattern = w.Palette.ActiveSwatch.Pattern
|
|
w.currentStroke.Thickness = w.BrushSize
|
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
|
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
|
w.AddStroke(w.currentStroke)
|
|
}
|
|
|
|
w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y)
|
|
} else {
|
|
w.commitStroke(w.Tool, true)
|
|
}
|
|
|
|
case drawtool.TextTool:
|
|
// The Text Tool popup should initialize this for us, if somehow not
|
|
// initialized skip this tool processing.
|
|
if w.Palette.ActiveSwatch == nil || drawtool.TT.IsZero() {
|
|
return nil
|
|
}
|
|
|
|
// Do we need to create the Label?
|
|
if drawtool.TT.Label == nil {
|
|
drawtool.TT.Label = ui.NewLabel(ui.Label{
|
|
Text: drawtool.TT.Message,
|
|
Font: render.Text{
|
|
FontFilename: drawtool.TT.Font,
|
|
Size: drawtool.TT.Size,
|
|
Color: w.Palette.ActiveSwatch.Color,
|
|
},
|
|
})
|
|
}
|
|
|
|
// Do we need to update the color of the label?
|
|
if drawtool.TT.Label.Font.Color != w.Palette.ActiveSwatch.Color {
|
|
drawtool.TT.Label.Font.Color = w.Palette.ActiveSwatch.Color
|
|
}
|
|
|
|
// NOTE: Canvas.presentStrokes() will handle drawing the font preview
|
|
// at the cursor location while the TextTool is active.
|
|
|
|
// On mouse click, commit the text to the drawing.
|
|
if keybind.LeftClick(ev) {
|
|
if stroke, err := drawtool.TT.ToStroke(shmem.CurrentRenderEngine, w.Palette.ActiveSwatch.Color, cursor); err != nil {
|
|
shmem.FlashError("Text Tool error: %s", err)
|
|
return nil
|
|
} else {
|
|
w.currentStroke = stroke
|
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
|
w.commitStroke(drawtool.PencilTool, true)
|
|
}
|
|
|
|
keybind.ClearLeftClick(ev)
|
|
}
|
|
|
|
case drawtool.FloodTool:
|
|
if w.Palette.ActiveSwatch == nil {
|
|
return nil
|
|
}
|
|
|
|
// Click to activate.
|
|
if keybind.LeftClick(ev) {
|
|
var (
|
|
chunker = w.Chunker()
|
|
stroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color)
|
|
)
|
|
|
|
// Set some max boundaries to prevent runaway infinite loops, e.g. if user
|
|
// clicked the wide open void the flood fill would never finish!
|
|
limit := balance.FloodToolLimit
|
|
|
|
// Get the original color at this location.
|
|
// Error cases can include: no chunk at this spot, or no pixel at this spot.
|
|
// Treat these as just a null color and proceed anyway, user should be able
|
|
// to flood fill blank areas of their level.
|
|
baseColor, err := chunker.Get(cursor)
|
|
if err != nil {
|
|
limit = balance.FloodToolVoidLimit
|
|
log.Warn("FloodTool: couldn't get base color at %s: %s (got %+v)", cursor, err, baseColor)
|
|
}
|
|
|
|
// If no change, do nothing.
|
|
if baseColor == w.Palette.ActiveSwatch {
|
|
break
|
|
}
|
|
|
|
// The flood fill algorithm.
|
|
queue := []render.Point{cursor}
|
|
for len(queue) > 0 {
|
|
node := queue[0]
|
|
queue = queue[1:]
|
|
|
|
colorAt, _ := chunker.Get(node)
|
|
if colorAt != baseColor {
|
|
continue
|
|
}
|
|
|
|
// For Undo history, store the original color at this point.
|
|
if colorAt != nil {
|
|
stroke.OriginalPoints[node] = colorAt
|
|
}
|
|
|
|
// Add the neighboring pixels.
|
|
for _, neighbor := range []render.Point{
|
|
{X: node.X - 1, Y: node.Y},
|
|
{X: node.X + 1, Y: node.Y},
|
|
{X: node.X, Y: node.Y - 1},
|
|
{X: node.X, Y: node.Y + 1},
|
|
} {
|
|
// Only if not too far from the origin!
|
|
if render.AbsInt(neighbor.X-cursor.X) <= limit && render.AbsInt(neighbor.Y-cursor.Y) <= limit {
|
|
queue = append(queue, neighbor)
|
|
}
|
|
}
|
|
|
|
stroke.AddPoint(node)
|
|
err = chunker.Set(node, w.Palette.ActiveSwatch)
|
|
if err != nil {
|
|
log.Error("FloodTool: error setting %s to %s: %s", node, w.Palette.ActiveSwatch, err)
|
|
}
|
|
}
|
|
|
|
w.strokeToHistory(stroke)
|
|
keybind.ClearLeftClick(ev)
|
|
}
|
|
|
|
case drawtool.EraserTool:
|
|
// Clicking? Log all the pixels while doing so.
|
|
if keybind.LeftClick(ev) {
|
|
// Initialize a new Stroke for this atomic drawing operation?
|
|
if w.currentStroke == nil {
|
|
// The color is white, will look like white-out that covers the
|
|
// wallpaper during the stroke.
|
|
w.currentStroke = drawtool.NewStroke(drawtool.Eraser, render.White)
|
|
w.currentStroke.Thickness = w.BrushSize
|
|
w.AddStroke(w.currentStroke)
|
|
}
|
|
|
|
lastPixel := w.lastPixel
|
|
pixel := &level.Pixel{
|
|
X: cursor.X,
|
|
Y: cursor.Y,
|
|
Swatch: w.Palette.ActiveSwatch,
|
|
}
|
|
|
|
// If the user is holding the mouse down over one spot and not
|
|
// moving, don't do anything. The pixel has already been set and
|
|
// needless writes to the map cause needless cache rewrites etc.
|
|
if lastPixel != nil {
|
|
if pixel.X == lastPixel.X && pixel.Y == lastPixel.Y {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Append unique new pixels.
|
|
if lastPixel == nil || lastPixel != pixel {
|
|
if lastPixel != nil && lastPixel != pixel {
|
|
for point := range render.IterLine(lastPixel.Point(), pixel.Point()) {
|
|
w.currentStroke.AddPoint(point)
|
|
}
|
|
}
|
|
|
|
w.lastPixel = pixel
|
|
w.currentStroke.AddPoint(render.Point{
|
|
X: cursor.X,
|
|
Y: cursor.Y,
|
|
})
|
|
}
|
|
} else {
|
|
w.commitStroke(w.Tool, true)
|
|
}
|
|
|
|
case drawtool.ActorTool:
|
|
// See if any of the actors are below the mouse cursor.
|
|
var WP = w.WorldIndexAt(cursor)
|
|
|
|
var deleteActors = []*Actor{}
|
|
for _, actor := range w.actors {
|
|
|
|
// Compute the bounding box on screen where this doodad
|
|
// visually appears.
|
|
var scrollBias = render.Point{
|
|
X: w.Scroll.X,
|
|
Y: w.Scroll.Y,
|
|
}
|
|
if w.Zoom != 0 {
|
|
scrollBias.X = w.ZoomDivide(scrollBias.X)
|
|
scrollBias.Y = w.ZoomDivide(scrollBias.Y)
|
|
}
|
|
box := render.Rect{
|
|
X: actor.Actor.Point.X - scrollBias.X - w.ZoomDivide(P.X),
|
|
Y: actor.Actor.Point.Y - scrollBias.Y - w.ZoomDivide(P.Y),
|
|
W: actor.Canvas.Size().W,
|
|
H: actor.Canvas.Size().H,
|
|
}
|
|
|
|
// Mouse hover?
|
|
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
|
|
})
|
|
|
|
// Show doodad buttons.
|
|
actor.Canvas.ShowDoodadButtons = true
|
|
|
|
// Check for a mouse down event to begin dragging this
|
|
// canvas around.
|
|
if keybind.LeftClick(ev) {
|
|
// Did they click onto the doodad buttons?
|
|
if shmem.Cursor.Inside(actor.Canvas.doodadButtonRect()) {
|
|
keybind.ClearLeftClick(ev)
|
|
if w.OnDoodadConfig != nil {
|
|
w.OnDoodadConfig(actor)
|
|
} else {
|
|
log.Error("OnDoodadConfig: handler not defined for parent canvas")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Pop this canvas out for the drag/drop.
|
|
if w.OnDragStart != nil {
|
|
deleteActors = append(deleteActors, actor)
|
|
w.OnDragStart(actor.Actor)
|
|
}
|
|
break
|
|
} else if ev.Button3 {
|
|
// Right click to delete an actor.
|
|
deleteActors = append(deleteActors, actor)
|
|
}
|
|
} else {
|
|
actor.Canvas.SetBorderSize(0)
|
|
actor.Canvas.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO
|
|
actor.Canvas.ShowDoodadButtons = false
|
|
}
|
|
}
|
|
|
|
// Change in actor count?
|
|
if len(deleteActors) > 0 && w.OnDeleteActors != nil {
|
|
w.OnDeleteActors(deleteActors)
|
|
}
|
|
|
|
case drawtool.LinkTool:
|
|
// See if any of the actors are below the mouse cursor.
|
|
var WP = w.WorldIndexAt(cursor)
|
|
|
|
for _, actor := range w.actors {
|
|
// Compute the bounding box on screen where this doodad
|
|
// visually appears.
|
|
var scrollBias = render.Point{
|
|
X: w.Scroll.X,
|
|
Y: w.Scroll.Y,
|
|
}
|
|
if w.Zoom != 0 {
|
|
scrollBias.X = w.ZoomDivide(scrollBias.X)
|
|
scrollBias.Y = w.ZoomDivide(scrollBias.Y)
|
|
}
|
|
box := render.Rect{
|
|
X: actor.Actor.Point.X - scrollBias.X - w.ZoomDivide(P.X),
|
|
Y: actor.Actor.Point.Y - scrollBias.Y - w.ZoomDivide(P.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, 255, 255),
|
|
BorderStyle: ui.BorderSolid,
|
|
Background: render.White, // TODO: cuz the border draws a bgcolor
|
|
})
|
|
|
|
// Click handler to start linking this actor.
|
|
if keybind.LeftClick(ev) {
|
|
if err := w.LinkAdd(actor); err != nil {
|
|
return err
|
|
}
|
|
|
|
// TODO: reset the Button1 state so we don't finish a
|
|
// link and then LinkAdd the clicked doodad immediately
|
|
// (causing link chaining)
|
|
keybind.ClearLeftClick(ev)
|
|
break
|
|
}
|
|
} else {
|
|
actor.Canvas.SetBorderSize(0)
|
|
actor.Canvas.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO
|
|
}
|
|
|
|
// Permanently color the actor if it's the current subject of the
|
|
// Link Tool (after 1st click, until 2nd click of other actor)
|
|
if w.linkFirst == actor {
|
|
actor.Canvas.Configure(ui.Config{
|
|
Background: render.RGBA(255, 153, 255, 153),
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|