package ui import ( "fmt" "image" "reflect" // For DeepEqual "github.com/hajimehoshi/ebiten" "code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/menus" ) // type Interface encapsulates a user interface, providing a means to track UI // state, draw the interface, and execute code when the widgets are interacted // with. // // The graphics for UI elements were all created with a 640x480 resolution in // mind. The interface transparently scales them all to the current screen size // to compensate. type Interface struct { menu *assetstore.Menu static []*assetstore.Sprite // Static elements in the interface ticks int widgets []*Widget } func NewInterface(menu *assetstore.Menu) (*Interface, error) { iface := &Interface{ menu: menu, } for _, record := range menu.Records() { if err := iface.addRecord(record); err != nil { return nil, err } } return iface, nil } // Find a widget by its hierarchical ID path func (i *Interface) Widget(path ...int) (*Widget, error) { for _, widget := range i.widgets { if reflect.DeepEqual(path, widget.path) { return widget, nil } } return nil, fmt.Errorf("Couldn't find widget %#+v", path) } func (i *Interface) Update(screenX, screenY int) error { // Used in animation effects i.ticks += 1 mousePos := i.getMousePos(screenX, screenY) // Iterate through all widgets, update mouse state for _, widget := range i.widgets { mouseIsOver := mousePos.In(widget.Bounds) widget.hovering(mouseIsOver) widget.mouseButton(mouseIsOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)) } return nil } func (i *Interface) Draw(screen *ebiten.Image) error { geo := i.scale(screen.Size()) do := &ebiten.DrawImageOptions{GeoM: geo} for _, sprite := range i.static { do.GeoM.Translate(geo.Apply(float64(sprite.XOffset), float64(sprite.YOffset))) if err := screen.DrawImage(sprite.Image, do); err != nil { return err } do.GeoM = geo } for _, widget := range i.widgets { img, err := widget.Image(i.ticks / 2) if err != nil { return err } if img == nil { continue } do.GeoM.Translate(geo.Apply(float64(widget.Bounds.Min.X), float64(widget.Bounds.Min.Y))) if err := screen.DrawImage(img, do); err != nil { return err } do.GeoM = geo } return nil } func (i *Interface) addRecord(record *menus.Record) error { switch record.Type { case menus.TypeStatic: // These are static if sprite, err := i.menu.Sprite(record.SpriteId[0]); err != nil { return err } else { i.static = append(i.static, sprite) } case menus.TypeMenu: // These aren't drawable and can be ignored case menus.TypeOverlay, menus.TypeMainButton: // Widgets \o/ if widget, err := i.widgetFromRecord(record); err != nil { return err } else { i.widgets = append(i.widgets, widget) } default: return fmt.Errorf("ui.interface: encountered unknown menu record: %#+v", record) } // Recursively add all children for _, record := range record.Children { if err := i.addRecord(record); err != nil { return err } } return nil } // Works out how much we have to scale the current screen by to draw correctly func (i *Interface) scale(w, h int) ebiten.GeoM { var geo ebiten.GeoM geo.Scale(float64(w)/640.0, float64(h)/480.0) return geo } func (i *Interface) unscale(w, h int) ebiten.GeoM { geo := i.scale(w, h) geo.Invert() return geo } // Returns the current position of the mouse in 640x480 coordinates. Needs the // actual size of the screen to do so. func (i *Interface) getMousePos(w, h int) image.Point { cX, cY := ebiten.CursorPosition() geo := i.unscale(w, h) sX, sY := geo.Apply(float64(cX), float64(cY)) return image.Pt(int(sX), int(sY)) } func (i *Interface) widgetFromRecord(record *menus.Record) (*Widget, error) { // FIXME: we assume that all widgets have a share sprite, but is that true? sprite, err := i.menu.Sprite(record.Share) if err != nil { return nil, err } var path []int for r := record; r != nil; r = r.Parent { path = append([]int{r.Id}, path...) } widget := &Widget{ Bounds: sprite.Rect, path: path, record: record, sprite: sprite, } switch record.Type { case menus.TypeMainButton: hovers, err := i.menu.Images(record.SpriteId[0], record.DrawType) if err != nil { return nil, err } widget.hoverAnimation = hovers sprite, err := i.menu.Sprite(record.Share + 1) if err != nil { return nil, err } widget.mouseButtonDownImage = sprite.Image } return widget, nil }