diff --git a/cmd/view-font/main.go b/cmd/view-font/main.go index 7d01462..2be58ae 100644 --- a/cmd/view-font/main.go +++ b/cmd/view-font/main.go @@ -84,15 +84,25 @@ func (e *env) Update(screenX, screenY int) error { } func (e *env) Draw(screen *ebiten.Image) error { - op := &ebiten.DrawImageOptions{} - op.GeoM.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor + xOff := 0 + for _, r := range *txt { + glyph, err := e.font.Glyph(r) + if err != nil { + return err + } - img, err := e.font.DrawLine(*txt) - if err != nil { - return err + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate(float64(xOff), 0) + op.GeoM.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor + + if err := screen.DrawImage(glyph.Image, op); err != nil { + return err + } + + xOff += glyph.Rect.Dx() } - return screen.DrawImage(img, op) + return nil } func (e *env) changeZoom(_, y float64) { diff --git a/internal/assetstore/font.go b/internal/assetstore/font.go index 1f7ffc6..bf48c11 100644 --- a/internal/assetstore/font.go +++ b/internal/assetstore/font.go @@ -2,10 +2,9 @@ package assetstore import ( "fmt" + "image" "log" - "github.com/hajimehoshi/ebiten" - "code.ur.gs/lupine/ordoor/internal/fonts" ) @@ -67,50 +66,32 @@ func (a *AssetStore) Font(name string) (*Font, error) { return out, nil } -// FIXME: this violates the ebiten rules for fast drawing. We may need to do the -// draw ourselves, with image.Paletted for each glyph to a single ebiten.Image -// -// FIXME: it'd be great if we didn't have to implement this all by ourselves; -// golang.org/x/image/font and github.com/hajimehoshi/ebiten/text are *almost* -// sufficient, but don't seem to like it when the glyphs are literal colours -// instead of a mask. -// -// TODO: draw text in a bounding box, multiple lines, etc -func (f *Font) DrawLine(text string) (*ebiten.Image, error) { - sprites := make([]*Sprite, 0, len(text)) +// CalculateBounds tries to work out what sort of size the string will be when +// rendered +func (f *Font) CalculateBounds(text string) image.Rectangle { width := 0 height := 0 for _, r := range text { spr, ok := f.mapping[r] if !ok { - return nil, fmt.Errorf("Font %v does not specify rune %v", f.Name, r) + continue // FIXME: we could add the space character or something? } width += spr.Rect.Dx() if y := spr.Rect.Dy(); y > height { height = y } - - sprites = append(sprites, spr) } - img, err := ebiten.NewImage(width, height, ebiten.FilterDefault) - if err != nil { - return nil, err - } - - xOff := 0 - for _, spr := range sprites { - op := &ebiten.DrawImageOptions{} - op.GeoM.Translate(float64(xOff), 0) - - xOff += spr.Rect.Dx() - - if err := img.DrawImage(spr.Image, op); err != nil { - return nil, err - } - } - - return img, nil + return image.Rect(0, 0, width, height) +} + +func (f *Font) Glyph(r rune) (*Sprite, error) { + glyph, ok := f.mapping[r] + if !ok { + return nil, fmt.Errorf("Font %v does not specify rune %v", f.Name, r) + } + + return glyph, nil } diff --git a/internal/ui/list_box.go b/internal/ui/list_box.go index 756feb9..c50c1ce 100644 --- a/internal/ui/list_box.go +++ b/internal/ui/list_box.go @@ -2,8 +2,9 @@ package ui import ( "fmt" - "log" + "image" + "code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/menus" ) @@ -25,7 +26,9 @@ type listBox struct { upBtn *button downBtn *button - // TODO: can we reuse the slider element for the thumb? + // FIXME: can we share code between slider and this element? + thumbBase *assetstore.Sprite // Bounds are given by this + thumbImg *assetstore.Sprite // This is displayed at offset * (height / steps) base *noninteractive // The menu itself has a sprite to display lines []*noninteractive // We display to these @@ -40,7 +43,7 @@ type listBox struct { func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { var upBtn *menus.Record var downBtn *menus.Record - var slider *menus.Record + var thumb *menus.Record var items []*menus.Record for _, rec := range menu.Children { @@ -58,20 +61,25 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { case menus.TypeLineKbd, menus.TypeLineBriefing: items = append(items, rec) case menus.TypeThumb: - if slider != nil { + if thumb != nil { return nil, fmt.Errorf("Duplicate thumbs in menu %v", menu.Locator()) } - slider = rec + thumb = rec default: return nil, fmt.Errorf("Unrecognised child in listbox menu: %v", rec.Locator()) } } - if len(items) == 0 || slider == nil || upBtn == nil || downBtn == nil { + if len(items) == 0 || thumb == nil || upBtn == nil || downBtn == nil { return nil, fmt.Errorf("Missing items in menu %v", menu.Locator()) } // Now build the wonderful thing + baseElem, err := registerNoninteractive(d, menu) + if err != nil { + return nil, err + } + elemUp, err := registerButton(d, upBtn, upBtn.SpriteId[0]) if err != nil { return nil, err @@ -82,18 +90,25 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { return nil, err } - element := &listBox{ - upBtn: elemUp, - downBtn: elemDown, + thumbBaseSpr, err := d.menu.Sprite(thumb.Share) + if err != nil { + return nil, err } - for _, rec := range items { - ni, err := registerNoninteractive(d, rec) - if err != nil { - return nil, err - } + thumbImgSpr, err := d.menu.Sprite(thumb.SpriteId[0]) + if err != nil { + return nil, err + } - element.lines = append(element.lines, ni) + element := &listBox{ + base: baseElem, + // TODO: upBtn needs to be frozen when offset == 0; downBtn when offset == max + upBtn: elemUp, + downBtn: elemDown, + + // TODO: need to be able to drag the thumb + thumbBase: thumbBaseSpr, + thumbImg: thumbImgSpr, } // Internal wiring-up @@ -105,8 +120,30 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) { element.strings = append(element.strings, fmt.Sprintf("FOO %v", i)) } - // Register everything - // FIXME: we should be able to freeze/unfreeze as a group. The buttons register themselves though... + // Register everything. Since we're a composite of other controls, they are + // mostly self-registered at the moment. + d.paintables = append(d.paintables, element) + + // FIXME: we should be able to freeze/unfreeze as a group. + + // HURK: These need to be registered after the other elements so they are + // drawn in the correct order to be visible + for _, rec := range items { + ni, err := registerNoninteractive(d, rec) + if err != nil { + return nil, err + } + // TODO: pick the correct font + ni.label = &label{ + align: AlignModeLeft, + font: d.menu.Font(0), + rect: ni.rect, + } + + element.lines = append(element.lines, ni) + } + + element.refresh() return nil, nil } @@ -148,11 +185,31 @@ func (l *listBox) refresh() { for i, ni := range l.lines { // FIXME: noninteractive isn't set up for dynamic text yet. Need to // generate textImg on demand instead of once at start. - ni.hoverImpl.text = "" - if len(l.strings) > l.offset+i { - ni.hoverImpl.text = l.strings[l.offset+i] + if ni.label != nil { + ni.label.text = "" + if len(l.strings) > l.offset+i { + ni.label.text = l.strings[l.offset+i] + } } } - - log.Printf("Listbox offset: %v", l.offset) +} + +func (l *listBox) thumbPos() image.Point { + pos := l.thumbImg.Rect.Min + if len(l.strings) == 0 { + return pos + } + + pixPerLine := (l.thumbBase.Rect.Dy()) / (len(l.strings) - len(l.lines)) + pos.Y += pixPerLine * l.offset + + return pos +} + +func (l *listBox) regions(tick int) []region { + // Draw the slider at the appropriate point + out := oneRegion(l.thumbBase.Rect.Min, l.thumbBase.Image) + out = append(out, oneRegion(l.thumbPos(), l.thumbImg.Image)...) + + return out } diff --git a/internal/ui/noninteractive.go b/internal/ui/noninteractive.go index 262bdf9..38021e9 100644 --- a/internal/ui/noninteractive.go +++ b/internal/ui/noninteractive.go @@ -4,11 +4,17 @@ import ( "image" "log" - "github.com/hajimehoshi/ebiten" - + "code.ur.gs/lupine/ordoor/internal/assetstore" "code.ur.gs/lupine/ordoor/internal/menus" ) +type AlignMode int + +const ( + AlignModeCentre AlignMode = 0 + AlignModeLeft AlignMode = 1 +) + func init() { registerBuilder(menus.TypeStatic, noChildren(registerStatic)) registerBuilder(menus.TypeHypertext, noChildren(registerHypertext)) @@ -27,13 +33,20 @@ type noninteractive struct { rect image.Rectangle // Some non-interactives, e.g., overlays, are an image + text to be shown - textImg *ebiten.Image - textOffset image.Point + 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 { @@ -106,34 +119,21 @@ func registerOverlay(d *Driver, r *menus.Record) error { rect: sprite.Rect, } + d.paintables = append(d.paintables, ni) + 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.label = &label{ + font: fnt, + rect: ni.rect, // We will be centered by default + text: r.Text, } - - 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 } @@ -206,17 +206,14 @@ func (n *noninteractive) bounds() image.Rectangle { 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)...) + // Text for a noninteractive is not registered separately + if n.label != nil { + out = append(out, n.label.regions(tick)...) } 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 @@ -248,3 +245,51 @@ func (a *animationHover) setHoverState(value bool) { 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 +}