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

@@ -7,20 +7,11 @@ import (
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
registerBuilder(menus.TypeSimpleButton, noChildren(registerSimpleButton))
registerBuilder(menus.TypeInvokeButton, noChildren(registerInvokeButton))
registerBuilder(menus.TypeMainButton, noChildren(registerMainButton))
registerBuilder(menus.TypeDoorHotspot, noChildren(registerDoorHotspot))
registerBuilder(menus.TypeDoorHotspot2, noChildren(registerDoorHotspot))
registerBuilder(menus.TypeDoorHotspot3, noChildren(registerDoorHotspot))
}
// A button without hover animation
// FIXME: Keyboard.mnu has TypeSimpleButton instances that seem to include a
// hover in the SpriteId field
type button struct {
path string
locator string
baseSpr *assetstore.Sprite
clickSpr *assetstore.Sprite
@@ -38,95 +29,89 @@ type mainButton struct {
button
}
func registerSimpleButton(d *Driver, r *menus.Record) error {
_, err := registerButton(d, r, r.SpriteId[0])
return err
}
func registerInvokeButton(d *Driver, r *menus.Record) error {
_, err := registerButton(d, r, r.Share)
return err
}
func registerMainButton(d *Driver, r *menus.Record) error {
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 3) // base, pressed, disabled
func (d *Driver) buildButton(p *menus.Properties) (*button, *Widget, error) {
sprites, err := d.menu.Sprites(p.ObjectIdx, p.BaseSpriteID(), 3) // base, pressed, disabled
if err != nil {
return err
return nil, nil, err
}
hovers, err := d.menu.Images(r.ObjectIdx, r.SpriteId[0], r.DrawType)
btn := &button{
locator: p.Locator,
baseSpr: sprites[0],
clickSpr: sprites[1],
frozenSpr: sprites[2],
hoverImpl: hoverImpl{text: p.Text},
}
widget := &Widget{
ownClickables: []clickable{btn},
ownFreezables: []freezable{btn},
ownHoverables: []hoverable{btn},
ownPaintables: []paintable{btn},
}
return btn, widget, nil
}
func (d *Driver) buildMainButton(p *menus.Properties) (*mainButton, *Widget, error) {
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 3) // base, pressed, disabled
if err != nil {
return err
return nil, nil, err
}
hovers, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
if err != nil {
return nil, nil, err
}
btn := &mainButton{
hoverAnim: animation(hovers),
button: button{
path: r.Path(),
locator: p.Locator,
baseSpr: sprites[0],
clickSpr: sprites[1],
frozenSpr: sprites[2],
hoverImpl: hoverImpl{text: r.Text},
hoverImpl: hoverImpl{text: p.Text},
},
}
d.clickables = append(d.clickables, btn)
d.freezables = append(d.freezables, btn)
d.hoverables = append(d.hoverables, btn)
d.paintables = append(d.paintables, btn)
widget := &Widget{
ownClickables: []clickable{btn},
ownFreezables: []freezable{btn},
ownHoverables: []hoverable{btn},
ownPaintables: []paintable{btn},
}
return nil
return btn, widget, nil
}
func registerDoorHotspot(d *Driver, r *menus.Record) error {
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 2) // base, pressed
func (d *Driver) buildDoorHotspot(p *menus.Properties) (*button, *Widget, error) {
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 2) // base, pressed
if err != nil {
return err
return nil, nil, err
}
btn := &button{
path: r.Path(),
locator: p.Locator,
baseSpr: sprites[0],
clickSpr: sprites[1],
frozenSpr: sprites[0], // No disabled sprite
hoverImpl: hoverImpl{text: r.Text},
hoverImpl: hoverImpl{text: p.Text},
}
d.clickables = append(d.clickables, btn)
d.freezables = append(d.freezables, btn)
d.hoverables = append(d.hoverables, btn)
d.paintables = append(d.paintables, btn)
return nil
}
func registerButton(d *Driver, r *menus.Record, spriteId int) (*button, error) {
sprites, err := d.menu.Sprites(r.ObjectIdx, spriteId, 3) // base, pressed, disabled
if err != nil {
return nil, err
widget := &Widget{
ownClickables: []clickable{btn},
ownFreezables: []freezable{btn},
ownHoverables: []hoverable{btn},
ownPaintables: []paintable{btn},
}
btn := &button{
path: r.Path(),
baseSpr: sprites[0],
clickSpr: sprites[1],
frozenSpr: sprites[2],
hoverImpl: hoverImpl{text: r.Text},
}
return btn, widget, nil
d.clickables = append(d.clickables, btn)
d.freezables = append(d.freezables, btn)
d.hoverables = append(d.hoverables, btn)
d.paintables = append(d.paintables, btn)
return btn, nil
}
func (b *button) id() string {
return b.path
return b.locator
}
func (b *button) bounds() image.Rectangle {

View File

@@ -1,20 +0,0 @@
package ui
import (
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
// Needed for Keyboard.mnu (main -> options -> keyboard).
// Dialogues can be active(?) or not. If they're not, then they are hidden.
// Dialogues seem to be modal in all cases?
registerBuilder(menus.TypeDialogue, registerDebug("WIP Dialogue", registerDialogue))
}
func registerDialogue(d *Driver, r *menus.Record) ([]*menus.Record, error) {
// The dialogue itself has a sprite
_, err := registerNoninteractive(d, r)
// TODO: we need to group these. Z levels seem overkill?
return r.Children, err
}

30
internal/ui/dialogues.go Normal file
View File

@@ -0,0 +1,30 @@
package ui
import (
"fmt"
)
func (d *Driver) Dialogues() []string {
out := make([]string, len(d.dialogues))
for i, dialogue := range d.dialogues {
out[i] = dialogue.Locator
}
return out
}
func (d *Driver) ShowDialogue(locator string) error {
for _, dialogue := range d.dialogues {
if dialogue.Locator == locator {
d.activeDialogue = dialogue
return nil
}
}
return fmt.Errorf("Couldn't find dialogue %v", locator)
}
func (d *Driver) HideDialogue() {
d.activeDialogue = nil
}

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
}

43
internal/ui/events.go Normal file
View File

@@ -0,0 +1,43 @@
package ui
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)
}
}
}

196
internal/ui/group.go Normal file
View File

@@ -0,0 +1,196 @@
package ui
import (
"code.ur.gs/lupine/ordoor/internal/menus"
"fmt"
"log"
)
func (d *Driver) registerGroup(group *menus.Group) error {
// log.Printf("Adding group %v: %#+v", group.Locator, group)
var dialogue bool
switch group.Type {
case menus.TypeStatic, menus.TypeMainBackground, menus.TypeMenu, menus.TypeDragMenu, menus.TypeRadioMenu:
case menus.TypeDialogue:
dialogue = true
default:
return fmt.Errorf("Unknown group type: %v", group.Type)
}
var groupWidget *Widget
// Groups have a background sprite (FIXME: always?)
if group.BaseSpriteID() >= 0 {
var err error
_, groupWidget, err = d.buildStatic(group.Props())
if err != nil {
return err
}
} else {
groupWidget = &Widget{Locator: group.Locator}
}
if dialogue {
d.dialogues = append(d.dialogues, groupWidget)
} else {
d.widgets = append(d.widgets, groupWidget)
}
// TRadioGroup is best handled like this
records, widget, err := d.maybeBuildInventorySelect(group, group.Records)
if err != nil {
return err
}
if widget != nil {
groupWidget.Children = append(groupWidget.Children, widget)
}
records, widget, err = d.maybeBuildListBox(group, records)
if err != nil {
return err
}
if widget != nil {
groupWidget.Children = append(groupWidget.Children, widget)
}
for _, record := range records {
child, err := d.buildRecord(record)
if err != nil {
return err
}
if child != nil {
groupWidget.Children = append(groupWidget.Children, child)
}
}
return nil
}
func (d *Driver) buildRecord(r *menus.Record) (*Widget, error) {
var widget *Widget
var err error
switch r.Type {
case menus.SubTypeSimpleButton, menus.SubTypeInvokeButton:
_, widget, err = d.buildButton(r.Props())
case menus.SubTypeDoorHotspot1, menus.SubTypeDoorHotspot2, menus.SubTypeDoorHotspot3:
_, widget, err = d.buildDoorHotspot(r.Props())
case menus.SubTypeOverlay:
_, widget, err = d.buildOverlay(r.Props())
case menus.SubTypeHypertext:
_, widget, err = d.buildHypertext(r.Props())
case menus.SubTypeCheckbox:
_, widget, err = d.buildCheckbox(r.Props())
case menus.SubTypeEditBox:
log.Printf("Unimplemented: SubTypeEditBox: %v", r.Locator) // TODO
case menus.SubTypeRadioButton:
log.Printf("Unimplemented: SubTypeRadioButton: %v", r.Locator) // TODO
case menus.SubTypeDropdownButton:
log.Printf("Unimplemented: SubTypeDropdownButton: %v", r.Locator) // TODO
case menus.SubTypeComboBoxItem:
log.Printf("Unimplemented: SubTypeComboBoxItem: %v", r.Locator) // TODO
case menus.SubTypeAnimationSample:
_, widget, err = d.buildAnimationSample(r.Props())
case menus.SubTypeAnimationHover:
_, widget, err = d.buildAnimationHover(r.Props())
case menus.SubTypeMainButton:
_, widget, err = d.buildMainButton(r.Props())
case menus.SubTypeSlider:
_, widget, err = d.buildSlider(r.Props()) // TODO: take sliders at an earlier point?
case menus.SubTypeStatusBar:
log.Printf("Unimplemented: SubTypeStatusBar: %v", r.Locator) // TODO
default:
return nil, fmt.Errorf("Unknown record type for %v: %v", r.Locator, r.Type)
}
return widget, err
}
func (d *Driver) maybeBuildInventorySelect(group *menus.Group, records []*menus.Record) ([]*menus.Record, *Widget, error) {
var untouched []*menus.Record
var touched []*menus.Record
for _, record := range records {
if record.Type == menus.SubTypeInventorySelect {
touched = append(touched, record)
} else {
untouched = append(untouched, record)
}
}
if len(touched) == 0 {
return untouched, nil, nil
}
elements := make([]*inventorySelect, len(touched))
widget := &Widget{
Locator: group.Locator,
}
for i, record := range touched {
element, childWidget, err := d.buildInventorySelect(record.Props())
if err != nil {
return nil, nil, err
}
elements[i] = element
widget.Children = append(widget.Children, childWidget)
}
elements[0].setValue("1")
for _, element := range elements {
element.others = elements
}
return untouched, widget, nil
}
func (d *Driver) maybeBuildListBox(group *menus.Group, records []*menus.Record) ([]*menus.Record, *Widget, error) {
// Unless up, down, thumb, and items, are all present, it's not a listbox
var up *menus.Record
var down *menus.Record
var thumb *menus.Record
var items []*menus.Record
var untouched []*menus.Record
for _, rec := range records {
switch rec.Type {
case menus.SubTypeListBoxUp:
if up != nil {
return nil, nil, fmt.Errorf("Duplicate up buttons in menu %v", group.Locator)
}
up = rec
case menus.SubTypeListBoxDown:
if down != nil {
return nil, nil, fmt.Errorf("Duplicate down buttons in menu %v", group.Locator)
}
down = rec
case menus.SubTypeLineKbd, menus.SubTypeLineBriefing:
items = append(items, rec)
case menus.SubTypeThumb:
if thumb != nil {
return nil, nil, fmt.Errorf("Duplicate thumbs in menu %v", group.Locator)
}
thumb = rec
default:
// e.g. maingame:18.12 includes a button that is not part of the box
untouched = append(untouched, rec)
}
}
// Since not all the elements are present, this isn't a listbox
if len(items) == 0 || thumb == nil || up == nil || down == nil {
return untouched, nil, nil
}
_, widget, err := d.buildListBox(group, up, down, thumb, items...)
if err != nil {
return nil, nil, err
}
return untouched, widget, nil
}

View File

@@ -4,10 +4,6 @@ import (
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
registerBuilder(menus.TypeInventorySelect, ownedByMenu)
}
// An inventory select is a sort of radio button. If 2 share the same menu,
// selecting one deselects the other. Otherwise, they act like checkboxes.
//
@@ -20,33 +16,18 @@ type inventorySelect struct {
}
// Called from the menu, which fills "others" for us
func registerInventorySelect(d *Driver, r *menus.Record) (*inventorySelect, error) {
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 3) // unchecked, checked, disabled
func (d *Driver) buildInventorySelect(p *menus.Properties) (*inventorySelect, *Widget, error) {
c, widget, err := d.buildCheckbox(p)
if err != nil {
return nil, err
return nil, nil, err
}
element := &inventorySelect{
checkbox: checkbox{
button: button{
path: r.Path(),
baseSpr: sprites[0], // unchecked
clickSpr: sprites[1], // checked
frozenSpr: sprites[2], // disabled
hoverImpl: hoverImpl{text: r.Text},
},
// In an inventorySelect, the frozen and click sprites are reversed
c.clickSpr, c.frozenSpr = c.frozenSpr, c.clickSpr
valueImpl: valueImpl{str: "0"},
},
}
element := &inventorySelect{checkbox: *c}
d.clickables = append(d.clickables, element)
d.freezables = append(d.freezables, element)
d.hoverables = append(d.hoverables, element)
d.paintables = append(d.paintables, element)
d.valueables = append(d.valueables, element)
return element, nil
return element, widget, nil
}
func (i *inventorySelect) registerMouseClick() {

View File

@@ -8,15 +8,6 @@ import (
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
registerBuilder(menus.TypeLineKbd, ownedByMenu)
registerBuilder(menus.TypeLineBriefing, ownedByMenu)
registerBuilder(menus.TypeThumb, ownedByMenu)
registerBuilder(menus.TypeListBoxUp, ownedByMenu)
registerBuilder(menus.TypeListBoxDown, ownedByMenu)
}
// listBox is a TListBox in VCL terms. It has a number of lines of text, one of
// which may be selected, and a slider with up and down buttons to scroll if the
// options in the box exceed its viewing capacity.
@@ -30,7 +21,6 @@ type listBox struct {
thumbBase *assetstore.Sprite // Bounds are given by this
thumbImg *assetstore.Sprite // This is displayed at offset * (height / steps)
base *noninteractive // The menu itself has a sprite to display
lines []*noninteractive // We display to these
// The list box acts as a window onto these
@@ -40,87 +30,31 @@ type listBox struct {
offset int
}
func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
var upBtn *menus.Record
var downBtn *menus.Record
var thumb *menus.Record
var items []*menus.Record
var otherChildren []*menus.Record
for _, rec := range menu.Children {
switch rec.Type {
case menus.TypeListBoxUp:
if upBtn != nil {
return nil, fmt.Errorf("Duplicate up buttons in menu %v", menu.Locator())
}
upBtn = rec
case menus.TypeListBoxDown:
if downBtn != nil {
return nil, fmt.Errorf("Duplicate down buttons in menu %v", menu.Locator())
}
downBtn = rec
case menus.TypeLineKbd, menus.TypeLineBriefing:
items = append(items, rec)
case menus.TypeThumb:
if thumb != nil {
return nil, fmt.Errorf("Duplicate thumbs in menu %v", menu.Locator())
}
thumb = rec
default:
// e.g. maingame:18.12 includes a button that is not part of the box
otherChildren = append(otherChildren, rec)
}
}
if len(items) == 0 || thumb == nil || upBtn == nil || downBtn == nil {
return nil, fmt.Errorf("Missing items in menu %v", menu.Locator())
}
// Now build the wonderful thing
baseElem, err := registerNoninteractive(d, menu)
func (d *Driver) buildListBox(group *menus.Group, up, down, thumb *menus.Record, items ...*menus.Record) (*listBox, *Widget, error) {
upElem, upWidget, err := d.buildButton(up.Props())
if err != nil {
return nil, err
return nil, nil, err
}
upSprId := upBtn.SpriteId[0]
if upSprId == -1 {
upSprId = upBtn.Share
}
elemUp, err := registerButton(d, upBtn, upSprId)
downElem, downWidget, err := d.buildButton(down.Props())
if err != nil {
return nil, err
return nil, nil, err
}
dnSprId := downBtn.SpriteId[0]
if dnSprId == -1 {
dnSprId = downBtn.Share
}
elemDown, err := registerButton(d, downBtn, dnSprId)
thumbBaseSpr, err := d.menu.Sprite(thumb.ObjectIdx, thumb.Share)
if err != nil {
return nil, err
return nil, nil, err
}
thumbBaseSpr, err := d.menu.Sprite(menu.ObjectIdx, thumb.Share)
thumbImgSpr, err := d.menu.Sprite(thumb.ObjectIdx, thumb.BaseSpriteID())
if err != nil {
return nil, err
}
thumbSprId := thumb.SpriteId[0]
if thumbSprId == -1 {
thumbSprId = thumb.Share
}
thumbImgSpr, err := d.menu.Sprite(menu.ObjectIdx, thumbSprId)
if err != nil {
return nil, err
return nil, nil, err
}
element := &listBox{
base: baseElem,
// TODO: upBtn needs to be frozen when offset == 0; downBtn when offset == max
upBtn: elemUp,
downBtn: elemDown,
upBtn: upElem,
downBtn: downElem,
// TODO: need to be able to drag the thumb
thumbBase: thumbBaseSpr,
@@ -128,8 +62,8 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
}
// Internal wiring-up
elemUp.onClick(element.up)
elemDown.onClick(element.down)
upElem.onClick(element.up)
downElem.onClick(element.down)
// FIXME: Test data for now
for i := 0; i < 50; i++ {
@@ -138,30 +72,34 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
// Register everything. Since we're a composite of other controls, they are
// mostly self-registered at the moment.
d.paintables = append(d.paintables, element)
widget := &Widget{
Children: []*Widget{upWidget, downWidget},
ownPaintables: []paintable{element},
}
// FIXME: we should be able to freeze/unfreeze as a group.
// HURK: These need to be registered after the other elements so they are
// drawn in the correct order to be visible
for _, rec := range items {
ni, err := registerNoninteractive(d, rec)
ni, niWidget, err := d.buildStatic(rec.Props())
if err != nil {
return nil, err
return nil, nil, err
}
// TODO: pick the correct font
ni.label = &label{
align: AlignModeLeft,
font: d.menu.Font(0),
rect: ni.rect,
}
element.lines = append(element.lines, ni)
widget.Children = append(widget.Children, niWidget)
}
element.refresh()
return otherChildren, nil
return element, widget, nil
}
func (l *listBox) SetStrings(to []string) {

View File

@@ -1,72 +0,0 @@
package ui
import (
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
// These menu types don't need driving, so we can ignore them
registerBuilder(menus.TypeMenu, registerMenu)
registerBuilder(menus.TypeDragMenu, nil) // Menus are just containers
}
func registerMenu(d *Driver, r *menus.Record) ([]*menus.Record, error) {
childrenLeft, err := listBoxFromMenu(d, r, r.Children)
if err != nil {
return nil, err
}
childrenLeft, err = inventorySelectFromMenu(d, r, childrenLeft)
if err != nil {
return nil, err
}
// Return all the unhandled children to be processed further
return childrenLeft, nil
}
func listBoxFromMenu(d *Driver, menu *menus.Record, children []*menus.Record) ([]*menus.Record, error) {
ok := false
for _, rec := range children {
if rec.Type == menus.TypeThumb { // FIXME: we're using this to indicate a listbox
ok = true
break
}
}
if !ok {
return children, nil
}
return registerListBox(d, menu)
}
// Group all inventory selects that share a menu together
func inventorySelectFromMenu(d *Driver, menu *menus.Record, children []*menus.Record) ([]*menus.Record, error) {
var childrenLeft []*menus.Record
var inventorySelects []*inventorySelect
for _, child := range children {
switch child.Type {
case menus.TypeInventorySelect:
is, err := registerInventorySelect(d, child)
if err != nil {
return nil, err
}
inventorySelects = append(inventorySelects, is)
default:
childrenLeft = append(childrenLeft, child)
}
}
if len(inventorySelects) > 0 {
inventorySelects[0].setValue("1") // Always start with one selected
for _, is := range inventorySelects {
is.others = inventorySelects
}
}
return childrenLeft, nil
}

View File

@@ -1,6 +1,7 @@
package ui
import (
"fmt"
"image"
"log"
@@ -15,22 +16,14 @@ const (
AlignModeLeft AlignMode = 1
)
func init() {
registerBuilder(menus.TypeStatic, registerStatic) // MainGame has a hypertext child
registerBuilder(menus.TypeHypertext, noChildren(registerHypertext))
registerBuilder(menus.TypeOverlay, noChildren(registerOverlay))
registerBuilder(menus.TypeAnimationSample, noChildren(registerAnimation))
registerBuilder(menus.TypeAnimationHover, noChildren(registerAnimationHover))
}
// A non-interactive element is not a widget; it merely displays some pixels and
// may optionally have a tooltip for display within bounds.
//
// For non-animated non-interactive elements, just give them a single frame.
type noninteractive struct {
path string
frames animation
rect image.Rectangle
locator string
frames animation
rect image.Rectangle
// Some non-interactives, e.g., overlays, are an image + text to be shown
label *label
@@ -58,145 +51,153 @@ type animationHover struct {
closing bool
}
func registerStatic(d *Driver, r *menus.Record) ([]*menus.Record, error) {
_, err := registerNoninteractive(d, r)
return r.Children, err
}
func registerNoninteractive(d *Driver, r *menus.Record) (*noninteractive, error) {
// FIXME: SpriteID takes precedence over SHARE if present, but is that right?
spriteId := r.Share
if len(r.SpriteId) > 0 && r.SpriteId[0] != -1 {
spriteId = r.SpriteId[0]
func (d *Driver) buildNoninteractive(p *menus.Properties) (*noninteractive, error) {
// FIXME: SpriteID takes precedence over SHARE if present, but is that
// always right?
spriteId := p.BaseSpriteID()
if spriteId < 0 {
return nil, fmt.Errorf("No base sprite for %v", p.Locator)
}
sprite, err := d.menu.Sprite(r.ObjectIdx, spriteId)
sprite, err := d.menu.Sprite(p.ObjectIdx, spriteId)
if err != nil {
return nil, err
}
ni := &noninteractive{
path: r.Path(),
frames: animation{sprite.Image},
hoverImpl: hoverImpl{text: r.Text},
rect: sprite.Rect,
locator: p.Locator,
frames: animation{sprite.Image},
rect: sprite.Rect,
}
d.hoverables = append(d.hoverables, ni)
d.paintables = append(d.paintables, ni)
return ni, nil
}
func registerHypertext(d *Driver, r *menus.Record) error {
sprite, err := d.menu.Sprite(r.ObjectIdx, r.Share)
func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p)
if err != nil {
return err
return nil, nil, err
}
ni := &noninteractive{
path: r.Path(),
hoverImpl: hoverImpl{text: r.Text},
rect: sprite.Rect,
ni.hoverImpl.text = p.Text
widget := &Widget{
Locator: ni.locator,
ownHoverables: []hoverable{ni},
ownPaintables: []paintable{ni},
}
d.clickables = append(d.clickables, ni)
d.hoverables = append(d.hoverables, ni)
return ni, widget, nil
}
return nil
func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p)
if err != nil {
return nil, nil, err
}
// FIXME: check if this is still needed on the bridge -> briefing transition
widget := &Widget{
Locator: ni.locator,
ownClickables: []clickable{ni},
ownHoverables: []hoverable{ni},
}
return ni, widget, nil
}
// An overlay is a static image + some text that needs to be rendered
func registerOverlay(d *Driver, r *menus.Record) error {
sprite, err := d.menu.Sprite(r.ObjectIdx, r.Share)
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p)
if err != nil {
return err
return nil, nil, err
}
ni := &noninteractive{
path: r.Path(),
frames: animation{sprite.Image},
rect: sprite.Rect,
widget := &Widget{
Locator: ni.locator,
ownPaintables: []paintable{ni},
}
d.paintables = append(d.paintables, ni)
if r.Text != "" {
if p.Text != "" {
// FIXME: is this always right? Seems to make sense for Main.mnu
fnt := d.menu.Font(r.FontType/10 - 1)
fnt := d.menu.Font(p.FontType/10 - 1)
ni.label = &label{
font: fnt,
rect: ni.rect, // We will be centered by default
text: r.Text,
text: p.Text,
}
} else {
log.Printf("Overlay without text detected: %#+v", r)
log.Printf("Overlay without text detected in %v", p.Locator)
}
return nil
return ni, widget, nil
}
// An animation is a non-interactive element that displays something in a loop
func registerAnimation(d *Driver, r *menus.Record) error {
sprite, err := d.menu.Sprite(r.ObjectIdx, r.SpriteId[0])
func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Widget, error) {
sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
if err != nil {
return err
return nil, nil, err
}
frames, err := d.menu.Images(r.ObjectIdx, r.SpriteId[0], r.DrawType)
frames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
if err != nil {
return err
return nil, nil, err
}
ani := &noninteractive{
path: r.Path(),
locator: p.Locator,
frames: animation(frames),
hoverImpl: hoverImpl{text: r.Text},
hoverImpl: hoverImpl{text: p.Text},
rect: sprite.Rect,
}
d.hoverables = append(d.hoverables, ani)
d.paintables = append(d.paintables, ani)
widget := &Widget{
ownHoverables: []hoverable{ani},
ownPaintables: []paintable{ani},
}
return nil
return ani, widget, nil
}
func registerAnimationHover(d *Driver, r *menus.Record) error {
sprite, err := d.menu.Sprite(r.ObjectIdx, r.SpriteId[0])
func (d *Driver) buildAnimationHover(p *menus.Properties) (*animationHover, *Widget, error) {
sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
if err != nil {
return err
return nil, nil, err
}
enterFrames, err := d.menu.Images(r.ObjectIdx, r.SpriteId[0], r.DrawType)
enterFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
if err != nil {
return err
return nil, nil, err
}
exitFrames, err := d.menu.Images(r.ObjectIdx, r.SpriteId[0]+r.DrawType, r.DrawType)
exitFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0]+p.DrawType, p.DrawType)
if err != nil {
return err
return nil, nil, err
}
ani := &animationHover{
noninteractive: noninteractive{
path: r.Path(),
locator: p.Locator,
frames: animation(enterFrames),
hoverImpl: hoverImpl{text: r.Text},
hoverImpl: hoverImpl{text: p.Text},
rect: sprite.Rect,
},
exitFrames: animation(exitFrames),
}
d.hoverables = append(d.hoverables, ani)
d.paintables = append(d.paintables, ani)
widget := &Widget{
ownHoverables: []hoverable{ani},
ownPaintables: []paintable{ani},
}
return nil
return ani, widget, nil
}
func (n *noninteractive) id() string {
return n.path
return n.locator
}
func (n *noninteractive) bounds() image.Rectangle {

View File

@@ -9,11 +9,6 @@ import (
"code.ur.gs/lupine/ordoor/internal/menus"
)
func init() {
registerBuilder(menus.TypeCheckbox, noChildren(registerCheckbox))
registerBuilder(menus.TypeSlider, noChildren(registerSlider))
}
// A checkbox can be a fancy button
type checkbox struct {
button
@@ -23,7 +18,7 @@ type checkbox struct {
// A slider is harder. Two separate elements to render
type slider struct {
path string
locator string
baseSpr *assetstore.Sprite
clickSpr *assetstore.Sprite
@@ -38,52 +33,56 @@ type slider struct {
}
// A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled.
func registerCheckbox(d *Driver, r *menus.Record) error {
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 3) // unchecked, disabled, checked
func (d *Driver) buildCheckbox(p *menus.Properties) (*checkbox, *Widget, error) {
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 3) // unchecked, disabled, checked
if err != nil {
return err
return nil, nil, err
}
checkbox := &checkbox{
button: button{
path: r.Path(),
locator: p.Locator,
baseSpr: sprites[0], // unchecked
clickSpr: sprites[2], // checked
frozenSpr: sprites[1],
hoverImpl: hoverImpl{text: r.Text},
frozenSpr: sprites[1], // disabled
hoverImpl: hoverImpl{text: p.Text},
},
valueImpl: valueImpl{str: "0"},
}
d.clickables = append(d.clickables, checkbox)
d.freezables = append(d.freezables, checkbox)
d.hoverables = append(d.hoverables, checkbox)
d.paintables = append(d.paintables, checkbox)
d.valueables = append(d.valueables, checkbox)
widget := &Widget{
ownClickables: []clickable{checkbox},
ownFreezables: []freezable{checkbox},
ownHoverables: []hoverable{checkbox},
ownPaintables: []paintable{checkbox},
ownValueables: []valueable{checkbox},
}
return nil
return checkbox, widget, nil
}
func registerSlider(d *Driver, r *menus.Record) error {
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 3) // base, clicked, slider element
func (d *Driver) buildSlider(p *menus.Properties) (*slider, *Widget, error) {
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 3) // base, clicked, slider element
if err != nil {
return err
return nil, nil, err
}
slider := &slider{
path: r.Path(),
locator: p.Locator,
baseSpr: sprites[0],
clickSpr: sprites[1],
sliderSpr: sprites[2],
hv: sprites[0].Rect.Dy() > sprites[0].Rect.Dx(), // A best guess
}
d.clickables = append(d.clickables, slider)
d.mouseables = append(d.mouseables, slider)
d.paintables = append(d.paintables, slider)
d.valueables = append(d.valueables, slider)
widget := &Widget{
ownClickables: []clickable{slider},
ownMouseables: []mouseable{slider},
ownPaintables: []paintable{slider},
ownValueables: []valueable{slider},
}
return nil
return slider, widget, nil
}
func (c *checkbox) registerMouseClick() {
@@ -107,7 +106,7 @@ func (c *checkbox) regions(tick int) []region {
}
func (s *slider) id() string {
return s.path
return s.locator
}
// The bounds of the slider are the whole thing

119
internal/ui/value.go Normal file
View File

@@ -0,0 +1,119 @@
package ui
import (
"fmt"
"strconv"
)
func (d *Driver) realId(id string) string {
return fmt.Sprintf("%v:%v", d.menu.Name, id)
}
func (d *Driver) Value(id string, into *string) error {
for _, valueable := range d.valueables() {
if valueable.id() == d.realId(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() == d.realId(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() == d.realId(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() == d.realId(id) {
clickable.onClick(f)
return nil
}
}
// We need to be able to wire up items inside dialogues too
for _, dialogue := range d.dialogues {
for _, clickable := range dialogue.clickables() {
if clickable.id() == d.realId(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() == d.realId(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)
}

73
internal/ui/widget.go Normal file
View File

@@ -0,0 +1,73 @@
package ui
type Widget struct {
Locator string
Children []*Widget
ownClickables []clickable
ownFreezables []freezable
ownHoverables []hoverable
ownMouseables []mouseable
ownPaintables []paintable
ownValueables []valueable
}
func (w *Widget) clickables() []clickable {
out := w.ownClickables
for _, widget := range w.Children {
out = append(out, widget.clickables()...)
}
return out
}
func (w *Widget) freezables() []freezable {
out := w.ownFreezables
for _, widget := range w.Children {
out = append(out, widget.freezables()...)
}
return out
}
func (w *Widget) hoverables() []hoverable {
out := w.ownHoverables
for _, widget := range w.Children {
out = append(out, widget.hoverables()...)
}
return out
}
func (w *Widget) mouseables() []mouseable {
out := w.ownMouseables
for _, widget := range w.Children {
out = append(out, widget.mouseables()...)
}
return out
}
func (w *Widget) paintables() []paintable {
out := w.ownPaintables
for _, widget := range w.Children {
out = append(out, widget.paintables()...)
}
return out
}
func (w *Widget) valueables() []valueable {
out := w.ownValueables
for _, widget := range w.Children {
out = append(out, widget.valueables()...)
}
return out
}