up3
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
Build and deploy / Build (push) Has been cancelled
Build and deploy / Update Contributors (push) Has been cancelled
Build and deploy / Deploy (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,344 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Element represents an element that may be added to a Form. Any of the types in this package that implement
|
||||
// the element interface may be used as struct fields when passing the form structure to form.New().
|
||||
type Element interface {
|
||||
json.Marshaler
|
||||
elem()
|
||||
}
|
||||
|
||||
// MenuElement represents an element that may be added to a Menu form. This includes buttons, dividers,
|
||||
// headers, and labels.
|
||||
type MenuElement interface {
|
||||
json.Marshaler
|
||||
menuElem()
|
||||
}
|
||||
|
||||
// Divider represents a visual separator element on a form. It displays a horizontal line.
|
||||
type Divider struct{}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (d Divider) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "divider",
|
||||
"text": "",
|
||||
})
|
||||
}
|
||||
|
||||
// Header represents a header element on a form. It displays larger, emphasised text for section titles.
|
||||
type Header struct {
|
||||
// Text is the text held by the header. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
}
|
||||
|
||||
// NewHeader creates and returns a new Header with the text passed.
|
||||
func NewHeader(text string) Header {
|
||||
return Header{Text: text}
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (h Header) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "header",
|
||||
"text": h.Text,
|
||||
})
|
||||
}
|
||||
|
||||
// Label represents a static label on a form. It serves only to display a box of text, and users cannot
|
||||
// submit values to it.
|
||||
type Label struct {
|
||||
// Text is the text held by the label. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
}
|
||||
|
||||
// NewLabel creates and returns a new Label with the values passed.
|
||||
func NewLabel(text string) Label {
|
||||
return Label{Text: text}
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (l Label) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "label",
|
||||
"text": l.Text,
|
||||
})
|
||||
}
|
||||
|
||||
// Input represents a text input box element. Submitters may write any text in these boxes with no specific
|
||||
// length.
|
||||
type Input struct {
|
||||
// Text is the text displayed over the input element. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
// Default is the default value filled out in the input. The user may remove this value and fill out its
|
||||
// own text. The text may contain Minecraft formatting codes.
|
||||
Default string
|
||||
// Placeholder is the text displayed in the input box if it does not contain any text filled out by the
|
||||
// user. The text may contain Minecraft formatting codes.
|
||||
Placeholder string
|
||||
// Tooltip is an optional text displayed when hovering over the element's info icon. The icon only
|
||||
// appears when a tooltip is set.
|
||||
Tooltip string
|
||||
|
||||
value string
|
||||
}
|
||||
|
||||
// NewInput creates and returns a new Input with the values passed.
|
||||
func NewInput(text, defaultValue, placeholder string) Input {
|
||||
return Input{Text: text, Default: defaultValue, Placeholder: placeholder}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the Input with the tooltip set.
|
||||
func (i Input) WithTooltip(tooltip string) Input {
|
||||
i.Tooltip = tooltip
|
||||
return i
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (i Input) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "input",
|
||||
"text": i.Text,
|
||||
"default": i.Default,
|
||||
"placeholder": i.Placeholder,
|
||||
}
|
||||
if i.Tooltip != "" {
|
||||
m["tooltip"] = i.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value filled out by the user.
|
||||
func (i Input) Value() string {
|
||||
return i.value
|
||||
}
|
||||
|
||||
// Toggle represents an on-off button element. Submitters may either toggle this on or off, which will then
|
||||
// hold a value of true or false respectively.
|
||||
type Toggle struct {
|
||||
// Text is the text displayed over the toggle element. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
// Default determines if the toggle should be on/off by default.
|
||||
Default bool
|
||||
// Tooltip is an optional text displayed when hovering over the element's info icon. The icon only
|
||||
// appears when a tooltip is set.
|
||||
Tooltip string
|
||||
|
||||
value bool
|
||||
}
|
||||
|
||||
// NewToggle creates and returns a new Toggle with the values passed.
|
||||
func NewToggle(text string, defaultValue bool) Toggle {
|
||||
return Toggle{Text: text, Default: defaultValue}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the Toggle with the tooltip set.
|
||||
func (t Toggle) WithTooltip(tooltip string) Toggle {
|
||||
t.Tooltip = tooltip
|
||||
return t
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (t Toggle) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "toggle",
|
||||
"text": t.Text,
|
||||
"default": t.Default,
|
||||
}
|
||||
if t.Tooltip != "" {
|
||||
m["tooltip"] = t.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value filled out by the user.
|
||||
func (t Toggle) Value() bool {
|
||||
return t.value
|
||||
}
|
||||
|
||||
// Slider represents a slider element. Submitters may move the slider to values within the range of the slider
|
||||
// to select a value.
|
||||
type Slider struct {
|
||||
// Text is the text displayed over the slider element. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
// Min and Max are used to specify the minimum and maximum range of the slider. A value lower or higher
|
||||
// than these values cannot be selected.
|
||||
Min, Max float64
|
||||
// StepSize is the size that one step of the slider takes up. When set to 1.0 for example, a submitter
|
||||
// will be able to select only whole values.
|
||||
StepSize float64
|
||||
// Default is the default value filled out for the slider.
|
||||
Default float64
|
||||
// Tooltip is an optional text displayed when hovering over the element's info icon. The icon only
|
||||
// appears when a tooltip is set.
|
||||
Tooltip string
|
||||
|
||||
value float64
|
||||
}
|
||||
|
||||
// NewSlider creates and returns a new Slider using the values passed.
|
||||
func NewSlider(text string, min, max, stepSize, defaultValue float64) Slider {
|
||||
return Slider{Text: text, Min: min, Max: max, StepSize: stepSize, Default: defaultValue}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the Slider with the tooltip set.
|
||||
func (s Slider) WithTooltip(tooltip string) Slider {
|
||||
s.Tooltip = tooltip
|
||||
return s
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (s Slider) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "slider",
|
||||
"text": s.Text,
|
||||
"min": s.Min,
|
||||
"max": s.Max,
|
||||
"step": s.StepSize,
|
||||
"default": s.Default,
|
||||
}
|
||||
if s.Tooltip != "" {
|
||||
m["tooltip"] = s.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value filled out by the user.
|
||||
func (s Slider) Value() float64 {
|
||||
return s.value
|
||||
}
|
||||
|
||||
// Dropdown represents a dropdown which, when clicked, opens a window with the options set in the Options
|
||||
// field. Submitters may select one of the options.
|
||||
type Dropdown struct {
|
||||
// Text is the text displayed over the dropdown element. The text may contain Minecraft formatting codes.
|
||||
Text string
|
||||
// Options holds a list of options that a Submitter may select. The order of these options is retained
|
||||
// when shown to the submitter of the form.
|
||||
Options []string
|
||||
// DefaultIndex is the index in the Options slice that is used as default. When sent to a Submitter, the
|
||||
// value at this index in the Options slice will be selected.
|
||||
DefaultIndex int
|
||||
// Tooltip is an optional text displayed when hovering over the element's info icon. The icon only
|
||||
// appears when a tooltip is set.
|
||||
Tooltip string
|
||||
|
||||
value int
|
||||
}
|
||||
|
||||
// NewDropdown creates and returns new Dropdown using the values passed.
|
||||
func NewDropdown(text string, options []string, defaultIndex int) Dropdown {
|
||||
return Dropdown{Text: text, Options: options, DefaultIndex: defaultIndex}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the Dropdown with the tooltip set.
|
||||
func (d Dropdown) WithTooltip(tooltip string) Dropdown {
|
||||
d.Tooltip = tooltip
|
||||
return d
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (d Dropdown) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "dropdown",
|
||||
"text": d.Text,
|
||||
"default": d.DefaultIndex,
|
||||
"options": d.Options,
|
||||
}
|
||||
if d.Tooltip != "" {
|
||||
m["tooltip"] = d.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value that the Submitter submitted. The value is an index pointing to the selected option
|
||||
// in the Options slice.
|
||||
func (d Dropdown) Value() int {
|
||||
return d.value
|
||||
}
|
||||
|
||||
// StepSlider represents a slider that has a number of options that may be selected. It is essentially a
|
||||
// combination of a Dropdown and a Slider, looking like a slider but having properties like a dropdown.
|
||||
type StepSlider Dropdown
|
||||
|
||||
// NewStepSlider creates and returns new StepSlider using the values passed.
|
||||
func NewStepSlider(text string, options []string, defaultIndex int) StepSlider {
|
||||
return StepSlider{Text: text, Options: options, DefaultIndex: defaultIndex}
|
||||
}
|
||||
|
||||
// WithTooltip returns a copy of the StepSlider with the tooltip set.
|
||||
func (s StepSlider) WithTooltip(tooltip string) StepSlider {
|
||||
s.Tooltip = tooltip
|
||||
return s
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (s StepSlider) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "step_slider",
|
||||
"text": s.Text,
|
||||
"default": s.DefaultIndex,
|
||||
"steps": s.Options,
|
||||
}
|
||||
if s.Tooltip != "" {
|
||||
m["tooltip"] = s.Tooltip
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
// Value returns the value that the Submitter submitted. The value is an index pointing to the selected option
|
||||
// in the Options slice.
|
||||
func (s StepSlider) Value() int {
|
||||
return s.value
|
||||
}
|
||||
|
||||
// Button represents a button added to a Menu or Modal form. The button has text on it and an optional image,
|
||||
// which may be either retrieved from a website or the local assets of the game.
|
||||
type Button struct {
|
||||
// Text holds the text displayed on the button. It may use Minecraft formatting codes and may have
|
||||
// newlines.
|
||||
Text string
|
||||
// Image holds a path to an image for the button. The Image may either be a URL pointing to an image,
|
||||
// such as 'https://someimagewebsite.com/someimage.png', or a path pointing to a local asset, such as
|
||||
// 'textures/blocks/grass_carried'.
|
||||
Image string
|
||||
}
|
||||
|
||||
// NewButton creates and returns a new Button using the text and image passed.
|
||||
func NewButton(text, image string) Button {
|
||||
return Button{Text: text, Image: image}
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (b Button) MarshalJSON() ([]byte, error) {
|
||||
m := map[string]any{
|
||||
"type": "button",
|
||||
"text": b.Text,
|
||||
}
|
||||
if b.Image != "" {
|
||||
buttonType := "path"
|
||||
if strings.HasPrefix(b.Image, "http:") || strings.HasPrefix(b.Image, "https:") {
|
||||
buttonType = "url"
|
||||
}
|
||||
m["image"] = map[string]any{"type": buttonType, "data": b.Image}
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func (Divider) elem() {}
|
||||
func (Header) elem() {}
|
||||
func (Label) elem() {}
|
||||
func (Input) elem() {}
|
||||
func (Toggle) elem() {}
|
||||
func (Slider) elem() {}
|
||||
func (Dropdown) elem() {}
|
||||
func (StepSlider) elem() {}
|
||||
|
||||
func (Divider) menuElem() {}
|
||||
func (Header) menuElem() {}
|
||||
func (Label) menuElem() {}
|
||||
func (Button) menuElem() {}
|
||||
@@ -0,0 +1,217 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
)
|
||||
|
||||
// Form represents a form that may be sent to a Submitter. The three types of forms, custom forms, menu forms
|
||||
// and modal forms implement this interface.
|
||||
type Form interface {
|
||||
json.Marshaler
|
||||
SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error
|
||||
}
|
||||
|
||||
// Custom represents a form that may be sent to a player and has fields that should be filled out by the
|
||||
// player that the form is sent to.
|
||||
type Custom struct {
|
||||
title string
|
||||
submittable Submittable
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (f Custom) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "custom_form",
|
||||
"title": f.title,
|
||||
"content": f.Elements(),
|
||||
})
|
||||
}
|
||||
|
||||
// New creates a new (custom) form with the title passed and returns it. The title is formatted according to
|
||||
// the rules of fmt.Sprintln.
|
||||
// The submittable passed is used to create the structure of the form. The values of the Submittable's form
|
||||
// fields are used to set text, defaults and placeholders. If the Submittable passed is not a struct, New
|
||||
// panics. New also panics if one of the exported field types of the Submittable is not one that implements
|
||||
// the Element interface.
|
||||
func New(submittable Submittable, title ...any) Custom {
|
||||
t := reflect.TypeOf(submittable)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic("submittable must be struct")
|
||||
}
|
||||
f := Custom{title: format(title), submittable: submittable}
|
||||
f.verify()
|
||||
return f
|
||||
}
|
||||
|
||||
// Title returns the formatted title passed when the form was created using New().
|
||||
func (f Custom) Title() string {
|
||||
return f.title
|
||||
}
|
||||
|
||||
// Elements returns a list of all elements as set in the Submittable passed to form.New().
|
||||
func (f Custom) Elements() []Element {
|
||||
v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(f.submittable))
|
||||
n := v.NumField()
|
||||
|
||||
elements := make([]Element, 0, n)
|
||||
for i := 0; i < n; i++ {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
// Each exported field is guaranteed to implement the Element interface.
|
||||
elements = append(elements, field.Interface().(Element))
|
||||
}
|
||||
return elements
|
||||
}
|
||||
|
||||
// SubmitJSON submits a JSON data slice to the form. The form will check all values in the JSON array passed,
|
||||
// making sure their values are valid for the form's elements.
|
||||
// If the values are valid and can be parsed properly, the Submittable.Submit() method of the form's Submittable is
|
||||
// called and the fields of the Submittable will be filled out.
|
||||
func (f Custom) SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error {
|
||||
if b == nil {
|
||||
if closer, ok := f.submittable.(Closer); ok {
|
||||
closer.Close(submitter, tx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(bytes.NewBuffer(b))
|
||||
dec.UseNumber()
|
||||
|
||||
var data []any
|
||||
if err := dec.Decode(&data); err != nil {
|
||||
return fmt.Errorf("error decoding JSON data to slice: %w", err)
|
||||
}
|
||||
|
||||
v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(f.submittable))
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
fieldV := v.Field(i)
|
||||
if !fieldV.CanSet() {
|
||||
continue
|
||||
}
|
||||
e := fieldV.Interface().(Element)
|
||||
if len(data) == 0 {
|
||||
return fmt.Errorf("form JSON data array does not have enough values")
|
||||
}
|
||||
if elementReadonly(e) {
|
||||
data = data[1:]
|
||||
continue
|
||||
}
|
||||
elem, err := f.parseValue(e, data[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing form response value: %w", err)
|
||||
}
|
||||
fieldV.Set(elem)
|
||||
data = data[1:]
|
||||
}
|
||||
|
||||
v.Interface().(Submittable).Submit(submitter, tx)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// elementReadonly returns true if the element is read only.
|
||||
func elementReadonly(e Element) bool {
|
||||
switch e.(type) {
|
||||
case Divider, Header, Label:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// parseValue parses a value into the Element passed and returns it as a reflection Value. If the value is not
|
||||
// valid for the element, an error is returned.
|
||||
func (f Custom) parseValue(elem Element, s any) (reflect.Value, error) {
|
||||
var ok bool
|
||||
var value reflect.Value
|
||||
|
||||
switch element := elem.(type) {
|
||||
case Input:
|
||||
element.value, ok = s.(string)
|
||||
if !ok {
|
||||
return value, fmt.Errorf("value %v is not allowed for input element", s)
|
||||
}
|
||||
if !utf8.ValidString(element.value) {
|
||||
return value, fmt.Errorf("value %v is not valid UTF8", s)
|
||||
}
|
||||
value = reflect.ValueOf(element)
|
||||
case Toggle:
|
||||
element.value, ok = s.(bool)
|
||||
if !ok {
|
||||
return value, fmt.Errorf("value %v is not allowed for toggle element", s)
|
||||
}
|
||||
value = reflect.ValueOf(element)
|
||||
case Slider:
|
||||
v, ok := s.(json.Number)
|
||||
f, err := v.Float64()
|
||||
if !ok || err != nil {
|
||||
return value, fmt.Errorf("value %v is not allowed for slider element", s)
|
||||
}
|
||||
if f > element.Max || f < element.Min {
|
||||
return value, fmt.Errorf("slider value %v is out of range %v-%v", f, element.Min, element.Max)
|
||||
}
|
||||
element.value = f
|
||||
value = reflect.ValueOf(element)
|
||||
case Dropdown:
|
||||
v, ok := s.(json.Number)
|
||||
f, err := v.Int64()
|
||||
if !ok || err != nil {
|
||||
return value, fmt.Errorf("value %v is not allowed for dropdown element", s)
|
||||
}
|
||||
if f < 0 || int(f) >= len(element.Options) {
|
||||
return value, fmt.Errorf("dropdown value %v is out of range %v-%v", f, 0, len(element.Options)-1)
|
||||
}
|
||||
element.value = int(f)
|
||||
value = reflect.ValueOf(element)
|
||||
case StepSlider:
|
||||
v, ok := s.(json.Number)
|
||||
f, err := v.Int64()
|
||||
if !ok || err != nil {
|
||||
return value, fmt.Errorf("value %v is not allowed for dropdown element", s)
|
||||
}
|
||||
if f < 0 || int(f) >= len(element.Options) {
|
||||
return value, fmt.Errorf("dropdown value %v is out of range %v-%v", f, 0, len(element.Options)-1)
|
||||
}
|
||||
element.value = int(f)
|
||||
value = reflect.ValueOf(element)
|
||||
}
|
||||
return value, nil
|
||||
}
|
||||
|
||||
// verify verifies if the form is valid, checking if the fields all implement the Element interface. It panics
|
||||
// if the form is not valid.
|
||||
func (f Custom) verify() {
|
||||
el := reflect.TypeOf((*Element)(nil)).Elem()
|
||||
|
||||
v := reflect.New(reflect.TypeOf(f.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(f.submittable))
|
||||
|
||||
t := reflect.TypeOf(f.submittable)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !v.Field(i).CanSet() {
|
||||
continue
|
||||
}
|
||||
if !t.Field(i).Type.Implements(el) {
|
||||
panic("all exported fields must implement form.Element interface")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// format is a utility function to format a list of values to have spaces between them, but no newline at the
|
||||
// end.
|
||||
func format(a []any) string {
|
||||
return strings.TrimSuffix(strings.TrimSuffix(fmt.Sprintln(a...), "\n"), "\n")
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
)
|
||||
|
||||
// Menu represents a menu form. These menus are made up of a title and a body, with a number of elements which
|
||||
// come below the body. These elements can include buttons, dividers, headers, and labels.
|
||||
type Menu struct {
|
||||
title, body string
|
||||
submittable MenuSubmittable
|
||||
elements []MenuElement
|
||||
}
|
||||
|
||||
// NewMenu creates a new Menu form using the MenuSubmittable passed to handle the output of the form. The
|
||||
// title passed is formatted following the rules of fmt.Sprintln.
|
||||
func NewMenu(submittable MenuSubmittable, title ...any) Menu {
|
||||
t := reflect.TypeOf(submittable)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic("submittable must be struct")
|
||||
}
|
||||
m := Menu{title: format(title), submittable: submittable}
|
||||
m.verify()
|
||||
return m
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (m Menu) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "form",
|
||||
"title": m.title,
|
||||
"content": m.body,
|
||||
"elements": m.Elements(),
|
||||
})
|
||||
}
|
||||
|
||||
// WithBody creates a copy of the Menu form and changes its body to the body passed, after which the new Menu
|
||||
// form is returned. The text is formatted following the rules of fmt.Sprintln.
|
||||
func (m Menu) WithBody(body ...any) Menu {
|
||||
m.body = format(body)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddButton appends a button to the menu's element list and returns the updated Menu.
|
||||
func (m Menu) AddButton(button Button) Menu {
|
||||
m.elements = append(m.elements, button)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddDivider appends a divider to the menu's element list and returns the updated Menu.
|
||||
func (m Menu) AddDivider(divider Divider) Menu {
|
||||
m.elements = append(m.elements, divider)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddHeader appends a header to the menu's element list and returns the updated Menu.
|
||||
func (m Menu) AddHeader(header Header) Menu {
|
||||
m.elements = append(m.elements, header)
|
||||
return m
|
||||
}
|
||||
|
||||
// AddLabel appends a label to the menu's element list and returns the updated Menu.
|
||||
func (m Menu) AddLabel(label Label) Menu {
|
||||
m.elements = append(m.elements, label)
|
||||
return m
|
||||
}
|
||||
|
||||
// WithButtons creates a copy of the Menu form and appends the buttons passed to the existing elements, after
|
||||
// which the new Menu form is returned.
|
||||
func (m Menu) WithButtons(buttons ...Button) Menu {
|
||||
for _, b := range buttons {
|
||||
m.elements = append(m.elements, b)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// WithElements creates a copy of the Menu form and appends the elements passed to the existing elements, after
|
||||
// which the new Menu form is returned. This allows adding any MenuElement type.
|
||||
func (m Menu) WithElements(elements ...MenuElement) Menu {
|
||||
m.elements = append(m.elements, elements...)
|
||||
return m
|
||||
}
|
||||
|
||||
// Title returns the formatted title passed to the menu upon construction using NewMenu().
|
||||
func (m Menu) Title() string {
|
||||
return m.title
|
||||
}
|
||||
|
||||
// Body returns the formatted text in the body passed to the menu using WithBody().
|
||||
func (m Menu) Body() string {
|
||||
return m.body
|
||||
}
|
||||
|
||||
// Buttons returns a list of all buttons of the MenuSubmittable. It collects buttons from the MenuSubmittable
|
||||
// fields and any buttons added via WithButtons(), AddButton().
|
||||
func (m Menu) Buttons() []Button {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
|
||||
buttons := make([]Button, 0)
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
if b, ok := field.Interface().(Button); ok {
|
||||
buttons = append(buttons, b)
|
||||
}
|
||||
}
|
||||
for _, elem := range m.elements {
|
||||
if b, ok := elem.(Button); ok {
|
||||
buttons = append(buttons, b)
|
||||
}
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
// Elements returns all elements of this menu form. It collects elements from the MenuSubmittable
|
||||
// fields and any elements added via WithElements().
|
||||
func (m Menu) Elements() []MenuElement {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
|
||||
elements := make([]MenuElement, 0, v.NumField()+len(m.elements))
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
elements = append(elements, field.Interface().(MenuElement))
|
||||
}
|
||||
elements = append(elements, m.elements...)
|
||||
return elements
|
||||
}
|
||||
|
||||
// SubmitJSON submits a JSON value to the menu, containing the index of the button clicked.
|
||||
func (m Menu) SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error {
|
||||
if b == nil {
|
||||
if closer, ok := m.submittable.(Closer); ok {
|
||||
closer.Close(submitter, tx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var index uint
|
||||
err := json.Unmarshal(b, &index)
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot parse button index as int: %w", err)
|
||||
}
|
||||
buttons := m.Buttons()
|
||||
if index >= uint(len(buttons)) {
|
||||
return fmt.Errorf("button index points to inexistent button: %v (only %v buttons present)", index, len(buttons))
|
||||
}
|
||||
m.submittable.Submit(submitter, buttons[index], tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// verify verifies if the form is valid, checking all exported fields implement MenuElement.
|
||||
// It panics if the form is not valid.
|
||||
func (m Menu) verify() {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !v.Field(i).CanSet() {
|
||||
continue
|
||||
}
|
||||
if _, ok := v.Field(i).Interface().(MenuElement); !ok {
|
||||
panic("all exported fields must implement form.MenuElement")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package form
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"github.com/df-mc/dragonfly/server/world"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
// Modal represents a modal form. These forms have a body with text and two buttons at the end, typically one
|
||||
// for Yes and one for No. These buttons may have custom text, but can, unlike with a Menu form, not have
|
||||
// images next to them.
|
||||
type Modal struct {
|
||||
title, body string
|
||||
submittable ModalSubmittable
|
||||
}
|
||||
|
||||
// NewModal creates a new Modal form using the ModalSubmittable passed to handle the output of the form. The
|
||||
// title passed is formatted following the fmt.Sprintln rules.
|
||||
// Default 'yes' and 'no' buttons may be passed by setting the two exported struct fields of the submittable
|
||||
// to YesButton() and NoButton() respectively.
|
||||
func NewModal(submittable ModalSubmittable, title ...any) Modal {
|
||||
t := reflect.TypeOf(submittable)
|
||||
if t.Kind() != reflect.Struct {
|
||||
panic("submittable must be struct")
|
||||
}
|
||||
m := Modal{title: format(title), submittable: submittable}
|
||||
m.verify()
|
||||
return m
|
||||
}
|
||||
|
||||
// YesButton returns a Button which may be used as a default 'yes' button for a modal form.
|
||||
func YesButton() Button {
|
||||
return Button{Text: "gui.yes"}
|
||||
}
|
||||
|
||||
// NoButton returns a Button which may be used as a default 'no' button for a modal form.
|
||||
func NoButton() Button {
|
||||
return Button{Text: "gui.no"}
|
||||
}
|
||||
|
||||
// MarshalJSON ...
|
||||
func (m Modal) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(map[string]any{
|
||||
"type": "modal",
|
||||
"title": m.title,
|
||||
"content": m.body,
|
||||
"button1": m.Buttons()[0].Text,
|
||||
"button2": m.Buttons()[1].Text,
|
||||
})
|
||||
}
|
||||
|
||||
// WithBody creates a copy of the Modal form and changes its body to the body passed, after which the new Modal
|
||||
// form is returned. The text is formatted following the rules of fmt.Sprintln.
|
||||
func (m Modal) WithBody(body ...any) Modal {
|
||||
m.body = format(body)
|
||||
return m
|
||||
}
|
||||
|
||||
// Title returns the formatted title passed to the menu upon construction using NewModal().
|
||||
func (m Modal) Title() string {
|
||||
return m.title
|
||||
}
|
||||
|
||||
// Body returns the formatted text in the body passed to the menu using WithBody().
|
||||
func (m Modal) Body() string {
|
||||
return m.body
|
||||
}
|
||||
|
||||
// SubmitJSON submits a JSON byte slice to the modal form. This byte slice contains a JSON encoded bool in it,
|
||||
// which is used to determine which button was clicked.
|
||||
func (m Modal) SubmitJSON(b []byte, submitter Submitter, tx *world.Tx) error {
|
||||
if b == nil {
|
||||
if closer, ok := m.submittable.(Closer); ok {
|
||||
closer.Close(submitter, tx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var value bool
|
||||
if err := json.Unmarshal(b, &value); err != nil {
|
||||
return fmt.Errorf("error parsing JSON as bool: %w", err)
|
||||
}
|
||||
if value {
|
||||
m.submittable.Submit(submitter, m.Buttons()[0], tx)
|
||||
return nil
|
||||
}
|
||||
m.submittable.Submit(submitter, m.Buttons()[1], tx)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Buttons returns a list of all buttons of the Modal form, which will always be a total of two buttons.
|
||||
func (m Modal) Buttons() []Button {
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
|
||||
buttons := make([]Button, 0, v.NumField())
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
field := v.Field(i)
|
||||
if !field.CanSet() {
|
||||
continue
|
||||
}
|
||||
// Each exported field is guaranteed to be of type Button.
|
||||
buttons = append(buttons, field.Interface().(Button))
|
||||
}
|
||||
return buttons
|
||||
}
|
||||
|
||||
// verify verifies that the Modal form is valid. It checks if exactly two exported fields are present and
|
||||
// ensures that both have the Button type.
|
||||
func (m Modal) verify() {
|
||||
var count int
|
||||
|
||||
v := reflect.New(reflect.TypeOf(m.submittable)).Elem()
|
||||
v.Set(reflect.ValueOf(m.submittable))
|
||||
|
||||
for i := 0; i < v.NumField(); i++ {
|
||||
if !v.Field(i).CanSet() {
|
||||
continue
|
||||
}
|
||||
if _, ok := v.Field(i).Interface().(Button); !ok {
|
||||
panic("both exported fields must be of the type form.Button")
|
||||
}
|
||||
count++
|
||||
}
|
||||
if count != 2 {
|
||||
panic("modal form must have exactly two exported fields of the type form.Button")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package form
|
||||
|
||||
import "github.com/df-mc/dragonfly/server/world"
|
||||
|
||||
// Submittable is a structure which may be submitted by sending it as a form using form.New(). When filled out
|
||||
// and submitted, the struct will have its Submit method called and its fields will have the values that the
|
||||
// Submitter passed filled out.
|
||||
// The fields of a Submittable struct must be either unexported or have a type of one of those that implement
|
||||
// the form.Element interface.
|
||||
type Submittable interface {
|
||||
// Submit is called when the Submitter submits the form sent to it. Once this method is called, all fields
|
||||
// in the struct will have their values filled out as filled out by the Submitter.
|
||||
Submit(submitter Submitter, tx *world.Tx)
|
||||
}
|
||||
|
||||
// MenuSubmittable is a structure which may be submitted by sending it as a form using form.NewMenu(), much
|
||||
// like a Submittable. The struct will have its Submit method called with the button pressed.
|
||||
// A struct that implements the MenuSubmittable interface must only have exported fields with the type
|
||||
// form.Button.
|
||||
type MenuSubmittable interface {
|
||||
// Submit is called when the Submitter submits the menu form sent to it. The method is called with the
|
||||
// button that was pressed. It may be compared with buttons in the MenuSubmittable struct to check which
|
||||
// button was pressed.
|
||||
Submit(submitter Submitter, pressed Button, tx *world.Tx)
|
||||
}
|
||||
|
||||
// ModalSubmittable is a structure which may be submitted by sending it as a form using form.NewModal(), much
|
||||
// like a Submittable and a MenuSubmittable. The struct will have its Submit method called with the button
|
||||
// pressed.
|
||||
// A struct that implements the ModalSubmittable interface must have exactly two exported fields with the type
|
||||
// form.Button, which may be used to specify the text of the Modal form's buttons. Unlike with a Menu form,
|
||||
// buttons on a Modal form will not have images.
|
||||
type ModalSubmittable MenuSubmittable
|
||||
|
||||
// Closer represents a form which has special logic when being closed by a Submitter.
|
||||
type Closer interface {
|
||||
// Close is called when the Submitter closes a form.
|
||||
Close(submitter Submitter, tx *world.Tx)
|
||||
}
|
||||
|
||||
// Submitter is an entity that is able to submit a form sent to it. It is able to fill out fields in the form
|
||||
// which will then be present when handled. The Submitter is also able to close the form.
|
||||
type Submitter interface {
|
||||
SendForm(form Form)
|
||||
CloseForm()
|
||||
}
|
||||
Reference in New Issue
Block a user