In this commit, we also remove code that doesn't properly belong in view-menu
213 lines
4.9 KiB
Go
213 lines
4.9 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"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 {
|
|
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)
|
|
}
|
|
|
|
default:
|
|
return fmt.Errorf("ui.interface: encountered unknown menu record: %#+v", record)
|
|
}
|
|
|
|
// 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))
|
|
}
|
|
|
|
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
|
|
}
|