2020-03-24 20:21:55 +00:00
|
|
|
package ui
|
|
|
|
|
|
|
|
import (
|
|
|
|
"fmt"
|
|
|
|
"image"
|
2020-03-27 02:07:28 +00:00
|
|
|
"runtime/debug"
|
2020-03-24 20:21:55 +00:00
|
|
|
|
|
|
|
"github.com/hajimehoshi/ebiten"
|
|
|
|
"github.com/hajimehoshi/ebiten/ebitenutil"
|
|
|
|
|
|
|
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
OriginalX = 640.0
|
|
|
|
OriginalY = 480.0
|
|
|
|
)
|
|
|
|
|
|
|
|
// 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.
|
2020-04-14 03:14:49 +01:00
|
|
|
//
|
|
|
|
// TODO: move scaling responsibilities to Window?
|
2020-03-24 20:21:55 +00:00
|
|
|
type Driver struct {
|
|
|
|
Name string
|
|
|
|
|
2020-04-10 20:54:58 +01:00
|
|
|
assets *assetstore.AssetStore
|
|
|
|
menu *assetstore.Menu
|
2020-03-24 20:21:55 +00:00
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
// UI elements we need to drive. Note that widgets are hierarchical - these
|
|
|
|
// are just the toplevel. Dialogues are separated out. We only want to show
|
|
|
|
// one dialogue at a time, and if a dialogue is active, the main widgets are
|
|
|
|
// unusable (i.e., dialogues are modal)
|
|
|
|
dialogues []*Widget
|
|
|
|
widgets []*Widget
|
|
|
|
|
|
|
|
activeDialogue *Widget
|
2020-03-24 20:21:55 +00:00
|
|
|
|
2020-04-10 20:54:58 +01:00
|
|
|
cursor assetstore.CursorName
|
|
|
|
|
2020-03-24 20:21:55 +00:00
|
|
|
// 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
|
|
|
|
}
|
|
|
|
|
2020-04-10 20:54:58 +01:00
|
|
|
func NewDriver(assets *assetstore.AssetStore, menu *assetstore.Menu) (*Driver, error) {
|
2020-03-24 20:21:55 +00:00
|
|
|
driver := &Driver{
|
|
|
|
Name: menu.Name,
|
2020-04-10 20:54:58 +01:00
|
|
|
|
|
|
|
assets: assets,
|
|
|
|
menu: menu,
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
for _, group := range menu.Groups() {
|
|
|
|
if err := driver.registerGroup(group); err != nil {
|
2020-03-24 20:21:55 +00:00
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return driver, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Driver) Update(screenX, screenY int) error {
|
2020-03-27 02:07:28 +00:00
|
|
|
if d == nil {
|
|
|
|
debug.PrintStack()
|
|
|
|
return fmt.Errorf("Tried to update a nil ui.Driver")
|
|
|
|
}
|
|
|
|
|
2020-03-24 20:21:55 +00:00
|
|
|
// 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
|
2020-04-14 12:12:37 +01:00
|
|
|
for _, hoverable := range d.activeHoverables() {
|
2020-03-24 20:21:55 +00:00
|
|
|
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)
|
2020-04-14 12:12:37 +01:00
|
|
|
for _, clickable := range d.activeClickables() {
|
2020-03-24 20:21:55 +00:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
for _, mouseable := range d.activeMouseables() {
|
2020-03-24 22:33:26 +00:00
|
|
|
mouseable.registerMousePosition(d.cursorOrig)
|
|
|
|
}
|
|
|
|
|
2020-03-24 20:21:55 +00:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Driver) Draw(screen *ebiten.Image) error {
|
2020-03-27 02:07:28 +00:00
|
|
|
if d == nil {
|
|
|
|
debug.PrintStack()
|
|
|
|
return fmt.Errorf("Tried to draw a nil ui.Driver")
|
|
|
|
}
|
|
|
|
|
2020-03-24 20:21:55 +00:00
|
|
|
var do ebiten.DrawImageOptions
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
for _, paint := range d.activePaintables() {
|
2020-03-24 20:21:55 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2020-04-10 20:54:58 +01:00
|
|
|
func (d *Driver) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
|
|
|
cursor, err := d.assets.Cursor(d.cursor)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
op := &ebiten.DrawImageOptions{}
|
|
|
|
op.GeoM.Translate(float64(d.cursorOrig.X), float64(d.cursorOrig.Y))
|
|
|
|
op.GeoM.Concat(d.orig2native)
|
|
|
|
op.GeoM.Translate(float64(-cursor.Hotspot.X), float64(-cursor.Hotspot.Y))
|
|
|
|
|
|
|
|
return cursor.Image, op, nil
|
|
|
|
}
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
func (d *Driver) allClickables() []clickable {
|
2020-04-14 03:14:49 +01:00
|
|
|
var out []clickable
|
2020-03-24 20:21:55 +00:00
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
for _, widget := range d.widgets {
|
|
|
|
out = append(out, widget.clickables()...)
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
for _, widget := range d.dialogues {
|
|
|
|
out = append(out, widget.clickables()...)
|
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
return out
|
|
|
|
}
|
2020-03-24 20:21:55 +00:00
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
func (d *Driver) allFreezables() []freezable {
|
2020-04-14 03:14:49 +01:00
|
|
|
var out []freezable
|
|
|
|
for _, widget := range d.widgets {
|
|
|
|
out = append(out, widget.freezables()...)
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
for _, widget := range d.dialogues {
|
|
|
|
out = append(out, widget.freezables()...)
|
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
return out
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
func (d *Driver) allValueables() []valueable {
|
|
|
|
var out []valueable
|
2020-04-14 03:14:49 +01:00
|
|
|
|
|
|
|
for _, widget := range d.widgets {
|
2020-04-14 12:12:37 +01:00
|
|
|
out = append(out, widget.valueables()...)
|
|
|
|
}
|
|
|
|
|
|
|
|
for _, widget := range d.dialogues {
|
|
|
|
out = append(out, widget.valueables()...)
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
2020-04-14 03:14:49 +01:00
|
|
|
|
|
|
|
return out
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
func (d *Driver) activeClickables() []clickable {
|
|
|
|
if d.activeDialogue != nil {
|
|
|
|
return d.activeDialogue.clickables()
|
|
|
|
}
|
2020-04-14 03:14:49 +01:00
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
var out []clickable
|
2020-04-14 03:14:49 +01:00
|
|
|
for _, widget := range d.widgets {
|
2020-04-14 12:12:37 +01:00
|
|
|
out = append(out, widget.clickables()...)
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
2020-04-14 03:14:49 +01:00
|
|
|
|
|
|
|
return out
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
func (d *Driver) activeHoverables() []hoverable {
|
|
|
|
if d.activeDialogue != nil {
|
|
|
|
return d.activeDialogue.hoverables()
|
|
|
|
}
|
2020-04-14 03:14:49 +01:00
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
var out []hoverable
|
2020-04-14 03:14:49 +01:00
|
|
|
for _, widget := range d.widgets {
|
2020-04-14 12:12:37 +01:00
|
|
|
out = append(out, widget.hoverables()...)
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
func (d *Driver) activeMouseables() []mouseable {
|
2020-04-14 03:14:49 +01:00
|
|
|
if d.activeDialogue != nil {
|
2020-04-14 12:12:37 +01:00
|
|
|
return d.activeDialogue.mouseables()
|
|
|
|
}
|
|
|
|
|
|
|
|
var out []mouseable
|
|
|
|
for _, widget := range d.widgets {
|
|
|
|
out = append(out, widget.mouseables()...)
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
2020-04-14 03:14:49 +01:00
|
|
|
|
|
|
|
return out
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 12:12:37 +01:00
|
|
|
func (d *Driver) activePaintables() []paintable {
|
|
|
|
var out []paintable
|
2020-04-14 03:14:49 +01:00
|
|
|
|
|
|
|
for _, widget := range d.widgets {
|
2020-04-14 12:12:37 +01:00
|
|
|
out = append(out, widget.paintables()...)
|
|
|
|
}
|
|
|
|
|
|
|
|
if d.activeDialogue != nil {
|
|
|
|
out = append(out, d.activeDialogue.paintables()...)
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
2020-04-14 03:14:49 +01:00
|
|
|
|
|
|
|
return out
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|