package ui import ( "fmt" "image" "log" "code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/menus" ) type AlignMode int const ( AlignModeCentre AlignMode = 0 AlignModeLeft AlignMode = 1 ) // 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 { locator string frames animation rect image.Rectangle // Some non-interactives, e.g., overlays, are an image + text to be shown label *label clickImpl // Alright, alright, it turns out the bridge mission briefing is clickable hoverImpl } // Paint some text to screen type label struct { align AlignMode rect image.Rectangle text string font *assetstore.Font } // 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 (d *Driver) buildNoninteractive(p *menus.Properties) (*noninteractive, error) { // FIXME: SpriteID takes precedence over SHARE if present, but is that // always right? spriteId := p.BaseSpriteID() if spriteId < 0 { return nil, fmt.Errorf("No base sprite for %v", p.Locator) } sprite, err := d.menu.Sprite(p.ObjectIdx, spriteId) if err != nil { return nil, err } ni := &noninteractive{ locator: p.Locator, frames: animation{sprite.Image}, rect: sprite.Rect, } return ni, nil } func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, error) { ni, err := d.buildNoninteractive(p) if err != nil { return nil, nil, err } ni.hoverImpl.text = p.Text widget := &Widget{ Locator: ni.locator, ownHoverables: []hoverable{ni}, ownPaintables: []paintable{ni}, } return ni, widget, nil } func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget, error) { ni, err := d.buildNoninteractive(p) if err != nil { return nil, nil, err } // FIXME: check if this is still needed on the bridge -> briefing transition widget := &Widget{ Locator: ni.locator, ownClickables: []clickable{ni}, ownHoverables: []hoverable{ni}, } return ni, widget, nil } // An overlay is a static image + some text that needs to be rendered func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) { ni, err := d.buildNoninteractive(p) if err != nil { return nil, nil, err } widget := &Widget{ Locator: ni.locator, ownPaintables: []paintable{ni}, } if p.Text != "" { // FIXME: is this always right? Seems to make sense for Main.mnu fnt := d.menu.Font(p.FontType/10 - 1) ni.label = &label{ font: fnt, rect: ni.rect, // We will be centered by default text: p.Text, } } else { log.Printf("Overlay without text detected in %v", p.Locator) } return ni, widget, nil } // An animation is a non-interactive element that displays something in a loop func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Widget, error) { sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0]) if err != nil { return nil, nil, err } frames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType) if err != nil { return nil, nil, err } ani := &noninteractive{ locator: p.Locator, frames: animation(frames), hoverImpl: hoverImpl{text: p.Text}, rect: sprite.Rect, } widget := &Widget{ ownHoverables: []hoverable{ani}, ownPaintables: []paintable{ani}, } return ani, widget, nil } func (d *Driver) buildAnimationHover(p *menus.Properties) (*animationHover, *Widget, error) { sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0]) if err != nil { return nil, nil, err } enterFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType) if err != nil { return nil, nil, err } exitFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0]+p.DrawType, p.DrawType) if err != nil { return nil, nil, err } ani := &animationHover{ noninteractive: noninteractive{ locator: p.Locator, frames: animation(enterFrames), hoverImpl: hoverImpl{text: p.Text}, rect: sprite.Rect, }, exitFrames: animation(exitFrames), } widget := &Widget{ ownHoverables: []hoverable{ani}, ownPaintables: []paintable{ani}, } return ani, widget, nil } func (n *noninteractive) id() string { return n.locator } 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)) // Text for a noninteractive is not registered separately if n.label != nil { out = append(out, n.label.regions(tick)...) } return out } 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) } // Top-left of where to start drawing the text. We want it to appear to be in // the centre of the rect. // // TODO: additional modes (left-aligned, especially) func (l *label) pos() image.Point { pos := l.rect.Min textRect := l.font.CalculateBounds(l.text) // Centre the text horizontally if l.align == AlignModeCentre { xSlack := l.rect.Dx() - textRect.Dx() if xSlack > 0 { pos.X += xSlack / 2 } } else { // FIXME: we're giving it 8pts of left to not look horrible pos.X += 8 } // Centre the text vertically ySlack := l.rect.Dy() - textRect.Dy() if ySlack > 0 { pos.Y += ySlack / 2 } return pos } func (l *label) regions(tick int) []region { var out []region pt := l.pos() for _, r := range l.text { glyph, err := l.font.Glyph(r) if err != nil { log.Printf("FIXME: ignoring misssing glyph %v", r) continue } out = append(out, oneRegion(pt, glyph.Image)...) pt.X += glyph.Rect.Dx() } return out }