WIP Publish Dialog + UI Improvements

* File->Publish Level in the Level Editor opens the Publish window,
  where you can embed custom doodads into your level and export a
  portable .level file you can share with others.
* Currently does not actually export a level file yet.
* The dialog lists all unique doodad names in use in your level, and
  designates which are built-ins and which are custom (paginated).
* A checkbox would let the user embed built-in doodads into their level,
  as well, locking it in to those versions and not using updated
  versions from future game releases.

UI Improvements:
* Added styling for a "Primary" UI button, rendered in deep blue.
* Pop-up modals (Alert, Confirm) color their Ok button as Primary.
* The Enter key pressed during an Alert or Confirm modal will invoke its
  default button and close the modal, corresponding to its Primary
  button.
* The developer console is now opened with the tilde/grave key ` instead
  of the Enter key, so that the Enter key is free to click through
  modals.
* In the "Open/Edit Drawing" window, a "Browse..." button is added to
  the level and doodad sections, spawning a native File Open dialog to
  pick a .level or .doodad outside the config root.
This commit is contained in:
Noah 2021-06-10 22:31:30 -07:00
parent eb24858830
commit d9bca2152a
15 changed files with 558 additions and 8 deletions

View File

@ -2,9 +2,12 @@ package balance
// Feature Flags to turn on/off experimental content. // Feature Flags to turn on/off experimental content.
var Feature = feature{ var Feature = feature{
Zoom: false, Zoom: false, // enable the zoom in/out feature (very buggy rn)
CustomWallpaper: true, CustomWallpaper: true, // attach custom wallpaper img to levels
ChangePalette: false, ChangePalette: false, // reset your palette after level creation to a diff preset
// Allow embedded doodads in levels.
EmbeddableDoodads: true,
} }
// FeaturesOn turns on all feature flags, from CLI --experimental option. // FeaturesOn turns on all feature flags, from CLI --experimental option.
@ -18,4 +21,5 @@ type feature struct {
Zoom bool Zoom bool
CustomWallpaper bool CustomWallpaper bool
ChangePalette bool ChangePalette bool
EmbeddableDoodads bool
} }

View File

@ -3,6 +3,7 @@ package balance
import ( import (
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
"git.kirsle.net/go/ui/style"
) )
// Theme and appearance variables. // Theme and appearance variables.
@ -97,4 +98,15 @@ var (
DoodadButtonSize = 64 DoodadButtonSize = 64
DoodadDropperCols = 6 // rows/columns of buttons DoodadDropperCols = 6 // rows/columns of buttons
DoodadDropperRows = 3 DoodadDropperRows = 3
// Button styles, customized in init().
ButtonPrimary = style.DefaultButton
) )
func init() {
// Customize button styles.
ButtonPrimary.Background = render.RGBA(0, 60, 153, 255)
ButtonPrimary.Foreground = render.RGBA(255, 255, 254, 255)
ButtonPrimary.HoverBackground = render.RGBA(0, 153, 255, 255)
ButtonPrimary.HoverForeground = ButtonPrimary.Foreground
}

View File

@ -41,6 +41,11 @@ func (c Command) Run(d *Doodle) error {
case "alert": case "alert":
modal.Alert(c.ArgsLiteral) modal.Alert(c.ArgsLiteral)
return nil return nil
case "confirm":
modal.Confirm(c.ArgsLiteral).Then(func() {
d.Flash("Confirmed.")
})
return nil
case "new": case "new":
return c.New(d) return c.New(d)
case "save": case "save":

View File

@ -63,6 +63,49 @@ func ListDoodads() ([]string, error) {
return result, err return result, err
} }
// ListBuiltin returns a listing of all built-in doodads.
// Exactly like ListDoodads() but doesn't return user home folder doodads.
func ListBuiltin() ([]string, error) {
var names []string
// List doodads embedded into the binary.
if files, err := bindata.AssetDir("assets/doodads"); err == nil {
names = append(names, files...)
}
// WASM
if runtime.GOOS == "js" {
// Return the array of doodads embedded in the bindata.
// TODO: append user doodads to the list.
return names, nil
}
// Read system-level doodads first. Ignore errors, if the system path is
// empty we still go on to read the user directory.
files, _ := ioutil.ReadDir(filesystem.SystemDoodadsPath)
for _, file := range files {
name := file.Name()
if strings.HasSuffix(strings.ToLower(name), enum.DoodadExt) {
names = append(names, name)
}
}
// Deduplicate names.
var uniq = map[string]interface{}{}
var result []string
for _, name := range names {
if _, ok := uniq[name]; !ok {
uniq[name] = nil
result = append(result, name)
}
}
sort.Strings(result)
return result, nil
}
// LoadFile reads a doodad file from disk, checking a few locations. // LoadFile reads a doodad file from disk, checking a few locations.
func LoadFile(filename string) (*Doodad, error) { func LoadFile(filename string) (*Doodad, error) {
if !strings.HasSuffix(filename, enum.DoodadExt) { if !strings.HasSuffix(filename, enum.DoodadExt) {

View File

@ -134,10 +134,9 @@ func (d *Doodle) Run() error {
// Command line shell. // Command line shell.
if d.shell.Open { if d.shell.Open {
} else if ev.Enter { } else if keybind.ShellKey(ev) {
log.Debug("Shell: opening shell") log.Debug("Shell: opening shell")
d.shell.Open = true d.shell.Open = true
ev.Enter = false
} else { } else {
// Global event handlers. // Global event handlers.
if keybind.Shutdown(ev) { if keybind.Shutdown(ev) {

View File

@ -49,6 +49,7 @@ type EditorUI struct {
doodadWindow *ui.Window doodadWindow *ui.Window
paletteEditor *ui.Window paletteEditor *ui.Window
layersWindow *ui.Window layersWindow *ui.Window
publishWindow *ui.Window
// Palette window. // Palette window.
Palette *ui.Window Palette *ui.Window
@ -522,6 +523,13 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
} }
}) })
}) })
if balance.Feature.EmbeddableDoodads && drawingType == "level" {
fileMenu.AddItem("Publish level", func() {
u.OpenPublishWindow()
})
}
fileMenu.AddItemAccel("Open...", "Ctrl-O*", func() { fileMenu.AddItemAccel("Open...", "Ctrl-O*", func() {
u.Scene.ConfirmUnload(func() { u.Scene.ConfirmUnload(func() {
d.GotoLoadMenu() d.GotoLoadMenu()

View File

@ -6,6 +6,7 @@ import (
"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/modal"
"git.kirsle.net/apps/doodle/pkg/windows" "git.kirsle.net/apps/doodle/pkg/windows"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
@ -43,6 +44,14 @@ func (u *EditorUI) OpenDoodadDropper() {
u.doodadWindow.Show() u.doodadWindow.Show()
} }
// OpenPublishWindow opens the Publisher window.
func (u *EditorUI) OpenPublishWindow() {
u.publishWindow.Hide()
u.publishWindow = nil
u.SetupPopups(u.d)
u.publishWindow.Show()
}
// SetupPopups preloads popup windows like the DoodadDropper. // SetupPopups preloads popup windows like the DoodadDropper.
func (u *EditorUI) SetupPopups(d *Doodle) { func (u *EditorUI) SetupPopups(d *Doodle) {
// Common window configure function. // Common window configure function.
@ -56,6 +65,8 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
X: (d.width / 2) - (size.W / 2), X: (d.width / 2) - (size.W / 2),
Y: (d.height / 2) - (size.H / 2), Y: (d.height / 2) - (size.H / 2),
}) })
window.Hide()
} }
// Doodad Dropper. // Doodad Dropper.
@ -94,6 +105,30 @@ func (u *EditorUI) SetupPopups(d *Doodle) {
configure(u.levelSettingsWindow) configure(u.levelSettingsWindow)
} }
// Publish Level (embed doodads)
if u.publishWindow == nil {
scene, _ := d.Scene.(*EditorScene)
u.publishWindow = windows.NewPublishWindow(windows.Publish{
Supervisor: u.Supervisor,
Engine: d.Engine,
Level: scene.Level,
OnPublish: func() {
modal.Alert("Not Yet Implemented")
// filename, err := native.SaveFile("Save your level", "*.level")
// if err != nil {
// modal.Alert(err.Error())
// }
// log.Info("Write to: %s", filename)
},
OnCancel: func() {
u.publishWindow.Hide()
},
})
configure(u.publishWindow)
}
// Palette Editor. // Palette Editor.
if u.paletteEditor == nil { if u.paletteEditor == nil {
scene, _ := d.Scene.(*EditorScene) scene, _ := d.Scene.(*EditorScene)

View File

@ -107,6 +107,20 @@ func DoodadDropper(ev *event.State) bool {
return ev.KeyDown("d") return ev.KeyDown("d")
} }
// ShellKey (`) opens the developer console.
func ShellKey(ev *event.State) bool {
v := ev.KeyDown("`")
ev.SetKeyDown("`", false)
return v
}
// Enter key.
func Enter(ev *event.State) bool {
v := ev.Enter
ev.Enter = false
return v
}
// Shift key. // Shift key.
func Shift(ev *event.State) bool { func Shift(ev *event.State) bool {
return ev.Shift return ev.Shift

View File

@ -54,6 +54,7 @@ func makeAlert(m *Modal) *ui.Window {
Text: "Ok", Text: "Ok",
Font: balance.MenuFont, Font: balance.MenuFont,
})) }))
button.SetStyle(&balance.ButtonPrimary)
button.Handle(ui.Click, func(ev ui.EventData) error { button.Handle(ui.Click, func(ev ui.EventData) error {
log.Info("clicked!") log.Info("clicked!")
m.Dismiss(true) m.Dismiss(true)

View File

@ -78,6 +78,11 @@ func makeConfirm(m *Modal) *ui.Window {
button.Compute(engine) button.Compute(engine)
supervisor.Add(button) supervisor.Add(button)
// OK Button is primary.
if btn.Label == "Ok" {
button.SetStyle(&balance.ButtonPrimary)
}
btnBar.Pack(button, ui.Pack{ btnBar.Pack(button, ui.Pack{
Side: ui.W, Side: ui.W,
PadX: 2, PadX: 2,

View File

@ -3,6 +3,7 @@ package modal
import ( import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/keybind"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/render/event" "git.kirsle.net/go/render/event"
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
@ -48,6 +49,12 @@ func Handled(ev *event.State) bool {
return false return false
} }
// Enter key submits the default button.
if keybind.Enter(ev) {
current.Dismiss(true)
return true
}
supervisor.Loop(ev) supervisor.Loop(ev)
// Has the window changed size? // Has the window changed size?

View File

@ -3,8 +3,9 @@
package native package native
import ( import (
"github.com/gen2brain/dlgs"
"errors" "errors"
"github.com/gen2brain/dlgs"
) )
func init() { func init() {
@ -28,3 +29,21 @@ func OpenFile(title string, filter string) (string, error) {
} }
return "", errors.New("canceled") return "", errors.New("canceled")
} }
// SaveFile invokes a native File Chooser dialog with the title
// and a set of file filters. The filters are a sequence of label
// and comma-separated file extensions.
//
// Example:
// SaveFile("Pick a file", "Images", "png,gif,jpg", "Audio", "mp3")
func SaveFile(title string, filter string) (string, error) {
filename, ok, err := dlgs.File(title, filter, false)
if err != nil {
return "", err
}
if ok {
return filename, nil
}
return "", errors.New("canceled")
}

View File

@ -6,6 +6,7 @@ import (
"strings" "strings"
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"git.kirsle.net/apps/doodle/pkg/keybind"
"git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/shmem" "git.kirsle.net/apps/doodle/pkg/shmem"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
@ -215,7 +216,7 @@ func (s *Shell) Draw(d *Doodle, ev *event.State) error {
if ev.Escape { if ev.Escape {
s.Close() s.Close()
return nil return nil
} else if ev.Enter { } else if keybind.Enter(ev) {
s.Execute(s.Text) s.Execute(s.Text)
// Auto-close the console unless in REPL mode. // Auto-close the console unless in REPL mode.
@ -223,7 +224,6 @@ func (s *Shell) Draw(d *Doodle, ev *event.State) error {
s.Close() s.Close()
} }
ev.Enter = false
return nil return nil
} else if (ev.Up || ev.Down) && len(s.History) > 0 { } else if (ev.Up || ev.Down) && len(s.History) > 0 {
// Paging through history. // Paging through history.

View File

@ -5,6 +5,8 @@ import (
"git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/balance"
"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/native"
"git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/go/render" "git.kirsle.net/go/render"
"git.kirsle.net/go/ui" "git.kirsle.net/go/ui"
@ -103,6 +105,40 @@ func NewOpenLevelEditor(config OpenLevelEditor) *ui.Window {
}(i, lvl) }(i, lvl)
} }
// Browse button for local filesystem.
browseLevelFrame := ui.NewFrame("Browse Level Frame")
frame.Pack(browseLevelFrame, ui.Pack{
Side: ui.N,
Expand: true,
FillX: true,
PadY: 1,
})
browseLevelButton := ui.NewButton("Browse Level", ui.NewLabel(ui.Label{
Text: "Browse...",
Font: balance.MenuFont,
}))
browseLevelButton.SetStyle(&balance.ButtonPrimary)
browseLevelFrame.Pack(browseLevelButton, ui.Pack{
Side: ui.W,
})
browseLevelButton.Handle(ui.Click, func(ed ui.EventData) error {
filename, err := native.OpenFile("Choose a .level file", "*.level")
if err != nil {
log.Error("Couldn't show file dialog: %s", err)
return nil
}
if config.LoadForPlay {
config.OnPlayLevel(filename)
} else {
config.OnEditLevel(filename)
}
return nil
})
config.Supervisor.Add(browseLevelButton)
/****************** /******************
* Frame for selecting User Doodads * Frame for selecting User Doodads
******************/ ******************/
@ -155,6 +191,40 @@ func NewOpenLevelEditor(config OpenLevelEditor) *ui.Window {
} }
} }
// Browse button for local filesystem.
browseDoodadFrame := ui.NewFrame("Browse Doodad Frame")
frame.Pack(browseDoodadFrame, ui.Pack{
Side: ui.N,
Expand: true,
FillX: true,
PadY: 1,
})
browseDoodadButton := ui.NewButton("Browse Doodad", ui.NewLabel(ui.Label{
Text: "Browse...",
Font: balance.MenuFont,
}))
browseDoodadButton.SetStyle(&balance.ButtonPrimary)
browseDoodadFrame.Pack(browseDoodadButton, ui.Pack{
Side: ui.W,
})
browseDoodadButton.Handle(ui.Click, func(ed ui.EventData) error {
filename, err := native.OpenFile("Choose a .doodad file", "*.doodad")
if err != nil {
log.Error("Couldn't show file dialog: %s", err)
return nil
}
if config.LoadForPlay {
config.OnPlayLevel(filename)
} else {
config.OnEditLevel(filename)
}
return nil
})
config.Supervisor.Add(browseDoodadButton)
/****************** /******************
* Confirm/cancel buttons. * Confirm/cancel buttons.
******************/ ******************/

View File

@ -0,0 +1,328 @@
package windows
import (
"fmt"
"math"
"sort"
"strings"
"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/go/render"
"git.kirsle.net/go/ui"
)
// Publish window.
type Publish struct {
// Settings passed in by doodle
Supervisor *ui.Supervisor
Engine render.Engine
Level *level.Level
OnPublish func()
OnCancel func()
// Private vars.
includeBuiltins bool // show built-in doodads in checkbox-list.
}
// NewPublishWindow initializes the window.
func NewPublishWindow(cfg Publish) *ui.Window {
var (
windowWidth = 400
windowHeight = 300
page = 1
perPage = 4
pages = 1
maxPageButtons = 8
// columns and sizes to draw the doodad list
columns = 3
btnWidth = 120
btnHeight = 14
)
window := ui.NewWindow("Publish Level")
window.SetButtons(ui.CloseButton)
window.Configure(ui.Config{
Width: windowWidth,
Height: windowHeight,
Background: render.RGBA(200, 200, 255, 255),
})
/////////////
// Intro text
introFrame := ui.NewFrame("Intro Frame")
window.Pack(introFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
lines := []struct {
Text string
Font render.Text
}{
{
Text: "About",
Font: balance.LabelFont,
},
{
Text: "Share your level easily! If you are using custom doodads in\n" +
"your level, you may attach them directly to your\n" +
"level file -- so it can easily run on another computer!",
Font: balance.UIFont,
},
{
Text: "List of Doodads in Your Level",
Font: balance.LabelFont,
},
}
for n, row := range lines {
frame := ui.NewFrame(fmt.Sprintf("Intro Line %d", n))
introFrame.Pack(frame, ui.Pack{
Side: ui.N,
FillX: true,
})
label := ui.NewLabel(ui.Label{
Text: row.Text,
Font: row.Font,
})
frame.Pack(label, ui.Pack{
Side: ui.W,
})
}
/////////////
// Custom Doodads checkbox-list.
doodadFrame := ui.NewFrame("Doodads Frame")
doodadFrame.Resize(render.Rect{
W: windowWidth,
H: btnHeight*perPage + 100,
})
window.Pack(doodadFrame, ui.Pack{
Side: ui.N,
FillX: true,
})
// First, the checkbox to show built-in doodads or not.
builtinRow := ui.NewFrame("Show Builtins Frame")
doodadFrame.Pack(builtinRow, ui.Pack{
Side: ui.N,
FillX: true,
})
builtinCB := ui.NewCheckbox("Show Builtins", &cfg.includeBuiltins, ui.NewLabel(ui.Label{
Text: "Attach built-in* doodads too",
Font: balance.UIFont,
}))
builtinCB.Supervise(cfg.Supervisor)
builtinRow.Pack(builtinCB, ui.Pack{
Side: ui.W,
PadX: 2,
})
// Collect all the doodad names in use in this level.
unique := map[string]interface{}{}
names := []string{}
if cfg.Level != nil {
for _, actor := range cfg.Level.Actors {
if _, ok := unique[actor.Filename]; ok {
continue
}
unique[actor.Filename] = nil
names = append(names, actor.Filename)
}
}
sort.Strings(names)
// Identify which of the doodads are built-ins.
usedBuiltins := []string{}
builtinMap := map[string]interface{}{}
usedCustom := []string{}
if builtins, err := doodads.ListBuiltin(); err == nil {
for _, filename := range builtins {
if _, ok := unique[filename]; ok {
usedBuiltins = append(usedBuiltins, filename)
builtinMap[filename] = nil
}
}
}
for _, name := range names {
if _, ok := builtinMap[name]; ok {
continue
}
usedCustom = append(usedCustom, name)
}
// Helper function to draw the button rows for a set of doodads.
mkDoodadRows := func(filenames []string, builtin bool) []*ui.Frame {
var (
curRow *ui.Frame // = ui.NewFrame("mkDoodadRows 0")
frames = []*ui.Frame{}
)
for i, name := range filenames {
if i%columns == 0 {
curRow = ui.NewFrame(fmt.Sprintf("mkDoodadRows %d", i))
frames = append(frames, curRow)
}
font := balance.UIFont
if builtin {
font.Color = render.Blue
name += "*"
}
btn := ui.NewLabel(ui.Label{
Text: strings.Replace(name, ".doodad", "", 1),
Font: font,
})
btn.Configure(ui.Config{
Width: btnWidth,
Height: btnHeight,
})
curRow.Pack(btn, ui.Pack{
Side: ui.W,
PadX: 2,
PadY: 2,
})
}
return frames
}
// 1. Draw the built-in doodads in use.
var (
btnRows = []*ui.Frame{}
builtinRows = []*ui.Frame{}
customRows = []*ui.Frame{}
)
if len(names) > 0 {
customRows = mkDoodadRows(usedCustom, false)
btnRows = append(btnRows, customRows...)
}
if len(usedBuiltins) > 0 {
builtinRows = mkDoodadRows(usedBuiltins, true)
btnRows = append(btnRows, builtinRows...)
}
for i, row := range btnRows {
doodadFrame.Pack(row, ui.Pack{
Side: ui.N,
FillX: true,
})
// Hide if too long for 1st page.
if i >= perPage {
row.Hide()
}
}
/////////////
// Buttons at bottom of window
bottomFrame := ui.NewFrame("Button Frame")
window.Pack(bottomFrame, ui.Pack{
Side: ui.S,
FillX: true,
})
// Pager for the doodads.
pages = int(
math.Ceil(
float64(len(btnRows)) / float64(perPage),
),
)
pagerOnChange := 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) * perPage
visible = 0
)
for i, row := range btnRows {
if visible >= perPage {
row.Hide()
continue
}
if i < minRow {
row.Hide()
} else {
row.Show()
visible++
}
}
}
pager := ui.NewPager(ui.Pager{
Name: "Doodads List Pager",
Page: page,
Pages: pages,
PerPage: perPage,
MaxPageButtons: maxPageButtons,
Font: balance.MenuFont,
OnChange: pagerOnChange,
})
pager.Compute(cfg.Engine)
pager.Supervise(cfg.Supervisor)
bottomFrame.Place(pager, ui.Place{
Top: 20,
Left: 20,
})
frame := ui.NewFrame("Button frame")
buttons := []struct {
label string
primary bool
f func()
}{
{"Export Level", true, func() {
if cfg.OnPublish != nil {
cfg.OnPublish()
}
}},
{"Close", false, func() {
if cfg.OnCancel != nil {
cfg.OnCancel()
}
}},
}
for _, button := range buttons {
button := button
btn := ui.NewButton(button.label, ui.NewLabel(ui.Label{
Text: button.label,
Font: balance.MenuFont,
}))
if button.primary {
btn.SetStyle(&balance.ButtonPrimary)
}
btn.Handle(ui.Click, func(ed ui.EventData) error {
button.f()
return nil
})
btn.Compute(cfg.Engine)
cfg.Supervisor.Add(btn)
frame.Pack(btn, ui.Pack{
Side: ui.W,
PadX: 4,
Expand: true,
Fill: true,
})
}
bottomFrame.Pack(frame, ui.Pack{
Side: ui.E,
Padding: 8,
})
return window
}