From 1cc6eee5c84330d52269893d8cac6930dfcb1598 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Mon, 17 Jan 2022 18:51:11 -0800 Subject: [PATCH] Refactor Level Publishing + MagicForm * magicform is a helper package that may eventually be part of the go/ui library, for easily creating structured form layouts. * The Level Publisher UI is the first to utilize magicform. Refactor how level publishing works: * Level data now stores SaveDoodads and SaveBuiltins (bools) and when the level editor saves the file, it will attach custom and/or builtin doodads just before save. * Move the menu item from the File menu to Level->Publish * The Publisher UI just shows the checkboxes to toggle the level settings and a convenient Save button along with descriptive text. * Free versions get the "Register" window popping up if they click the Save Now button from within the publisher window. Note: free versions can still toggle the booleans on/off but their game will not attach any new doodads on save. * Free games which open a level w/ embedded doodads will get a pop-up warning that the doodads aren't available. * If they DON'T turn off the SaveDoodads option, they can still edit and save the level and keep the existing doodads attached. * If they UNCHECK the option and save, all attached doodads are removed from the level. --- pkg/editor_scene.go | 6 + pkg/editor_ui_menubar.go | 15 +- pkg/editor_ui_popups.go | 128 ++++++-------- pkg/level/filesystem.go | 12 ++ pkg/level/publishing/publishing.go | 40 ++++- pkg/level/types.go | 4 + pkg/uix/magic-form/magic_form.go | 271 +++++++++++++++++++++++++++++ pkg/windows/publish_level.go | 216 ++++++++++------------- 8 files changed, 473 insertions(+), 219 deletions(-) create mode 100644 pkg/uix/magic-form/magic_form.go diff --git a/pkg/editor_scene.go b/pkg/editor_scene.go index 3a510cf..446cc61 100644 --- a/pkg/editor_scene.go +++ b/pkg/editor_scene.go @@ -13,6 +13,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/keybind" "git.kirsle.net/apps/doodle/pkg/level" + "git.kirsle.net/apps/doodle/pkg/level/publishing" "git.kirsle.net/apps/doodle/pkg/license" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/modal" @@ -513,6 +514,11 @@ func (s *EditorScene) SaveLevel(filename string) error { // Clear the modified flag on the level. s.UI.Canvas.SetModified(false) + // Attach doodads to the level on save. + if err := publishing.Publish(m); err != nil { + log.Error("Error publishing level: %s", err.Error()) + } + return m.WriteFile(filename) } diff --git a/pkg/editor_ui_menubar.go b/pkg/editor_ui_menubar.go index 6b74cb4..9109a8b 100644 --- a/pkg/editor_ui_menubar.go +++ b/pkg/editor_ui_menubar.go @@ -5,7 +5,6 @@ package doodle // The rest of it is controlled in editor_ui.go import ( - "git.kirsle.net/apps/doodle/pkg/balance" "git.kirsle.net/apps/doodle/pkg/drawtool" "git.kirsle.net/apps/doodle/pkg/enum" "git.kirsle.net/apps/doodle/pkg/level/giant_screenshot" @@ -25,13 +24,11 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { // Save and Save As common menu handler var ( - drawingType string - saveFunc func(filename string) + saveFunc func(filename string) ) switch u.Scene.DrawingType { case enum.LevelDrawing: - drawingType = "level" saveFunc = func(filename string) { if err := u.Scene.SaveLevel(filename); err != nil { d.FlashError("Error: %s", err) @@ -40,7 +37,6 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { } } case enum.DoodadDrawing: - drawingType = "doodad" saveFunc = func(filename string) { if err := u.Scene.SaveDoodad(filename); err != nil { d.FlashError("Error: %s", err) @@ -71,12 +67,6 @@ 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", u.Scene.MenuOpen) fileMenu.AddSeparator() fileMenu.AddItem("Exit to menu", func() { @@ -125,6 +115,9 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar { levelMenu.AddItemAccel("Playtest", "P", func() { u.Scene.Playtest() }) + levelMenu.AddItem("Publish", func() { + u.OpenPublishWindow() + }) levelMenu.AddSeparator() levelMenu.AddItem("Giant Screenshot", func() { diff --git a/pkg/editor_ui_popups.go b/pkg/editor_ui_popups.go index db8c4cf..d8ce5eb 100644 --- a/pkg/editor_ui_popups.go +++ b/pkg/editor_ui_popups.go @@ -2,14 +2,12 @@ package doodle import ( "fmt" - "os" "path/filepath" "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/level/publishing" "git.kirsle.net/apps/doodle/pkg/license" "git.kirsle.net/apps/doodle/pkg/log" "git.kirsle.net/apps/doodle/pkg/modal" @@ -52,8 +50,38 @@ func (u *EditorUI) OpenDoodadDropper() { // OpenPublishWindow opens the Publisher window. func (u *EditorUI) OpenPublishWindow() { + scene, _ := u.d.Scene.(*EditorScene) + + u.publishWindow = windows.NewPublishWindow(windows.Publish{ + Supervisor: u.Supervisor, + Engine: u.d.Engine, + Level: scene.Level, + + OnPublish: func(includeBuiltins bool) { + u.d.FlashError("OnPublish Called") + // XXX: Paid Version Only. + if !license.IsRegistered() { + if u.licenseWindow != nil { + u.licenseWindow.Show() + u.Supervisor.FocusWindow(u.licenseWindow) + } + u.d.FlashError("Level Publishing is only available in the full version of the game.") + return + } + + // NOTE: this function just saves the level. SaveDoodads and SaveBuiltins + // are toggled in the publish window and the save handler does publishing. + u.Scene.SaveLevel(u.Scene.filename) + u.d.Flash("Saved level: %s", u.Scene.filename) + }, + OnCancel: func() { + u.publishWindow.Hide() + }, + }) + u.ConfigureWindow(u.d, u.publishWindow) + u.publishWindow.Hide() - u.publishWindow = nil + // u.publishWindow = nil u.SetupPopups(u.d) u.publishWindow.Show() } @@ -66,23 +94,24 @@ func (u *EditorUI) OpenFileSystemWindow() { u.filesystemWindow.Show() } +// ConfigureWindow sets default window config functions, like +// centering them on screen. +func (u *EditorUI) ConfigureWindow(d *Doodle, 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), + }) + + window.Hide() +} + // 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), - }) - - window.Hide() - } - // License Registration Window. if u.licenseWindow == nil { cfg := windows.License{ @@ -115,7 +144,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { u.doodadWindow.Hide() }, }) - configure(u.doodadWindow) + u.ConfigureWindow(d, u.doodadWindow) } // Page Settings @@ -141,7 +170,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { u.levelSettingsWindow.Hide() }, }) - configure(u.levelSettingsWindow) + u.ConfigureWindow(d, u.levelSettingsWindow) } // Doodad Properties @@ -163,58 +192,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { } u.doodadPropertiesWindow = windows.NewDoodadPropertiesWindow(cfg) - configure(u.doodadPropertiesWindow) - } - - // 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(includeBuiltins bool) { - // XXX: Paid Version Only. - if !license.IsRegistered() { - if u.licenseWindow != nil { - u.licenseWindow.Show() - u.Supervisor.FocusWindow(u.licenseWindow) - } - d.FlashError("Level Publishing is only available in the full version of the game.") - // modal.Alert( - // "This feature is only available in the full version of the game.", - // ).WithTitle("Please register") - return - } - - log.Debug("OnPublish: include builtins=%+v", includeBuiltins) - cwd, _ := os.Getwd() - d.Prompt(fmt.Sprintf("File name (relative to %s)> ", cwd), func(answer string) { - if answer == "" { - d.FlashError("A file name is required to publish this level.") - return - } - - if !strings.HasSuffix(answer, ".level") { - answer += ".level" - } - - answer = filepath.Join(cwd, answer) - log.Debug("call with includeBuiltins=%+v", includeBuiltins) - if _, err := publishing.Publish(scene.Level, answer, includeBuiltins); err != nil { - modal.Alert("Error when publishing the level: %s", err) - return - } - d.Flash("Exported published level to: %s", answer) - }) - }, - OnCancel: func() { - u.publishWindow.Hide() - }, - }) - configure(u.publishWindow) + u.ConfigureWindow(d, u.doodadPropertiesWindow) } // Level FileSystem Viewer. @@ -262,7 +240,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { u.filesystemWindow.Hide() }, }) - configure(u.filesystemWindow) + u.ConfigureWindow(d, u.filesystemWindow) } // Palette Editor. @@ -315,7 +293,7 @@ func (u *EditorUI) SetupPopups(d *Doodle) { u.paletteEditor.Hide() }, }) - configure(u.paletteEditor) + u.ConfigureWindow(d, u.paletteEditor) } // Layers window (doodad editor) @@ -376,6 +354,6 @@ func (u *EditorUI) SetupPopups(d *Doodle) { u.layersWindow.Hide() }, }) - configure(u.layersWindow) + u.ConfigureWindow(d, u.layersWindow) } } diff --git a/pkg/level/filesystem.go b/pkg/level/filesystem.go index a79763f..191a645 100644 --- a/pkg/level/filesystem.go +++ b/pkg/level/filesystem.go @@ -50,6 +50,18 @@ func (l *Level) DeleteFile(filename string) bool { return false } +// DeleteFiles removes all files beginning with the prefix. +func (l *Level) DeleteFiles(prefix string) int { + var count int + for filename := range l.Files { + if strings.HasPrefix(filename, prefix) { + delete(l.Files, filename) + count++ + } + } + return count +} + // ListFiles returns the list of all embedded file names, alphabetically. func (l *Level) ListFiles() []string { var files []string diff --git a/pkg/level/publishing/publishing.go b/pkg/level/publishing/publishing.go index fe898a2..7e8f7ed 100644 --- a/pkg/level/publishing/publishing.go +++ b/pkg/level/publishing/publishing.go @@ -9,12 +9,15 @@ levels. package publishing import ( + "errors" "fmt" "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/license" "git.kirsle.net/apps/doodle/pkg/log" ) @@ -24,32 +27,55 @@ doodads within level files. */ // Publish writes a published level file, with embedded doodads included. -func Publish(lvl *level.Level, filename string, includeBuiltins bool) (*level.Level, error) { +func Publish(lvl *level.Level) error { + // Not embedding doodads? + if !lvl.SaveDoodads { + if removed := lvl.DeleteFiles(balance.EmbeddedDoodadsBasePath); removed > 0 { + log.Info("Note: removed %d attached doodads because SaveDoodads is false", removed) + } + return nil + } + + // Registered games only. + if !license.IsRegistered() { + return errors.New("only registered versions of the game can attach doodads to levels") + } + // Get and embed the doodads. builtins, customs := GetUsedDoodadNames(lvl) - if includeBuiltins { + var names = map[string]interface{}{} + if lvl.SaveBuiltins { log.Debug("including builtins: %+v", builtins) customs = append(customs, builtins...) } for _, filename := range customs { log.Debug("Embed filename: %s", filename) + names[filename] = nil + doodad, err := doodads.LoadFromEmbeddable(filename, lvl) if err != nil { - return nil, fmt.Errorf("couldn't load doodad %s: %s", filename, err) + return fmt.Errorf("couldn't load doodad %s: %s", filename, err) } bin, err := doodad.Serialize() if err != nil { - return nil, fmt.Errorf("couldn't serialize doodad %s: %s", filename, err) + return fmt.Errorf("couldn't serialize doodad %s: %s", filename, err) } // Embed it. lvl.SetFile(balance.EmbeddedDoodadsBasePath+filename, bin) } - log.Info("Publish: write file to %s", filename) - err := lvl.WriteFile(filename) - return lvl, err + // Trim any doodads not currently in the level. + for _, filename := range lvl.ListFilesAt(balance.EmbeddedDoodadsBasePath) { + basename := strings.TrimPrefix(filename, balance.EmbeddedDoodadsBasePath) + if _, ok := names[basename]; !ok { + log.Debug("Remove embedded doodad %s (cleanup)", basename) + lvl.DeleteFile(filename) + } + } + + return nil } // GetUsedDoodadNames returns the lists of doodad filenames in use in a level, diff --git a/pkg/level/types.go b/pkg/level/types.go index f1bc074..d682c8c 100644 --- a/pkg/level/types.go +++ b/pkg/level/types.go @@ -48,6 +48,10 @@ type Level struct { // Actors keep a list of the doodad instances in this map. Actors ActorMap `json:"actors"` + // Publishing: attach any custom doodads the map uses on save. + SaveDoodads bool `json:"saveDoodads"` + SaveBuiltins bool `json:"saveBuiltins"` + // Undo history, temporary live data not persisted to the level file. UndoHistory *drawtool.History `json:"-"` } diff --git a/pkg/uix/magic-form/magic_form.go b/pkg/uix/magic-form/magic_form.go new file mode 100644 index 0000000..eedc08e --- /dev/null +++ b/pkg/uix/magic-form/magic_form.go @@ -0,0 +1,271 @@ +// Package magicform helps create simple form layouts with go/ui. +package magicform + +import ( + "fmt" + + "git.kirsle.net/apps/doodle/pkg/log" + "git.kirsle.net/go/render" + "git.kirsle.net/go/ui" + "git.kirsle.net/go/ui/style" +) + +type Type int + +const ( + Auto Type = iota + Text // free, wide Label row + Frame // custom frame from the caller + Button // Single button with a label + Textbox + Checkbox + Radiobox + Selectbox +) + +// Form configuration. +type Form struct { + Supervisor *ui.Supervisor // Required for most useful forms + Engine render.Engine + + // For vertical forms. + Vertical bool + LabelWidth int // size of left frame for labels. +} + +/* +Field for your form (or form-aligned label sections, etc.) + +The type of Form control to render is inferred based on bound +variables and other configuration. +*/ +type Field struct { + // Type may be inferred by presence of other params. + Type Type + + // Set a text string and font for simple labels or paragraphs. + Label string + Font render.Text + + // Easy button row: make Buttons an array of Button fields + Buttons []Field + ButtonStyle *style.Button + + // Easy Paginator. DO NOT SUPERVISE, let the Create do so! + Pager *ui.Pager + + // If you send a *ui.Frame to insert, the Type is inferred + // to be Frame. + Frame *ui.Frame + + // Variable bindings, the type may infer to be: + BoolVariable *bool // Checkbox + TextVariable *string // Textbox + Options []Option // Selectbox + + // Tooltip to add to a form control. + // Checkbox only for now. + Tooltip ui.Tooltip // config for the tooltip only + + // Handlers you can configure + OnSelect func(value interface{}) // Selectbox + OnClick func() // Button +} + +// Option used in Selectbox or Radiobox fields. +type Option struct { + Value interface{} + Label string +} + +/* +Create the form field and populate it into the given Frame. + +Renders the form vertically. +*/ +func (form Form) Create(into *ui.Frame, fields []Field) { + for n, row := range fields { + row := row + + if row.Frame != nil { + into.Pack(row.Frame, ui.Pack{ + Side: ui.N, + FillX: true, + }) + continue + } + + frame := ui.NewFrame(fmt.Sprintf("Line %d", n)) + into.Pack(frame, ui.Pack{ + Side: ui.N, + FillX: true, + }) + + // Pager row? + if row.Pager != nil { + row.Pager.Compute(form.Engine) + form.Supervisor.Add(row.Pager) + frame.Pack(row.Pager, ui.Pack{ + Side: ui.W, + Expand: true, + }) + + } + + // Buttons row? + if row.Buttons != nil && len(row.Buttons) > 0 { + for _, row := range row.Buttons { + row := row + + btn := ui.NewButton(row.Label, ui.NewLabel(ui.Label{ + Text: row.Label, + Font: row.Font, + })) + if row.ButtonStyle != nil { + btn.SetStyle(row.ButtonStyle) + } + + btn.Handle(ui.Click, func(ed ui.EventData) error { + if row.OnClick != nil { + row.OnClick() + } else { + log.Error("No OnClick handler for button %s", row.Label) + } + return nil + }) + + btn.Compute(form.Engine) + form.Supervisor.Add(btn) + + frame.Pack(btn, ui.Pack{ + Side: ui.W, + PadX: 4, + PadY: 2, + }) + } + + continue + } + + // Infer the type of the form field. + if row.Type == Auto { + row.Type = row.Infer() + if row.Type == Auto { + continue + } + } + + // Is there a label frame to the left? + // - Checkbox gets a full row. + if row.Label != "" && row.Type != Checkbox { + labFrame := ui.NewFrame("Label Frame") + labFrame.Configure(ui.Config{ + Width: form.LabelWidth, + }) + frame.Pack(labFrame, ui.Pack{ + Side: ui.W, + }) + + // Draw the label text into it. + label := ui.NewLabel(ui.Label{ + Text: row.Label, + Font: row.Font, + }) + labFrame.Pack(label, ui.Pack{ + Side: ui.W, + }) + } + + // Checkbox? + if row.Type == Checkbox { + cb := ui.NewCheckbox("Checkbox", row.BoolVariable, ui.NewLabel(ui.Label{ + Text: row.Label, + Font: row.Font, + })) + cb.Supervise(form.Supervisor) + frame.Pack(cb, ui.Pack{ + Side: ui.W, + FillX: true, + }) + + // Tooltip? TODO - make nicer. + if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil { + ui.NewTooltip(cb, row.Tooltip) + } + + // Handlers + cb.Handle(ui.Click, func(ed ui.EventData) error { + if row.OnClick != nil { + row.OnClick() + } + return nil + }) + } + + // Selectbox? also Radiobox for now. + if row.Type == Selectbox || row.Type == Radiobox { + btn := ui.NewSelectBox("Select", ui.Label{ + Font: row.Font, + }) + frame.Pack(btn, ui.Pack{ + Side: ui.W, + FillX: true, + }) + + if row.Options != nil { + for _, option := range row.Options { + btn.AddItem(option.Label, option.Value, func() {}) + } + } + + btn.Handle(ui.Click, func(ed ui.EventData) error { + if selection, ok := btn.GetValue(); ok { + if row.OnSelect != nil { + row.OnSelect(selection.Value) + } + } + return nil + }) + + form.Supervisor.Add(btn) + } + } +} + +/* +Infer the type if the field was of type Auto. + +Returns the first Type inferred from the field by checking in +this order: + +- Frame if the field has a *Frame +- Checkbox if there is a *BoolVariable +- Selectbox if there are Options +- Textbox if there is a *TextVariable +- Text if there is a Label + +May return Auto if none of the above and be ignored. +*/ +func (field Field) Infer() Type { + if field.Frame != nil { + return Frame + } + + if field.BoolVariable != nil { + return Checkbox + } + + if field.Options != nil && len(field.Options) > 0 { + return Selectbox + } + + if field.TextVariable != nil { + return Textbox + } + + if field.Label != "" { + return Text + } + + return Auto +} diff --git a/pkg/windows/publish_level.go b/pkg/windows/publish_level.go index f39cbf6..240637a 100644 --- a/pkg/windows/publish_level.go +++ b/pkg/windows/publish_level.go @@ -9,6 +9,7 @@ import ( "git.kirsle.net/apps/doodle/pkg/level" "git.kirsle.net/apps/doodle/pkg/level/publishing" "git.kirsle.net/apps/doodle/pkg/log" + magicform "git.kirsle.net/apps/doodle/pkg/uix/magic-form" "git.kirsle.net/go/render" "git.kirsle.net/go/ui" ) @@ -30,8 +31,8 @@ type Publish struct { // NewPublishWindow initializes the window. func NewPublishWindow(cfg Publish) *ui.Window { var ( - windowWidth = 400 - windowHeight = 300 + windowWidth = 380 + windowHeight = 220 page = 1 perPage = 4 pages = 1 @@ -51,76 +52,12 @@ func NewPublishWindow(cfg Publish) *ui.Window { 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, + H: btnHeight*perPage + 40, }) // Collect the doodads named in this level. @@ -190,15 +127,6 @@ func NewPublishWindow(cfg Publish) *ui.Window { } } - ///////////// - // 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( @@ -237,59 +165,95 @@ func NewPublishWindow(cfg Publish) *ui.Window { Font: balance.MenuFont, OnChange: pagerOnChange, }) - pager.Compute(cfg.Engine) - pager.Supervise(cfg.Supervisor) - bottomFrame.Place(pager, ui.Place{ - Top: 20, - Left: 20, + _ = pager + + ///////////// + // Intro text + + introFrame := ui.NewFrame("Intro Frame") + window.Pack(introFrame, ui.Pack{ + Side: ui.N, + FillX: true, }) - frame := ui.NewFrame("Button frame") - buttons := []struct { - label string - primary bool - f func() - }{ - {"Export Level", true, func() { - if cfg.OnPublish != nil { - cfg.OnPublish(cfg.includeBuiltins) - } - }}, - {"Close", false, func() { - if cfg.OnCancel != nil { - cfg.OnCancel() - } - }}, + // Render the form, putting it all together. + form := magicform.Form{ + Supervisor: cfg.Supervisor, + Engine: cfg.Engine, + Vertical: true, + LabelWidth: 100, } - 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, + form.Create(introFrame, []magicform.Field{ + { + Label: "About", + Font: balance.LabelFont, + }, + { + Label: "Share your level easily! If you are using custom doodads in\n" + + "your level, you may attach them directly to your level file\n" + + "so it can easily run on another computer!", + Font: balance.UIFont, + }, + { + Label: "Attach custom doodads when I save the level", + Font: balance.UIFont, + BoolVariable: &cfg.Level.SaveDoodads, + }, + { + Label: "Attach built-in doodads too", + Font: balance.UIFont.Update(render.Text{ + Color: render.Red, + }), + BoolVariable: &cfg.Level.SaveBuiltins, + Tooltip: ui.Tooltip{ + Edge: ui.Top, + Text: "If enabled, the attached doodads will override the built-ins\n" + + "for this level. Bugfixes or updates to the built-ins will not\n" + + "affect your level, either.", + }, + }, + { + Label: "The above settings are saved with your level file, and each\n" + + "time you save, custom doodads will be re-attached.", + Font: balance.UIFont, + }, + // Pager is broken, Supervisor doesn't pick it up, TODO + /*{ + Label: "Doodads currently used on this level:", + Font: balance.LabelFont, + }, + { + Frame: doodadFrame, + }, + { + Label: "* Built-in doodad", + Font: balance.UIFont, + }, + { + Pager: pager, + },*/ + { + Buttons: []magicform.Field{ + { + ButtonStyle: &balance.ButtonPrimary, + Label: "Save Level Now", + OnClick: func() { + if cfg.OnPublish != nil { + cfg.OnPublish(cfg.includeBuiltins) + } + }, + }, + { + Type: magicform.Button, + Label: "Close", + OnClick: func() { + if cfg.OnCancel != nil { + cfg.OnCancel() + } + }, + }, + }, + }, }) return window