package ui import ( "fmt" "image" "log" "reflect" // For DeepEqual "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" "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 { Name string menu *assetstore.Menu static []*staticElement ticks int widgets []*Widget } func NewInterface(menu *assetstore.Menu) (*Interface, error) { iface := &Interface{ Name: menu.Name, 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 { if widget.disabled { continue // No activity for disabled 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 { var tooltip string // Draw this last, so it's on top of everything mousePt := i.getMousePos(screen.Size()) geo := i.scale(screen.Size()) do := &ebiten.DrawImageOptions{GeoM: geo} for _, s := range i.static { if s.image != nil { do.GeoM.Translate(geo.Apply(float64(s.bounds.Min.X), float64(s.bounds.Min.X))) if err := screen.DrawImage(s.image, do); err != nil { return err } do.GeoM = geo } if mousePt.In(s.bounds) && s.tooltip != "" { tooltip = s.tooltip } } 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 if widget.hoverState && widget.Tooltip != "" { tooltip = widget.Tooltip } } if tooltip != "" { cX, cY := ebiten.CursorPosition() ebitenutil.DebugPrintAt(screen, tooltip, cX+16, cY-16) } return nil } func (i *Interface) addRecord(record *menus.Record) error { log.Printf("Adding record: %#+v", record) handler, ok := setupHandlers[record.Type] if !ok { return fmt.Errorf("ui.interface: encountered unknown menu record: %#+v", record) } if handler != nil { if err := handler(i, record); err != nil { return err } } // 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)) }