doodle/pkg/uix/magic-form/magic_form.go
Noah Petherbridge 4de0126b19 Game Controller Support
Adds support for Xbox and Nintendo style game controllers. The gamepad
controls are documented on the README and in the game's Settings window.

The buttons are not customizable yet, except that the player can choose
between two button styles:

* X Style (default): "A" button is on the bottom and "B" on the right.
* N Style: swaps the A/B and the X/Y buttons to use a Nintendo-style
  layout instead of an Xbox-style.
2022-02-19 18:31:22 -08:00

279 lines
5.6 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/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
SelectValue interface{} // Selectbox default choice
// 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,
Expand: true,
})
if row.Options != nil {
for _, option := range row.Options {
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
})
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 {
return Textbox
}
if field.Label != "" {
return Text
}
return Auto
}