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.
163 lines
3.7 KiB
Go
163 lines
3.7 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"log"
|
|
"reflect" // For DeepEqual
|
|
|
|
"github.com/hajimehoshi/ebiten"
|
|
"github.com/hajimehoshi/ebiten/ebitenutil"
|
|
|
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
|
)
|
|
|
|
// type Interface encapsulates a user interface, providing a means to track UI
|
|
// state, draw the interface, and execute code when the widgets are interacted
|
|
// with.
|
|
//
|
|
// The graphics for UI elements were all created with a 640x480 resolution in
|
|
// mind. The interface transparently scales them all to the current screen size
|
|
// to compensate.
|
|
type Interface struct {
|
|
Name string
|
|
menu *assetstore.Menu
|
|
static []*assetstore.Sprite // Static elements in the interface
|
|
ticks int
|
|
widgets []*Widget
|
|
}
|
|
|
|
func NewInterface(menu *assetstore.Menu) (*Interface, error) {
|
|
iface := &Interface{
|
|
Name: menu.Name,
|
|
menu: menu,
|
|
}
|
|
|
|
for _, record := range menu.Records() {
|
|
if err := iface.addRecord(record); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return iface, nil
|
|
}
|
|
|
|
// Find a widget by its hierarchical ID path
|
|
func (i *Interface) Widget(path ...int) (*Widget, error) {
|
|
for _, widget := range i.widgets {
|
|
if reflect.DeepEqual(path, widget.path) {
|
|
return widget, nil
|
|
}
|
|
}
|
|
|
|
return nil, fmt.Errorf("Couldn't find widget %#+v", path)
|
|
}
|
|
|
|
func (i *Interface) Update(screenX, screenY int) error {
|
|
// Used in animation effects
|
|
i.ticks += 1
|
|
|
|
mousePos := i.getMousePos(screenX, screenY)
|
|
|
|
// Iterate through all widgets, update mouse state
|
|
for _, widget := range i.widgets {
|
|
if widget.disabled {
|
|
continue // No activity for disabled widgets
|
|
}
|
|
|
|
mouseIsOver := mousePos.In(widget.Bounds)
|
|
widget.hovering(mouseIsOver)
|
|
widget.mouseButton(mouseIsOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft))
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *Interface) Draw(screen *ebiten.Image) error {
|
|
geo := i.scale(screen.Size())
|
|
do := &ebiten.DrawImageOptions{GeoM: geo}
|
|
|
|
for _, sprite := range i.static {
|
|
do.GeoM.Translate(geo.Apply(float64(sprite.XOffset), float64(sprite.YOffset)))
|
|
if err := screen.DrawImage(sprite.Image, do); err != nil {
|
|
return err
|
|
}
|
|
do.GeoM = geo
|
|
}
|
|
|
|
for _, widget := range i.widgets {
|
|
img, err := widget.Image(i.ticks / 2)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if img == nil {
|
|
continue
|
|
}
|
|
|
|
do.GeoM.Translate(geo.Apply(float64(widget.Bounds.Min.X), float64(widget.Bounds.Min.Y)))
|
|
if err := screen.DrawImage(img, do); err != nil {
|
|
return err
|
|
}
|
|
do.GeoM = geo
|
|
|
|
if widget.hoverState && widget.Tooltip != "" {
|
|
mouseX, mouseY := ebiten.CursorPosition()
|
|
ebitenutil.DebugPrintAt(screen, widget.Tooltip, mouseX+16, mouseY-16)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (i *Interface) addRecord(record *menus.Record) error {
|
|
log.Printf("Adding record: %#+v", record)
|
|
|
|
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 {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Works out how much we have to scale the current screen by to draw correctly
|
|
func (i *Interface) scale(w, h int) ebiten.GeoM {
|
|
var geo ebiten.GeoM
|
|
geo.Scale(float64(w)/640.0, float64(h)/480.0)
|
|
|
|
return geo
|
|
}
|
|
|
|
func (i *Interface) unscale(w, h int) ebiten.GeoM {
|
|
geo := i.scale(w, h)
|
|
geo.Invert()
|
|
|
|
return geo
|
|
}
|
|
|
|
// Returns the current position of the mouse in 640x480 coordinates. Needs the
|
|
// actual size of the screen to do so.
|
|
func (i *Interface) getMousePos(w, h int) image.Point {
|
|
cX, cY := ebiten.CursorPosition()
|
|
geo := i.unscale(w, h)
|
|
|
|
sX, sY := geo.Apply(float64(cX), float64(cY))
|
|
|
|
return image.Pt(int(sX), int(sY))
|
|
}
|