Display listbox text
This commit is contained in:
@@ -84,15 +84,25 @@ func (e *env) Update(screenX, screenY int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (e *env) Draw(screen *ebiten.Image) error {
|
func (e *env) Draw(screen *ebiten.Image) error {
|
||||||
op := &ebiten.DrawImageOptions{}
|
xOff := 0
|
||||||
op.GeoM.Scale(e.state.zoom, e.state.zoom) // apply current zoom factor
|
for _, r := range *txt {
|
||||||
|
glyph, err := e.font.Glyph(r)
|
||||||
img, err := e.font.DrawLine(*txt)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return screen.DrawImage(img, op)
|
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 nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *env) changeZoom(_, y float64) {
|
func (e *env) changeZoom(_, y float64) {
|
||||||
|
@@ -2,10 +2,9 @@ package assetstore
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"image"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten"
|
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/fonts"
|
"code.ur.gs/lupine/ordoor/internal/fonts"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -67,50 +66,32 @@ func (a *AssetStore) Font(name string) (*Font, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: this violates the ebiten rules for fast drawing. We may need to do the
|
// CalculateBounds tries to work out what sort of size the string will be when
|
||||||
// draw ourselves, with image.Paletted for each glyph to a single ebiten.Image
|
// rendered
|
||||||
//
|
func (f *Font) CalculateBounds(text string) image.Rectangle {
|
||||||
// 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))
|
|
||||||
width := 0
|
width := 0
|
||||||
height := 0
|
height := 0
|
||||||
|
|
||||||
for _, r := range text {
|
for _, r := range text {
|
||||||
spr, ok := f.mapping[r]
|
spr, ok := f.mapping[r]
|
||||||
if !ok {
|
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()
|
width += spr.Rect.Dx()
|
||||||
if y := spr.Rect.Dy(); y > height {
|
if y := spr.Rect.Dy(); y > height {
|
||||||
height = y
|
height = y
|
||||||
}
|
}
|
||||||
|
|
||||||
sprites = append(sprites, spr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
img, err := ebiten.NewImage(width, height, ebiten.FilterDefault)
|
return image.Rect(0, 0, width, height)
|
||||||
if err != nil {
|
}
|
||||||
return nil, err
|
|
||||||
}
|
func (f *Font) Glyph(r rune) (*Sprite, error) {
|
||||||
|
glyph, ok := f.mapping[r]
|
||||||
xOff := 0
|
if !ok {
|
||||||
for _, spr := range sprites {
|
return nil, fmt.Errorf("Font %v does not specify rune %v", f.Name, r)
|
||||||
op := &ebiten.DrawImageOptions{}
|
}
|
||||||
op.GeoM.Translate(float64(xOff), 0)
|
|
||||||
|
return glyph, nil
|
||||||
xOff += spr.Rect.Dx()
|
|
||||||
|
|
||||||
if err := img.DrawImage(spr.Image, op); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return img, nil
|
|
||||||
}
|
}
|
||||||
|
@@ -2,8 +2,9 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"image"
|
||||||
|
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -25,7 +26,9 @@ type listBox struct {
|
|||||||
upBtn *button
|
upBtn *button
|
||||||
downBtn *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
|
base *noninteractive // The menu itself has a sprite to display
|
||||||
lines []*noninteractive // We display to these
|
lines []*noninteractive // We display to these
|
||||||
@@ -40,7 +43,7 @@ type listBox struct {
|
|||||||
func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
||||||
var upBtn *menus.Record
|
var upBtn *menus.Record
|
||||||
var downBtn *menus.Record
|
var downBtn *menus.Record
|
||||||
var slider *menus.Record
|
var thumb *menus.Record
|
||||||
var items []*menus.Record
|
var items []*menus.Record
|
||||||
|
|
||||||
for _, rec := range menu.Children {
|
for _, rec := range menu.Children {
|
||||||
@@ -58,20 +61,25 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
|||||||
case menus.TypeLineKbd, menus.TypeLineBriefing:
|
case menus.TypeLineKbd, menus.TypeLineBriefing:
|
||||||
items = append(items, rec)
|
items = append(items, rec)
|
||||||
case menus.TypeThumb:
|
case menus.TypeThumb:
|
||||||
if slider != nil {
|
if thumb != nil {
|
||||||
return nil, fmt.Errorf("Duplicate thumbs in menu %v", menu.Locator())
|
return nil, fmt.Errorf("Duplicate thumbs in menu %v", menu.Locator())
|
||||||
}
|
}
|
||||||
slider = rec
|
thumb = rec
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("Unrecognised child in listbox menu: %v", rec.Locator())
|
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())
|
return nil, fmt.Errorf("Missing items in menu %v", menu.Locator())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now build the wonderful thing
|
// Now build the wonderful thing
|
||||||
|
baseElem, err := registerNoninteractive(d, menu)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
elemUp, err := registerButton(d, upBtn, upBtn.SpriteId[0])
|
elemUp, err := registerButton(d, upBtn, upBtn.SpriteId[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -82,18 +90,25 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
element := &listBox{
|
thumbBaseSpr, err := d.menu.Sprite(thumb.Share)
|
||||||
upBtn: elemUp,
|
|
||||||
downBtn: elemDown,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, rec := range items {
|
|
||||||
ni, err := registerNoninteractive(d, rec)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
element.lines = append(element.lines, ni)
|
thumbImgSpr, err := d.menu.Sprite(thumb.SpriteId[0])
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
// 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))
|
element.strings = append(element.strings, fmt.Sprintf("FOO %v", i))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Register everything
|
// Register everything. Since we're a composite of other controls, they are
|
||||||
// FIXME: we should be able to freeze/unfreeze as a group. The buttons register themselves though...
|
// 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
|
return nil, nil
|
||||||
}
|
}
|
||||||
@@ -148,11 +185,31 @@ func (l *listBox) refresh() {
|
|||||||
for i, ni := range l.lines {
|
for i, ni := range l.lines {
|
||||||
// FIXME: noninteractive isn't set up for dynamic text yet. Need to
|
// FIXME: noninteractive isn't set up for dynamic text yet. Need to
|
||||||
// generate textImg on demand instead of once at start.
|
// generate textImg on demand instead of once at start.
|
||||||
ni.hoverImpl.text = ""
|
if ni.label != nil {
|
||||||
|
ni.label.text = ""
|
||||||
if len(l.strings) > l.offset+i {
|
if len(l.strings) > l.offset+i {
|
||||||
ni.hoverImpl.text = l.strings[l.offset+i]
|
ni.label.text = l.strings[l.offset+i]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *listBox) thumbPos() image.Point {
|
||||||
|
pos := l.thumbImg.Rect.Min
|
||||||
|
if len(l.strings) == 0 {
|
||||||
|
return pos
|
||||||
|
}
|
||||||
|
|
||||||
log.Printf("Listbox offset: %v", l.offset)
|
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
|
||||||
}
|
}
|
||||||
|
@@ -4,11 +4,17 @@ import (
|
|||||||
"image"
|
"image"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type AlignMode int
|
||||||
|
|
||||||
|
const (
|
||||||
|
AlignModeCentre AlignMode = 0
|
||||||
|
AlignModeLeft AlignMode = 1
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
registerBuilder(menus.TypeStatic, noChildren(registerStatic))
|
registerBuilder(menus.TypeStatic, noChildren(registerStatic))
|
||||||
registerBuilder(menus.TypeHypertext, noChildren(registerHypertext))
|
registerBuilder(menus.TypeHypertext, noChildren(registerHypertext))
|
||||||
@@ -27,13 +33,20 @@ type noninteractive struct {
|
|||||||
rect image.Rectangle
|
rect image.Rectangle
|
||||||
|
|
||||||
// Some non-interactives, e.g., overlays, are an image + text to be shown
|
// Some non-interactives, e.g., overlays, are an image + text to be shown
|
||||||
textImg *ebiten.Image
|
label *label
|
||||||
textOffset image.Point
|
|
||||||
|
|
||||||
clickImpl // Alright, alright, it turns out the bridge mission briefing is clickable
|
clickImpl // Alright, alright, it turns out the bridge mission briefing is clickable
|
||||||
hoverImpl
|
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
|
// This particular animation has entry and exit sequences, which are invoked
|
||||||
// when entering and leaving hover, respectively. Example: bridge doors
|
// when entering and leaving hover, respectively. Example: bridge doors
|
||||||
type animationHover struct {
|
type animationHover struct {
|
||||||
@@ -106,34 +119,21 @@ func registerOverlay(d *Driver, r *menus.Record) error {
|
|||||||
rect: sprite.Rect,
|
rect: sprite.Rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
d.paintables = append(d.paintables, ni)
|
||||||
|
|
||||||
if r.Text != "" {
|
if r.Text != "" {
|
||||||
// FIXME: is this always right? Seems to make sense for Main.mnu
|
// FIXME: is this always right? Seems to make sense for Main.mnu
|
||||||
fnt := d.menu.Font(r.FontType/10 - 1)
|
fnt := d.menu.Font(r.FontType/10 - 1)
|
||||||
|
|
||||||
textImg, err := fnt.DrawLine(r.Text)
|
ni.label = &label{
|
||||||
if err != nil {
|
font: fnt,
|
||||||
return err
|
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 {
|
} else {
|
||||||
log.Printf("Overlay without text detected: %#+v", r)
|
log.Printf("Overlay without text detected: %#+v", r)
|
||||||
}
|
}
|
||||||
|
|
||||||
d.paintables = append(d.paintables, ni)
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -206,17 +206,14 @@ func (n *noninteractive) bounds() image.Rectangle {
|
|||||||
func (n *noninteractive) regions(tick int) []region {
|
func (n *noninteractive) regions(tick int) []region {
|
||||||
out := oneRegion(n.bounds().Min, n.frames.image(tick))
|
out := oneRegion(n.bounds().Min, n.frames.image(tick))
|
||||||
|
|
||||||
if n.textImg != nil {
|
// Text for a noninteractive is not registered separately
|
||||||
out = append(out, oneRegion(n.textPos(), n.textImg)...)
|
if n.label != nil {
|
||||||
|
out = append(out, n.label.regions(tick)...)
|
||||||
}
|
}
|
||||||
|
|
||||||
return out
|
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 {
|
func (a *animationHover) regions(tick int) []region {
|
||||||
if a.opening || a.closing {
|
if a.opening || a.closing {
|
||||||
var anim animation
|
var anim animation
|
||||||
@@ -248,3 +245,51 @@ func (a *animationHover) setHoverState(value bool) {
|
|||||||
|
|
||||||
a.hoverImpl.setHoverState(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
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user