package ui import ( "image" "log" "github.com/hajimehoshi/ebiten" "code.ur.gs/lupine/ordoor/internal/menus" ) func init() { registerBuilder(menus.TypeStatic, registerStatic) registerBuilder(menus.TypeHypertext, registerHypertext) registerBuilder(menus.TypeOverlay, registerOverlay) registerBuilder(menus.TypeAnimationSample, registerAnimation) registerBuilder(menus.TypeAnimationHover, 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 // Some non-interactives, e.g., overlays, are an image + text to be shown textImg *ebiten.Image textOffset image.Point clickImpl // Alright, alright, it turns out the bridge mission briefing is clickable hoverImpl } // This particular animation has entry and exit sequences, which are invoked // when entering and leaving hover, respectively. Example: bridge doors type animationHover struct { noninteractive // Use the frames in here for the "enter hover" animation exitFrames animation // and here the "exit hover" animation atTick int // Tracks progress through the frames opening bool closing bool } func registerStatic(d *Driver, r *menus.Record) 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] } sprite, err := d.menu.Sprite(spriteId) if err != nil { return err } ni := &noninteractive{ path: r.Path(), frames: animation{sprite.Image}, hoverImpl: hoverImpl{text: r.Text}, rect: sprite.Rect, } d.hoverables = append(d.hoverables, ni) d.paintables = append(d.paintables, ni) return nil } func registerHypertext(d *Driver, r *menus.Record) error { sprite, err := d.menu.Sprite(r.Share) if err != nil { return err } ni := &noninteractive{ path: r.Path(), hoverImpl: hoverImpl{text: r.Text}, rect: sprite.Rect, } d.clickables = append(d.clickables, ni) d.hoverables = append(d.hoverables, ni) return 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.Share) if err != nil { return err } ni := &noninteractive{ path: r.Path(), frames: animation{sprite.Image}, rect: sprite.Rect, } if r.Text != "" { // FIXME: is this always right? Seems to make sense for Main.mnu fnt := d.menu.Font(r.FontType/10 - 1) textImg, err := fnt.DrawLine(r.Text) if err != nil { return err } ni.textImg = textImg // Centre the image xSlack := ni.rect.Dx() - textImg.Bounds().Dx() if xSlack > 0 { ni.textOffset.X = xSlack / 2 } ySlack := ni.rect.Dy() - textImg.Bounds().Dy() if ySlack > 0 { ni.textOffset.Y = ySlack / 2 } } else { log.Printf("Overlay without text detected: %#+v", r) } d.paintables = append(d.paintables, ni) return 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.SpriteId[0]) if err != nil { return err } frames, err := d.menu.Images(r.SpriteId[0], r.DrawType) if err != nil { return err } ani := &noninteractive{ path: r.Path(), frames: animation(frames), hoverImpl: hoverImpl{text: r.Text}, rect: sprite.Rect, } d.hoverables = append(d.hoverables, ani) d.paintables = append(d.paintables, ani) return nil } func registerAnimationHover(d *Driver, r *menus.Record) error { sprite, err := d.menu.Sprite(r.SpriteId[0]) if err != nil { return err } enterFrames, err := d.menu.Images(r.SpriteId[0], r.DrawType) if err != nil { return err } exitFrames, err := d.menu.Images(r.SpriteId[0]+r.DrawType, r.DrawType) if err != nil { return err } ani := &animationHover{ noninteractive: noninteractive{ path: r.Path(), frames: animation(enterFrames), hoverImpl: hoverImpl{text: r.Text}, rect: sprite.Rect, }, exitFrames: animation(exitFrames), } d.hoverables = append(d.hoverables, ani) d.paintables = append(d.paintables, ani) return nil } func (n *noninteractive) id() string { return n.path } func (n *noninteractive) bounds() image.Rectangle { return n.rect } func (n *noninteractive) regions(tick int) []region { out := oneRegion(n.bounds().Min, n.frames.image(tick)) if n.textImg != nil { out = append(out, oneRegion(n.textPos(), n.textImg)...) } return out } func (n *noninteractive) textPos() image.Point { return image.Pt(n.rect.Min.X+n.textOffset.X, n.rect.Min.Y+n.textOffset.Y) } func (a *animationHover) regions(tick int) []region { if a.opening || a.closing { var anim animation if a.opening { anim = a.frames } else { anim = a.exitFrames } out := oneRegion(a.bounds().Min, anim[a.atTick]) if a.atTick < len(anim)-1 { a.atTick += 1 } else if !a.hoverState() { a.closing = false } return out } // Nothing doing, show a closed door return oneRegion(a.bounds().Min, a.frames.image(0)) } func (a *animationHover) setHoverState(value bool) { a.atTick = 0 a.opening = value a.closing = !value a.hoverImpl.setHoverState(value) }