erm/app/darktile/gui/gui.go

175 lines
4.4 KiB
Go

package gui
import (
"fmt"
"image"
"log/slog"
"os"
"strings"
"time"
"github.com/hajimehoshi/ebiten/v2"
"go.uber.org/fx"
"tuxpa.in/t/erm/app/darktile/config"
"tuxpa.in/t/erm/app/darktile/font"
"tuxpa.in/t/erm/app/darktile/gui/popup"
"tuxpa.in/t/erm/app/darktile/hinters"
"tuxpa.in/t/erm/app/darktile/termutil"
termutil2 "tuxpa.in/t/erm/app/darktile/termutil"
)
type GUI struct {
mouseStateLeft MouseState
mouseStateRight MouseState
mouseStateMiddle MouseState
mouseDrag bool
size image.Point // pixels
terminal *termutil2.Terminal
updateChan chan struct{}
lastClick time.Time
clickCount int
fontManager *font.Manager
mousePos termutil2.Position
hinters []hinters.Hinter
activeHinter int
popupMessages []popup.Message
screenshotRequested bool
screenshotFilename string
startupFuncs []func(g *GUI)
keyState *keyState
opacity float64
enableLigatures bool
cursorImage *ebiten.Image
log *slog.Logger
theme *termutil.Theme
c *config.Lark
}
type MouseState uint8
const (
MouseStateNone MouseState = iota
MouseStatePressed
)
func New(terminal *termutil2.Terminal, c *config.Lark, log *slog.Logger) (*GUI, error) {
g := &GUI{
terminal: terminal,
updateChan: make(chan struct{}),
fontManager: font.NewManager(),
activeHinter: -1,
keyState: newKeyState(),
enableLigatures: true,
c: c,
log: log,
theme: termutil.ThemeFromLark(c),
}
terminal.SetWindowManipulator(NewManipulator(g))
font, err := g.c.Font("font")
if err != nil {
return nil, err
}
g.log.Info("running gui", "font", font)
g.fontManager.SetFontByFamilyName(font.Family)
if font.Size > 0 {
g.fontManager.SetSize(font.Size)
}
if len(font.Style) > 0 {
g.fontManager.SetFontStyle(font.Style)
}
g.fontManager.SetDPI(96)
g.enableLigatures = font.Ligatures
title := config.Default("erm")(g.c.Str("title"))
ebiten.SetWindowTitle(title)
ebiten.SetScreenClearedEveryFrame(true)
ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled)
ebiten.SetRunnableOnUnfocused(true)
ebiten.SetTPS(144)
ebiten.SetScreenClearedEveryFrame(false)
if g.c.Truthy("vsync") {
ebiten.SetVsyncEnabled(true)
} else {
ebiten.SetVsyncEnabled(false)
}
rows := config.Default(128)(g.c.Int("initial_rows"))
cols := config.Default(128)(g.c.Int("initial_cols"))
scale := ebiten.Monitor().DeviceScaleFactor()
w := int(cols * g.fontManager.CharSize().X)
h := int(rows * g.fontManager.CharSize().Y)
initialSize := image.Point{
int(float64(w) * scale), int(float64(h) * scale),
}
g.size = initialSize
ebiten.SetWindowSize(w, h)
return g, nil
}
func (g *GUI) Run(s fx.Shutdowner) error {
rows := config.Default(128)(g.c.Int("initial_rows"))
cols := config.Default(128)(g.c.Int("initial_cols"))
go func() {
if err := g.terminal.Run(g.updateChan, uint16(rows), uint16(cols)); err != nil {
fmt.Fprintf(os.Stderr, "Fatal error: %s", err)
s.Shutdown(fx.ExitCode(1))
}
s.Shutdown(fx.ExitCode(0))
}()
title := config.Default("erm")(g.c.Str("title"))
for _, f := range g.startupFuncs {
go f(g)
}
g.log.Info("initial size", "size", g.size)
return ebiten.RunGameWithOptions(g, &ebiten.RunGameOptions{
X11ClassName: title,
})
}
func (g *GUI) CellSize() image.Point {
return g.fontManager.CharSize()
}
func (g *GUI) Highlight(start termutil2.Position, end termutil2.Position, label string, img image.Image) {
if label == "" && img == nil {
g.terminal.GetActiveBuffer().Highlight(start, end, nil)
return
}
annotation := &termutil2.Annotation{
Text: label,
Image: img,
}
if label != "" {
lines := strings.Split(label, "\n")
annotation.Height = float64(len(lines))
for _, line := range lines {
if float64(len(line)) > annotation.Width {
annotation.Width = float64(len(line))
}
}
}
if img != nil {
annotation.Height += float64(img.Bounds().Dy() / g.fontManager.CharSize().Y)
if label != "" {
annotation.Height += 0.5 // half line spacing between image + text
}
imgCellWidth := img.Bounds().Dx() / g.fontManager.CharSize().X
if float64(imgCellWidth) > annotation.Width {
annotation.Width = float64(imgCellWidth)
}
}
g.terminal.GetActiveBuffer().Highlight(start, end, annotation)
}
func (g *GUI) ClearHighlight() {
g.terminal.GetActiveBuffer().ClearHighlight()
}