package ui import ( "fmt" "image" "runtime/debug" "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" "code.ur.gs/lupine/ordoor/internal/assetstore" ) const ( OriginalX = 640.0 OriginalY = 480.0 ) // 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. 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 // 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 _, group := range menu.Groups() { if err := driver.registerGroup(group); err != nil { return nil, err } } return driver, nil } 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.activeHoverables() { 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.activeClickables() { 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.activeMouseables() { 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.activePaintables() { 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) allClickables() []clickable { var out []clickable for _, widget := range d.widgets { out = append(out, widget.allClickables()...) } for _, widget := range d.dialogues { out = append(out, widget.allClickables()...) } return out } func (d *Driver) allFreezables() []freezable { var out []freezable for _, widget := range d.widgets { out = append(out, widget.allFreezables()...) } for _, widget := range d.dialogues { out = append(out, widget.allFreezables()...) } return out } func (d *Driver) allValueables() []valueable { var out []valueable for _, widget := range d.widgets { out = append(out, widget.allValueables()...) } for _, widget := range d.dialogues { out = append(out, widget.allValueables()...) } return out } func (d *Driver) activeClickables() []clickable { if d.activeDialogue != nil { return d.activeDialogue.activeClickables() } var out []clickable for _, widget := range d.widgets { out = append(out, widget.activeClickables()...) } return out } func (d *Driver) activeHoverables() []hoverable { if d.activeDialogue != nil { return d.activeDialogue.activeHoverables() } var out []hoverable for _, widget := range d.widgets { out = append(out, widget.activeHoverables()...) } return out } func (d *Driver) activeMouseables() []mouseable { if d.activeDialogue != nil { return d.activeDialogue.activeMouseables() } var out []mouseable for _, widget := range d.widgets { out = append(out, widget.activeMouseables()...) } return out } func (d *Driver) activePaintables() []paintable { var out []paintable for _, widget := range d.widgets { out = append(out, widget.activePaintables()...) } if d.activeDialogue != nil { out = append(out, d.activeDialogue.activePaintables()...) } return out } func (d *Driver) findWidget(locator string) *Widget { toplevels := append(d.widgets, d.dialogues...) for _, widget := range toplevels { if w := widget.findWidget(locator); w != nil { return w } } return nil }