Implement the options menu, part 1

This commit implements loading and saving options from/to config, and
enough UI toolkit magic to allow changes to boolean options to be
persisted.

We now respect the "play movies" setting and take screen resolution
from the config file.
This commit is contained in:
2020-03-22 22:12:59 +00:00
parent 0adbfaa573
commit 971b3178d6
9 changed files with 481 additions and 96 deletions

View File

@@ -3,6 +3,7 @@ package ui
import (
"fmt"
"image"
"log"
"reflect" // For DeepEqual
"github.com/hajimehoshi/ebiten"
@@ -30,7 +31,6 @@ type Interface struct {
func NewInterface(menu *assetstore.Menu) (*Interface, error) {
iface := &Interface{
Name: menu.Name,
menu: menu,
}
@@ -112,25 +112,19 @@ func (i *Interface) Draw(screen *ebiten.Image) error {
}
func (i *Interface) addRecord(record *menus.Record) error {
switch record.Type {
case menus.TypeStatic: // These are static
if sprite, err := i.menu.Sprite(record.SpriteId[0]); err != nil {
return err
} else {
i.static = append(i.static, sprite)
}
case menus.TypeMenu: // These aren't drawable and can be ignored
case menus.TypeOverlay, menus.TypeMainButton: // Widgets \o/
if widget, err := i.widgetFromRecord(record); err != nil {
return err
} else {
i.widgets = append(i.widgets, widget)
}
log.Printf("Adding record: %#+v", record)
default:
handler, ok := setupHandlers[record.Type]
if !ok {
return fmt.Errorf("ui.interface: encountered unknown menu record: %#+v", record)
}
if handler != nil {
if err := handler(i, record); err != nil {
return err
}
}
// Recursively add all children
for _, record := range record.Children {
if err := i.addRecord(record); err != nil {
@@ -166,47 +160,3 @@ func (i *Interface) getMousePos(w, h int) image.Point {
return image.Pt(int(sX), int(sY))
}
func (i *Interface) widgetFromRecord(record *menus.Record) (*Widget, error) {
// FIXME: we assume that all widgets have a share sprite, but is that true?
sprite, err := i.menu.Sprite(record.Share)
if err != nil {
return nil, err
}
var path []int
for r := record; r != nil; r = r.Parent {
path = append([]int{r.Id}, path...)
}
widget := &Widget{
Bounds: sprite.Rect,
Tooltip: record.Desc,
path: path,
record: record,
sprite: sprite,
}
switch record.Type {
case menus.TypeMainButton:
hovers, err := i.menu.Images(record.SpriteId[0], record.DrawType)
if err != nil {
return nil, err
}
widget.hoverAnimation = hovers
pressed, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return nil, err
}
widget.mouseButtonDownImage = pressed.Image
disabled, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return nil, err
}
widget.disabledImage = disabled.Image
}
return widget, nil
}

View File

@@ -0,0 +1,185 @@
package ui
import (
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/menus"
)
// Setup handlers know how to handle each type of widget
var setupHandlers = map[menus.MenuType]func(i *Interface, r *menus.Record) error {
menus.TypeStatic: handleStatic,
menus.TypeMenu: nil,
menus.TypeButton: handleButton,
menus.TypeInvokeButton: handleInvokeButton,
menus.TypeOverlay: handleStatic, // FIXME: more?
menus.TypeHypertext: nil, // FIXME: handle this
menus.TypeCheckbox: handleCheckbox,
menus.TypeAnimationSample: nil, // FIXME: handle this
menus.TypeMainButton: handleMainButton,
menus.TypeSlider: nil, // FIXME: handle this
}
func handleStatic(i *Interface, record *menus.Record) error {
spriteId := record.Share
// FIXME: SpriteID takes precedence over SHARE if present, but is that right?
if len(record.SpriteId) > 0 && record.SpriteId[0] != -1 {
spriteId = record.SpriteId[0]
}
sprite, err := i.menu.Sprite(spriteId)
if err != nil {
return err
}
i.static = append(i.static, sprite)
return nil
}
// A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled.
func handleCheckbox(i *Interface, record *menus.Record) error {
widget, err := i.widgetFromRecord(record, record.Share)
if err != nil {
return err
}
unchecked := widget.sprite
disabled, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return err
}
checked, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return err
}
widget.Value = "0"
widget.OnMouseClick = func() {
if widget.Value == "1" { // Click disables
widget.Value = "0"
} else { // Click enables
widget.Value = "1"
}
}
widget.disabledImage = disabled.Image
widget.valueToImage = func() *ebiten.Image {
if widget.Value == "1" {
return checked.Image
}
return unchecked.Image
}
i.widgets = append(i.widgets, widget)
return nil
}
func handleButton(i *Interface, record *menus.Record) error {
spriteId := record.SpriteId[0]
widget, err := i.widgetFromRecord(record, spriteId)
if err != nil {
return err
}
pressed, err := i.menu.Sprite(spriteId + 1)
if err != nil {
return err
}
disabled, err := i.menu.Sprite(spriteId + 2)
if err != nil {
return err
}
widget.mouseButtonDownImage = pressed.Image
widget.disabledImage = disabled.Image
i.widgets = append(i.widgets, widget)
return nil
}
func handleInvokeButton(i *Interface, record *menus.Record) error {
widget, err := i.widgetFromRecord(record, record.Share)
if err != nil {
return err
}
pressed, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return err
}
disabled, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return err
}
widget.mouseButtonDownImage = pressed.Image
widget.disabledImage = disabled.Image
i.widgets = append(i.widgets, widget)
return nil
}
// A main button is quite complex. It has 3 main sprites and a hover animation
func handleMainButton(i *Interface, record *menus.Record) error {
widget, err := i.widgetFromRecord(record, record.Share)
if err != nil {
return err
}
pressed, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return err
}
disabled, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return err
}
hovers, err := i.menu.Images(record.SpriteId[0], record.DrawType)
if err != nil {
return err
}
widget.mouseButtonDownImage = pressed.Image
widget.disabledImage = disabled.Image
widget.hoverAnimation = hovers
i.widgets = append(i.widgets, widget)
return nil
}
// Widgets need a bounding box determined by a sprite. Different widgets specify
// their sprites in different attributes, so pass in the right sprite externally
func (i *Interface) widgetFromRecord(record *menus.Record, spriteId int) (*Widget, error) {
sprite, err := i.menu.Sprite(spriteId)
if err != nil {
return nil, err
}
var path []int
for r := record; r != nil; r = r.Parent {
path = append([]int{r.Id}, path...)
}
widget := &Widget{
Bounds: sprite.Rect,
Tooltip: record.Desc,
path: path,
record: record,
sprite: sprite,
}
return widget, nil
}

View File

@@ -15,6 +15,7 @@ type Widget struct {
// Position on the screen in original (i.e., unscaled) coordinates
Bounds image.Rectangle
Tooltip string
Value string // #dealwithit for bools and ints and so on :p
OnHoverEnter func()
OnHoverLeave func()
@@ -40,6 +41,8 @@ type Widget struct {
path []int
record *menus.Record
sprite *assetstore.Sprite
valueToImage func() *ebiten.Image
}
func (w *Widget) Disable() {
@@ -86,7 +89,7 @@ func (w *Widget) Image(aniStep int) (*ebiten.Image, error) {
return w.disabledImage, nil
}
if w.hoverState && w.mouseButtonState {
if w.mouseButtonDownImage != nil && w.hoverState && w.mouseButtonState {
return w.mouseButtonDownImage, nil
}
@@ -94,5 +97,9 @@ func (w *Widget) Image(aniStep int) (*ebiten.Image, error) {
return w.hoverAnimation[(aniStep)%len(w.hoverAnimation)], nil
}
if w.valueToImage != nil {
return w.valueToImage(), nil
}
return w.sprite.Image, nil
}