tui/screen.go

207 lines
4.1 KiB
Go

package tui
import (
"fmt"
"git.milar.in/milarin/adverr"
"git.milar.in/milarin/buf2d"
"git.milar.in/milarin/ds"
"github.com/gdamore/tcell"
)
type Screen struct {
EventTmpl
scr tcell.Screen
buf *ViewBuffer
stopCh chan error
redrawCh chan struct{}
started bool
modals ds.Stack[View]
// Root is the root view which is currently shown on screen
Root View
// Some unicode characters need more bytes than one. For these characters,
// an additional last column will be provided in the internal view buffer.
// That way, these characters can also be shown on the right most column.
//
// You should enable this flag if you have missing unicode characters in the last column.
UnicodeSupport bool
}
func NewScreen(root View) (*Screen, error) {
scr, err := tcell.NewScreen()
if err != nil {
return nil, err
}
s := &Screen{
Root: root,
scr: scr,
stopCh: make(chan error, 1),
redrawCh: make(chan struct{}, 1),
modals: ds.NewArrayStack[View](),
}
s.KeyPressed = CloseOnCtrlC(s)
return s, nil
}
func (s *Screen) Start() error {
err := s.scr.Init()
if err != nil {
return err
}
defer s.scr.Fini()
defer close(s.redrawCh)
s.scr.EnableMouse()
go s.eventloop()
go s.drawloop()
s.started = true
return <-s.stopCh
}
func (s *Screen) Stop() {
s.StopWithError(nil)
}
func (s *Screen) StopWithError(err error) {
s.stopCh <- err
}
func (s *Screen) onKeyPressed(event *KeyEvent) {
if !s.modals.Empty() {
s.modals.Peek().OnKeyPressed(event)
return
}
if s.KeyPressed == nil || !s.KeyPressed(event) {
s.Root.OnKeyPressed(event)
}
s.Redraw()
}
func (s *Screen) onMouseEvent(event *MouseEvent) {
if event.Button != MouseButtonNone {
defer s.Redraw()
}
if !s.modals.Empty() {
s.modals.Peek().OnMouseEvent(event)
return
}
if s.MouseEvent != nil && s.MouseEvent(event) {
return
}
s.Root.OnMouseEvent(event)
}
func convertMouseEvent(original *tcell.EventMouse) *MouseEvent {
x, y := original.Position()
return &MouseEvent{
Position: P(x, y),
Button: convertMouseButton(original.Buttons()),
Modifiers: original.Modifiers(),
}
}
func (s *Screen) Redraw() {
if s.started {
s.redrawCh <- struct{}{}
}
}
func (s *Screen) eventloop() {
defer s.handlePanic("panicked while handling event")
for evt := s.scr.PollEvent(); evt != nil; evt = s.scr.PollEvent() {
switch event := evt.(type) {
case *tcell.EventResize:
s.Redraw()
case *tcell.EventKey:
s.startPanicSafeThread("panicked while handling key event", func() { s.onKeyPressed(event) })
case *tcell.EventMouse:
s.startPanicSafeThread("panicked while handling mouse event", func() { s.onMouseEvent(convertMouseEvent(event)) })
default:
s.StopWithError(fmt.Errorf("%#v", event))
}
}
}
func (s *Screen) startPanicSafeThread(errorMessage string, f func()) {
go func() {
defer s.handlePanic(errorMessage)
f()
}()
}
func (s *Screen) drawloop() {
defer s.handlePanic("panicked while redrawing")
for range s.redrawCh {
s.prepareViewBuffer()
// draw root view
rw, rh := s.Root.Layout()
s.Root.Draw(truncateBuffer(s.buf, rw, rh))
// draw modals
for i := 0; i < s.modals.Size(); i++ {
v := s.modals.PeekAt(i)
mw, mh := v.Layout()
v.Draw(s.buf.Sub(0, 0, iff(mw >= 0, mw, s.buf.Width()), iff(mh >= 0, mh, s.buf.Height())))
}
// draw buffer onto screen
drawBuffer(s.scr, s.buf)
}
}
func (s *Screen) prepareViewBuffer() {
w, h := s.scr.Size()
if s.UnicodeSupport {
if s.buf == nil || s.buf.Width() != w-1 || s.buf.Height() != h {
s.buf = buf2d.NewBuffer(w, h, DefaultRune).Sub(0, 0, w-1, h)
} else {
s.buf.Fill(DefaultRune)
}
} else {
if s.buf == nil || s.buf.Width() != w || s.buf.Height() != h {
s.buf = buf2d.NewBuffer(w, h, DefaultRune)
} else {
s.buf.Fill(DefaultRune)
}
}
}
func (s *Screen) handlePanic(msg string) {
if err := recover(); err != nil {
if e, ok := err.(error); ok {
s.StopWithError(adverr.Wrap(msg, e))
} else {
s.StopWithError(adverr.Wrap(msg, fmt.Errorf("%v", err)))
}
}
}
func (s *Screen) OpenModal(v View) {
s.modals.Push(v)
s.Redraw()
}
func (s *Screen) CloseModal() {
s.modals.Pop()
s.Redraw()
}