369 lines
8.9 KiB
Go
369 lines
8.9 KiB
Go
package ui
|
|
|
|
import (
|
|
"fmt"
|
|
"image"
|
|
"log"
|
|
"strconv"
|
|
|
|
"github.com/hajimehoshi/ebiten"
|
|
"github.com/hajimehoshi/ebiten/ebitenutil"
|
|
|
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
|
)
|
|
|
|
func init() {
|
|
// These menu types don't need driving, so we can ignore them
|
|
registerBuilder(menus.TypeMenu, nil) // Menus are just containers
|
|
registerBuilder(menus.TypeDragMenu, nil) // Menus are just containers
|
|
|
|
// FIXME: these need implementing
|
|
|
|
// Needed for Keyboard.mnu (main -> options -> keyboard)
|
|
registerBuilder(menus.TypeLineKbd, registerDebug("Unimplemented LineKbd", nil))
|
|
registerBuilder(menus.TypeDialogue, registerDebug("Unimplemented Dialogue", nil))
|
|
|
|
// Needed for Arrange.mnu (???)
|
|
registerBuilder(menus.TypeSquadButton, registerDebug("Unimplemented SquadButton", nil))
|
|
registerBuilder(menus.TypeAnimationToo, registerDebug("Unimplemented AnimationToo", nil))
|
|
|
|
// Needed for Bridge.mnu
|
|
registerBuilder(menus.TypeDoorHotspot, registerDebug("Unimplemented DoorHotspot", nil))
|
|
|
|
// Needed for Briefing.mnu
|
|
registerBuilder(menus.TypeLineBriefing, registerDebug("Unimplemented LineBriefing", nil))
|
|
|
|
// Needed for ChaEquip.mnu
|
|
registerBuilder(menus.TypeUnknown1, registerDebug("Unimplemented Unknown1", nil))
|
|
registerBuilder(menus.TypeThumb, registerDebug("Unimplemented Thumb", nil))
|
|
|
|
// Needed for MainGameChaos.mnu
|
|
registerBuilder(menus.TypeStatusBar, registerDebug("Unimplemented StatusBar", nil))
|
|
|
|
// Needed for Multiplayer_Choose.mnu
|
|
registerBuilder(menus.TypeComboBoxItem, registerDebug("Unimplemented ComboBoxItem", nil))
|
|
registerBuilder(menus.TypeDropdownButton, registerDebug("Unimplemented DropdownButton", nil))
|
|
|
|
// Needed for Multiplayer_Configure.mnu
|
|
registerBuilder(menus.TypeEditBox, registerDebug("Unimplemented EditBox", nil))
|
|
|
|
// Needed for Multiplayer_Connect.mnu
|
|
registerBuilder(menus.TypeRadioButton, registerDebug("Unimplemented RadioButton", nil))
|
|
}
|
|
|
|
const (
|
|
OriginalX = 640.0
|
|
OriginalY = 480.0
|
|
)
|
|
|
|
var (
|
|
// Widgets register their builder here
|
|
widgetBuilders = map[menus.MenuType]builderFunc{}
|
|
)
|
|
|
|
// Used to add widgets to a driver
|
|
type builderFunc func(d *Driver, r *menus.Record) error
|
|
|
|
func registerDebug(reason string, onward builderFunc) builderFunc {
|
|
return func(d *Driver, r *menus.Record) error {
|
|
log.Printf("%v: %#+v", reason, r)
|
|
if onward == nil {
|
|
return registerStatic(d, r)
|
|
} else {
|
|
return onward(d, r)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
func registerBuilder(t menus.MenuType, f builderFunc) {
|
|
if _, ok := widgetBuilders[t]; ok {
|
|
panic(fmt.Sprintf("A builder for menu type %v already exists", t))
|
|
}
|
|
|
|
widgetBuilders[t] = f
|
|
}
|
|
|
|
// Driver acts as an interface between the main loop and the widgets specified
|
|
// in a menu.
|
|
//
|
|
// Menu assets assume a 640x480 screen; Driver is responsible for scaling to the
|
|
// actual screen size when drawing.
|
|
type Driver struct {
|
|
Name string
|
|
|
|
menu *assetstore.Menu
|
|
|
|
// UI elements we need to drive
|
|
clickables []clickable
|
|
freezables []freezable
|
|
hoverables []hoverable
|
|
mouseables []mouseable
|
|
paintables []paintable
|
|
valueables []valueable
|
|
|
|
// The cursor in two different coordinate spaces: original, and screen-scaled
|
|
cursorOrig image.Point
|
|
cursorScaled image.Point
|
|
|
|
// These two matrices are used for scaling between the two
|
|
orig2native ebiten.GeoM
|
|
native2orig ebiten.GeoM
|
|
|
|
ticks int // Used in animation effects
|
|
tooltip string
|
|
}
|
|
|
|
func NewDriver(menu *assetstore.Menu) (*Driver, error) {
|
|
driver := &Driver{
|
|
Name: menu.Name,
|
|
menu: menu,
|
|
}
|
|
|
|
for _, record := range menu.Records() {
|
|
if err := driver.addRecord(record); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return driver, nil
|
|
}
|
|
|
|
func (d *Driver) Value(id string, into *string) error {
|
|
for _, valueable := range d.valueables {
|
|
if valueable.id() == id {
|
|
*into = valueable.value()
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("Couldn't find valueable widget %q", id)
|
|
}
|
|
|
|
func (d *Driver) SetValue(id, value string) error {
|
|
for _, valueable := range d.valueables {
|
|
if valueable.id() == id {
|
|
valueable.setValue(value)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("Couldn't find valueable widget %q", id)
|
|
}
|
|
|
|
func (d *Driver) ValueBool(id string, into *bool) error {
|
|
var vStr string
|
|
if err := d.Value(id, &vStr); err != nil {
|
|
return err
|
|
}
|
|
|
|
*into = vStr == "1"
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) SetValueBool(id string, value bool) error {
|
|
vStr := "0"
|
|
if value {
|
|
vStr = "1"
|
|
}
|
|
|
|
return d.SetValue(id, vStr)
|
|
}
|
|
|
|
func (d *Driver) SetFreeze(id string, value bool) error {
|
|
for _, freezable := range d.freezables {
|
|
if freezable.id() == id {
|
|
freezable.setFreezeState(value)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("Couldn't find clickable widget %q", id)
|
|
}
|
|
|
|
func (d *Driver) OnClick(id string, f func()) error {
|
|
for _, clickable := range d.clickables {
|
|
if clickable.id() == id {
|
|
clickable.onClick(f)
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("Couldn't find clickable widget %q", id)
|
|
}
|
|
|
|
// FIXME: HURK. Surely I'm missing something? steps is value:offset
|
|
func (d *Driver) ConfigureSlider(id string, steps map[int]int) error {
|
|
for _, clickable := range d.clickables {
|
|
if slider, ok := clickable.(*slider); ok && slider.id() == id {
|
|
slider.steps = steps
|
|
|
|
return nil
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("Couldn't find slider %q", id)
|
|
}
|
|
|
|
func (d *Driver) ValueInt(id string, into *int) error {
|
|
var vStr string
|
|
if err := d.Value(id, &vStr); err != nil {
|
|
return err
|
|
}
|
|
|
|
value, err := strconv.Atoi(vStr)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
*into = value
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) SetValueInt(id string, value int) error {
|
|
vStr := strconv.Itoa(value)
|
|
|
|
return d.SetValue(id, vStr)
|
|
}
|
|
|
|
func (d *Driver) Update(screenX, screenY int) error {
|
|
// This will be updated while processing hovers
|
|
d.tooltip = ""
|
|
d.ticks += 1
|
|
|
|
// Update translation matrices
|
|
d.orig2native.Reset()
|
|
d.orig2native.Scale(float64(screenX)/OriginalX, float64(screenY)/OriginalY)
|
|
|
|
d.native2orig = d.orig2native
|
|
d.native2orig.Invert()
|
|
|
|
// Update original and scaled mouse coordinates
|
|
mouseX, mouseY := ebiten.CursorPosition()
|
|
d.cursorScaled = image.Pt(mouseX, mouseY)
|
|
|
|
mnX, mnY := d.native2orig.Apply(float64(mouseX), float64(mouseY))
|
|
d.cursorOrig = image.Pt(int(mnX), int(mnY))
|
|
|
|
// Dispatch notifications to our widgets
|
|
for _, hoverable := range d.hoverables {
|
|
inBounds := d.cursorOrig.In(hoverable.bounds())
|
|
|
|
d.hoverStartEvent(hoverable, inBounds)
|
|
d.hoverEndEvent(hoverable, inBounds)
|
|
|
|
if hoverable.hoverState() && hoverable.tooltip() != "" {
|
|
d.tooltip = hoverable.tooltip()
|
|
}
|
|
}
|
|
|
|
mouseIsDown := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)
|
|
for _, clickable := range d.clickables {
|
|
inBounds := d.cursorOrig.In(clickable.bounds())
|
|
mouseWasDown := clickable.mouseDownState()
|
|
|
|
d.mouseDownEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
|
|
d.mouseClickEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
|
|
d.mouseUpEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
|
|
}
|
|
|
|
for _, mouseable := range d.mouseables {
|
|
mouseable.registerMousePosition(d.cursorOrig)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) Draw(screen *ebiten.Image) error {
|
|
var do ebiten.DrawImageOptions
|
|
|
|
for _, paint := range d.paintables {
|
|
for _, region := range paint.regions(d.ticks) {
|
|
x, y := d.orig2native.Apply(float64(region.offset.X), float64(region.offset.Y))
|
|
|
|
do.GeoM = d.orig2native
|
|
do.GeoM.Translate(x, y)
|
|
|
|
if err := screen.DrawImage(region.image, &do); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if d.tooltip != "" {
|
|
x, y := d.cursorScaled.X+16, d.cursorScaled.Y-16
|
|
ebitenutil.DebugPrintAt(screen, d.tooltip, x, y)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) addRecord(record *menus.Record) error {
|
|
//log.Printf("Adding record: %#+v", record)
|
|
|
|
handler, ok := widgetBuilders[record.Type]
|
|
if !ok {
|
|
return fmt.Errorf("UI driver encountered unknown menu record: %#+v", record)
|
|
}
|
|
|
|
if handler != nil {
|
|
if err := handler(d, record); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Recursively add all children of this record
|
|
for _, record := range record.Children {
|
|
if err := d.addRecord(record); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (d *Driver) hoverStartEvent(h hoverable, inBounds bool) {
|
|
if inBounds && !h.hoverState() {
|
|
//log.Printf("hoverable false -> true")
|
|
h.setHoverState(true)
|
|
}
|
|
}
|
|
|
|
func (d *Driver) hoverEndEvent(h hoverable, inBounds bool) {
|
|
if !inBounds && h.hoverState() {
|
|
//log.Printf("hoverable true -> false")
|
|
h.setHoverState(false)
|
|
}
|
|
}
|
|
|
|
func (d *Driver) mouseDownEvent(c clickable, inBounds, wasDown, isDown bool) {
|
|
if inBounds && !wasDown && isDown {
|
|
//log.Printf("mouse down false -> true")
|
|
c.setMouseDownState(true)
|
|
}
|
|
}
|
|
|
|
func (d *Driver) mouseClickEvent(c clickable, inBounds, wasDown, isDown bool) {
|
|
if inBounds && wasDown && !isDown {
|
|
//log.Printf("mouse click")
|
|
c.registerMouseClick()
|
|
}
|
|
}
|
|
|
|
func (d *Driver) mouseUpEvent(c clickable, inBounds, wasDown, isDown bool) {
|
|
if inBounds {
|
|
if wasDown && !isDown {
|
|
//log.Printf("mouse down true -> false")
|
|
c.setMouseDownState(false)
|
|
}
|
|
} else {
|
|
if wasDown {
|
|
//log.Printf("mouse down true -> false")
|
|
c.setMouseDownState(false)
|
|
}
|
|
}
|
|
}
|