Files
LilGuy/internal/ui/hud/elements.go
2025-11-24 12:18:32 -07:00

224 lines
4.5 KiB
Go

package hud
import (
"fmt"
"image/color"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/text/v2"
"golang.org/x/image/font/basicfont"
"github.com/atridad/LilGuy/internal/status"
)
var (
basicFace = text.NewGoXFace(basicfont.Face7x13)
basicFaceAscent = basicFace.Metrics().HAscent
rectPixel = newRectPixel()
)
func newRectPixel() *ebiten.Image {
img := ebiten.NewImage(1, 1)
img.Fill(color.White)
return img
}
func drawHUDText(screen *ebiten.Image, txt string, clr color.Color, x, y int) {
if clr == nil {
clr = color.White
}
op := &text.DrawOptions{}
op.GeoM.Translate(float64(x), float64(y)-basicFaceAscent)
op.ColorScale.ScaleWithColor(clr)
text.Draw(screen, txt, basicFace, op)
}
// Drawable HUD chunk.
type Element interface {
Draw(screen *ebiten.Image, x, y int) (width, height int)
}
// Plain text node.
type Label struct {
Text string
Color color.Color
}
func (l Label) Draw(screen *ebiten.Image, x, y int) (int, int) {
if l.Color == nil {
l.Color = color.White
}
drawHUDText(screen, l.Text, l.Color, x, y)
width := len(l.Text) * 7 // approximate width for basicfont
return width, 13
}
// Percent readout node.
type PercentageDisplay struct {
Meter status.Meter
Color color.Color
}
func (p PercentageDisplay) Draw(screen *ebiten.Image, x, y int) (int, int) {
if p.Color == nil {
p.Color = color.White
}
txt := fmt.Sprintf("%3.0f%%", p.Meter.Level)
drawHUDText(screen, txt, p.Color, x, y)
return len(txt) * 7, 13
}
// Combined label and percent.
type MeterLabel struct {
Meter status.Meter
Color color.Color
}
func (m MeterLabel) Draw(screen *ebiten.Image, x, y int) (int, int) {
if m.Color == nil {
m.Color = color.White
}
var txt string
if m.Meter.Base < 0 {
// Text-only display without percentage.
txt = m.Meter.Label
} else {
// Standard meter with percentage.
txt = fmt.Sprintf("%s: %3.0f%%", m.Meter.Label, m.Meter.Level)
}
drawHUDText(screen, txt, m.Meter.Color, x, y)
return len(txt) * 7, 13
}
// Horizontal meter bar.
type Bar struct {
Meter status.Meter
MaxWidth int
Height int
BorderColor color.Color
ShowBorder bool
}
func (b Bar) Draw(screen *ebiten.Image, x, y int) (int, int) {
maxWidth := b.MaxWidth
if maxWidth <= 0 {
maxWidth = 180
}
height := b.Height
if height <= 0 {
height = 8
}
fillWidth := int((b.Meter.Level / 100.0) * float64(maxWidth))
if fillWidth < 0 {
fillWidth = 0
}
if fillWidth > maxWidth {
fillWidth = maxWidth
}
// Draw border if requested
if b.ShowBorder {
borderColor := b.BorderColor
if borderColor == nil {
borderColor = color.RGBA{R: 80, G: 80, B: 80, A: 255}
}
// Top border
drawRect(screen, x, y, maxWidth, 1, borderColor)
// Bottom border
drawRect(screen, x, y+height-1, maxWidth, 1, borderColor)
// Left border
drawRect(screen, x, y, 1, height, borderColor)
// Right border
drawRect(screen, x+maxWidth-1, y, 1, height, borderColor)
}
// Draw filled portion
if fillWidth > 0 {
drawRect(screen, x, y, fillWidth, height, b.Meter.Color)
}
return maxWidth, height
}
// Helper for filled rectangles.
func drawRect(screen *ebiten.Image, x, y, width, height int, clr color.Color) {
if width <= 0 || height <= 0 {
return
}
if clr == nil {
clr = color.White
}
op := &ebiten.DrawImageOptions{}
op.GeoM.Scale(float64(width), float64(height))
op.GeoM.Translate(float64(x), float64(y))
op.ColorScale.ScaleWithColor(clr)
screen.DrawImage(rectPixel, op)
}
// Empty padding block.
type Spacer struct {
Width int
Height int
}
func (s Spacer) Draw(screen *ebiten.Image, x, y int) (int, int) {
return s.Width, s.Height
}
// Horizontal layout row.
type Row struct {
Elements []Element
Spacing int
}
func (r Row) Draw(screen *ebiten.Image, x, y int) (int, int) {
totalWidth := 0
maxHeight := 0
currentX := x
for i, elem := range r.Elements {
w, h := elem.Draw(screen, currentX, y)
totalWidth += w
if h > maxHeight {
maxHeight = h
}
currentX += w
if i < len(r.Elements)-1 {
currentX += r.Spacing
totalWidth += r.Spacing
}
}
return totalWidth, maxHeight
}
// Vertical stack layout.
type Column struct {
Elements []Element
Spacing int
}
func (c Column) Draw(screen *ebiten.Image, x, y int) (int, int) {
maxWidth := 0
totalHeight := 0
currentY := y
for i, elem := range c.Elements {
w, h := elem.Draw(screen, x, currentY)
if w > maxWidth {
maxWidth = w
}
totalHeight += h
currentY += h
if i < len(c.Elements)-1 {
currentY += c.Spacing
totalHeight += c.Spacing
}
}
return maxWidth, totalHeight
}