diff --git a/pkg/balance/feature_flags.go b/pkg/balance/feature_flags.go index e53cb01..e7f5ad0 100644 --- a/pkg/balance/feature_flags.go +++ b/pkg/balance/feature_flags.go @@ -2,9 +2,12 @@ package balance // Feature Flags to turn on/off experimental content. var Feature = feature{ - Zoom: false, - CustomWallpaper: true, - ChangePalette: false, + Zoom: false, // enable the zoom in/out feature (very buggy rn) + CustomWallpaper: true, // attach custom wallpaper img to levels + 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. @@ -18,4 +21,5 @@ type feature struct { Zoom bool CustomWallpaper bool ChangePalette bool + EmbeddableDoodads bool } diff --git a/pkg/balance/theme.go b/pkg/balance/theme.go index 1c42677..53d19f7 100644 --- a/pkg/balance/theme.go +++ b/pkg/balance/theme.go @@ -3,6 +3,7 @@ package balance import ( "git.kirsle.net/go/render" "git.kirsle.net/go/ui" + "git.kirsle.net/go/ui/style" ) // Theme and appearance variables. @@ -97,4 +98,15 @@ var ( DoodadButtonSize = 64 DoodadDropperCols = 6 // rows/columns of buttons 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 +} diff --git a/pkg/commands.go b/pkg/commands.go index 6209418..3468553 100644 --- a/pkg/commands.go +++ b/pkg/commands.go @@ -41,6 +41,11 @@ func (c Command) Run(d *Doodle) error { case "alert": modal.Alert(c.ArgsLiteral) return nil + case "confirm": + modal.Confirm(c.ArgsLiteral).Then(func() { + d.Flash("Confirmed.") + }) + return nil case "new": return c.New(d) case "save": diff --git a/pkg/doodads/fmt_readwrite.go b/pkg/doodads/fmt_readwrite.go index 4d13bdb..31b9d99 100644 --- a/pkg/doodads/fmt_readwrite.go +++ b/pkg/doodads/fmt_readwrite.go @@ -63,6 +63,49 @@ func ListDoodads() ([]string, error) { 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. func LoadFile(filename string) (*Doodad, error) { if !strings.HasSuffix(filename, enum.DoodadExt) { diff --git a/pkg/doodle.go b/pkg/doodle.go index 26690d9..375a466 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -134,10 +134,9 @@ func (d *Doodle) Run() error { // Command line shell. if d.shell.Open { - } else if ev.Enter { + } else if keybind.ShellKey(ev) { log.Debug("Shell: opening shell") d.shell.Open = true - ev.Enter = false } else { // Global event handlers. if keybind.Shutdown(ev) { diff --git a/pkg/editor_ui.go b/pkg/editor_ui.go index 0f8ef0e..e3af690 100644 --- a/pkg/editor_ui.go +++ b/pkg/editor_ui.go @@ -49,6 +49,7 @@ type EditorUI struct { doodadWindow *ui.Window paletteEditor *ui.Window layersWindow *ui.Window + publishWindow *ui.Window // Palette 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() { u.Scene.ConfirmUnload(func() { d.GotoLoadMenu() diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index 2697d01..2011f87 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -6,6 +6,7 @@ import ( "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/modal" "git.kirsle.net/apps/doodle/pkg/windows" "git.kirsle.net/go/render" "git.kirsle.net/go/ui" @@ -43,6 +44,14 @@ func (u *EditorUI) OpenDoodadDropper() { 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. func (u *EditorUI) SetupPopups(d *Doodle) { // Common window configure function. @@ -56,6 +65,8 @@ func (u *EditorUI) SetupPopups(d *Doodle) { X: (d.width / 2) - (size.W / 2), Y: (d.height / 2) - (size.H / 2), }) + + window.Hide() } // Doodad Dropper. @@ -94,6 +105,30 @@ func (u *EditorUI) SetupPopups(d *Doodle) { 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. if u.paletteEditor == nil { scene, _ := d.Scene.(*EditorScene) diff --git a/pkg/keybind/keybind.go b/pkg/keybind/keybind.go index 6d90bcb..3623f40 100644 --- a/pkg/keybind/keybind.go +++ b/pkg/keybind/keybind.go @@ -107,6 +107,20 @@ func DoodadDropper(ev *event.State) bool { 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. func Shift(ev *event.State) bool { return ev.Shift diff --git a/pkg/modal/alert.go b/pkg/modal/alert.go index a653cda..595a2fa 100644 --- a/pkg/modal/alert.go +++ b/pkg/modal/alert.go @@ -54,6 +54,7 @@ func makeAlert(m *Modal) *ui.Window { Text: "Ok", Font: balance.MenuFont, })) + button.SetStyle(&balance.ButtonPrimary) button.Handle(ui.Click, func(ev ui.EventData) error { log.Info("clicked!") m.Dismiss(true) diff --git a/pkg/modal/confirm.go b/pkg/modal/confirm.go index 31497e5..c5131a5 100644 --- a/pkg/modal/confirm.go +++ b/pkg/modal/confirm.go @@ -78,6 +78,11 @@ func makeConfirm(m *Modal) *ui.Window { button.Compute(engine) supervisor.Add(button) + // OK Button is primary. + if btn.Label == "Ok" { + button.SetStyle(&balance.ButtonPrimary) + } + btnBar.Pack(button, ui.Pack{ Side: ui.W, PadX: 2, diff --git a/pkg/modal/modal.go b/pkg/modal/modal.go index 8160475..3959f8b 100644 --- a/pkg/modal/modal.go +++ b/pkg/modal/modal.go @@ -3,6 +3,7 @@ package modal import ( "git.kirsle.net/apps/doodle/pkg/balance" + "git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/go/render" "git.kirsle.net/go/render/event" "git.kirsle.net/go/ui" @@ -48,6 +49,12 @@ func Handled(ev *event.State) bool { return false } + // Enter key submits the default button. + if keybind.Enter(ev) { + current.Dismiss(true) + return true + } + supervisor.Loop(ev) // Has the window changed size? diff --git a/pkg/native/file_dialog_native.go b/pkg/native/file_dialog_native.go index c7811e4..0a7e920 100644 --- a/pkg/native/file_dialog_native.go +++ b/pkg/native/file_dialog_native.go @@ -3,8 +3,9 @@ package native import ( - "github.com/gen2brain/dlgs" "errors" + + "github.com/gen2brain/dlgs" ) func init() { @@ -28,3 +29,21 @@ func OpenFile(title string, filter string) (string, error) { } 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") +} diff --git a/pkg/shell.go b/pkg/shell.go index b599e4b..2a72048 100644 --- a/pkg/shell.go +++ b/pkg/shell.go @@ -6,6 +6,7 @@ import ( "strings" "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/shmem" "git.kirsle.net/go/render" @@ -215,7 +216,7 @@ func (s *Shell) Draw(d *Doodle, ev *event.State) error { if ev.Escape { s.Close() return nil - } else if ev.Enter { + } else if keybind.Enter(ev) { s.Execute(s.Text) // Auto-close the console unless in REPL mode. @@ -223,7 +224,6 @@ func (s *Shell) Draw(d *Doodle, ev *event.State) error { s.Close() } - ev.Enter = false return nil } else if (ev.Up || ev.Down) && len(s.History) > 0 { // Paging through history. diff --git a/pkg/windows/open_level_editor.go b/pkg/windows/open_level_editor.go index 8f57754..05c4b48 100644 --- a/pkg/windows/open_level_editor.go +++ b/pkg/windows/open_level_editor.go @@ -5,6 +5,8 @@ import ( "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/native" "git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/go/render" "git.kirsle.net/go/ui" @@ -103,6 +105,40 @@ func NewOpenLevelEditor(config OpenLevelEditor) *ui.Window { }(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 ******************/ @@ -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. ******************/ diff --git a/pkg/windows/publish_level.go b/pkg/windows/publish_level.go new file mode 100644 index 0000000..0de56b2 --- /dev/null +++ b/pkg/windows/publish_level.go @@ -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 +}