Allow dialogues to be hidden or shown

To do this, MENU and SUBMENU are split into two types (at last), and
a Widget type is introduced. This should allow lots of code to be
removed at some point.
This commit is contained in:
2020-04-14 03:14:49 +01:00
parent dc131939f4
commit 786d261f98
18 changed files with 1034 additions and 847 deletions

View File

@@ -3,98 +3,40 @@ package ui
import (
"fmt"
"image"
"log"
"runtime/debug"
"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() {
// FIXME: these need implementing
// 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) (children []*menus.Record, err error)
func registerDebug(reason string, onward builderFunc) builderFunc {
return func(d *Driver, r *menus.Record) ([]*menus.Record, error) {
log.Printf("%v: %v: %#+v", reason, r.Locator(), r)
if onward == nil {
return r.Children, nil
}
return onward(d, r)
}
}
func noChildren(f func(d *Driver, r *menus.Record) error) builderFunc {
return func(d *Driver, r *menus.Record) ([]*menus.Record, error) {
if len(r.Children) > 0 {
return nil, fmt.Errorf("Children in record %v:%v (%#+v)", r.Menu.Name, r.Path(), r)
}
return nil, f(d, r)
}
}
func ownedByMenu(d *Driver, r *menus.Record) ([]*menus.Record, error) {
return nil, fmt.Errorf("This record should be handled by a menu: %v:%v (%#+v)", r.Menu.Name, r.Path(), r)
}
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.
//
// TODO: move scaling responsibilities to Window?
type Driver struct {
Name string
assets *assetstore.AssetStore
menu *assetstore.Menu
// UI elements we need to drive
clickables []clickable
freezables []freezable
hoverables []hoverable
mouseables []mouseable
paintables []paintable
valueables []valueable
// 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
cursor assetstore.CursorName
@@ -118,8 +60,8 @@ func NewDriver(assets *assetstore.AssetStore, menu *assetstore.Menu) (*Driver, e
menu: menu,
}
for _, record := range menu.Records() {
if err := driver.addRecord(record); err != nil {
for _, group := range menu.Groups() {
if err := driver.registerGroup(group); err != nil {
return nil, err
}
}
@@ -127,104 +69,6 @@ func NewDriver(assets *assetstore.AssetStore, menu *assetstore.Menu) (*Driver, e
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 %v:%v", d.menu.Name, 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 %v:%v", d.menu.Name, 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 %v:%v", d.menu.Name, 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 %v:%v", d.menu.Name, 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 %v:%v", d.menu.Name, 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 {
if d == nil {
debug.PrintStack()
@@ -250,7 +94,7 @@ func (d *Driver) Update(screenX, screenY int) error {
d.cursorOrig = image.Pt(int(mnX), int(mnY))
// Dispatch notifications to our widgets
for _, hoverable := range d.hoverables {
for _, hoverable := range d.hoverables() {
inBounds := d.cursorOrig.In(hoverable.bounds())
d.hoverStartEvent(hoverable, inBounds)
@@ -262,7 +106,7 @@ func (d *Driver) Update(screenX, screenY int) error {
}
mouseIsDown := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)
for _, clickable := range d.clickables {
for _, clickable := range d.clickables() {
inBounds := d.cursorOrig.In(clickable.bounds())
mouseWasDown := clickable.mouseDownState()
@@ -271,7 +115,7 @@ func (d *Driver) Update(screenX, screenY int) error {
d.mouseUpEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
}
for _, mouseable := range d.mouseables {
for _, mouseable := range d.mouseables() {
mouseable.registerMousePosition(d.cursorOrig)
}
@@ -286,7 +130,7 @@ func (d *Driver) Draw(screen *ebiten.Image) error {
var do ebiten.DrawImageOptions
for _, paint := range d.paintables {
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))
@@ -321,71 +165,66 @@ func (d *Driver) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
return cursor.Image, op, nil
}
func (d *Driver) addRecord(record *menus.Record) error {
//log.Printf("Adding record %v: %#+v", record.Locator(), record)
children := record.Children
func (d *Driver) clickables() []clickable {
var out []clickable
handler, ok := widgetBuilders[record.Type]
if !ok {
return fmt.Errorf("UI driver encountered unknown menu record: %#+v", record)
for _, widget := range d.widgets {
out = append(out, widget.clickables()...)
}
if handler != nil {
var err error
children, err = handler(d, record)
if err != nil {
return err
}
}
// Recursively add all remaining children of this record
for _, record := range children {
if err := d.addRecord(record); err != nil {
return err
}
}
return nil
return out
}
func (d *Driver) hoverStartEvent(h hoverable, inBounds bool) {
if inBounds && !h.hoverState() {
//log.Printf("hoverable false -> true")
h.setHoverState(true)
func (d *Driver) freezables() []freezable {
var out []freezable
for _, widget := range d.widgets {
out = append(out, widget.freezables()...)
}
return out
}
func (d *Driver) hoverEndEvent(h hoverable, inBounds bool) {
if !inBounds && h.hoverState() {
//log.Printf("hoverable true -> false")
h.setHoverState(false)
func (d *Driver) hoverables() []hoverable {
var out []hoverable
for _, widget := range d.widgets {
out = append(out, widget.hoverables()...)
}
return out
}
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) mouseables() []mouseable {
var out []mouseable
for _, widget := range d.widgets {
out = append(out, widget.mouseables()...)
}
return out
}
func (d *Driver) mouseClickEvent(c clickable, inBounds, wasDown, isDown bool) {
if inBounds && wasDown && !isDown {
//log.Printf("mouse click")
c.registerMouseClick()
func (d *Driver) paintables() []paintable {
var out []paintable
for _, widget := range d.widgets {
out = append(out, widget.paintables()...)
}
if d.activeDialogue != nil {
out = append(out, d.activeDialogue.paintables()...)
}
return out
}
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)
}
func (d *Driver) valueables() []valueable {
var out []valueable
for _, widget := range d.widgets {
out = append(out, widget.valueables()...)
}
return out
}