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. 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 cursor assetstore.CursorName // 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(assets *assetstore.AssetStore, menu *assetstore.Menu) (*Driver, error) { driver := &Driver{ Name: menu.Name, assets: assets, 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 %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() return fmt.Errorf("Tried to update a nil ui.Driver") } // 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 { if d == nil { debug.PrintStack() return fmt.Errorf("Tried to draw a nil ui.Driver") } 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) 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 } func (d *Driver) addRecord(record *menus.Record) error { //log.Printf("Adding record %v: %#+v", record.Locator(), record) children := record.Children handler, ok := widgetBuilders[record.Type] if !ok { return fmt.Errorf("UI driver encountered unknown menu record: %#+v", record) } 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 } 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) } } }