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, size: image.Point{80, 30}, 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)) return g, nil } func (g *GUI) Run(s fx.Shutdowner) error { go func() { if err := g.terminal.Run(g.updateChan, uint16(g.size.X), uint16(g.size.Y)); err != nil { fmt.Fprintf(os.Stderr, "Fatal error: %s", err) s.Shutdown(fx.ExitCode(1)) } s.Shutdown(fx.ExitCode(0)) }() font, err := g.c.Font("font") if err != nil { return 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 ebiten.SetWindowTitle("erm") 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) } for _, f := range g.startupFuncs { go f(g) } return ebiten.RunGame(g) } 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() }