Drag Doodads Onto Levels in Edit Mode
Add the ability to drag and drop Doodads onto the level. The Doodad buttons on the palette now trigger a Drag/Drop behavior when clicked, and a "blueprint colored" version of the Doodad follows your cursor, centered on it. Actors are assigned a random UUID ID when they are placed into a level. The Canvas gained a MaskColor property that forces all pixels in the drawing to render as the same color. This is a visual-only effect, and is used when dragging Doodads in so they render as "blueprints" instead of their actual colors until they are dropped. Fix the chunk bitmap cache system so it saves in the $XDG_CACHE_FOLDER instead of /tmp and has better names. They go into `~/.config/doodle/chunks/` and have UUID file names -- but they disappear quickly! As soon as they are cached into SDL2 they are removed from disk. Other changes: - UI: Add Hovering() method that returns the widgets that are beneath a point (your cursor) and those that are not, for easy querying for event propagation. - UI: Add ability to return an ErrStopPropagation to tell the master Scene (outside the UI) not to continue sending events to other parts of the code, so that you don't draw pixels during a drag event.
This commit is contained in:
parent
20771fbe13
commit
0044b72943
|
@ -15,3 +15,9 @@ var (
|
||||||
// Default size for a new Doodad.
|
// Default size for a new Doodad.
|
||||||
DoodadSize = 100
|
DoodadSize = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Edit Mode Values
|
||||||
|
var (
|
||||||
|
// Number of Doodads per row in the palette.
|
||||||
|
UIDoodadsPerRow = 2
|
||||||
|
)
|
||||||
|
|
|
@ -36,4 +36,7 @@ var (
|
||||||
Padding: 4,
|
Padding: 4,
|
||||||
Color: render.Black,
|
Color: render.Black,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Color for draggable doodad.
|
||||||
|
DragColor = render.MustHexColor("#0099FF")
|
||||||
)
|
)
|
||||||
|
|
18
docs/UI Ideas.md
Normal file
18
docs/UI Ideas.md
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
# UI Toolkit Ideas
|
||||||
|
|
||||||
|
The UI toolkit was loosely inspired by Tk and could copy more of their ideas.
|
||||||
|
|
||||||
|
* **Anchor vs. Side:** currently use Anchor to mean Side when packing widgets
|
||||||
|
into a Frame. It should be renamed to Side, and then Anchor should be how a
|
||||||
|
widget centers itself in its space, making it easy to have a Center Middle
|
||||||
|
widget inside a large frame.
|
||||||
|
* **Hover Background:** currently the Button sets its own color with its own
|
||||||
|
events, but this could be moved into the BaseWidget. Tk analog is
|
||||||
|
`activeBackground`
|
||||||
|
* **Mouse Cursor:** the BaseWidget should provide a way to configure a mouse
|
||||||
|
cursor when hovering over the widget.
|
||||||
|
|
||||||
|
## Label
|
||||||
|
|
||||||
|
* **Text Justify:** when multiple lines of text, align them all to the
|
||||||
|
left, center, or right.
|
|
@ -3,14 +3,16 @@ package doodads
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/level"
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Doodad is a reusable component for Levels that have scripts and graphics.
|
// Doodad is a reusable component for Levels that have scripts and graphics.
|
||||||
type Doodad struct {
|
type Doodad struct {
|
||||||
level.Base
|
level.Base
|
||||||
Palette *level.Palette `json:"palette"`
|
Filename string `json:"-"` // used internally, not saved in json
|
||||||
Script string `json:"script"`
|
Palette *level.Palette `json:"palette"`
|
||||||
Layers []Layer `json:"layers"`
|
Script string `json:"script"`
|
||||||
|
Layers []Layer `json:"layers"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Layer holds a layer of drawing data for a Doodad.
|
// Layer holds a layer of drawing data for a Doodad.
|
||||||
|
@ -39,6 +41,20 @@ func New(size int) *Doodad {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ChunkSize returns the chunk size of the Doodad's first layer.
|
||||||
|
func (d *Doodad) ChunkSize() int {
|
||||||
|
return d.Layers[0].Chunker.Size
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rect returns a rect of the ChunkSize for scaling a Canvas widget.
|
||||||
|
func (d *Doodad) Rect() render.Rect {
|
||||||
|
var size = d.ChunkSize()
|
||||||
|
return render.Rect{
|
||||||
|
W: int32(size),
|
||||||
|
H: int32(size),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Inflate attaches the pixels to their swatches after loading from disk.
|
// Inflate attaches the pixels to their swatches after loading from disk.
|
||||||
func (d *Doodad) Inflate() {
|
func (d *Doodad) Inflate() {
|
||||||
d.Palette.Inflate()
|
d.Palette.Inflate()
|
||||||
|
|
|
@ -6,6 +6,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ToJSON serializes the doodad as JSON.
|
// ToJSON serializes the doodad as JSON.
|
||||||
|
@ -29,6 +30,7 @@ func (d *Doodad) WriteJSON(filename string) error {
|
||||||
return fmt.Errorf("Doodad.WriteJSON: WriteFile error: %s", err)
|
return fmt.Errorf("Doodad.WriteJSON: WriteFile error: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.Filename = filepath.Base(filename)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,6 +51,7 @@ func LoadJSON(filename string) (*Doodad, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inflate the chunk metadata to map the pixels to their palette indexes.
|
// Inflate the chunk metadata to map the pixels to their palette indexes.
|
||||||
|
d.Filename = filepath.Base(filename)
|
||||||
d.Inflate()
|
d.Inflate()
|
||||||
return d, err
|
return d, err
|
||||||
}
|
}
|
||||||
|
|
229
editor_ui.go
229
editor_ui.go
|
@ -2,14 +2,13 @@ package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
"git.kirsle.net/apps/doodle/doodads"
|
|
||||||
"git.kirsle.net/apps/doodle/enum"
|
"git.kirsle.net/apps/doodle/enum"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/events"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/level"
|
||||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/ui"
|
||||||
"git.kirsle.net/apps/doodle/uix"
|
"git.kirsle.net/apps/doodle/uix"
|
||||||
|
@ -29,8 +28,8 @@ type EditorUI struct {
|
||||||
StatusPaletteText string
|
StatusPaletteText string
|
||||||
StatusFilenameText string
|
StatusFilenameText string
|
||||||
StatusScrollText string
|
StatusScrollText string
|
||||||
selectedSwatch string // name of selected swatch in palette
|
selectedSwatch string // name of selected swatch in palette
|
||||||
selectedDoodad string
|
cursor render.Point // remember the cursor position in Loop
|
||||||
|
|
||||||
// Widgets
|
// Widgets
|
||||||
Supervisor *ui.Supervisor
|
Supervisor *ui.Supervisor
|
||||||
|
@ -44,6 +43,9 @@ type EditorUI struct {
|
||||||
PaletteTab *ui.Frame
|
PaletteTab *ui.Frame
|
||||||
DoodadTab *ui.Frame
|
DoodadTab *ui.Frame
|
||||||
|
|
||||||
|
// Draggable Doodad canvas.
|
||||||
|
DraggableActor *DraggableActor
|
||||||
|
|
||||||
// Palette variables.
|
// Palette variables.
|
||||||
paletteTab string // selected tab, Palette or Doodads
|
paletteTab string // selected tab, Palette or Doodads
|
||||||
}
|
}
|
||||||
|
@ -144,47 +146,60 @@ func (u *EditorUI) Resized(d *Doodle) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loop to process events and update the UI.
|
// Loop to process events and update the UI.
|
||||||
func (u *EditorUI) Loop(ev *events.State) {
|
func (u *EditorUI) Loop(ev *events.State) error {
|
||||||
u.Supervisor.Loop(ev)
|
u.cursor = render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
|
||||||
|
|
||||||
|
// Loop the UI and see whether we're told to stop event propagation.
|
||||||
|
var stopPropagation bool
|
||||||
|
if err := u.Supervisor.Loop(ev); err != nil {
|
||||||
|
if err == ui.ErrStopPropagation {
|
||||||
|
stopPropagation = true
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update status bar labels.
|
||||||
{
|
{
|
||||||
var P = u.Workspace.Point()
|
debugWorldIndex = u.Canvas.WorldIndexAt(u.cursor)
|
||||||
debugWorldIndex = render.NewPoint(
|
u.StatusMouseText = fmt.Sprintf("Rel:(%d,%d) Abs:(%s)",
|
||||||
ev.CursorX.Now-P.X-u.Canvas.Scroll.X,
|
|
||||||
ev.CursorY.Now-P.Y-u.Canvas.Scroll.Y,
|
|
||||||
)
|
|
||||||
u.StatusMouseText = fmt.Sprintf("Mouse: (%d,%d) Px: (%s)",
|
|
||||||
ev.CursorX.Now,
|
ev.CursorX.Now,
|
||||||
ev.CursorY.Now,
|
ev.CursorY.Now,
|
||||||
debugWorldIndex,
|
debugWorldIndex,
|
||||||
)
|
)
|
||||||
}
|
u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
|
||||||
u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
|
u.Canvas.Palette.ActiveSwatch,
|
||||||
u.Canvas.Palette.ActiveSwatch,
|
)
|
||||||
)
|
u.StatusScrollText = fmt.Sprintf("Scroll: %s Viewport: %s",
|
||||||
u.StatusScrollText = fmt.Sprintf("Scroll: %s Viewport: %s",
|
u.Canvas.Scroll,
|
||||||
u.Canvas.Scroll,
|
u.Canvas.Viewport(),
|
||||||
u.Canvas.Viewport(),
|
)
|
||||||
)
|
|
||||||
|
|
||||||
// Statusbar filename label.
|
// Statusbar filename label.
|
||||||
filename := "untitled.map"
|
filename := "untitled.map"
|
||||||
fileType := "Level"
|
fileType := "Level"
|
||||||
if u.Scene.filename != "" {
|
if u.Scene.filename != "" {
|
||||||
filename = u.Scene.filename
|
filename = u.Scene.filename
|
||||||
|
}
|
||||||
|
if u.Scene.DrawingType == enum.DoodadDrawing {
|
||||||
|
fileType = "Doodad"
|
||||||
|
}
|
||||||
|
u.StatusFilenameText = fmt.Sprintf("Filename: %s (%s)",
|
||||||
|
filepath.Base(filename),
|
||||||
|
fileType,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if u.Scene.DrawingType == enum.DoodadDrawing {
|
|
||||||
fileType = "Doodad"
|
|
||||||
}
|
|
||||||
u.StatusFilenameText = fmt.Sprintf("Filename: %s (%s)",
|
|
||||||
filename,
|
|
||||||
fileType,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
// Recompute widgets.
|
||||||
u.MenuBar.Compute(u.d.Engine)
|
u.MenuBar.Compute(u.d.Engine)
|
||||||
u.StatusBar.Compute(u.d.Engine)
|
u.StatusBar.Compute(u.d.Engine)
|
||||||
u.Palette.Compute(u.d.Engine)
|
u.Palette.Compute(u.d.Engine)
|
||||||
u.Canvas.Loop(ev)
|
|
||||||
|
// Only forward events to the Canvas if the UI hasn't stopped them.
|
||||||
|
if !stopPropagation {
|
||||||
|
u.Canvas.Loop(ev)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present the UI to the screen.
|
// Present the UI to the screen.
|
||||||
|
@ -199,6 +214,17 @@ func (u *EditorUI) Present(e render.Engine) {
|
||||||
u.MenuBar.Present(e, u.MenuBar.Point())
|
u.MenuBar.Present(e, u.MenuBar.Point())
|
||||||
u.StatusBar.Present(e, u.StatusBar.Point())
|
u.StatusBar.Present(e, u.StatusBar.Point())
|
||||||
u.Workspace.Present(e, u.Workspace.Point())
|
u.Workspace.Present(e, u.Workspace.Point())
|
||||||
|
|
||||||
|
// Are we dragging a Doodad canvas?
|
||||||
|
if u.Supervisor.IsDragging() {
|
||||||
|
if actor := u.DraggableActor; actor != nil {
|
||||||
|
var size = actor.canvas.Size()
|
||||||
|
actor.canvas.Present(u.d.Engine, render.NewPoint(
|
||||||
|
u.cursor.X-(size.W/2),
|
||||||
|
u.cursor.Y-(size.H/2),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupWorkspace configures the main Workspace frame that takes up the full
|
// SetupWorkspace configures the main Workspace frame that takes up the full
|
||||||
|
@ -214,9 +240,40 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
|
||||||
drawing := uix.NewCanvas(balance.ChunkSize, true)
|
drawing := uix.NewCanvas(balance.ChunkSize, true)
|
||||||
drawing.Name = "edit-canvas"
|
drawing.Name = "edit-canvas"
|
||||||
drawing.Palette = level.DefaultPalette()
|
drawing.Palette = level.DefaultPalette()
|
||||||
|
drawing.SetBackground(render.White)
|
||||||
if len(drawing.Palette.Swatches) > 0 {
|
if len(drawing.Palette.Swatches) > 0 {
|
||||||
drawing.SetSwatch(drawing.Palette.Swatches[0])
|
drawing.SetSwatch(drawing.Palette.Swatches[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set up the drop handler for draggable doodads.
|
||||||
|
// NOTE: The drag event begins at editor_ui_doodad.go when configuring the
|
||||||
|
// Doodad Palette buttons.
|
||||||
|
drawing.Handle(ui.Drop, func(e render.Point) {
|
||||||
|
log.Info("Drawing canvas has received a drop!")
|
||||||
|
var P = ui.AbsolutePosition(drawing)
|
||||||
|
|
||||||
|
// Was it an actor from the Doodad Palette?
|
||||||
|
if actor := u.DraggableActor; actor != nil {
|
||||||
|
log.Info("Actor is a %s", actor.doodad.Filename)
|
||||||
|
if u.Scene.Level == nil {
|
||||||
|
u.d.Flash("Can't drop doodads onto doodad drawings!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
size := actor.canvas.Size()
|
||||||
|
u.Scene.Level.Actors.Add(&level.Actor{
|
||||||
|
// Uncenter the drawing from the cursor.
|
||||||
|
Point: render.Point{
|
||||||
|
X: (u.cursor.X - drawing.Scroll.X - (size.W / 2)) - P.X,
|
||||||
|
Y: (u.cursor.Y - drawing.Scroll.Y - (size.H / 2)) - P.Y,
|
||||||
|
},
|
||||||
|
Filename: actor.doodad.Filename,
|
||||||
|
})
|
||||||
|
|
||||||
|
drawing.InstallActors(u.Scene.Level.Actors)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
u.Supervisor.Add(drawing)
|
||||||
return drawing
|
return drawing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -395,106 +452,28 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
|
||||||
|
|
||||||
// Doodad frame.
|
// Doodad frame.
|
||||||
{
|
{
|
||||||
u.DoodadTab = ui.NewFrame("Doodad Tab")
|
frame, err := u.setupDoodadFrame(d.Engine, window)
|
||||||
|
if err != nil {
|
||||||
|
d.Flash(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Even if there was an error (userdir.ListDoodads couldn't read the
|
||||||
|
// config folder on disk or whatever) the Frame is still valid but
|
||||||
|
// empty, which is still the intended behavior.
|
||||||
|
u.DoodadTab = frame
|
||||||
u.DoodadTab.Hide()
|
u.DoodadTab.Hide()
|
||||||
window.Pack(u.DoodadTab, ui.Pack{
|
window.Pack(u.DoodadTab, ui.Pack{
|
||||||
Anchor: ui.N,
|
Anchor: ui.N,
|
||||||
Fill: true,
|
Fill: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
doodadsAvailable, err := userdir.ListDoodads()
|
|
||||||
if err != nil {
|
|
||||||
d.Flash("ListDoodads: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var buttonSize = (paletteWidth - window.BoxThickness(2)) / 2
|
|
||||||
|
|
||||||
// Draw the doodad buttons in a grid 2 wide.
|
|
||||||
var row *ui.Frame
|
|
||||||
for i, filename := range doodadsAvailable {
|
|
||||||
si := fmt.Sprintf("%d", i)
|
|
||||||
if row == nil || i%2 == 0 {
|
|
||||||
row = ui.NewFrame("Doodad Row " + si)
|
|
||||||
row.SetBackground(balance.WindowBackground)
|
|
||||||
u.DoodadTab.Pack(row, ui.Pack{
|
|
||||||
Anchor: ui.N,
|
|
||||||
Fill: true,
|
|
||||||
// Expand: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
doodad, err := doodads.LoadJSON(userdir.DoodadPath(filename))
|
|
||||||
if err != nil {
|
|
||||||
log.Error(err.Error())
|
|
||||||
doodad = doodads.New(balance.DoodadSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
can := uix.NewCanvas(int(buttonSize), true)
|
|
||||||
can.Name = filename
|
|
||||||
can.LoadDoodad(doodad)
|
|
||||||
btn := ui.NewRadioButton(filename, &u.selectedDoodad, si, can)
|
|
||||||
btn.Resize(render.NewRect(
|
|
||||||
buttonSize-2, // TODO: without the -2 the button border
|
|
||||||
buttonSize-2, // rests on top of the window border.
|
|
||||||
))
|
|
||||||
u.Supervisor.Add(btn)
|
|
||||||
row.Pack(btn, ui.Pack{
|
|
||||||
Anchor: ui.W,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Resize the canvas to fill the button interior.
|
|
||||||
btnSize := btn.Size()
|
|
||||||
can.Resize(render.NewRect(
|
|
||||||
btnSize.W-btn.BoxThickness(2),
|
|
||||||
btnSize.H-btn.BoxThickness(2),
|
|
||||||
))
|
|
||||||
|
|
||||||
btn.Compute(d.Engine)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Color Palette Frame.
|
// Color Palette Frame.
|
||||||
{
|
u.PaletteTab = u.setupPaletteFrame(window)
|
||||||
u.PaletteTab = ui.NewFrame("Palette Tab")
|
window.Pack(u.PaletteTab, ui.Pack{
|
||||||
u.PaletteTab.SetBackground(balance.WindowBackground)
|
Anchor: ui.N,
|
||||||
window.Pack(u.PaletteTab, ui.Pack{
|
Fill: true,
|
||||||
Anchor: ui.N,
|
})
|
||||||
Fill: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handler function for the radio buttons being clicked.
|
|
||||||
onClick := func(p render.Point) {
|
|
||||||
name := u.selectedSwatch
|
|
||||||
swatch, ok := u.Canvas.Palette.Get(name)
|
|
||||||
if !ok {
|
|
||||||
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info("Set swatch: %s", swatch)
|
|
||||||
u.Canvas.SetSwatch(swatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw the radio buttons for the palette.
|
|
||||||
if u.Canvas != nil && u.Canvas.Palette != nil {
|
|
||||||
for _, swatch := range u.Canvas.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.selectedSwatch, swatch.Name, label)
|
|
||||||
btn.Handle(ui.Click, onClick)
|
|
||||||
u.Supervisor.Add(btn)
|
|
||||||
|
|
||||||
u.PaletteTab.Pack(btn, ui.Pack{
|
|
||||||
Anchor: ui.N,
|
|
||||||
Fill: true,
|
|
||||||
PadY: 4,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return window
|
return window
|
||||||
}
|
}
|
||||||
|
|
115
editor_ui_doodad.go
Normal file
115
editor_ui_doodad.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
package doodle
|
||||||
|
|
||||||
|
// XXX REFACTOR XXX
|
||||||
|
// This function only uses EditorUI and not Doodle and is a candidate for
|
||||||
|
// refactor into a subpackage if EditorUI itself can ever be decoupled.
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
|
"git.kirsle.net/apps/doodle/doodads"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DraggableActor is a Doodad being dragged from the Doodad palette.
|
||||||
|
type DraggableActor struct {
|
||||||
|
canvas *uix.Canvas
|
||||||
|
doodad *doodads.Doodad
|
||||||
|
}
|
||||||
|
|
||||||
|
// setupDoodadFrame configures the Doodad Palette tab for Edit Mode.
|
||||||
|
// This is a subroutine of editor_ui.go#SetupPalette()
|
||||||
|
//
|
||||||
|
// Can return an error if userdir.ListDoodads() returns an error (like directory
|
||||||
|
// not found), but it will *ALWAYS* return a valid ui.Frame -- it will just be
|
||||||
|
// empty and uninitialized.
|
||||||
|
func (u *EditorUI) setupDoodadFrame(e render.Engine, window *ui.Window) (*ui.Frame, error) {
|
||||||
|
var (
|
||||||
|
frame = ui.NewFrame("Doodad Tab")
|
||||||
|
perRow = balance.UIDoodadsPerRow
|
||||||
|
)
|
||||||
|
|
||||||
|
doodadsAvailable, err := userdir.ListDoodads()
|
||||||
|
if err != nil {
|
||||||
|
return frame, fmt.Errorf(
|
||||||
|
"setupDoodadFrame: userdir.ListDoodads: %s",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
var buttonSize = (paletteWidth - window.BoxThickness(2)) / int32(perRow)
|
||||||
|
|
||||||
|
// Draw the doodad buttons in a grid `perRow` buttons wide.
|
||||||
|
var (
|
||||||
|
row *ui.Frame
|
||||||
|
rowCount int // for labeling the ui.Frame for each row
|
||||||
|
)
|
||||||
|
for i, filename := range doodadsAvailable {
|
||||||
|
if row == nil || i%perRow == 0 {
|
||||||
|
rowCount++
|
||||||
|
row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount))
|
||||||
|
row.SetBackground(balance.WindowBackground)
|
||||||
|
frame.Pack(row, ui.Pack{
|
||||||
|
Anchor: ui.N,
|
||||||
|
Fill: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func(filename string) {
|
||||||
|
doodad, err := doodads.LoadJSON(userdir.DoodadPath(filename))
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err.Error())
|
||||||
|
doodad = doodads.New(balance.DoodadSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
can := uix.NewCanvas(int(buttonSize), true)
|
||||||
|
can.Name = filename
|
||||||
|
can.SetBackground(render.White)
|
||||||
|
can.LoadDoodad(doodad)
|
||||||
|
|
||||||
|
btn := ui.NewButton(filename, can)
|
||||||
|
btn.Resize(render.NewRect(
|
||||||
|
buttonSize-2, // TODO: without the -2 the button border
|
||||||
|
buttonSize-2, // rests on top of the window border.
|
||||||
|
))
|
||||||
|
row.Pack(btn, ui.Pack{
|
||||||
|
Anchor: ui.W,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Begin the drag event to grab this Doodad.
|
||||||
|
// 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.Supervisor.Add(btn)
|
||||||
|
|
||||||
|
// Resize the canvas to fill the button interior.
|
||||||
|
btnSize := btn.Size()
|
||||||
|
can.Resize(render.NewRect(
|
||||||
|
btnSize.W-btn.BoxThickness(2),
|
||||||
|
btnSize.H-btn.BoxThickness(2),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
btn.Compute(e)
|
||||||
|
}(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame, nil
|
||||||
|
}
|
49
editor_ui_palette.go
Normal file
49
editor_ui_palette.go
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
package doodle
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
"git.kirsle.net/apps/doodle/ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
// setupPaletteFrame configures the Color Palette tab for Edit Mode.
|
||||||
|
// This is a subroutine of editor_ui.go#SetupPalette()
|
||||||
|
func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame {
|
||||||
|
frame := ui.NewFrame("Palette Tab")
|
||||||
|
frame.SetBackground(balance.WindowBackground)
|
||||||
|
|
||||||
|
// Handler function for the radio buttons being clicked.
|
||||||
|
onClick := func(p render.Point) {
|
||||||
|
name := u.selectedSwatch
|
||||||
|
swatch, ok := u.Canvas.Palette.Get(name)
|
||||||
|
if !ok {
|
||||||
|
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Info("Set swatch: %s", swatch)
|
||||||
|
u.Canvas.SetSwatch(swatch)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the radio buttons for the palette.
|
||||||
|
if u.Canvas != nil && u.Canvas.Palette != nil {
|
||||||
|
for _, swatch := range u.Canvas.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.selectedSwatch, swatch.Name, label)
|
||||||
|
btn.Handle(ui.Click, onClick)
|
||||||
|
u.Supervisor.Add(btn)
|
||||||
|
|
||||||
|
frame.Pack(btn, ui.Pack{
|
||||||
|
Anchor: ui.N,
|
||||||
|
Fill: true,
|
||||||
|
PadY: 4,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return frame
|
||||||
|
}
|
|
@ -1,6 +1,9 @@
|
||||||
package level
|
package level
|
||||||
|
|
||||||
import "git.kirsle.net/apps/doodle/render"
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
uuid "github.com/satori/go.uuid"
|
||||||
|
)
|
||||||
|
|
||||||
// ActorMap holds the doodad information by their ID in the level data.
|
// ActorMap holds the doodad information by their ID in the level data.
|
||||||
type ActorMap map[string]*Actor
|
type ActorMap map[string]*Actor
|
||||||
|
@ -12,6 +15,15 @@ func (m ActorMap) Inflate() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add a new Actor to the map. If it doesn't already have an ID it will be
|
||||||
|
// given a random UUIDv4 ID.
|
||||||
|
func (m ActorMap) Add(a *Actor) {
|
||||||
|
if a.id == "" {
|
||||||
|
a.id = uuid.Must(uuid.NewV4()).String()
|
||||||
|
}
|
||||||
|
m[a.id] = a
|
||||||
|
}
|
||||||
|
|
||||||
// Actor is an instance of a Doodad in the level.
|
// Actor is an instance of a Doodad in the level.
|
||||||
type Actor struct {
|
type Actor struct {
|
||||||
id string // NOTE: read only, use ID() to access.
|
id string // NOTE: read only, use ID() to access.
|
||||||
|
|
|
@ -8,7 +8,9 @@ import (
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
"github.com/satori/go.uuid"
|
||||||
"golang.org/x/image/bmp"
|
"golang.org/x/image/bmp"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -28,8 +30,10 @@ type Chunk struct {
|
||||||
Size int
|
Size int
|
||||||
|
|
||||||
// Texture cache properties so we don't redraw pixel-by-pixel every frame.
|
// Texture cache properties so we don't redraw pixel-by-pixel every frame.
|
||||||
texture render.Texturer
|
uuid uuid.UUID
|
||||||
dirty bool
|
texture render.Texturer
|
||||||
|
textureMasked render.Texturer
|
||||||
|
dirty bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// JSONChunk holds a lightweight (interface-free) copy of the Chunk for
|
// JSONChunk holds a lightweight (interface-free) copy of the Chunk for
|
||||||
|
@ -63,26 +67,66 @@ func NewChunk() *Chunk {
|
||||||
|
|
||||||
// Texture will return a cached texture for the rendering engine for this
|
// Texture will return a cached texture for the rendering engine for this
|
||||||
// chunk's pixel data. If the cache is dirty it will be rebuilt in this func.
|
// chunk's pixel data. If the cache is dirty it will be rebuilt in this func.
|
||||||
func (c *Chunk) Texture(e render.Engine, name string) render.Texturer {
|
func (c *Chunk) Texture(e render.Engine) render.Texturer {
|
||||||
if c.texture == nil || c.dirty {
|
if c.texture == nil || c.dirty {
|
||||||
err := c.ToBitmap("/tmp/" + name + ".bmp")
|
// Generate the normal bitmap and one with a color mask if applicable.
|
||||||
if err != nil {
|
bitmap := c.toBitmap(render.Invisible)
|
||||||
log.Error("Texture: %s", err)
|
defer os.Remove(bitmap)
|
||||||
}
|
tex, err := e.NewBitmap(bitmap)
|
||||||
|
|
||||||
tex, err := e.NewBitmap("/tmp/" + name + ".bmp")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error("Texture: %s", err)
|
log.Error("Texture: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
c.texture = tex
|
c.texture = tex
|
||||||
|
c.textureMasked = nil // invalidate until next call
|
||||||
c.dirty = false
|
c.dirty = false
|
||||||
}
|
}
|
||||||
return c.texture
|
return c.texture
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TextureMasked returns a cached texture with the ColorMask applied.
|
||||||
|
func (c *Chunk) TextureMasked(e render.Engine, mask render.Color) render.Texturer {
|
||||||
|
if c.textureMasked == nil {
|
||||||
|
// Generate the normal bitmap and one with a color mask if applicable.
|
||||||
|
bitmap := c.toBitmap(mask)
|
||||||
|
defer os.Remove(bitmap)
|
||||||
|
tex, err := e.NewBitmap(bitmap)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Texture: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.textureMasked = tex
|
||||||
|
}
|
||||||
|
return c.textureMasked
|
||||||
|
}
|
||||||
|
|
||||||
|
// toBitmap puts the texture in a well named bitmap path in the cache folder.
|
||||||
|
func (c *Chunk) toBitmap(mask render.Color) string {
|
||||||
|
// Generate a unique filename for this chunk cache.
|
||||||
|
var filename string
|
||||||
|
if c.uuid == uuid.Nil {
|
||||||
|
c.uuid = uuid.Must(uuid.NewV4())
|
||||||
|
}
|
||||||
|
filename = c.uuid.String()
|
||||||
|
|
||||||
|
if mask != render.Invisible {
|
||||||
|
filename += fmt.Sprintf("-%02x%02x%02x%02x",
|
||||||
|
mask.Red, mask.Green, mask.Blue, mask.Alpha,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the temp bitmap image.
|
||||||
|
bitmap := userdir.CacheFilename("chunk", filename+".bmp")
|
||||||
|
err := c.ToBitmap(bitmap, mask)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("Texture: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return bitmap
|
||||||
|
}
|
||||||
|
|
||||||
// ToBitmap exports the chunk's pixels as a bitmap image.
|
// ToBitmap exports the chunk's pixels as a bitmap image.
|
||||||
func (c *Chunk) ToBitmap(filename string) error {
|
func (c *Chunk) ToBitmap(filename string, mask render.Color) error {
|
||||||
canvas := c.SizePositive()
|
canvas := c.SizePositive()
|
||||||
imgSize := image.Rectangle{
|
imgSize := image.Rectangle{
|
||||||
Min: image.Point{},
|
Min: image.Point{},
|
||||||
|
@ -117,10 +161,14 @@ func (c *Chunk) ToBitmap(filename string) error {
|
||||||
|
|
||||||
// Blot all the pixels onto it.
|
// Blot all the pixels onto it.
|
||||||
for px := range c.Iter() {
|
for px := range c.Iter() {
|
||||||
|
var color = px.Swatch.Color
|
||||||
|
if mask != render.Invisible {
|
||||||
|
color = mask
|
||||||
|
}
|
||||||
img.Set(
|
img.Set(
|
||||||
int(px.X-pointOffset.X),
|
int(px.X-pointOffset.X),
|
||||||
int(px.Y-pointOffset.Y),
|
int(px.Y-pointOffset.Y),
|
||||||
px.Swatch.Color.ToColor(),
|
color.ToColor(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,7 @@ func New() *Level {
|
||||||
},
|
},
|
||||||
Chunker: NewChunker(balance.ChunkSize),
|
Chunker: NewChunker(balance.ChunkSize),
|
||||||
Palette: &Palette{},
|
Palette: &Palette{},
|
||||||
|
Actors: ActorMap{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -55,6 +55,16 @@ func DoodadPath(filename string) string {
|
||||||
return resolvePath(DoodadDirectory, filename, extDoodad)
|
return resolvePath(DoodadDirectory, filename, extDoodad)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CacheFilename returns a path to a file in the cache folder. Send in path
|
||||||
|
// components and not literal slashes, like
|
||||||
|
// CacheFilename("images", "chunks", "id.bmp")
|
||||||
|
func CacheFilename(filename ...string) string {
|
||||||
|
paths := append([]string{CacheDirectory}, filename...)
|
||||||
|
dir := paths[:len(paths)-1]
|
||||||
|
configdir.MakePath(filepath.Join(dir...))
|
||||||
|
return filepath.Join(paths[0], filepath.Join(paths[1:]...))
|
||||||
|
}
|
||||||
|
|
||||||
// ListDoodads returns a listing of all available doodads.
|
// ListDoodads returns a listing of all available doodads.
|
||||||
func ListDoodads() ([]string, error) {
|
func ListDoodads() ([]string, error) {
|
||||||
var names []string
|
var names []string
|
||||||
|
|
|
@ -38,6 +38,10 @@ func (r *Renderer) NewBitmap(filename string) (render.Texturer, error) {
|
||||||
}
|
}
|
||||||
defer surface.Free()
|
defer surface.Free()
|
||||||
|
|
||||||
|
// TODO: chroma key color hardcoded to white here
|
||||||
|
key := sdl.MapRGB(surface.Format, 255, 255, 255)
|
||||||
|
surface.SetColorKey(true, key)
|
||||||
|
|
||||||
tex, err := r.renderer.CreateTextureFromSurface(surface)
|
tex, err := r.renderer.CreateTextureFromSurface(surface)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("NewBitmap: create texture: %s", err)
|
return nil, fmt.Errorf("NewBitmap: create texture: %s", err)
|
||||||
|
|
28
ui/dragdrop.go
Normal file
28
ui/dragdrop.go
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
package ui
|
||||||
|
|
||||||
|
// DragDrop is a state machine to manage draggable UI components.
|
||||||
|
type DragDrop struct {
|
||||||
|
isDragging bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDragDrop initializes the DragDrop struct. Normally your Supervisor
|
||||||
|
// will manage the drag/drop object, but you can use your own if you don't
|
||||||
|
// use a Supervisor.
|
||||||
|
func NewDragDrop() *DragDrop {
|
||||||
|
return &DragDrop{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDragging returns whether the drag state is active.
|
||||||
|
func (dd *DragDrop) IsDragging() bool {
|
||||||
|
return dd.isDragging
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the drag state.
|
||||||
|
func (dd *DragDrop) Start() {
|
||||||
|
dd.isDragging = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop dragging.
|
||||||
|
func (dd *DragDrop) Stop() {
|
||||||
|
dd.isDragging = false
|
||||||
|
}
|
174
ui/supervisor.go
174
ui/supervisor.go
|
@ -1,6 +1,7 @@
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/events"
|
||||||
|
@ -21,6 +22,7 @@ const (
|
||||||
KeyDown
|
KeyDown
|
||||||
KeyUp
|
KeyUp
|
||||||
KeyPress
|
KeyPress
|
||||||
|
Drop
|
||||||
)
|
)
|
||||||
|
|
||||||
// Supervisor keeps track of widgets of interest to notify them about
|
// Supervisor keeps track of widgets of interest to notify them about
|
||||||
|
@ -28,22 +30,56 @@ const (
|
||||||
// vicinity.
|
// vicinity.
|
||||||
type Supervisor struct {
|
type Supervisor struct {
|
||||||
lock sync.RWMutex
|
lock sync.RWMutex
|
||||||
widgets []Widget
|
serial int // ID number of each widget added in order
|
||||||
hovering map[int]interface{}
|
widgets map[int]WidgetSlot // map of widget ID to WidgetSlot
|
||||||
clicked map[int]interface{}
|
hovering map[int]interface{} // map of widgets under the cursor
|
||||||
|
clicked map[int]interface{} // map of widgets being clicked
|
||||||
|
dd *DragDrop
|
||||||
|
}
|
||||||
|
|
||||||
|
// WidgetSlot holds a widget with a unique ID number in a sorted list.
|
||||||
|
type WidgetSlot struct {
|
||||||
|
id int
|
||||||
|
widget Widget
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSupervisor creates a supervisor.
|
// NewSupervisor creates a supervisor.
|
||||||
func NewSupervisor() *Supervisor {
|
func NewSupervisor() *Supervisor {
|
||||||
return &Supervisor{
|
return &Supervisor{
|
||||||
widgets: []Widget{},
|
widgets: map[int]WidgetSlot{},
|
||||||
hovering: map[int]interface{}{},
|
hovering: map[int]interface{}{},
|
||||||
clicked: map[int]interface{}{},
|
clicked: map[int]interface{}{},
|
||||||
|
dd: NewDragDrop(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DragStart sets the drag state.
|
||||||
|
func (s *Supervisor) DragStart() {
|
||||||
|
s.dd.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DragStop stops the drag state.
|
||||||
|
func (s *Supervisor) DragStop() {
|
||||||
|
s.dd.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsDragging returns whether the drag state is enabled.
|
||||||
|
func (s *Supervisor) IsDragging() bool {
|
||||||
|
return s.dd.IsDragging()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error messages that may be returned by Supervisor.Loop()
|
||||||
|
var (
|
||||||
|
// The caller should STOP forwarding any mouse or keyboard events to any
|
||||||
|
// other handles for the remainder of this tick.
|
||||||
|
ErrStopPropagation = errors.New("stop all event propagation")
|
||||||
|
)
|
||||||
|
|
||||||
// Loop to check events and pass them to managed widgets.
|
// Loop to check events and pass them to managed widgets.
|
||||||
func (s *Supervisor) Loop(ev *events.State) {
|
//
|
||||||
|
// Useful errors returned by this may be:
|
||||||
|
// - ErrStopPropagation
|
||||||
|
func (s *Supervisor) Loop(ev *events.State) error {
|
||||||
var (
|
var (
|
||||||
XY = render.Point{
|
XY = render.Point{
|
||||||
X: ev.CursorX.Now,
|
X: ev.CursorX.Now,
|
||||||
|
@ -52,14 +88,84 @@ func (s *Supervisor) Loop(ev *events.State) {
|
||||||
)
|
)
|
||||||
|
|
||||||
// See if we are hovering over any widgets.
|
// See if we are hovering over any widgets.
|
||||||
for id, w := range s.widgets {
|
hovering, outside := s.Hovering(XY)
|
||||||
|
|
||||||
|
// If we are dragging something around, do not trigger any mouse events
|
||||||
|
// to other widgets but DO notify any widget we dropped on top of!
|
||||||
|
if s.dd.IsDragging() {
|
||||||
|
if !ev.Button1.Now && !ev.Button2.Now {
|
||||||
|
// The mouse has been released. TODO: make mouse button important?
|
||||||
|
log.Info("Supervisor: STOP DRAGGING")
|
||||||
|
for _, child := range hovering {
|
||||||
|
child.widget.Event(Drop, XY)
|
||||||
|
}
|
||||||
|
s.DragStop()
|
||||||
|
}
|
||||||
|
return ErrStopPropagation
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, child := range hovering {
|
||||||
|
var (
|
||||||
|
id = child.id
|
||||||
|
w = child.widget
|
||||||
|
)
|
||||||
if w.Hidden() {
|
if w.Hidden() {
|
||||||
// TODO: somehow the Supervisor wasn't triggering hidden widgets
|
// TODO: somehow the Supervisor wasn't triggering hidden widgets
|
||||||
// anyway, but I don't know why. Adding this check for safety.
|
// anyway, but I don't know why. Adding this check for safety.
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cursor has intersected the widget.
|
||||||
|
if _, ok := s.hovering[id]; !ok {
|
||||||
|
w.Event(MouseOver, XY)
|
||||||
|
s.hovering[id] = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, isClicked := s.clicked[id]
|
||||||
|
if ev.Button1.Now {
|
||||||
|
if !isClicked {
|
||||||
|
w.Event(MouseDown, XY)
|
||||||
|
s.clicked[id] = nil
|
||||||
|
}
|
||||||
|
} else if isClicked {
|
||||||
|
w.Event(MouseUp, XY)
|
||||||
|
w.Event(Click, XY)
|
||||||
|
delete(s.clicked, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, child := range outside {
|
||||||
var (
|
var (
|
||||||
|
id = child.id
|
||||||
|
w = child.widget
|
||||||
|
)
|
||||||
|
|
||||||
|
// Cursor is not intersecting the widget.
|
||||||
|
if _, ok := s.hovering[id]; ok {
|
||||||
|
w.Event(MouseOut, XY)
|
||||||
|
delete(s.hovering, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := s.clicked[id]; ok {
|
||||||
|
w.Event(MouseUp, XY)
|
||||||
|
delete(s.clicked, id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hovering returns all of the widgets managed by Supervisor that are under
|
||||||
|
// the mouse cursor. Returns the set of widgets below the cursor and the set
|
||||||
|
// of widgets not below the cursor.
|
||||||
|
func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSlot) {
|
||||||
|
var XY = cursor // for shorthand
|
||||||
|
hovering = []WidgetSlot{}
|
||||||
|
outside = []WidgetSlot{}
|
||||||
|
|
||||||
|
// Check all the widgets under our care.
|
||||||
|
for child := range s.Widgets() {
|
||||||
|
var (
|
||||||
|
w = child.widget
|
||||||
P = w.Point()
|
P = w.Point()
|
||||||
S = w.Size()
|
S = w.Size()
|
||||||
P2 = render.Point{
|
P2 = render.Point{
|
||||||
|
@ -69,36 +175,29 @@ func (s *Supervisor) Loop(ev *events.State) {
|
||||||
)
|
)
|
||||||
|
|
||||||
if XY.X >= P.X && XY.X <= P2.X && XY.Y >= P.Y && XY.Y <= P2.Y {
|
if XY.X >= P.X && XY.X <= P2.X && XY.Y >= P.Y && XY.Y <= P2.Y {
|
||||||
// Cursor has intersected the widget.
|
// Cursor intersects the widget.
|
||||||
if _, ok := s.hovering[id]; !ok {
|
hovering = append(hovering, child)
|
||||||
w.Event(MouseOver, XY)
|
|
||||||
s.hovering[id] = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, isClicked := s.clicked[id]
|
|
||||||
if ev.Button1.Now {
|
|
||||||
if !isClicked {
|
|
||||||
w.Event(MouseDown, XY)
|
|
||||||
s.clicked[id] = nil
|
|
||||||
}
|
|
||||||
} else if isClicked {
|
|
||||||
w.Event(MouseUp, XY)
|
|
||||||
w.Event(Click, XY)
|
|
||||||
delete(s.clicked, id)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Cursor is not intersecting the widget.
|
outside = append(outside, child)
|
||||||
if _, ok := s.hovering[id]; ok {
|
|
||||||
w.Event(MouseOut, XY)
|
|
||||||
delete(s.hovering, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := s.clicked[id]; ok {
|
|
||||||
w.Event(MouseUp, XY)
|
|
||||||
delete(s.clicked, id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return hovering, outside
|
||||||
|
}
|
||||||
|
|
||||||
|
// Widgets returns a channel of widgets managed by the supervisor in the order
|
||||||
|
// they were added.
|
||||||
|
func (s *Supervisor) Widgets() <-chan WidgetSlot {
|
||||||
|
pipe := make(chan WidgetSlot)
|
||||||
|
go func() {
|
||||||
|
for i := 0; i < s.serial; i++ {
|
||||||
|
if w, ok := s.widgets[i]; ok {
|
||||||
|
pipe <- w
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(pipe)
|
||||||
|
}()
|
||||||
|
return pipe
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present all widgets managed by the supervisor.
|
// Present all widgets managed by the supervisor.
|
||||||
|
@ -106,7 +205,8 @@ func (s *Supervisor) Present(e render.Engine) {
|
||||||
s.lock.RLock()
|
s.lock.RLock()
|
||||||
defer s.lock.RUnlock()
|
defer s.lock.RUnlock()
|
||||||
|
|
||||||
for _, w := range s.widgets {
|
for child := range s.Widgets() {
|
||||||
|
var w = child.widget
|
||||||
w.Present(e, w.Point())
|
w.Present(e, w.Point())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -114,6 +214,10 @@ func (s *Supervisor) Present(e render.Engine) {
|
||||||
// Add a widget to be supervised.
|
// Add a widget to be supervised.
|
||||||
func (s *Supervisor) Add(w Widget) {
|
func (s *Supervisor) Add(w Widget) {
|
||||||
s.lock.Lock()
|
s.lock.Lock()
|
||||||
s.widgets = append(s.widgets, w)
|
s.widgets[s.serial] = WidgetSlot{
|
||||||
|
id: s.serial,
|
||||||
|
widget: w,
|
||||||
|
}
|
||||||
|
s.serial++
|
||||||
s.lock.Unlock()
|
s.lock.Unlock()
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,9 +18,16 @@ type Canvas struct {
|
||||||
ui.Frame
|
ui.Frame
|
||||||
Palette *level.Palette
|
Palette *level.Palette
|
||||||
|
|
||||||
// Set to true to allow clicking to edit this canvas.
|
// Editable and Scrollable go hand in hand and, if you initialize a
|
||||||
Editable bool
|
// NewCanvas() with editable=true, they are both enabled.
|
||||||
Scrollable bool
|
Editable bool // Clicking will edit pixels of this canvas.
|
||||||
|
Scrollable bool // Cursor keys will scroll the viewport of this canvas.
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
// to remove the mask.
|
||||||
|
MaskColor render.Color
|
||||||
|
|
||||||
// Underlying chunk data for the drawing.
|
// Underlying chunk data for the drawing.
|
||||||
chunks *level.Chunker
|
chunks *level.Chunker
|
||||||
|
@ -111,6 +118,9 @@ func (w *Canvas) InstallActors(actors level.ActorMap) error {
|
||||||
can := NewCanvas(int(size), false)
|
can := NewCanvas(int(size), false)
|
||||||
can.Name = id
|
can.Name = id
|
||||||
can.actor = actor
|
can.actor = actor
|
||||||
|
// TODO: if the Background is render.Invisible it gets defaulted to
|
||||||
|
// White somewhere and the Doodad masks the level drawing behind it.
|
||||||
|
can.SetBackground(render.RGBA(0, 0, 1, 0))
|
||||||
can.LoadDoodad(doodad)
|
can.LoadDoodad(doodad)
|
||||||
can.Resize(render.NewRect(size, size))
|
can.Resize(render.NewRect(size, size))
|
||||||
w.actors = append(w.actors, &Actor{
|
w.actors = append(w.actors, &Actor{
|
||||||
|
@ -128,8 +138,6 @@ func (w *Canvas) SetSwatch(s *level.Swatch) {
|
||||||
|
|
||||||
// setup common configs between both initializers of the canvas.
|
// setup common configs between both initializers of the canvas.
|
||||||
func (w *Canvas) setup() {
|
func (w *Canvas) setup() {
|
||||||
w.SetBackground(render.White)
|
|
||||||
|
|
||||||
// XXX: Debug code.
|
// XXX: Debug code.
|
||||||
if balance.DebugCanvasBorder != render.Invisible {
|
if balance.DebugCanvasBorder != render.Invisible {
|
||||||
w.Configure(ui.Config{
|
w.Configure(ui.Config{
|
||||||
|
@ -138,13 +146,6 @@ func (w *Canvas) setup() {
|
||||||
BorderStyle: ui.BorderSolid,
|
BorderStyle: ui.BorderSolid,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
// Loop is called on the scene's event loop to handle mouse interaction with
|
||||||
|
@ -258,6 +259,18 @@ func (w *Canvas) ViewportRelative() render.Rect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WorldIndexAt returns the World Index that corresponds to a Screen Pixel
|
||||||
|
// on the screen. If the screen pixel is the mouse coordinate (relative to
|
||||||
|
// the application window) this will return the World Index of the pixel below
|
||||||
|
// the mouse cursor.
|
||||||
|
func (w *Canvas) WorldIndexAt(screenPixel render.Point) render.Point {
|
||||||
|
var P = ui.AbsolutePosition(w)
|
||||||
|
return render.Point{
|
||||||
|
X: screenPixel.X - P.X - w.Scroll.X,
|
||||||
|
Y: screenPixel.Y - P.Y - w.Scroll.Y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Chunker returns the underlying Chunker object.
|
// Chunker returns the underlying Chunker object.
|
||||||
func (w *Canvas) Chunker() *level.Chunker {
|
func (w *Canvas) Chunker() *level.Chunker {
|
||||||
return w.chunks
|
return w.chunks
|
||||||
|
@ -297,7 +310,12 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||||
// Get the chunks in the viewport and cache their textures.
|
// Get the chunks in the viewport and cache their textures.
|
||||||
for coord := range w.chunks.IterViewportChunks(Viewport) {
|
for coord := range w.chunks.IterViewportChunks(Viewport) {
|
||||||
if chunk, ok := w.chunks.GetChunk(coord); ok {
|
if chunk, ok := w.chunks.GetChunk(coord); ok {
|
||||||
tex := chunk.Texture(e, w.Name+coord.String())
|
var tex render.Texturer
|
||||||
|
if w.MaskColor != render.Invisible {
|
||||||
|
tex = chunk.TextureMasked(e, w.MaskColor)
|
||||||
|
} else {
|
||||||
|
tex = chunk.Texture(e)
|
||||||
|
}
|
||||||
src := render.Rect{
|
src := render.Rect{
|
||||||
W: tex.Size().W,
|
W: tex.Size().W,
|
||||||
H: tex.Size().H,
|
H: tex.Size().H,
|
||||||
|
|
Loading…
Reference in New Issue
Block a user