Noah Petherbridge
647124495b
Added a new level property: Difficulty * An enum ranging from -1, 0, 1 (Peaceful, Normal, Hard) * Default difficulty is Normal; pre-existing levels are Normal by default per the zero value. Doodad scripts can read the difficulty via the new global variable `Level.Difficulty` and some doodads have been updated: * Azulians: on Peaceful they ignore all player characters, and on Hard they are in "hunt mode": infinite aggro radius and they're aggressive to all characters. * Bird: on Peaceful they will not dive and attack any player character. Other spit and polish: * New Level/Level Properties UI reworked into a magicform. * New "PromptPre(question, answer, func)" function for prompting the user with the developer shell, but pre-filling in an answer for them to either post or edit. * magicform has a PromptUser field option for simple Text/Int fields which present as buttons, so magicform can prompt and update the variable itself. * Don't show the _autosave.doodad in the Doodad Dropper window.
419 lines
9.2 KiB
Go
419 lines
9.2 KiB
Go
// Package magicform helps create simple form layouts with go/ui.
|
|
package magicform
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"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"
|
|
)
|
|
|
|
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
|
|
Color
|
|
)
|
|
|
|
// 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.
|
|
PadY int // spacer between (vertical) forms
|
|
PadX int
|
|
}
|
|
|
|
/*
|
|
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
|
|
IntVariable *int // Textbox
|
|
Options []Option // Selectbox
|
|
SelectValue interface{} // Selectbox default choice
|
|
Color *render.Color // Color
|
|
|
|
// For text-type fields, opt-in to let magicform prompt the
|
|
// user using the game's developer shell.
|
|
PromptUser func(answer string)
|
|
|
|
// 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
|
|
Separator bool
|
|
}
|
|
|
|
/*
|
|
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,
|
|
PadY: form.PadY,
|
|
})
|
|
|
|
// 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,
|
|
})
|
|
}
|
|
|
|
// Color picker button.
|
|
if row.Type == Color && row.Color != nil {
|
|
btn := ui.NewButton("ColorPicker", ui.NewLabel(ui.Label{
|
|
Text: " ",
|
|
Font: row.Font,
|
|
}))
|
|
style := style.DefaultButton
|
|
style.Background = *row.Color
|
|
style.HoverBackground = style.Background.Lighten(20)
|
|
btn.SetStyle(&style)
|
|
|
|
form.Supervisor.Add(btn)
|
|
frame.Pack(btn, ui.Pack{
|
|
Side: ui.W,
|
|
FillX: true,
|
|
Expand: true,
|
|
})
|
|
|
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
|
// Open a ColorPicker widget.
|
|
picker, err := ui.NewColorPicker(ui.ColorPicker{
|
|
Title: "Select a color",
|
|
Supervisor: form.Supervisor,
|
|
Engine: form.Engine,
|
|
Color: *row.Color,
|
|
OnManualInput: func(callback func(render.Color)) {
|
|
// Prompt the user to enter a hex color using the developer shell.
|
|
shmem.Prompt("New color in hex notation: ", func(answer string) {
|
|
if answer != "" {
|
|
// XXX: pure white renders as invisible, fudge it a bit.
|
|
if answer == "FFFFFF" {
|
|
answer = "FFFFFE"
|
|
}
|
|
|
|
color, err := render.HexColor(answer)
|
|
if err != nil {
|
|
shmem.Flash("Error with that color code: %s", err)
|
|
return
|
|
}
|
|
|
|
// Reconfigure the button now.
|
|
style.Background = color
|
|
style.HoverBackground = style.Background.Lighten(20)
|
|
|
|
callback(color)
|
|
}
|
|
})
|
|
},
|
|
})
|
|
if err != nil {
|
|
log.Error("Couldn't open ColorPicker: %s", err)
|
|
return err
|
|
}
|
|
|
|
picker.Then(func(color render.Color) {
|
|
*row.Color = color
|
|
style.Background = color
|
|
style.HoverBackground = style.Background.Lighten(20)
|
|
|
|
// call onClick to save change to disk now
|
|
if row.OnClick != nil {
|
|
row.OnClick()
|
|
}
|
|
})
|
|
|
|
picker.Center(shmem.CurrentRenderEngine.WindowSize())
|
|
picker.Show()
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// Buttons and Text fields (for now).
|
|
if row.Type == Button || row.Type == Textbox {
|
|
btn := ui.NewButton("Button", ui.NewLabel(ui.Label{
|
|
Text: row.Label,
|
|
Font: row.Font,
|
|
TextVariable: row.TextVariable,
|
|
IntVariable: row.IntVariable,
|
|
}))
|
|
form.Supervisor.Add(btn)
|
|
frame.Pack(btn, ui.Pack{
|
|
Side: ui.W,
|
|
FillX: true,
|
|
Expand: true,
|
|
})
|
|
|
|
// Tooltip? TODO - make nicer.
|
|
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
|
|
tt := ui.NewTooltip(btn, row.Tooltip)
|
|
tt.Supervise(form.Supervisor)
|
|
}
|
|
|
|
// Handlers
|
|
btn.Handle(ui.Click, func(ed ui.EventData) error {
|
|
// Text boxes, we want to prompt the user to enter new value?
|
|
if row.PromptUser != nil {
|
|
var value string
|
|
if row.TextVariable != nil {
|
|
value = *row.TextVariable
|
|
} else if row.IntVariable != nil {
|
|
value = fmt.Sprintf("%d", *row.IntVariable)
|
|
}
|
|
|
|
shmem.PromptPre("Enter new value: ", value, func(answer string) {
|
|
if answer != "" {
|
|
row.PromptUser(answer)
|
|
}
|
|
})
|
|
}
|
|
|
|
if row.OnClick != nil {
|
|
row.OnClick()
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// 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 {
|
|
tt := ui.NewTooltip(cb, row.Tooltip)
|
|
tt.Supervise(form.Supervisor)
|
|
}
|
|
|
|
// 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,
|
|
Expand: true,
|
|
})
|
|
|
|
if row.Options != nil {
|
|
for _, option := range row.Options {
|
|
if option.Separator {
|
|
btn.AddSeparator()
|
|
continue
|
|
}
|
|
btn.AddItem(option.Label, option.Value, func() {})
|
|
}
|
|
}
|
|
|
|
if row.SelectValue != nil {
|
|
btn.SetValue(row.SelectValue)
|
|
}
|
|
|
|
btn.Handle(ui.Change, func(ed ui.EventData) error {
|
|
if selection, ok := btn.GetValue(); ok {
|
|
if row.OnSelect != nil {
|
|
row.OnSelect(selection.Value)
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
|
|
// Tooltip? TODO - make nicer.
|
|
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
|
|
tt := ui.NewTooltip(btn, row.Tooltip)
|
|
tt.Supervise(form.Supervisor)
|
|
}
|
|
|
|
btn.Supervise(form.Supervisor)
|
|
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 || field.IntVariable != nil {
|
|
return Textbox
|
|
}
|
|
|
|
if field.Label != "" {
|
|
return Text
|
|
}
|
|
|
|
return Auto
|
|
}
|