Palette Editor and Doodad Dropper Windows

* Start the program window maximized with the `-w maximized` CLI option.
* Move the Doodad Palette off the right-side dock of the Editor Scene and
  into its own pop-up window: the DoodadDropper.
* Shrink the width of the Color Palette panel and show only the colors in
  the buttons. The name of the swatch is available in the mouse-over tooltip.
* Added an "Edit" button to the Color Palette. It opens a Palette Editor
  window where you can rename, change colors and attributes of existing colors
  OR insert new colors into your palette. (Deleting colors not yet supported).
* level.Chunker gets a Redraw method: invalidates all cached textures of all
  chunks forcing the level to redraw itself, possibly with an updated palette.
This commit is contained in:
Noah 2020-07-09 19:38:37 -07:00
parent 5f75168235
commit 47cca8c7c6
16 changed files with 838 additions and 336 deletions

View File

@ -3,7 +3,6 @@ package main
import ( import (
"errors" "errors"
"fmt" "fmt"
"log"
"os" "os"
"regexp" "regexp"
"runtime" "runtime"
@ -15,6 +14,7 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/bindata" "git.kirsle.net/apps/doodle/pkg/bindata"
"git.kirsle.net/apps/doodle/pkg/branding" "git.kirsle.net/apps/doodle/pkg/branding"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/sound" "git.kirsle.net/apps/doodle/pkg/sound"
"git.kirsle.net/go/render/sdl" "git.kirsle.net/go/render/sdl"
"github.com/urfave/cli" "github.com/urfave/cli"
@ -71,7 +71,7 @@ func main() {
&cli.StringFlag{ &cli.StringFlag{
Name: "window", Name: "window",
Aliases: []string{"w"}, Aliases: []string{"w"},
Usage: "set the window size (e.g. -w 1024x768) or special value: desktop, mobile, landscape", Usage: "set the window size (e.g. -w 1024x768) or special value: desktop, mobile, landscape, maximized",
}, },
&cli.BoolFlag{ &cli.BoolFlag{
Name: "guitest", Name: "guitest",
@ -127,6 +127,13 @@ func main() {
game.PlayLevel(filename) game.PlayLevel(filename)
} }
} }
// Maximizing the window? with `-w maximized`
if c.String("window") == "maximized" {
log.Info("Maximize main window")
engine.Maximize()
}
game.Run() game.Run()
return nil return nil
} }
@ -136,13 +143,13 @@ func main() {
err := app.Run(os.Args) err := app.Run(os.Args)
if err != nil { if err != nil {
log.Fatal(err) log.Error(err.Error())
} }
} }
func setResolution(value string) error { func setResolution(value string) error {
switch value { switch value {
case "desktop": case "desktop", "maximized":
return nil return nil
case "mobile": case "mobile":
balance.Width = 375 balance.Width = 375

View File

@ -23,7 +23,7 @@ var (
} }
TitleFont = render.Text{ TitleFont = render.Text{
FontFilename: "DejaVuSans-Bold.ttf", FontFilename: "DejaVuSans-Bold.ttf",
Size: 12, Size: 9,
Padding: 4, Padding: 4,
Color: render.White, Color: render.White,
Stroke: render.Red, Stroke: render.Red,
@ -37,6 +37,11 @@ var (
Size: 12, Size: 12,
PadX: 4, PadX: 4,
} }
MenuFontBold = render.Text{
FontFilename: "DejaVuSans-Bold.ttf",
Size: 12,
PadX: 4,
}
// StatusFont is the font for the status bar. // StatusFont is the font for the status bar.
StatusFont = render.Text{ StatusFont = render.Text{
@ -83,4 +88,10 @@ var (
Color: render.RGBA(255, 255, 0, 255), Color: render.RGBA(255, 255, 0, 255),
Stroke: render.RGBA(100, 100, 0, 255), Stroke: render.RGBA(100, 100, 0, 255),
} }
// Doodad Dropper Window settings.
DoodadButtonBackground = render.RGBA(255, 255, 200, 255)
DoodadButtonSize = 64
DoodadDropperCols = 6 // rows/columns of buttons
DoodadDropperRows = 3
) )

View File

@ -61,6 +61,7 @@ func New(debug bool, engine render.Engine) *Doodle {
// Make the render engine globally available. TODO: for wasm/ToBitmap // Make the render engine globally available. TODO: for wasm/ToBitmap
shmem.CurrentRenderEngine = engine shmem.CurrentRenderEngine = engine
shmem.Flash = d.Flash shmem.Flash = d.Flash
shmem.Prompt = d.Prompt
if debug { if debug {
log.Logger.Config.Level = golog.DebugLevel log.Logger.Config.Level = golog.DebugLevel

View File

@ -20,9 +20,6 @@ import (
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
) )
// Width of the panel frame.
var paletteWidth = 160
// EditorUI manages the user interface for the Editor Scene. // EditorUI manages the user interface for the Editor Scene.
type EditorUI struct { type EditorUI struct {
d *Doodle d *Doodle
@ -50,6 +47,8 @@ type EditorUI struct {
// Popup windows. // Popup windows.
levelSettingsWindow *ui.Window levelSettingsWindow *ui.Window
aboutWindow *ui.Window aboutWindow *ui.Window
doodadWindow *ui.Window
paletteEditor *ui.Window
// Palette window. // Palette window.
Palette *ui.Window Palette *ui.Window
@ -107,6 +106,9 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
u.ToolBar = u.SetupToolbar(d) u.ToolBar = u.SetupToolbar(d)
u.Workspace = u.SetupWorkspace(d) // important that this is last! u.Workspace = u.SetupWorkspace(d) // important that this is last!
// Preload pop-up windows before they're needed.
u.SetupPopups(d)
log.Error("menu size: %s", u.MenuBar.Rect()) log.Error("menu size: %s", u.MenuBar.Rect())
u.screen.Pack(u.MenuBar, ui.Pack{ u.screen.Pack(u.MenuBar, ui.Pack{
Side: ui.N, Side: ui.N,
@ -175,8 +177,6 @@ func (u *EditorUI) Resized(d *Doodle) {
menuHeight, menuHeight,
)) ))
u.Palette.Compute(d.Engine) u.Palette.Compute(d.Engine)
u.scrollDoodadFrame(0)
} }
var innerHeight = u.d.height - menuHeight - u.StatusBar.Size().H var innerHeight = u.d.height - menuHeight - u.StatusBar.Size().H
@ -312,6 +312,9 @@ func (u *EditorUI) Present(e render.Engine) {
u.screen.Present(e, render.Origin) u.screen.Present(e, render.Origin)
// Draw any windows being managed by Supervisor.
u.Supervisor.Present(e)
// Are we dragging a Doodad canvas? // Are we dragging a Doodad canvas?
if u.Supervisor.IsDragging() { if u.Supervisor.IsDragging() {
if actor := u.DraggableActor; actor != nil { if actor := u.DraggableActor; actor != nil {
@ -322,9 +325,6 @@ func (u *EditorUI) Present(e render.Engine) {
)) ))
} }
} }
// Draw any windows being managed by Supervisor.
u.Supervisor.Present(e)
} }
// SetupWorkspace configures the main Workspace frame that takes up the full // SetupWorkspace configures the main Workspace frame that takes up the full
@ -359,6 +359,7 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
// mode when you click an existing Doodad and it "pops" out of the canvas // mode when you click an existing Doodad and it "pops" out of the canvas
// and onto the cursor to be repositioned. // and onto the cursor to be repositioned.
drawing.OnDragStart = func(actor *level.Actor) { drawing.OnDragStart = func(actor *level.Actor) {
log.Warn("drawing.OnDragStart: grab actor %s", actor)
u.startDragActor(nil, actor) u.startDragActor(nil, actor)
} }
@ -384,11 +385,22 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
// Was it an actor from the Doodad Palette? // Was it an actor from the Doodad Palette?
if actor := u.DraggableActor; actor != nil { if actor := u.DraggableActor; actor != nil {
log.Info("Actor is a %s", actor.doodad.Filename) log.Info("Actor is a %s", actor.doodad.Filename)
// The actor has been dropped so null it out.
defer func() {
u.DraggableActor = nil
}()
if u.Scene.Level == nil { if u.Scene.Level == nil {
u.d.Flash("Can't drop doodads onto doodad drawings!") u.d.Flash("Can't drop doodads onto doodad drawings!")
return nil return nil
} }
// If they dropped it onto a UI window, ignore it.
if u.Supervisor.IsPointInWindow(ed.Point) {
return nil
}
var ( var (
// Uncenter the drawing from the cursor. // Uncenter the drawing from the cursor.
size = actor.canvas.Size() size = actor.canvas.Size()
@ -530,38 +542,13 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
}) })
editMenu.AddSeparator() editMenu.AddSeparator()
editMenu.AddItem("Level options", func() { editMenu.AddItem("Level options", func() {
scene, _ := d.Scene.(*EditorScene)
log.Info("Opening the window") log.Info("Opening the window")
// Open the New Level window in edit-settings mode. // Open the New Level window in edit-settings mode.
if u.levelSettingsWindow == nil {
u.levelSettingsWindow = windows.NewAddEditLevel(windows.AddEditLevel{
Supervisor: u.Supervisor,
Engine: d.Engine,
EditLevel: scene.Level,
OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) {
log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper)
scene.Level.PageType = pageType
scene.Level.Wallpaper = wallpaper
u.Canvas.LoadLevel(d.Engine, scene.Level)
},
OnCancel: func() {
u.levelSettingsWindow.Hide() u.levelSettingsWindow.Hide()
}, u.levelSettingsWindow = nil
}) u.SetupPopups(u.d)
u.levelSettingsWindow.Compute(d.Engine)
u.levelSettingsWindow.Supervise(u.Supervisor)
// Center the window.
u.levelSettingsWindow.MoveTo(render.Point{
X: (d.width / 2) - (u.levelSettingsWindow.Size().W / 2),
Y: 60,
})
} else {
u.levelSettingsWindow.Show() u.levelSettingsWindow.Show()
}
}) })
//////// ////////
@ -585,6 +572,10 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
toolMenu.AddItemAccel("Command shell", "Enter", func() { toolMenu.AddItemAccel("Command shell", "Enter", func() {
d.shell.Open = true d.shell.Open = true
}) })
toolMenu.AddItemAccel("Doodads", "d", func() {
log.Info("Open the DoodadDropper")
u.doodadWindow.Show()
})
//////// ////////
// Help menu // Help menu
@ -618,6 +609,96 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
return menu return menu
} }
// SetupPopups preloads popup windows like the DoodadDropper.
func (u *EditorUI) SetupPopups(d *Doodle) {
// Common window configure function.
var configure = func(window *ui.Window) {
var size = window.Size()
window.Compute(d.Engine)
window.Supervise(u.Supervisor)
// Center the window.
window.MoveTo(render.Point{
X: (d.width / 2) - (size.W / 2),
Y: (d.height / 2) - (size.H / 2),
})
}
// Doodad Dropper.
if u.doodadWindow == nil {
u.doodadWindow = windows.NewDoodadDropper(windows.DoodadDropper{
Supervisor: u.Supervisor,
Engine: d.Engine,
OnStartDragActor: u.startDragActor,
OnCancel: func() {
u.doodadWindow.Hide()
},
})
configure(u.doodadWindow)
}
// Page Settings
if u.levelSettingsWindow == nil {
scene, _ := d.Scene.(*EditorScene)
u.levelSettingsWindow = windows.NewAddEditLevel(windows.AddEditLevel{
Supervisor: u.Supervisor,
Engine: d.Engine,
EditLevel: scene.Level,
OnChangePageTypeAndWallpaper: func(pageType level.PageType, wallpaper string) {
log.Info("OnChangePageTypeAndWallpaper called: %+v, %+v", pageType, wallpaper)
scene.Level.PageType = pageType
scene.Level.Wallpaper = wallpaper
u.Canvas.LoadLevel(d.Engine, scene.Level)
},
OnCancel: func() {
u.levelSettingsWindow.Hide()
},
})
configure(u.levelSettingsWindow)
}
// Palette Editor.
if u.paletteEditor == nil {
scene, _ := d.Scene.(*EditorScene)
u.paletteEditor = windows.NewPaletteEditor(windows.PaletteEditor{
Supervisor: u.Supervisor,
Engine: d.Engine,
EditLevel: scene.Level,
OnChange: func() {
// Reload the level.
log.Warn("RELOAD LEVEL")
u.Canvas.LoadLevel(d.Engine, scene.Level)
scene.Level.Chunker.Redraw()
// Reload the palette frame to reflect the changed data.
u.Palette.Hide()
u.Palette = u.SetupPalette(d)
u.Resized(d)
},
OnAddColor: func() {
// Adding a new color to the palette.
sw := scene.Level.Palette.AddSwatch()
log.Info("Added new palette color: %+v", sw)
// Awkward but... reload this very same window.
u.paletteEditor.Hide()
u.paletteEditor = nil
u.SetupPopups(d)
u.paletteEditor.Show()
},
OnCancel: func() {
u.paletteEditor.Hide()
},
})
configure(u.paletteEditor)
}
}
// SetupStatusBar sets up the status bar widget along the bottom of the window. // SetupStatusBar sets up the status bar widget along the bottom of the window.
func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame { func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
frame := ui.NewFrame("Status Bar") frame := ui.NewFrame("Status Bar")

View File

@ -5,15 +5,12 @@ package doodle
// refactor into a subpackage if EditorUI itself can ever be decoupled. // refactor into a subpackage if EditorUI itself can ever be decoupled.
import ( import (
"fmt"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/doodads" "git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/uix" "git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
) )
// DraggableActor is a Doodad being dragged from the Doodad palette. // DraggableActor is a Doodad being dragged from the Doodad palette.
@ -54,225 +51,3 @@ func (u *EditorUI) startDragActor(doodad *doodads.Doodad, actor *level.Actor) {
actor: actor, actor: actor,
} }
} }
// 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
)
// Pager buttons on top of the doodad list.
pager := ui.NewFrame("Doodad Pager")
pager.SetBackground(render.RGBA(255, 0, 0, 20)) // TODO: if I don't set a background color,
// this frame will light up the same color as the Link button on mouse
// over. somewhere some memory might be shared between the recent widgets
{
leftBtn := ui.NewButton("Prev Page", ui.NewLabel(ui.Label{
Text: "<",
Font: balance.MenuFont,
}))
leftBtn.Handle(ui.Click, func(ed ui.EventData) error {
u.scrollDoodadFrame(-1)
return nil
})
u.Supervisor.Add(leftBtn)
pager.Pack(leftBtn, ui.Pack{
Side: ui.W,
})
scroller := ui.NewFrame("Doodad Scroll Progressbar")
scroller.Configure(ui.Config{
Width: 20,
Height: 20,
Background: render.RGBA(128, 128, 128, 128),
})
pager.Pack(scroller, ui.Pack{
Side: ui.W,
})
u.doodadScroller = scroller
rightBtn := ui.NewButton("Next Page", ui.NewLabel(ui.Label{
Text: ">",
Font: balance.MenuFont,
}))
rightBtn.Handle(ui.Click, func(ed ui.EventData) error {
u.scrollDoodadFrame(1)
return nil
})
u.Supervisor.Add(rightBtn)
pager.Pack(rightBtn, ui.Pack{
Side: ui.E,
})
}
u.doodadPager = pager
frame.Pack(pager, ui.Pack{
Side: ui.N,
Fill: true,
PadY: 5,
})
doodadsAvailable, err := doodads.ListDoodads()
if err != nil {
return frame, fmt.Errorf(
"setupDoodadFrame: doodads.ListDoodads: %s",
err,
)
}
var buttonSize = (paletteWidth - window.BoxThickness(2)) / perRow
u.doodadButtonSize = buttonSize
// Load all the doodads, skip hidden ones.
var items []*doodads.Doodad
for _, filename := range doodadsAvailable {
doodad, err := doodads.LoadFile(filename)
if err != nil {
log.Error(err.Error())
doodad = doodads.New(balance.DoodadSize)
}
// Skip hidden doodads.
if doodad.Hidden && !balance.ShowHiddenDoodads {
log.Info("skip %s: hidden doodad", filename)
continue
}
doodad.Filename = filename
items = append(items, doodad)
}
// Draw the doodad buttons in a grid `perRow` buttons wide.
var (
row *ui.Frame
rowCount int // for labeling the ui.Frame for each row
btnRows = []*ui.Frame{} // Collect the row frames for the buttons.
)
for i, doodad := range items {
doodad := doodad
if row == nil || i%perRow == 0 {
rowCount++
row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount))
row.SetBackground(balance.WindowBackground)
btnRows = append(btnRows, row)
frame.Pack(row, ui.Pack{
Side: ui.N,
Fill: true,
})
}
can := uix.NewCanvas(int(buttonSize), true)
can.Name = doodad.Title
can.SetBackground(render.White)
can.LoadDoodad(doodad)
btn := ui.NewButton(doodad.Title, 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{
Side: ui.W,
})
// Tooltip hover to show the doodad's name.
ui.NewTooltip(btn, ui.Tooltip{
Text: doodad.Title,
Edge: ui.Top,
})
// 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(ed ui.EventData) error {
log.Warn("MouseDown on doodad %s (%s)", doodad.Filename, doodad.Title)
u.startDragActor(doodad, nil)
return nil
})
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)
}
u.doodadRows = btnRows
u.scrollDoodadFrame(0)
return frame, nil
}
// scrollDoodadFrame handles the Page Up/Down buttons to adjust the number of
// Doodads visible on screen.
//
// rows is the number of rows to scroll. Positive values mean scroll *down*
// the list.
func (u *EditorUI) scrollDoodadFrame(rows int) {
u.doodadSkip += rows
if u.doodadSkip < 0 {
u.doodadSkip = 0
}
// Calculate about how many rows we can see given our current window size.
var (
maxVisibleHeight = u.d.height - 86
calculatedHeight int
rowsBefore int // count of rows hidden before
rowsVisible int
rowsAfter int // count of rows hidden after
rowsEstimated = maxVisibleHeight / u.doodadButtonSize // estimated number rows shown
maxSkip = ((len(u.doodadRows) * int(u.doodadButtonSize)) - int(u.doodadButtonSize*rowsEstimated)) / int(u.doodadButtonSize)
)
if maxSkip < 0 {
maxSkip = 0
}
if u.doodadSkip > maxSkip {
u.doodadSkip = maxSkip
}
// If the window is big enough to encompass all the doodads, don't show
// the pager toolbar, its just confusing.
if maxSkip == 0 {
u.doodadPager.Hide()
} else {
u.doodadPager.Show()
}
// Comb through the doodads and show/hide the relevant buttons.
for i, row := range u.doodadRows {
if i < u.doodadSkip {
row.Hide()
rowsBefore++
continue
}
calculatedHeight += u.doodadButtonSize
if calculatedHeight > maxVisibleHeight {
row.Hide()
rowsAfter++
} else {
row.Show()
rowsVisible++
}
}
var viewPercent = float64(rowsBefore+rowsVisible) / float64(len(u.doodadRows))
u.doodadScroller.Configure(ui.Config{
Width: int(float64(paletteWidth-50) * viewPercent), // TODO: hacky magic number
})
}

View File

@ -8,34 +8,20 @@ import (
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
) )
// Width of the panel frame.
var paletteWidth = 50
// SetupPalette sets up the palette panel. // SetupPalette sets up the palette panel.
func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window { func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
window := ui.NewWindow("Palette") window := ui.NewWindow("Palette")
window.ConfigureTitle(balance.TitleConfig) window.ConfigureTitle(balance.TitleConfig)
// window.TitleBar().Font = balance.TitleFont _, label := window.TitleBar()
label.Font = balance.TitleFont
window.Configure(ui.Config{ window.Configure(ui.Config{
Background: balance.WindowBackground, Background: balance.WindowBackground,
BorderColor: balance.WindowBorder, BorderColor: balance.WindowBorder,
}) })
// Doodad frame.
{
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()
window.Pack(u.DoodadTab, ui.Pack{
Side: ui.N,
Fill: true,
})
}
// Color Palette Frame. // Color Palette Frame.
u.PaletteTab = u.setupPaletteFrame(window) u.PaletteTab = u.setupPaletteFrame(window)
window.Pack(u.PaletteTab, ui.Pack{ window.Pack(u.PaletteTab, ui.Pack{
@ -65,30 +51,16 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame {
return nil return nil
} }
var buttonSize = 32
// Draw the radio buttons for the palette. // Draw the radio buttons for the palette.
if u.Canvas != nil && u.Canvas.Palette != nil { if u.Canvas != nil && u.Canvas.Palette != nil {
for _, swatch := range u.Canvas.Palette.Swatches { for _, swatch := range u.Canvas.Palette.Swatches {
swFrame := ui.NewFrame(fmt.Sprintf("Swatch(%s) Button Frame", swatch.Name)) swFrame := ui.NewFrame(fmt.Sprintf("Swatch(%s) Button Frame", swatch.Name))
swFrame.Configure(ui.Config{
colorFrame := ui.NewFrame(fmt.Sprintf("Swatch(%s) Color Box", swatch.Name)) Width: buttonSize,
colorFrame.Configure(ui.Config{ Height: buttonSize,
Width: 16,
Height: 16,
Background: swatch.Color, Background: swatch.Color,
BorderSize: 1,
BorderStyle: ui.BorderSunken,
})
swFrame.Pack(colorFrame, ui.Pack{
Side: ui.W,
})
label := ui.NewLabel(ui.Label{
Text: swatch.Name,
Font: balance.StatusFont,
})
label.Font.Color = swatch.Color.Darken(128)
swFrame.Pack(label, ui.Pack{
Side: ui.W,
}) })
btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, swFrame) btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, swFrame)
@ -97,18 +69,11 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame {
// Add a tooltip showing the swatch attributes. // Add a tooltip showing the swatch attributes.
ui.NewTooltip(btn, ui.Tooltip{ ui.NewTooltip(btn, ui.Tooltip{
Text: "Attributes: " + swatch.Attributes(), Text: fmt.Sprintf("Name: %s\nAttributes: %s", swatch.Name, swatch.Attributes()),
Edge: ui.Left, Edge: ui.Left,
}) })
btn.Compute(u.d.Engine) btn.Compute(u.d.Engine)
swFrame.Configure(ui.Config{
Height: label.Size().H,
// TODO: magic number, trying to left-align
// the label by making the frame as wide as possible.
Width: paletteWidth - 16,
})
frame.Pack(btn, ui.Pack{ frame.Pack(btn, ui.Pack{
Side: ui.N, Side: ui.N,
@ -118,5 +83,27 @@ func (u *EditorUI) setupPaletteFrame(window *ui.Window) *ui.Frame {
} }
} }
// Draw the Edit Palette button.
btn := ui.NewButton("Edit Palette", ui.NewLabel(ui.Label{
Text: "Edit",
Font: balance.MenuFont,
}))
btn.Handle(ui.Click, func(ed ui.EventData) error {
// TODO: recompute the window so the actual loaded level palette gets in
u.paletteEditor.Hide()
u.paletteEditor = nil
u.SetupPopups(u.d)
u.paletteEditor.Show()
return nil
})
u.Supervisor.Add(btn)
btn.Compute(u.d.Engine)
frame.Pack(btn, ui.Pack{
Side: ui.N,
Fill: true,
PadY: 4,
})
return frame return frame
} }

View File

@ -27,18 +27,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Side: ui.N, Side: ui.N,
}) })
// Helper functions to toggle the correct palette panel.
var (
showSwatchPalette = func() {
u.DoodadTab.Hide()
u.PaletteTab.Show()
}
showDoodadPalette = func() {
u.PaletteTab.Hide()
u.DoodadTab.Show()
}
)
// Buttons. // Buttons.
var buttons = []struct { var buttons = []struct {
Value string Value string
@ -52,7 +40,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Tooltip: "Pencil Tool", Tooltip: "Pencil Tool",
Click: func() { Click: func() {
u.Canvas.Tool = drawtool.PencilTool u.Canvas.Tool = drawtool.PencilTool
showSwatchPalette()
d.Flash("Pencil Tool selected.") d.Flash("Pencil Tool selected.")
}, },
}, },
@ -63,7 +50,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Tooltip: "Line Tool", Tooltip: "Line Tool",
Click: func() { Click: func() {
u.Canvas.Tool = drawtool.LineTool u.Canvas.Tool = drawtool.LineTool
showSwatchPalette()
d.Flash("Line Tool selected.") d.Flash("Line Tool selected.")
}, },
}, },
@ -74,7 +60,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Tooltip: "Rectangle Tool", Tooltip: "Rectangle Tool",
Click: func() { Click: func() {
u.Canvas.Tool = drawtool.RectTool u.Canvas.Tool = drawtool.RectTool
showSwatchPalette()
d.Flash("Rectangle Tool selected.") d.Flash("Rectangle Tool selected.")
}, },
}, },
@ -85,7 +70,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Tooltip: "Ellipse Tool", Tooltip: "Ellipse Tool",
Click: func() { Click: func() {
u.Canvas.Tool = drawtool.EllipseTool u.Canvas.Tool = drawtool.EllipseTool
showSwatchPalette()
d.Flash("Ellipse Tool selected.") d.Flash("Ellipse Tool selected.")
}, },
}, },
@ -96,7 +80,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Tooltip: "Doodad Tool\nDrag-and-drop objects into your map", Tooltip: "Doodad Tool\nDrag-and-drop objects into your map",
Click: func() { Click: func() {
u.Canvas.Tool = drawtool.ActorTool u.Canvas.Tool = drawtool.ActorTool
showDoodadPalette() u.doodadWindow.Show()
d.Flash("Actor Tool selected. Drag a Doodad from the drawer into your level.") d.Flash("Actor Tool selected. Drag a Doodad from the drawer into your level.")
}, },
}, },
@ -107,7 +91,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Tooltip: "Link Tool\nConnect doodads to each other", Tooltip: "Link Tool\nConnect doodads to each other",
Click: func() { Click: func() {
u.Canvas.Tool = drawtool.LinkTool u.Canvas.Tool = drawtool.LinkTool
showDoodadPalette() u.doodadWindow.Show()
d.Flash("Link Tool selected. Click a doodad in your level to link it to another.") d.Flash("Link Tool selected. Click a doodad in your level to link it to another.")
}, },
}, },
@ -126,7 +110,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
u.Canvas.BrushSize = balance.MaxEraserBrushSize u.Canvas.BrushSize = balance.MaxEraserBrushSize
} }
showSwatchPalette()
d.Flash("Eraser Tool selected.") d.Flash("Eraser Tool selected.")
}, },
}, },

View File

@ -104,6 +104,12 @@ func (c *Chunk) TextureMasked(e render.Engine, mask render.Color) render.Texture
return c.textureMasked return c.textureMasked
} }
// SetDirty sets the `dirty` flag to true and forces the texture to be
// re-computed next frame.
func (c *Chunk) SetDirty() {
c.dirty = true
}
// toBitmap puts the texture in a well named bitmap path in the cache folder. // toBitmap puts the texture in a well named bitmap path in the cache folder.
func (c *Chunk) toBitmap(mask render.Color) (render.Texturer, error) { func (c *Chunk) toBitmap(mask render.Color) (render.Texturer, error) {
// Generate a unique name for this chunk cache. // Generate a unique name for this chunk cache.

View File

@ -180,6 +180,14 @@ func (c *Chunker) GetChunk(p render.Point) (*Chunk, bool) {
return chunk, ok return chunk, ok
} }
// Redraw marks every chunk as dirty and invalidates all their texture caches,
// forcing the drawing to re-generate from scratch.
func (c *Chunker) Redraw() {
for _, chunk := range c.Chunks {
chunk.SetDirty()
}
}
// Get a pixel at the given coordinate. Returns the Palette entry for that // Get a pixel at the given coordinate. Returns the Palette entry for that
// pixel or else returns an error if not found. // pixel or else returns an error if not found.
func (c *Chunker) Get(p render.Point) (*Swatch, error) { func (c *Chunker) Get(p render.Point) (*Swatch, error) {

View File

@ -1,6 +1,10 @@
package level package level
import "git.kirsle.net/go/render" import (
"fmt"
"git.kirsle.net/go/render"
)
// DefaultPalette returns a sensible default palette. // DefaultPalette returns a sensible default palette.
func DefaultPalette() *Palette { func DefaultPalette() *Palette {
@ -85,6 +89,25 @@ func (p *Palette) Inflate() {
p.update() p.update()
} }
// AddSwatch adds a new swatch to the palette.
func (p *Palette) AddSwatch() *Swatch {
p.update()
var (
index = len(p.Swatches)
name = fmt.Sprintf("color %d", len(p.Swatches)+1)
)
p.Swatches = append(p.Swatches, &Swatch{
Name: name,
Color: render.Magenta,
index: index,
})
p.byName[name] = index
return p.Swatches[index]
}
// Get a swatch by name. // Get a swatch by name.
func (p *Palette) Get(name string) (result *Swatch, exists bool) { func (p *Palette) Get(name string) (result *Swatch, exists bool) {
p.update() p.update()

View File

@ -146,6 +146,8 @@ func (s *MenuScene) setupNewWindow(d *Doodle) error {
}, },
}) })
s.window = window s.window = window
window.SetButtons(0)
window.Show()
return nil return nil
} }

View File

@ -22,6 +22,9 @@ var (
// Globally available Flash() function so we can emit text to the Doodle UI. // Globally available Flash() function so we can emit text to the Doodle UI.
Flash func(string, ...interface{}) Flash func(string, ...interface{})
// Globally available Prompt() function.
Prompt func(string, func(string))
// Ajax file cache for WASM use. // Ajax file cache for WASM use.
AjaxCache map[string][]byte AjaxCache map[string][]byte
) )

View File

@ -351,6 +351,11 @@ func (w *Canvas) loopEditable(ev *event.State) error {
if err := w.LinkAdd(actor); err != nil { if err := w.LinkAdd(actor); err != nil {
return err return err
} }
// TODO: reset the Button1 state so we don't finish a
// link and then LinkAdd the clicked doodad immediately
// (causing link chaining)
ev.Button1 = false
break break
} }
} else { } else {

View File

@ -42,8 +42,8 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
window := ui.NewWindow(title) window := ui.NewWindow(title)
window.SetButtons(ui.CloseButton) window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{ window.Configure(ui.Config{
Width: 540, Width: 400,
Height: 350, Height: 180,
Background: render.Grey, Background: render.Grey,
}) })
@ -234,5 +234,6 @@ func NewAddEditLevel(config AddEditLevel) *ui.Window {
} }
} }
window.Hide()
return window return window
} }

View File

@ -0,0 +1,244 @@
package windows
import (
"fmt"
"math"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/doodads"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/uix"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
)
// DoodadDropper is the doodad palette pop-up window for Editor Mode.
type DoodadDropper struct {
Supervisor *ui.Supervisor
Engine render.Engine
// Editing settings for an existing level?
EditLevel *level.Level
// Callback functions.
OnStartDragActor func(doodad *doodads.Doodad, actor *level.Actor)
OnCancel func()
}
// NewDoodadDropper initializes the window.
func NewDoodadDropper(config DoodadDropper) *ui.Window {
// Default options.
var (
title = "Doodads"
buttonSize = balance.DoodadButtonSize
columns = balance.DoodadDropperCols
rows = balance.DoodadDropperRows
// size of the doodad window
width = buttonSize * columns
height = (buttonSize * rows) + 64 // account for button borders :(
// pagination values
page = 1
pages int
perPage = 20
)
window := ui.NewWindow(title)
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: width,
Height: height,
Background: render.Grey,
})
frame := ui.NewFrame("Window Body Frame")
window.Pack(frame, ui.Pack{
Side: ui.N,
Fill: true,
Expand: true,
})
/*******
* Display the Doodads in rows of buttons
*******/
doodadsAvailable, err := doodads.ListDoodads()
if err != nil {
log.Error("NewDoodadDropper: doodads.ListDoodads: %s", err)
}
// Load all the doodads, skip hidden ones.
var items []*doodads.Doodad
for _, filename := range doodadsAvailable {
doodad, err := doodads.LoadFile(filename)
if err != nil {
log.Error(err.Error())
doodad = doodads.New(balance.DoodadSize)
}
// Skip hidden doodads.
if doodad.Hidden && !balance.ShowHiddenDoodads {
continue
}
doodad.Filename = filename
items = append(items, doodad)
}
// Compute the number of pages for the pager widget.
pages = int(
math.Ceil(
float64(len(items)) / float64(columns*rows),
),
)
// Draw the doodad buttons in rows.
var btnRows = []*ui.Frame{}
{
var (
row *ui.Frame
rowCount int // for labeling the ui.Frame for each row
// TODO: pre-size btnRows by calculating how many needed
)
for i, doodad := range items {
doodad := doodad
if row == nil || i%columns == 0 {
var hidden = rowCount >= rows
rowCount++
row = ui.NewFrame(fmt.Sprintf("Doodad Row %d", rowCount))
row.SetBackground(balance.DoodadButtonBackground)
btnRows = append(btnRows, row)
frame.Pack(row, ui.Pack{
Side: ui.N,
// Fill: true,
})
// Hide overflowing rows until we scroll to them.
if hidden {
row.Hide()
}
}
can := uix.NewCanvas(int(buttonSize), true)
can.Name = doodad.Title
can.SetBackground(balance.DoodadButtonBackground)
can.LoadDoodad(doodad)
btn := ui.NewButton(doodad.Title, 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{
Side: ui.W,
})
// Tooltip hover to show the doodad's name.
ui.NewTooltip(btn, ui.Tooltip{
Text: doodad.Title,
Edge: ui.Top,
})
// 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(ed ui.EventData) error {
log.Warn("MouseDown on doodad %s (%s)", doodad.Filename, doodad.Title)
config.OnStartDragActor(doodad, nil)
return nil
})
config.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(config.Engine)
}
}
{
/******************
* Confirm/cancel buttons.
******************/
bottomFrame := ui.NewFrame("Button Frame")
frame.Pack(bottomFrame, ui.Pack{
Side: ui.S,
FillX: true,
})
// Pager for the doodads.
pager := ui.NewPager(ui.Pager{
Page: page,
Pages: pages,
PerPage: perPage,
Font: balance.MenuFont,
OnChange: func(newPage, perPage int) {
page = newPage
log.Info("Page: %d, %d", page, perPage)
// Re-evaluate which rows are shown/hidden for the page we're on.
var (
minRow = (page - 1) * rows
visible = 0
)
for i, row := range btnRows {
if visible >= rows {
row.Hide()
continue
}
if i < minRow {
row.Hide()
} else {
row.Show()
visible++
}
}
},
})
pager.Compute(config.Engine)
pager.Supervise(config.Supervisor)
bottomFrame.Place(pager, ui.Place{
Top: 20,
Left: 20,
})
var buttons = []struct {
Label string
F func(ui.EventData) error
}{
// OK button is for editing an existing level.
{"Close", func(ed ui.EventData) error {
config.OnCancel()
return nil
}},
}
for _, t := range buttons {
btn := ui.NewButton(t.Label, ui.NewLabel(ui.Label{
Text: t.Label,
Font: balance.MenuFont,
}))
btn.Handle(ui.Click, t.F)
config.Supervisor.Add(btn)
bottomFrame.Place(btn, ui.Place{
Top: 20,
Right: 20,
})
}
}
window.Hide()
return window
}

View File

@ -0,0 +1,365 @@
package windows
import (
"fmt"
"math"
"git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/go/render"
"git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/style"
)
// PaletteEditor lets you customize the level palette in Edit Mode.
type PaletteEditor struct {
Supervisor *ui.Supervisor
Engine render.Engine
// Pointer to the currently edited level.
EditLevel *level.Level
// Callback functions.
OnChange func()
OnAddColor func()
OnCancel func()
}
// NewPaletteEditor initializes the window.
func NewPaletteEditor(config PaletteEditor) *ui.Window {
// Default options.
var (
title = "Level Palette"
buttonSize = balance.DoodadButtonSize
columns = balance.DoodadDropperCols
rows = []*ui.Frame{}
// size of the popup window
width = buttonSize * columns
height = (buttonSize * balance.DoodadDropperRows) + 64 // account for button borders :(
// Column sizes of the palette table.
col1 = 30 // ID no.
col2 = 24 // Color
col3 = 120 // Name
col4 = 140 // Attributes
// col5 = 150 // Delete
// pagination values
page = 1
perPage = 5
)
window := ui.NewWindow(title)
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: width,
Height: height,
Background: render.Grey,
})
frame := ui.NewFrame("Window Body Frame")
window.Pack(frame, ui.Pack{
Side: ui.N,
Fill: true,
Expand: true,
})
log.Info("SETUP PALETTE WINDOW")
// Draw the header row.
headers := []struct {
Name string
Size int
}{
{"ID", col1},
{"Col", col2},
{"Name", col3},
{"Attributes", col4},
// {"Delete", col5},
}
header := ui.NewFrame("Palette Header")
for _, col := range headers {
labelFrame := ui.NewFrame(col.Name)
labelFrame.Configure(ui.Config{
Width: col.Size,
Height: 24,
})
label := ui.NewLabel(ui.Label{
Text: col.Name,
Font: balance.MenuFontBold,
})
labelFrame.Pack(label, ui.Pack{
Side: ui.N,
})
header.Pack(labelFrame, ui.Pack{
Side: ui.W,
Padding: 2,
})
}
header.Compute(config.Engine)
frame.Pack(header, ui.Pack{
Side: ui.N,
})
// Draw the main table of Palette rows.
if level := config.EditLevel; level != nil {
for i, swatch := range level.Palette.Swatches {
var idStr = fmt.Sprintf("%d", i)
swatch := swatch
row := ui.NewFrame("Swatch " + idStr)
rows = append(rows, row)
// Off the end of the first page?
if i >= perPage {
row.Hide()
}
// ID label.
idLabel := ui.NewLabel(ui.Label{
Text: idStr + ".",
Font: balance.MenuFont,
})
idLabel.Configure(ui.Config{
Width: col1,
Height: 24,
})
// Name button (click to rename the swatch)
btnName := ui.NewButton("Name", ui.NewLabel(ui.Label{
TextVariable: &swatch.Name,
}))
btnName.Configure(ui.Config{
Width: col3,
Height: 24,
})
btnName.Handle(ui.Click, func(ed ui.EventData) error {
shmem.Prompt("New swatch name ["+swatch.Name+"]: ", func(answer string) {
log.Warn("Answer: %s", answer)
if answer != "" {
swatch.Name = answer
if config.OnChange != nil {
config.OnChange()
}
}
})
return nil
})
config.Supervisor.Add(btnName)
// Color Choice button.
btnColor := ui.NewButton("Color", ui.NewFrame("Color Frame"))
btnColor.SetStyle(&style.Button{
Background: swatch.Color,
HoverBackground: swatch.Color.Lighten(40),
OutlineColor: render.Black,
OutlineSize: 1,
BorderStyle: style.BorderRaised,
BorderSize: 2,
})
btnColor.Configure(ui.Config{
Background: swatch.Color,
Width: col2,
Height: 24,
})
btnColor.Handle(ui.Click, func(ed ui.EventData) error {
shmem.Prompt(fmt.Sprintf(
"New color in hex notation [%s]: ", swatch.Color.ToHex()), func(answer string) {
if answer != "" {
color, err := render.HexColor(answer)
if err != nil {
shmem.Flash("Error with that color code: %s", err)
return
}
swatch.Color = color
// TODO: redundant from above, consolidate these
fmt.Printf("Set button style to: %s\n", swatch.Color)
btnColor.SetStyle(&style.Button{
Background: swatch.Color,
HoverBackground: swatch.Color.Lighten(40),
OutlineColor: render.Black,
OutlineSize: 1,
BorderStyle: style.BorderRaised,
BorderSize: 2,
})
if config.OnChange != nil {
config.OnChange()
}
}
})
return nil
})
config.Supervisor.Add(btnColor)
// Attribute flags
attrFrame := ui.NewFrame("Attributes")
attrFrame.Configure(ui.Config{
Width: col4,
Height: 24,
})
attributes := []struct {
Label string
Var *bool
}{
{
Label: "Solid",
Var: &swatch.Solid,
},
{
Label: "Fire",
Var: &swatch.Fire,
},
{
Label: "Water",
Var: &swatch.Water,
},
}
for _, attr := range attributes {
attr := attr
btn := ui.NewCheckButton(attr.Label, attr.Var, ui.NewLabel(ui.Label{
Text: attr.Label,
Font: balance.MenuFont,
}))
btn.Handle(ui.Click, func(ed ui.EventData) error {
if config.OnChange != nil {
config.OnChange()
}
return nil
})
config.Supervisor.Add(btn)
attrFrame.Pack(btn, ui.Pack{
Side: ui.W,
})
}
// Pack all the widgets.
row.Pack(idLabel, ui.Pack{
Side: ui.W,
PadX: 2,
})
row.Pack(btnColor, ui.Pack{
Side: ui.W,
PadX: 2,
})
row.Pack(btnName, ui.Pack{
Side: ui.W,
PadX: 2,
})
row.Pack(attrFrame, ui.Pack{
Side: ui.W,
PadX: 2,
})
row.Compute(config.Engine)
frame.Pack(row, ui.Pack{
Side: ui.N,
PadY: 2,
})
}
}
{
/******************
* Confirm/cancel buttons.
******************/
bottomFrame := ui.NewFrame("Button Frame")
frame.Pack(bottomFrame, ui.Pack{
Side: ui.S,
FillX: true,
})
// Pager for the doodads.
pager := ui.NewPager(ui.Pager{
Page: page,
Pages: int(math.Ceil(
float64(len(rows)) / float64(perPage),
)),
PerPage: perPage,
Font: balance.MenuFont,
OnChange: func(newPage, perPage int) {
page = newPage
log.Info("Page: %d, %d", page, perPage)
// Re-evaluate which rows are shown/hidden for this page.
var (
minRow = (page - 1) * perPage
visible = 0
)
for i, row := range rows {
if visible >= perPage {
row.Hide()
continue
}
if i < minRow {
row.Hide()
} else {
row.Show()
visible++
}
}
},
})
pager.Compute(config.Engine)
pager.Supervise(config.Supervisor)
bottomFrame.Place(pager, ui.Place{
Top: 20,
Left: 20,
})
btnFrame := ui.NewFrame("Window Buttons")
var buttons = []struct {
Label string
F func(ui.EventData) error
}{
{"Add Color", func(ed ui.EventData) error {
if config.OnAddColor != nil {
config.OnAddColor()
}
if config.OnChange != nil {
config.OnChange()
}
return nil
}},
{"Close", func(ed ui.EventData) error {
if config.OnCancel != nil {
config.OnCancel()
}
return nil
}},
}
for _, t := range buttons {
btn := ui.NewButton(t.Label, ui.NewLabel(ui.Label{
Text: t.Label,
Font: balance.MenuFont,
}))
btn.Handle(ui.Click, t.F)
btn.Compute(config.Engine)
config.Supervisor.Add(btn)
btnFrame.Pack(btn, ui.Pack{
Side: ui.W,
PadX: 4,
})
}
bottomFrame.Place(btnFrame, ui.Place{
Top: 20,
Right: 20,
})
}
window.Hide()
return window
}