Files
ordoor/internal/ui/interface.go

177 lines
3.9 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 []*staticElement
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 {
var tooltip string // Draw this last, so it's on top of everything
mousePt := i.getMousePos(screen.Size())
geo := i.scale(screen.Size())
do := &ebiten.DrawImageOptions{GeoM: geo}
for _, s := range i.static {
if s.image != nil {
do.GeoM.Translate(geo.Apply(float64(s.bounds.Min.X), float64(s.bounds.Min.X)))
if err := screen.DrawImage(s.image, do); err != nil {
return err
}
do.GeoM = geo
}
if mousePt.In(s.bounds) && s.tooltip != "" {
tooltip = s.tooltip
}
}
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 != "" {
tooltip = widget.Tooltip
}
}
if tooltip != "" {
cX, cY := ebiten.CursorPosition()
ebitenutil.DebugPrintAt(screen, tooltip, cX+16, cY-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))
}