2
0
mirror of https://github.com/rivo/tview.git synced 2024-11-15 06:12:46 +00:00
tview/application.go

729 lines
19 KiB
Go
Raw Normal View History

package tview
import (
"sync"
"time"
"github.com/gdamore/tcell"
)
const (
// The size of the event/update/redraw channels.
queueSize = 100
// The minimum time between two consecutive redraws.
redrawPause = 50 * time.Millisecond
)
2018-10-28 12:42:49 +00:00
// DoubleClickInterval specifies the maximum time between clicks to register a
// double click rather than click.
var DoubleClickInterval = 500 * time.Millisecond
// MouseAction indicates one of the actions the mouse is logically doing.
type MouseAction int16
// Available mouse actions.
const (
MouseMove MouseAction = iota
MouseLeftDown
MouseLeftUp
MouseLeftClick
MouseLeftDoubleClick
MouseMiddleDown
MouseMiddleUp
MouseMiddleClick
MouseMiddleDoubleClick
MouseRightDown
MouseRightUp
MouseRightClick
MouseRightDoubleClick
WheelUp
WheelDown
WheelLeft
WheelRight
)
// queuedUpdate represented the execution of f queued by
// Application.QueueUpdate(). The "done" channel receives exactly one element
// after f has executed.
type queuedUpdate struct {
f func()
done chan struct{}
}
// Application represents the top node of an application.
//
// It is not strictly required to use this class as none of the other classes
// depend on it. However, it provides useful tools to set up an application and
// plays nicely with all widgets.
//
// The following command displays a primitive p on the screen until Ctrl-C is
// pressed:
//
// if err := tview.NewApplication().SetRoot(p, true).Run(); err != nil {
// panic(err)
// }
type Application struct {
sync.RWMutex
// The application's screen. Apart from Run(), this variable should never be
// set directly. Always use the screenReplacement channel after calling
// Fini(), to set a new screen (or nil to stop the application).
screen tcell.Screen
// The primitive which currently has the keyboard focus.
focus Primitive
// The root primitive to be seen on the screen.
root Primitive
// Whether or not the application resizes the root primitive.
2018-03-05 15:37:10 +00:00
rootFullscreen bool
// Set to true if mouse events are enabled.
2019-11-04 05:34:46 +00:00
enableMouse bool
// An optional capture function which receives a key event and returns the
// event to be forwarded to the default input handler (nil if nothing should
// be forwarded).
inputCapture func(event *tcell.EventKey) *tcell.EventKey
// An optional callback function which is invoked just before the root
// primitive is drawn.
beforeDraw func(screen tcell.Screen) bool
// An optional callback function which is invoked after the root primitive
// was drawn.
afterDraw func(screen tcell.Screen)
// Used to send screen events from separate goroutine to main event loop
events chan tcell.Event
2018-10-28 12:42:49 +00:00
// Functions queued from goroutines, used to serialize updates to primitives.
updates chan queuedUpdate
2018-10-28 12:42:49 +00:00
// An object that the screen variable will be set to after Fini() was called.
// Use this channel to set a new screen object for the application
// (screen.Init() and draw() will be called implicitly). A value of nil will
// stop the application.
screenReplacement chan tcell.Screen
2019-11-04 05:34:46 +00:00
// An optional capture function which receives a mouse event and returns the
// event to be forwarded to the default mouse handler (nil if nothing should
// be forwarded).
mouseCapture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)
mouseCapturingPrimitive Primitive // A Primitive returned by a MouseHandler which will capture future mouse events.
lastMouseX, lastMouseY int // The last position of the mouse.
mouseDownX, mouseDownY int // The position of the mouse when its button was last pressed.
lastMouseClick time.Time // The time when a mouse button was last clicked.
lastMouseButtons tcell.ButtonMask // The last mouse button state.
}
// NewApplication creates and returns a new application.
func NewApplication() *Application {
return &Application{
events: make(chan tcell.Event, queueSize),
updates: make(chan queuedUpdate, queueSize),
screenReplacement: make(chan tcell.Screen, 1),
}
}
// SetInputCapture sets a function which captures all key events before they are
// forwarded to the key event handler of the primitive which currently has
// focus. This function can then choose to forward that key event (or a
// different one) by returning it or stop the key event processing by returning
// nil.
//
// Note that this also affects the default event handling of the application
// itself: Such a handler can intercept the Ctrl-C event which closes the
// application.
func (a *Application) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *Application {
a.inputCapture = capture
return a
}
// GetInputCapture returns the function installed with SetInputCapture() or nil
// if no such function has been installed.
func (a *Application) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
return a.inputCapture
}
// SetMouseCapture sets a function which captures mouse events (consisting of
// the original tcell mouse event and the semantic mouse action) before they are
// forwarded to the appropriate mouse event handler. This function can then
// choose to forward that event (or a different one) by returning it or stop
// the event processing by returning a nil mouse event.
func (a *Application) SetMouseCapture(capture func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction)) *Application {
2019-11-04 05:34:46 +00:00
a.mouseCapture = capture
return a
}
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (a *Application) GetMouseCapture() func(event *tcell.EventMouse, action MouseAction) (*tcell.EventMouse, MouseAction) {
2019-11-04 05:34:46 +00:00
return a.mouseCapture
}
// SetScreen allows you to provide your own tcell.Screen object. For most
// applications, this is not needed and you should be familiar with
// tcell.Screen when using this function.
//
// This function is typically called before the first call to Run(). Init() need
// not be called on the screen.
func (a *Application) SetScreen(screen tcell.Screen) *Application {
if screen == nil {
return a // Invalid input. Do nothing.
}
a.Lock()
if a.screen == nil {
// Run() has not been called yet.
a.screen = screen
a.Unlock()
return a
}
// Run() is already in progress. Exchange screen.
oldScreen := a.screen
a.Unlock()
oldScreen.Fini()
a.screenReplacement <- screen
return a
}
2019-11-04 05:34:46 +00:00
// EnableMouse enables mouse events.
func (a *Application) EnableMouse(enable bool) *Application {
2019-11-04 05:34:46 +00:00
a.Lock()
defer a.Unlock()
if enable != a.enableMouse && a.screen != nil {
if enable {
a.screen.EnableMouse()
} else {
a.screen.DisableMouse()
}
}
a.enableMouse = enable
2019-11-04 05:42:46 +00:00
return a
2019-11-04 05:34:46 +00:00
}
// Run starts the application and thus the event loop. This function returns
// when Stop() was called.
func (a *Application) Run() error {
var (
err error
width, height int // The current size of the screen.
lastRedraw time.Time // The time the screen was last redrawn.
redrawTimer *time.Timer // A timer to schedule the next redraw.
)
a.Lock()
// Make a screen if there is none yet.
if a.screen == nil {
a.screen, err = tcell.NewScreen()
if err != nil {
a.Unlock()
return err
}
if err = a.screen.Init(); err != nil {
a.Unlock()
return err
}
2019-11-04 05:34:46 +00:00
if a.enableMouse {
a.screen.EnableMouse()
}
}
// We catch panics to clean up because they mess up the terminal.
defer func() {
if p := recover(); p != nil {
if a.screen != nil {
a.screen.Fini()
}
panic(p)
}
}()
// Draw the screen for the first time.
a.Unlock()
2018-10-28 12:42:49 +00:00
a.draw()
2018-10-28 12:42:49 +00:00
// Separate loop to wait for screen events.
var wg sync.WaitGroup
wg.Add(1)
go func() {
2018-10-28 12:42:49 +00:00
defer wg.Done()
for {
a.RLock()
screen := a.screen
a.RUnlock()
if screen == nil {
// We have no screen. Let's stop.
a.QueueEvent(nil)
break
}
2018-10-28 12:42:49 +00:00
// Wait for next event and queue it.
event := screen.PollEvent()
if event != nil {
// Regular event. Queue.
a.QueueEvent(event)
continue
}
2018-10-28 12:42:49 +00:00
// A screen was finalized (event is nil). Wait for a new scren.
screen = <-a.screenReplacement
if screen == nil {
// No new screen. We're done.
a.QueueEvent(nil)
return
}
2018-10-28 12:42:49 +00:00
// We have a new screen. Keep going.
a.Lock()
a.screen = screen
a.Unlock()
// Initialize and draw this screen.
if err := screen.Init(); err != nil {
panic(err)
}
a.draw()
}
}()
// Start event loop.
2018-10-28 12:42:49 +00:00
EventLoop:
for {
select {
case event := <-a.events:
if event == nil {
2018-10-28 12:42:49 +00:00
break EventLoop
}
switch event := event.(type) {
case *tcell.EventKey:
a.RLock()
p := a.focus
2018-10-28 12:42:49 +00:00
inputCapture := a.inputCapture
a.RUnlock()
// Intercept keys.
2018-10-28 12:42:49 +00:00
if inputCapture != nil {
event = inputCapture(event)
if event == nil {
2018-12-26 20:24:39 +00:00
a.draw()
continue // Don't forward event.
}
}
// Ctrl-C closes the application.
if event.Key() == tcell.KeyCtrlC {
a.Stop()
}
// Pass other key events to the currently focused primitive.
if p != nil {
if handler := p.InputHandler(); handler != nil {
handler(event, func(p Primitive) {
a.SetFocus(p)
})
2018-10-28 12:42:49 +00:00
a.draw()
}
}
case *tcell.EventResize:
if time.Since(lastRedraw) < redrawPause {
if redrawTimer != nil {
redrawTimer.Stop()
}
redrawTimer = time.AfterFunc(redrawPause, func() {
a.events <- event
})
}
a.RLock()
screen := a.screen
a.RUnlock()
if screen == nil {
continue
}
newWidth, newHeight := screen.Size()
if newWidth == width && newHeight == height {
continue
}
width, height = newWidth, newHeight
2020-01-29 06:55:29 +00:00
lastRedraw = time.Now()
screen.Clear()
2018-10-28 12:42:49 +00:00
a.draw()
2019-11-04 05:34:46 +00:00
case *tcell.EventMouse:
2020-03-03 18:07:39 +00:00
consumed, isMouseDownAction := a.fireMouseActions(event)
if consumed {
a.draw()
2020-02-28 01:19:36 +00:00
}
a.lastMouseButtons = event.Buttons()
2020-03-03 16:16:56 +00:00
if isMouseDownAction {
2020-02-28 01:19:36 +00:00
a.mouseDownX, a.mouseDownY = event.Position()
2019-11-04 05:34:46 +00:00
}
}
2018-10-28 12:42:49 +00:00
// If we have updates, now is the time to execute them.
case update := <-a.updates:
update.f()
update.done <- struct{}{}
2018-10-28 12:42:49 +00:00
}
}
// Wait for the event loop to finish.
2018-10-28 12:42:49 +00:00
wg.Wait()
a.screen = nil
2018-10-28 12:42:49 +00:00
return nil
}
// fireMouseActions analyzes the provided mouse event, derives mouse actions
// from it and then forwards them to the corresponding primitives.
2020-03-03 18:07:39 +00:00
func (a *Application) fireMouseActions(event *tcell.EventMouse) (consumed, isMouseDownAction bool) {
// We want to relay follow-up events to the same target primitive.
var targetPrimitive Primitive
2020-02-28 01:19:36 +00:00
// Helper function to fire a mouse action.
fire := func(action MouseAction) {
2020-03-03 18:07:39 +00:00
switch action {
case MouseLeftDown, MouseMiddleDown, MouseRightDown:
isMouseDownAction = true
}
2020-02-28 01:19:36 +00:00
2020-03-03 18:07:39 +00:00
// Intercept event.
if a.mouseCapture != nil {
event, action = a.mouseCapture(event, action)
2020-03-03 18:07:39 +00:00
if event == nil {
consumed = true
return // Don't forward event.
2020-02-28 01:19:36 +00:00
}
}
// Determine the target primitive.
var primitive, capturingPrimitive Primitive
if a.mouseCapturingPrimitive != nil {
primitive = a.mouseCapturingPrimitive
targetPrimitive = a.mouseCapturingPrimitive
} else if targetPrimitive != nil {
primitive = targetPrimitive
2020-02-28 01:19:36 +00:00
} else {
primitive = a.root
2020-03-03 18:07:39 +00:00
}
if primitive != nil {
if handler := primitive.MouseHandler(); handler != nil {
var wasConsumed bool
wasConsumed, capturingPrimitive = handler(action, event, func(p Primitive) {
2020-03-03 18:07:39 +00:00
a.SetFocus(p)
})
if wasConsumed {
2020-03-03 18:07:39 +00:00
consumed = true
2020-02-28 01:19:36 +00:00
}
}
}
a.mouseCapturingPrimitive = capturingPrimitive
2020-02-28 01:19:36 +00:00
}
x, y := event.Position()
buttons := event.Buttons()
clickMoved := x != a.mouseDownX || y != a.mouseDownY
buttonChanges := buttons ^ a.lastMouseButtons
2020-03-03 18:07:39 +00:00
if x != a.lastMouseX || y != a.lastMouseY {
fire(MouseMove)
a.lastMouseX = x
a.lastMouseY = y
2020-03-03 18:07:39 +00:00
}
for _, buttonEvent := range []struct {
2020-03-03 18:07:39 +00:00
button tcell.ButtonMask
down, up, click, dclick MouseAction
}{
{tcell.Button1, MouseLeftDown, MouseLeftUp, MouseLeftClick, MouseLeftDoubleClick},
{tcell.Button2, MouseMiddleDown, MouseMiddleUp, MouseMiddleClick, MouseMiddleDoubleClick},
{tcell.Button3, MouseRightDown, MouseRightUp, MouseRightClick, MouseRightDoubleClick},
} {
if buttonChanges&buttonEvent.button != 0 {
if buttons&buttonEvent.button != 0 {
fire(buttonEvent.down)
2020-03-03 18:07:39 +00:00
} else {
fire(buttonEvent.up)
2020-03-03 18:07:39 +00:00
if !clickMoved {
if a.lastMouseClick.Add(DoubleClickInterval).Before(time.Now()) {
fire(buttonEvent.click)
a.lastMouseClick = time.Now()
2020-03-03 18:07:39 +00:00
} else {
fire(buttonEvent.dclick)
a.lastMouseClick = time.Time{} // reset
2020-03-03 18:07:39 +00:00
}
2020-02-28 01:19:36 +00:00
}
}
}
}
for _, wheelEvent := range []struct {
2020-03-03 18:07:39 +00:00
button tcell.ButtonMask
action MouseAction
}{
{tcell.WheelUp, WheelUp},
{tcell.WheelDown, WheelDown},
{tcell.WheelLeft, WheelLeft},
{tcell.WheelRight, WheelRight}} {
if buttons&wheelEvent.button != 0 {
fire(wheelEvent.action)
2020-03-03 18:07:39 +00:00
}
2020-02-28 01:19:36 +00:00
}
2020-03-03 18:07:39 +00:00
return consumed, isMouseDownAction
2020-02-28 01:19:36 +00:00
}
// Stop stops the application, causing Run() to return.
func (a *Application) Stop() {
2018-06-28 12:49:42 +00:00
a.Lock()
defer a.Unlock()
2018-10-28 12:42:49 +00:00
screen := a.screen
if screen == nil {
return
}
a.screen = nil
2018-10-28 12:42:49 +00:00
screen.Fini()
a.screenReplacement <- nil
}
// Suspend temporarily suspends the application by exiting terminal UI mode and
// invoking the provided function "f". When "f" returns, terminal UI mode is
// entered again and the application resumes.
//
// A return value of true indicates that the application was suspended and "f"
// was called. If false is returned, the application was already suspended,
// terminal UI mode was not exited, and "f" was not called.
func (a *Application) Suspend(f func()) bool {
a.RLock()
2018-10-28 12:42:49 +00:00
screen := a.screen
a.RUnlock()
2018-10-28 12:42:49 +00:00
if screen == nil {
return false // Screen has not yet been initialized.
}
// Enter suspended mode.
2018-10-28 12:42:49 +00:00
screen.Fini()
// Wait for "f" to return.
f()
// Make a new screen.
var err error
screen, err = tcell.NewScreen()
if err != nil {
panic(err)
}
a.screenReplacement <- screen
2018-10-28 12:42:49 +00:00
// One key event will get lost, see https://github.com/gdamore/tcell/issues/194
// Continue application loop.
return true
}
// Draw refreshes the screen (during the next update cycle). It calls the Draw()
// function of the application's root primitive and then syncs the screen
// buffer. It is almost never necessary to call this function. Please see
// https://github.com/rivo/tview/wiki/Concurrency for details.
func (a *Application) Draw() *Application {
a.QueueUpdate(func() {
a.draw()
})
2018-10-28 12:42:49 +00:00
return a
}
2018-12-03 09:50:38 +00:00
// ForceDraw refreshes the screen immediately. Use this function with caution as
// it may lead to race conditions with updates to primitives in other
// goroutines. It is always preferrable to use Draw() instead. Never call this
// function from a goroutine.
2018-12-03 09:50:38 +00:00
//
// It is safe to call this function during queued updates and direct event
// handling.
func (a *Application) ForceDraw() *Application {
return a.draw()
}
2018-10-28 12:42:49 +00:00
// draw actually does what Draw() promises to do.
func (a *Application) draw() *Application {
a.Lock()
defer a.Unlock()
screen := a.screen
root := a.root
2018-03-05 15:37:10 +00:00
fullscreen := a.rootFullscreen
before := a.beforeDraw
after := a.afterDraw
// Maybe we're not ready yet or not anymore.
if screen == nil || root == nil {
return a
}
2018-01-01 20:50:20 +00:00
// Resize if requested.
if fullscreen && root != nil {
width, height := screen.Size()
root.SetRect(0, 0, width, height)
}
// Call before handler if there is one.
if before != nil {
if before(screen) {
screen.Show()
return a
}
2018-01-01 20:50:20 +00:00
}
// Draw all primitives.
root.Draw(screen)
// Call after handler if there is one.
if after != nil {
after(screen)
}
// Sync screen.
screen.Show()
return a
}
// SetBeforeDrawFunc installs a callback function which is invoked just before
// the root primitive is drawn during screen updates. If the function returns
// true, drawing will not continue, i.e. the root primitive will not be drawn
// (and an after-draw-handler will not be called).
//
// Note that the screen is not cleared by the application. To clear the screen,
// you may call screen.Clear().
//
// Provide nil to uninstall the callback function.
func (a *Application) SetBeforeDrawFunc(handler func(screen tcell.Screen) bool) *Application {
a.beforeDraw = handler
return a
}
// GetBeforeDrawFunc returns the callback function installed with
// SetBeforeDrawFunc() or nil if none has been installed.
func (a *Application) GetBeforeDrawFunc() func(screen tcell.Screen) bool {
return a.beforeDraw
}
// SetAfterDrawFunc installs a callback function which is invoked after the root
// primitive was drawn during screen updates.
//
// Provide nil to uninstall the callback function.
func (a *Application) SetAfterDrawFunc(handler func(screen tcell.Screen)) *Application {
a.afterDraw = handler
return a
}
// GetAfterDrawFunc returns the callback function installed with
// SetAfterDrawFunc() or nil if none has been installed.
func (a *Application) GetAfterDrawFunc() func(screen tcell.Screen) {
return a.afterDraw
}
2018-03-05 15:37:10 +00:00
// SetRoot sets the root primitive for this application. If "fullscreen" is set
// to true, the root primitive's position will be changed to fill the screen.
//
// This function must be called at least once or nothing will be displayed when
// the application starts.
2018-01-01 16:16:36 +00:00
//
// It also calls SetFocus() on the primitive.
2018-03-05 15:37:10 +00:00
func (a *Application) SetRoot(root Primitive, fullscreen bool) *Application {
2018-01-01 16:16:36 +00:00
a.Lock()
a.root = root
2018-03-05 15:37:10 +00:00
a.rootFullscreen = fullscreen
2018-01-01 20:50:20 +00:00
if a.screen != nil {
a.screen.Clear()
}
2018-01-01 16:16:36 +00:00
a.Unlock()
a.SetFocus(root)
return a
}
// ResizeToFullScreen resizes the given primitive such that it fills the entire
// screen.
func (a *Application) ResizeToFullScreen(p Primitive) *Application {
a.RLock()
width, height := a.screen.Size()
a.RUnlock()
p.SetRect(0, 0, width, height)
return a
}
// SetFocus sets the focus on a new primitive. All key events will be redirected
// to that primitive. Callers must ensure that the primitive will handle key
// events.
//
// Blur() will be called on the previously focused primitive. Focus() will be
// called on the new primitive.
func (a *Application) SetFocus(p Primitive) *Application {
a.Lock()
if a.focus != nil {
a.focus.Blur()
}
a.focus = p
if a.screen != nil {
a.screen.HideCursor()
}
a.Unlock()
if p != nil {
p.Focus(func(p Primitive) {
a.SetFocus(p)
})
}
return a
}
2017-12-20 19:54:49 +00:00
// GetFocus returns the primitive which has the current focus. If none has it,
// nil is returned.
func (a *Application) GetFocus() Primitive {
a.RLock()
defer a.RUnlock()
2017-12-20 19:54:49 +00:00
return a.focus
}
2018-10-28 12:42:49 +00:00
// QueueUpdate is used to synchronize access to primitives from non-main
// goroutines. The provided function will be executed as part of the event loop
// and thus will not cause race conditions with other such update functions or
// the Draw() function.
//
// Note that Draw() is not implicitly called after the execution of f as that
// may not be desirable. You can call Draw() from f if the screen should be
// refreshed after each update. Alternatively, use QueueUpdateDraw() to follow
// up with an immediate refresh of the screen.
//
// This function returns after f has executed.
func (a *Application) QueueUpdate(f func()) *Application {
ch := make(chan struct{})
a.updates <- queuedUpdate{f: f, done: ch}
<-ch
return a
}
// QueueUpdateDraw works like QueueUpdate() except it refreshes the screen
// immediately after executing f.
func (a *Application) QueueUpdateDraw(f func()) *Application {
a.QueueUpdate(func() {
f()
a.draw()
})
return a
}
2018-10-28 12:42:49 +00:00
// QueueEvent sends an event to the Application event loop.
//
// It is not recommended for event to be nil.
func (a *Application) QueueEvent(event tcell.Event) *Application {
a.events <- event
return a
}