doodle/pkg/uix/magic-form/magic_form.go
Noah Petherbridge ddcad27485 WIP: Chunker size to uint8 and Rectangular Doodads
Convert the Chunker size to a uint8 so chunk sizes are limited to 255px. This
means that inside of a chunk, uint8's can track the relative pixel coordinates
and result in a great memory savings since all of these uint8's are currently
64-bits wide apiece.

WIP on rectangular shaped doodads:
* You can create such a doodad in the editor and draw it normally.
* It doesn't draw the right size when dragged into your level however:
  - In uix.Actor.Size() it gets a rect of the doodad's square Chunker size,
    instead of getting the proper doodad.Size rect.
  - If you give it the doodad.Size rect, it draws the Canvas size correctly
    instead of a square - the full drawing appears and in gameplay its hitbox
    (assuming the same large rectangle size) works correctly in-game.
  - But, the doodad has scrolling issues when it gets to the top or left edge
    of the screen! This old gnarly bug has come back. For some reason square
    canvas doodads draw correctly but rectangular ones have the drawing scroll
    just a bit - how far it scrolls is proportional to how big the doodad is,
    with the Start Flag only scrolling a few pixels before it stops.
2023-02-16 21:47:18 -08:00

462 lines
10 KiB
Go

// Package magicform helps create simple form layouts with go/ui.
package magicform
import (
"fmt"
"git.kirsle.net/SketchyMaze/doodle/pkg/log"
"git.kirsle.net/SketchyMaze/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
Value // a Label & Value row (value not editable)
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
Readonly bool // draw the value as a flat label
// 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)
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
tt := ui.NewTooltip(btn, row.Tooltip)
tt.Supervise(form.Supervisor)
}
frame.Pack(btn, ui.Pack{
Side: ui.W,
PadX: 2,
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,
})
}
// Simple "Value" row with a Label to its left.
if row.Type == Value {
lbl := ui.NewLabel(ui.Label{
Text: row.Label,
Font: row.Font,
TextVariable: row.TextVariable,
IntVariable: row.IntVariable,
})
frame.Pack(lbl, ui.Pack{
Side: ui.W,
FillX: true,
Expand: true,
})
// Tooltip? TODO - make nicer.
if row.Tooltip.Text != "" || row.Tooltip.TextVariable != nil {
tt := ui.NewTooltip(lbl, row.Tooltip)
tt.Supervise(form.Supervisor)
}
}
// 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,
}))
frame.Pack(btn, ui.Pack{
Side: ui.W,
FillX: true,
Expand: true,
})
// Not clickable if Readonly.
if !row.Readonly {
form.Supervisor.Add(btn)
}
// 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)
}
// Update bound variables.
if v, ok := selection.Value.(int); ok && row.IntVariable != nil {
*row.IntVariable = v
}
if v, ok := selection.Value.(string); ok && row.TextVariable != nil {
*row.TextVariable = v
}
}
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
}