Make cview thread-safe

This commit is contained in:
Trevor Slocum 2020-03-25 14:32:57 +00:00
parent d045073571
commit e29d4b73b9
22 changed files with 1291 additions and 87 deletions

View file

@ -1,3 +1,6 @@
v1.4.5 (WIP)
- Add multithreading support
v1.4.4 (2020-02-24)
- Fix panic when navigating empty list
- Fix resize event dimensions on Windows

View file

@ -26,6 +26,10 @@ maintainers and allowing code changes which may be outside of tview's scope.
# Differences
## cview is thread-safe
tview [is not thread-safe](https://godoc.org/github.com/rivo/tview#hdr-Concurrency).
## Application.QueueUpdate and Application.QueueUpdateDraw do not block
tview [blocks until the queued function returns](https://github.com/rivo/tview/blob/fe3052019536251fd145835dbaa225b33b7d3088/application.go#L510).

View file

@ -67,9 +67,6 @@ the program in the "demos/presentation" subdirectory.
Package documentation is available via [godoc](https://docs.rocketnine.space/gitlab.com/tslocum/cview).
**This package is not thread-safe.** Most functions may only be called from the
main thread, as documented in [Concurrency](https://docs.rocketnine.space/gitlab.com/tslocum/cview/#hdr-Concurrency).
An [introduction tutorial](https://rocketnine.space/post/tview-and-you/) is also available.
## Dependencies

View file

@ -27,8 +27,6 @@ const resizeEventThrottle = 200 * time.Millisecond
// 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).
@ -94,6 +92,8 @@ type Application struct {
lastMouseX, lastMouseY int
lastMouseBtn tcell.ButtonMask
lastMouseTarget Primitive // nil if none
sync.RWMutex
}
// NewApplication creates and returns a new application.
@ -115,6 +115,9 @@ func NewApplication() *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.Lock()
defer a.Unlock()
a.inputCapture = capture
return a
}
@ -122,6 +125,9 @@ func (a *Application) SetInputCapture(capture func(event *tcell.EventKey) *tcell
// 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 {
a.RLock()
defer a.RUnlock()
return a.inputCapture
}
@ -139,6 +145,9 @@ func (a *Application) SetMouseCapture(capture func(event *EventMouse) *EventMous
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (a *Application) GetMouseCapture() func(event *EventMouse) *EventMouse {
a.RLock()
defer a.RUnlock()
return a.mouseCapture
}
@ -155,6 +164,9 @@ func (a *Application) SetTemporaryMouseCapture(capture func(event *EventMouse) *
// GetTemporaryMouseCapture returns the function installed with
// SetTemporaryMouseCapture() or nil if no such function has been installed.
func (a *Application) GetTemporaryMouseCapture() func(event *EventMouse) *EventMouse {
a.RLock()
defer a.RUnlock()
return a.tempMouseCapture
}
@ -478,6 +490,7 @@ func findAtPoint(atX, atY int, p Primitive, capture func(p Primitive)) Primitive
func (a *Application) GetPrimitiveAtPoint(atX, atY int) Primitive {
a.RLock()
defer a.RUnlock()
return findAtPoint(atX, atY, a.root, nil)
}
@ -486,6 +499,7 @@ func (a *Application) GetPrimitiveAtPoint(atX, atY int) Primitive {
func (a *Application) appendStackAtPoint(buf []Primitive, atX, atY int) []Primitive {
a.RLock()
defer a.RUnlock()
findAtPoint(atX, atY, a.root, func(p Primitive) {
buf = append(buf, p)
})
@ -496,6 +510,7 @@ func (a *Application) appendStackAtPoint(buf []Primitive, atX, atY int) []Primit
func (a *Application) Stop() {
a.Lock()
defer a.Unlock()
screen := a.screen
if screen == nil {
return
@ -566,7 +581,6 @@ func (a *Application) ForceDraw() *Application {
// draw actually does what Draw() promises to do.
func (a *Application) draw() *Application {
a.Lock()
defer a.Unlock()
screen := a.screen
root := a.root
@ -576,6 +590,7 @@ func (a *Application) draw() *Application {
// Maybe we're not ready yet or not anymore.
if screen == nil || root == nil {
a.Unlock()
return a
}
@ -587,10 +602,13 @@ func (a *Application) draw() *Application {
// Call before handler if there is one.
if before != nil {
a.Unlock()
if before(screen) {
screen.Show()
return a
}
} else {
a.Unlock()
}
// Draw all primitives.
@ -617,6 +635,9 @@ func (a *Application) draw() *Application {
//
// Provide nil to uninstall the callback function.
func (a *Application) SetBeforeDrawFunc(handler func(screen tcell.Screen) bool) *Application {
a.Lock()
defer a.Unlock()
a.beforeDraw = handler
return a
}
@ -624,6 +645,9 @@ func (a *Application) SetBeforeDrawFunc(handler func(screen tcell.Screen) bool)
// GetBeforeDrawFunc returns the callback function installed with
// SetBeforeDrawFunc() or nil if none has been installed.
func (a *Application) GetBeforeDrawFunc() func(screen tcell.Screen) bool {
a.RLock()
defer a.RUnlock()
return a.beforeDraw
}
@ -632,6 +656,9 @@ func (a *Application) GetBeforeDrawFunc() func(screen tcell.Screen) bool {
//
// Provide nil to uninstall the callback function.
func (a *Application) SetAfterDrawFunc(handler func(screen tcell.Screen)) *Application {
a.Lock()
defer a.Unlock()
a.afterDraw = handler
return a
}
@ -639,6 +666,9 @@ func (a *Application) SetAfterDrawFunc(handler func(screen tcell.Screen)) *Appli
// GetAfterDrawFunc returns the callback function installed with
// SetAfterDrawFunc() or nil if none has been installed.
func (a *Application) GetAfterDrawFunc() func(screen tcell.Screen) {
a.RLock()
defer a.RUnlock()
return a.afterDraw
}
@ -680,6 +710,9 @@ func (a *Application) ResizeToFullScreen(p Primitive) *Application {
//
// Provide nil to uninstall the callback function.
func (a *Application) SetAfterResizeFunc(handler func(width int, height int)) *Application {
a.Lock()
defer a.Unlock()
a.afterResize = handler
return a
}
@ -687,6 +720,9 @@ func (a *Application) SetAfterResizeFunc(handler func(width int, height int)) *A
// GetAfterResizeFunc returns the callback function installed with
// SetAfterResizeFunc() or nil if none has been installed.
func (a *Application) GetAfterResizeFunc() func(width int, height int) {
a.RLock()
defer a.RUnlock()
return a.afterResize
}
@ -720,13 +756,11 @@ func (a *Application) SetFocus(p Primitive) *Application {
func (a *Application) GetFocus() Primitive {
a.RLock()
defer a.RUnlock()
return a.focus
}
// 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.
// QueueUpdate queues a function to be executed as part of the event loop.
//
// 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

103
box.go
View file

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -60,6 +62,8 @@ type Box struct {
// event to be forwarded to the primitive's default mouse event handler (nil if
// nothing should be forwarded).
mouseCapture func(event *EventMouse) *EventMouse
l sync.RWMutex
}
// NewBox returns a Box without a border.
@ -79,6 +83,9 @@ func NewBox() *Box {
// SetBorderPadding sets the size of the borders around the box content.
func (b *Box) SetBorderPadding(top, bottom, left, right int) *Box {
b.l.Lock()
defer b.l.Unlock()
b.paddingTop, b.paddingBottom, b.paddingLeft, b.paddingRight = top, bottom, left, right
return b
}
@ -86,6 +93,9 @@ func (b *Box) SetBorderPadding(top, bottom, left, right int) *Box {
// GetRect returns the current position of the rectangle, x, y, width, and
// height.
func (b *Box) GetRect() (int, int, int, int) {
b.l.RLock()
defer b.l.RUnlock()
return b.x, b.y, b.width, b.height
}
@ -93,10 +103,15 @@ func (b *Box) GetRect() (int, int, int, int) {
// height), without the border and without any padding. Width and height values
// will clamp to 0 and thus never be negative.
func (b *Box) GetInnerRect() (int, int, int, int) {
b.l.RLock()
if b.innerX >= 0 {
defer b.l.RUnlock()
return b.innerX, b.innerY, b.innerWidth, b.innerHeight
}
b.l.RUnlock()
x, y, width, height := b.GetRect()
b.l.RLock()
if b.border {
x++
y++
@ -113,6 +128,7 @@ func (b *Box) GetInnerRect() (int, int, int, int) {
if height < 0 {
height = 0
}
b.l.RUnlock()
return x, y, width, height
}
@ -122,6 +138,9 @@ func (b *Box) GetInnerRect() (int, int, int, int) {
//
// application.SetRoot(b, true)
func (b *Box) SetRect(x, y, width, height int) {
b.l.Lock()
defer b.l.Unlock()
b.x = x
b.y = y
b.width = width
@ -138,6 +157,9 @@ func (b *Box) SetRect(x, y, width, height int) {
// returned by GetInnerRect(), used by descendent primitives to draw their own
// content.
func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height int) (int, int, int, int)) *Box {
b.l.Lock()
defer b.l.Unlock()
b.draw = handler
return b
}
@ -145,6 +167,9 @@ func (b *Box) SetDrawFunc(handler func(screen tcell.Screen, x, y, width, height
// GetDrawFunc returns the callback function which was installed with
// SetDrawFunc() or nil if no such function has been installed.
func (b *Box) GetDrawFunc() func(screen tcell.Screen, x, y, width, height int) (int, int, int, int) {
b.l.RLock()
defer b.l.RUnlock()
return b.draw
}
@ -166,6 +191,9 @@ func (b *Box) WrapInputHandler(inputHandler func(*tcell.EventKey, func(p Primiti
// InputHandler returns nil.
func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
b.l.RLock()
defer b.l.RUnlock()
return b.WrapInputHandler(nil)
}
@ -184,6 +212,9 @@ func (b *Box) InputHandler() func(event *tcell.EventKey, setFocus func(p Primiti
// to their contained primitives and thus never receive any key events
// themselves. Therefore, they cannot intercept key events.
func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKey) *Box {
b.l.Lock()
defer b.l.Unlock()
b.inputCapture = capture
return b
}
@ -191,6 +222,9 @@ func (b *Box) SetInputCapture(capture func(event *tcell.EventKey) *tcell.EventKe
// GetInputCapture returns the function installed with SetInputCapture() or nil
// if no such function has been installed.
func (b *Box) GetInputCapture() func(event *tcell.EventKey) *tcell.EventKey {
b.l.RLock()
defer b.l.RUnlock()
return b.inputCapture
}
@ -212,6 +246,9 @@ func (b *Box) WrapMouseHandler(mouseHandler func(*EventMouse)) func(*EventMouse)
// MouseHandler returns nil.
func (b *Box) MouseHandler() func(event *EventMouse) {
b.l.RLock()
defer b.l.RUnlock()
return b.WrapMouseHandler(nil)
}
@ -223,6 +260,9 @@ func (b *Box) MouseHandler() func(event *EventMouse) {
//
// Providing a nil handler will remove a previously existing handler.
func (b *Box) SetMouseCapture(capture func(*EventMouse) *EventMouse) *Box {
b.l.Lock()
defer b.l.Unlock()
b.mouseCapture = capture
return b
}
@ -230,11 +270,17 @@ func (b *Box) SetMouseCapture(capture func(*EventMouse) *EventMouse) *Box {
// GetMouseCapture returns the function installed with SetMouseCapture() or nil
// if no such function has been installed.
func (b *Box) GetMouseCapture() func(*EventMouse) *EventMouse {
b.l.RLock()
defer b.l.RUnlock()
return b.mouseCapture
}
// SetBackgroundColor sets the box's background color.
func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
b.l.Lock()
defer b.l.Unlock()
b.backgroundColor = color
return b
}
@ -242,12 +288,18 @@ func (b *Box) SetBackgroundColor(color tcell.Color) *Box {
// SetBorder sets the flag indicating whether or not the box should have a
// border.
func (b *Box) SetBorder(show bool) *Box {
b.l.Lock()
defer b.l.Unlock()
b.border = show
return b
}
// SetBorderColor sets the box's border color.
func (b *Box) SetBorderColor(color tcell.Color) *Box {
b.l.Lock()
defer b.l.Unlock()
b.borderColor = color
return b
}
@ -257,23 +309,35 @@ func (b *Box) SetBorderColor(color tcell.Color) *Box {
//
// box.SetBorderAttributes(tcell.AttrUnderline | tcell.AttrBold)
func (b *Box) SetBorderAttributes(attr tcell.AttrMask) *Box {
b.l.Lock()
defer b.l.Unlock()
b.borderAttributes = attr
return b
}
// SetTitle sets the box's title.
func (b *Box) SetTitle(title string) *Box {
b.l.Lock()
defer b.l.Unlock()
b.title = title
return b
}
// GetTitle returns the box's current title.
func (b *Box) GetTitle() string {
b.l.RLock()
defer b.l.RUnlock()
return b.title
}
// SetTitleColor sets the box's title color.
func (b *Box) SetTitleColor(color tcell.Color) *Box {
b.l.Lock()
defer b.l.Unlock()
b.titleColor = color
return b
}
@ -281,14 +345,20 @@ func (b *Box) SetTitleColor(color tcell.Color) *Box {
// SetTitleAlign sets the alignment of the title, one of AlignLeft, AlignCenter,
// or AlignRight.
func (b *Box) SetTitleAlign(align int) *Box {
b.l.Lock()
defer b.l.Unlock()
b.titleAlign = align
return b
}
// Draw draws this primitive onto the screen.
func (b *Box) Draw(screen tcell.Screen) {
b.l.Lock()
// Don't draw anything if there is no space.
if b.width <= 0 || b.height <= 0 {
b.l.Unlock()
return
}
@ -306,7 +376,14 @@ func (b *Box) Draw(screen tcell.Screen) {
if b.border && b.width >= 2 && b.height >= 2 {
border := background.Foreground(b.borderColor) | tcell.Style(b.borderAttributes)
var vertical, horizontal, topLeft, topRight, bottomLeft, bottomRight rune
if b.focus.HasFocus() {
var hasFocus bool
if b.focus == b {
hasFocus = b.hasFocus
} else {
hasFocus = b.focus.HasFocus()
}
if hasFocus {
horizontal = Borders.HorizontalFocus
vertical = Borders.VerticalFocus
topLeft = Borders.TopLeftFocus
@ -347,11 +424,17 @@ func (b *Box) Draw(screen tcell.Screen) {
// Call custom draw function.
if b.draw != nil {
b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.draw(screen, b.x, b.y, b.width, b.height)
b.l.Unlock()
newX, newY, newWidth, newHeight := b.draw(screen, b.x, b.y, b.width, b.height)
b.l.Lock()
b.innerX, b.innerY, b.innerWidth, b.innerHeight = newX, newY, newWidth, newHeight
} else {
// Remember the inner rect.
b.innerX = -1
b.innerX, b.innerY, b.innerWidth, b.innerHeight = b.GetInnerRect()
b.l.Unlock()
newX, newY, newWidth, newHeight := b.GetInnerRect()
b.l.Lock()
b.innerX, b.innerY, b.innerWidth, b.innerHeight = newX, newY, newWidth, newHeight
}
// Clamp inner rect to screen.
@ -376,25 +459,39 @@ func (b *Box) Draw(screen tcell.Screen) {
if b.innerHeight < 0 {
b.innerHeight = 0
}
b.l.Unlock()
}
// Focus is called when this primitive receives focus.
func (b *Box) Focus(delegate func(p Primitive)) {
b.l.Lock()
defer b.l.Unlock()
b.hasFocus = true
}
// Blur is called when this primitive loses focus.
func (b *Box) Blur() {
b.l.Lock()
defer b.l.Unlock()
b.hasFocus = false
}
// HasFocus returns whether or not this primitive has focus.
func (b *Box) HasFocus() bool {
b.l.RLock()
defer b.l.RUnlock()
return b.hasFocus
}
// GetFocusable returns the item's Focusable.
func (b *Box) GetFocusable() Focusable {
b.l.RLock()
defer b.l.RUnlock()
return b.focus
}

View file

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -28,6 +30,8 @@ type Button struct {
// An optional function which is called when the user leaves the button. A
// key is provided indicating which key was pressed to leave (tab or backtab).
blur func(tcell.Key)
sync.Mutex
}
// NewButton returns a new input field.
@ -45,17 +49,26 @@ func NewButton(label string) *Button {
// SetLabel sets the button text.
func (b *Button) SetLabel(label string) *Button {
b.Lock()
defer b.Unlock()
b.label = label
return b
}
// GetLabel returns the button text.
func (b *Button) GetLabel() string {
b.Lock()
defer b.Unlock()
return b.label
}
// SetLabelColor sets the color of the button text.
func (b *Button) SetLabelColor(color tcell.Color) *Button {
b.Lock()
defer b.Unlock()
b.labelColor = color
return b
}
@ -63,6 +76,9 @@ func (b *Button) SetLabelColor(color tcell.Color) *Button {
// SetLabelColorActivated sets the color of the button text when the button is
// in focus.
func (b *Button) SetLabelColorActivated(color tcell.Color) *Button {
b.Lock()
defer b.Unlock()
b.labelColorActivated = color
return b
}
@ -70,12 +86,18 @@ func (b *Button) SetLabelColorActivated(color tcell.Color) *Button {
// SetBackgroundColorActivated sets the background color of the button text when
// the button is in focus.
func (b *Button) SetBackgroundColorActivated(color tcell.Color) *Button {
b.Lock()
defer b.Unlock()
b.backgroundColorActivated = color
return b
}
// SetSelectedFunc sets a handler which is called when the button was selected.
func (b *Button) SetSelectedFunc(handler func()) *Button {
b.Lock()
defer b.Unlock()
b.selected = handler
return b
}
@ -88,12 +110,18 @@ func (b *Button) SetSelectedFunc(handler func()) *Button {
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (b *Button) SetBlurFunc(handler func(key tcell.Key)) *Button {
b.Lock()
defer b.Unlock()
b.blur = handler
return b
}
// Draw draws this primitive onto the screen.
func (b *Button) Draw(screen tcell.Screen) {
b.Lock()
defer b.Unlock()
// Draw the box.
borderColor := b.borderColor
backgroundColor := b.backgroundColor
@ -104,7 +132,9 @@ func (b *Button) Draw(screen tcell.Screen) {
b.borderColor = borderColor
}()
}
b.Unlock()
b.Box.Draw(screen)
b.Lock()
b.backgroundColor = backgroundColor
// Draw label.

View file

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -45,6 +47,8 @@ type Checkbox struct {
// A callback function set by the Form class and called when the user leaves
// this form item.
finished func(tcell.Key)
sync.Mutex
}
// NewCheckbox returns a new input field.
@ -59,64 +63,97 @@ func NewCheckbox() *Checkbox {
// SetChecked sets the state of the checkbox.
func (c *Checkbox) SetChecked(checked bool) *Checkbox {
c.Lock()
defer c.Unlock()
c.checked = checked
return c
}
// IsChecked returns whether or not the box is checked.
func (c *Checkbox) IsChecked() bool {
c.Lock()
defer c.Unlock()
return c.checked
}
// SetLabel sets the text to be displayed before the input area.
func (c *Checkbox) SetLabel(label string) *Checkbox {
c.Lock()
defer c.Unlock()
c.label = label
return c
}
// GetLabel returns the text to be displayed before the input area.
func (c *Checkbox) GetLabel() string {
c.Lock()
defer c.Unlock()
return c.label
}
// SetMessage sets the text to be displayed after the checkbox
func (c *Checkbox) SetMessage(message string) *Checkbox {
c.Lock()
defer c.Unlock()
c.message = message
return c
}
// GetMessage returns the text to be displayed after the checkbox
func (c *Checkbox) GetMessage() string {
c.Lock()
defer c.Unlock()
return c.message
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (c *Checkbox) SetLabelWidth(width int) *Checkbox {
c.Lock()
defer c.Unlock()
c.labelWidth = width
return c
}
// SetLabelColor sets the color of the label.
func (c *Checkbox) SetLabelColor(color tcell.Color) *Checkbox {
c.Lock()
defer c.Unlock()
c.labelColor = color
return c
}
// SetFieldBackgroundColor sets the background color of the input area.
func (c *Checkbox) SetFieldBackgroundColor(color tcell.Color) *Checkbox {
c.Lock()
defer c.Unlock()
c.fieldBackgroundColor = color
return c
}
// SetFieldTextColor sets the text color of the input area.
func (c *Checkbox) SetFieldTextColor(color tcell.Color) *Checkbox {
c.Lock()
defer c.Unlock()
c.fieldTextColor = color
return c
}
// SetFormAttributes sets attributes shared by all form items.
func (c *Checkbox) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
c.Lock()
defer c.Unlock()
c.labelWidth = labelWidth
c.labelColor = labelColor
c.backgroundColor = bgColor
@ -127,6 +164,9 @@ func (c *Checkbox) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldT
// GetFieldWidth returns this primitive's field width.
func (c *Checkbox) GetFieldWidth() int {
c.Lock()
defer c.Unlock()
if c.message == "" {
return 1
}
@ -138,6 +178,9 @@ func (c *Checkbox) GetFieldWidth() int {
// checkbox was changed by the user. The handler function receives the new
// state.
func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
c.Lock()
defer c.Unlock()
c.changed = handler
return c
}
@ -150,12 +193,18 @@ func (c *Checkbox) SetChangedFunc(handler func(checked bool)) *Checkbox {
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (c *Checkbox) SetDoneFunc(handler func(key tcell.Key)) *Checkbox {
c.Lock()
defer c.Unlock()
c.done = handler
return c
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
c.Lock()
defer c.Unlock()
c.finished = handler
return c
}
@ -164,6 +213,9 @@ func (c *Checkbox) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
func (c *Checkbox) Draw(screen tcell.Screen) {
c.Box.Draw(screen)
c.Lock()
defer c.Unlock()
// Prepare
x, y, width, height := c.GetInnerRect()
rightLimit := x + width
@ -209,7 +261,9 @@ func (c *Checkbox) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
if key == tcell.KeyRune && event.Rune() != ' ' {
break
}
c.Lock()
c.checked = !c.checked
c.Unlock()
if c.changed != nil {
c.changed(c.checked)
}
@ -229,7 +283,9 @@ func (c *Checkbox) MouseHandler() func(event *EventMouse) {
return c.WrapMouseHandler(func(event *EventMouse) {
// Process mouse event.
if event.Action()&MouseClick != 0 {
c.Lock()
c.checked = !c.checked
c.Unlock()
if c.changed != nil {
c.changed(c.checked)
}

View file

@ -13,7 +13,11 @@ the following shortcuts can be used:
package main
import (
"flag"
"fmt"
"log"
"net/http"
_ "net/http/pprof"
"strconv"
"github.com/gdamore/tcell"
@ -30,6 +34,16 @@ var app = cview.NewApplication()
// Starting point for the presentation.
func main() {
var debugPort int
flag.IntVar(&debugPort, "debug", 0, "port to serve debug info")
flag.Parse()
if debugPort > 0 {
go func() {
log.Println(http.ListenAndServe(fmt.Sprintf("localhost:%d", debugPort), nil))
}()
}
// The presentation slides.
slides := []Slide{
Cover,

34
doc.go
View file

@ -34,26 +34,18 @@ primitive, Box, and thus inherit its functions. This isn't necessarily
required, but it makes more sense than reimplementing Box's functionality in
each widget.
Types
This package is a fork of https://github.com/rivo/tview which is based on
https://github.com/gdamore/tcell. It uses types and constants from tcell
(e.g. colors and keyboard values).
Concurrency
Most of cview's functions are not thread-safe. You must synchronize execution
via Application.QueueUpdate or Application.QueueUpdateDraw (see function
documentation for more information):
go func() {
// Queue a UI change from a goroutine.
app.QueueUpdateDraw(func() {
// This function will execute on the main thread.
table.SetCellSimple(0, 0, "Foo bar")
})
}()
One exception to this is the io.Writer interface implemented by TextView; you
may safely write to a TextView from any goroutine. You may also call
Application.Draw from any goroutine.
Event handlers execute on the main goroutine and thus do not require
synchronization.
All functions may be called concurrently (they are thread-safe). When called
from multiple threads, functions will block until the application or widget
becomes available. Function calls may be queued with Application.QueueUpdate to
avoid blocking.
Unicode Support
@ -80,12 +72,6 @@ developers to permanently intercept mouse events.
Event handlers may return nil to stop propagation.
Types
This package is a fork of https://github.com/rivo/tview which is based on
https://github.com/gdamore/tcell. It uses types and constants from tcell
(e.g. colors and keyboard values).
Colors
Throughout this package, colors are specified using the tcell.Color type.

View file

@ -2,6 +2,7 @@ package cview
import (
"strings"
"sync"
"github.com/gdamore/tcell"
)
@ -79,6 +80,8 @@ type DropDown struct {
// A callback function which is called when the user changes the drop-down's
// selection.
selected func(text string, index int)
sync.RWMutex
}
// NewDropDown returns a new drop-down.
@ -110,20 +113,29 @@ func NewDropDown() *DropDown {
// be a negative value to indicate that no option is currently selected. Calling
// this function will also trigger the "selected" callback (if there is one).
func (d *DropDown) SetCurrentOption(index int) *DropDown {
d.Lock()
defer d.Unlock()
if index >= 0 && index < len(d.options) {
d.currentOption = index
d.list.SetCurrentItem(index)
if d.selected != nil {
d.Unlock()
d.selected(d.options[index].Text, index)
d.Lock()
}
if d.options[index].Selected != nil {
d.Unlock()
d.options[index].Selected()
d.Lock()
}
} else {
d.currentOption = -1
d.list.SetCurrentItem(0) // Set to 0 because -1 means "last item".
if d.selected != nil {
d.Unlock()
d.selected("", -1)
d.Lock()
}
}
return d
@ -132,6 +144,9 @@ func (d *DropDown) SetCurrentOption(index int) *DropDown {
// GetCurrentOption returns the index of the currently selected option as well
// as its text. If no option was selected, -1 and an empty string is returned.
func (d *DropDown) GetCurrentOption() (int, string) {
d.RLock()
defer d.RUnlock()
var text string
if d.currentOption >= 0 && d.currentOption < len(d.options) {
text = d.options[d.currentOption].Text
@ -145,6 +160,9 @@ func (d *DropDown) GetCurrentOption() (int, string) {
// displayed when no option is currently selected. Per default, all of these
// strings are empty.
func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix, noSelection string) *DropDown {
d.Lock()
defer d.Unlock()
d.currentOptionPrefix = currentPrefix
d.currentOptionSuffix = currentSuffix
d.noSelection = noSelection
@ -158,36 +176,54 @@ func (d *DropDown) SetTextOptions(prefix, suffix, currentPrefix, currentSuffix,
// SetLabel sets the text to be displayed before the input area.
func (d *DropDown) SetLabel(label string) *DropDown {
d.Lock()
defer d.Unlock()
d.label = label
return d
}
// GetLabel returns the text to be displayed before the input area.
func (d *DropDown) GetLabel() string {
d.RLock()
defer d.RUnlock()
return d.label
}
// SetLabelWidth sets the screen width of the label. A value of 0 will cause the
// primitive to use the width of the label string.
func (d *DropDown) SetLabelWidth(width int) *DropDown {
d.Lock()
defer d.Unlock()
d.labelWidth = width
return d
}
// SetLabelColor sets the color of the label.
func (d *DropDown) SetLabelColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.labelColor = color
return d
}
// SetFieldBackgroundColor sets the background color of the options area.
func (d *DropDown) SetFieldBackgroundColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.fieldBackgroundColor = color
return d
}
// SetFieldTextColor sets the text color of the options area.
func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.fieldTextColor = color
return d
}
@ -196,12 +232,18 @@ func (d *DropDown) SetFieldTextColor(color tcell.Color) *DropDown {
// shown when the user starts typing text, which directly selects the first
// option that starts with the typed string.
func (d *DropDown) SetPrefixTextColor(color tcell.Color) *DropDown {
d.Lock()
defer d.Unlock()
d.prefixTextColor = color
return d
}
// SetFormAttributes sets attributes shared by all form items.
func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldTextColor, fieldBgColor tcell.Color) FormItem {
d.Lock()
defer d.Unlock()
d.labelWidth = labelWidth
d.labelColor = labelColor
d.backgroundColor = bgColor
@ -213,12 +255,18 @@ func (d *DropDown) SetFormAttributes(labelWidth int, labelColor, bgColor, fieldT
// SetFieldWidth sets the screen width of the options area. A value of 0 means
// extend to as long as the longest option text.
func (d *DropDown) SetFieldWidth(width int) *DropDown {
d.Lock()
defer d.Unlock()
d.fieldWidth = width
return d
}
// GetFieldWidth returns this primitive's field screen width.
func (d *DropDown) GetFieldWidth() int {
d.RLock()
defer d.RUnlock()
if d.fieldWidth > 0 {
return d.fieldWidth
}
@ -235,6 +283,13 @@ func (d *DropDown) GetFieldWidth() int {
// AddOption adds a new selectable option to this drop-down. The "selected"
// callback is called when this option was selected. It may be nil.
func (d *DropDown) AddOption(text string, selected func()) *DropDown {
d.Lock()
defer d.Unlock()
return d.addOption(text, selected)
}
func (d *DropDown) addOption(text string, selected func()) *DropDown {
d.options = append(d.options, &dropDownOption{Text: text, Selected: selected})
d.list.AddItem(d.optionPrefix+text+d.optionSuffix, "", 0, nil)
return d
@ -245,11 +300,14 @@ func (d *DropDown) AddOption(text string, selected func()) *DropDown {
// It will be called with the option's text and its index into the options
// slice. The "selected" parameter may be nil.
func (d *DropDown) SetOptions(texts []string, selected func(text string, index int)) *DropDown {
d.Lock()
defer d.Unlock()
d.list.Clear()
d.options = nil
for index, text := range texts {
func(t string, i int) {
d.AddOption(text, nil)
d.addOption(text, nil)
}(text, index)
}
d.selected = selected
@ -262,6 +320,9 @@ func (d *DropDown) SetOptions(texts []string, selected func(text string, index i
// selected option's text and index. If "no option" was selected, these values
// are an empty string and -1.
func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDown {
d.Lock()
defer d.Unlock()
d.selected = handler
return d
}
@ -274,12 +335,18 @@ func (d *DropDown) SetSelectedFunc(handler func(text string, index int)) *DropDo
// - KeyTab: Move to the next field.
// - KeyBacktab: Move to the previous field.
func (d *DropDown) SetDoneFunc(handler func(key tcell.Key)) *DropDown {
d.Lock()
defer d.Unlock()
d.done = handler
return d
}
// SetFinishedFunc sets a callback invoked when the user leaves this form item.
func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
d.Lock()
defer d.Unlock()
d.finished = handler
return d
}
@ -287,6 +354,10 @@ func (d *DropDown) SetFinishedFunc(handler func(key tcell.Key)) FormItem {
// Draw draws this primitive onto the screen.
func (d *DropDown) Draw(screen tcell.Screen) {
d.Box.Draw(screen)
hasFocus := d.GetFocusable().HasFocus()
d.Lock()
defer d.Unlock()
// Prepare.
x, y, width, height := d.GetInnerRect()
@ -338,7 +409,7 @@ func (d *DropDown) Draw(screen tcell.Screen) {
fieldWidth = rightLimit - x
}
fieldStyle := tcell.StyleDefault.Background(d.fieldBackgroundColor)
if d.GetFocusable().HasFocus() && !d.open {
if hasFocus && !d.open {
fieldStyle = fieldStyle.Background(d.fieldTextColor)
}
for index := 0; index < fieldWidth; index++ {
@ -363,14 +434,14 @@ func (d *DropDown) Draw(screen tcell.Screen) {
text = d.currentOptionPrefix + d.options[d.currentOption].Text + d.currentOptionSuffix
}
// Just show the current selection.
if d.GetFocusable().HasFocus() && !d.open {
if hasFocus && !d.open {
color = d.fieldBackgroundColor
}
Print(screen, text, x, y, fieldWidth, AlignLeft, color)
}
// Draw options list.
if d.HasFocus() && d.open {
if hasFocus && d.open {
// We prefer to drop down but if there is no space, maybe drop up?
lx := x
ly := y + 1
@ -400,6 +471,9 @@ func (d *DropDown) InputHandler() func(event *tcell.EventKey, setFocus func(p Pr
// Process key event.
switch key := event.Key(); key {
case tcell.KeyEnter, tcell.KeyRune, tcell.KeyDown:
d.Lock()
defer d.Unlock()
d.prefix = ""
// If the first key was a letter already, it becomes part of the prefix.
@ -447,10 +521,14 @@ func (d *DropDown) openList(setFocus func(Primitive), app *Application) {
// Trigger "selected" event.
if d.selected != nil {
d.Unlock()
d.selected(d.options[d.currentOption].Text, d.currentOption)
d.Lock()
}
if d.options[d.currentOption].Selected != nil {
d.Unlock()
d.options[d.currentOption].Selected()
d.Lock()
}
}).SetInputCapture(func(event *tcell.EventKey) *tcell.EventKey {
if event.Key() == tcell.KeyRune {
@ -524,6 +602,9 @@ func (d *DropDown) Focus(delegate func(p Primitive)) {
// HasFocus returns whether or not this primitive has focus.
func (d *DropDown) HasFocus() bool {
d.RLock()
defer d.RUnlock()
if d.open {
return d.list.HasFocus()
}
@ -535,6 +616,9 @@ func (d *DropDown) MouseHandler() func(event *EventMouse) {
return d.WrapMouseHandler(func(event *EventMouse) {
// Process mouse event.
if event.Action()&MouseDown != 0 && event.Buttons()&tcell.Button1 != 0 {
d.Lock()
defer d.Unlock()
//d.open = !d.open
//event.SetFocus(d)
if d.open {

33
flex.go
View file

@ -1,6 +1,8 @@
package cview
import (
"sync"
"github.com/gdamore/tcell"
)
@ -36,6 +38,8 @@ type Flex struct {
// If set to true, Flex will use the entire screen as its available space
// instead its box dimensions.
fullScreen bool
sync.Mutex
}
// NewFlex returns a new flexbox layout container with no primitives and its
@ -60,6 +64,9 @@ func NewFlex() *Flex {
// SetDirection sets the direction in which the contained primitives are
// distributed. This can be either FlexColumn (default) or FlexRow.
func (f *Flex) SetDirection(direction int) *Flex {
f.Lock()
defer f.Unlock()
f.direction = direction
return f
}
@ -67,6 +74,9 @@ func (f *Flex) SetDirection(direction int) *Flex {
// SetFullScreen sets the flag which, when true, causes the flex layout to use
// the entire screen space instead of whatever size it is currently assigned to.
func (f *Flex) SetFullScreen(fullScreen bool) *Flex {
f.Lock()
defer f.Unlock()
f.fullScreen = fullScreen
return f
}
@ -86,6 +96,9 @@ func (f *Flex) SetFullScreen(fullScreen bool) *Flex {
// You can provide a nil value for the primitive. This will still consume screen
// space but nothing will be drawn.
func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *Flex {
f.Lock()
defer f.Unlock()
f.items = append(f.items, &flexItem{Item: item, FixedSize: fixedSize, Proportion: proportion, Focus: focus})
return f
}
@ -93,6 +106,9 @@ func (f *Flex) AddItem(item Primitive, fixedSize, proportion int, focus bool) *F
// RemoveItem removes all items for the given primitive from the container,
// keeping the order of the remaining items intact.
func (f *Flex) RemoveItem(p Primitive) *Flex {
f.Lock()
defer f.Unlock()
for index := len(f.items) - 1; index >= 0; index-- {
if f.items[index].Item == p {
f.items = append(f.items[:index], f.items[index+1:]...)
@ -105,6 +121,9 @@ func (f *Flex) RemoveItem(p Primitive) *Flex {
// are multiple Flex items with the same primitive, they will all receive the
// same size. For details regarding the size parameters, see AddItem().
func (f *Flex) ResizeItem(p Primitive, fixedSize, proportion int) *Flex {
f.Lock()
defer f.Unlock()
for _, item := range f.items {
if item.Item == p {
item.FixedSize = fixedSize
@ -118,6 +137,9 @@ func (f *Flex) ResizeItem(p Primitive, fixedSize, proportion int) *Flex {
func (f *Flex) Draw(screen tcell.Screen) {
f.Box.Draw(screen)
f.Lock()
defer f.Unlock()
// Calculate size and position of the items.
// Do we use the entire screen?
@ -178,16 +200,24 @@ func (f *Flex) Draw(screen tcell.Screen) {
// Focus is called when this primitive receives focus.
func (f *Flex) Focus(delegate func(p Primitive)) {
f.Lock()
for _, item := range f.items {
if item.Item != nil && item.Focus {
f.Unlock()
delegate(item.Item)
return
}
}
f.Unlock()
}
// HasFocus returns whether or not this primitive has focus.
func (f *Flex) HasFocus() bool {
f.Lock()
defer f.Unlock()
for _, item := range f.items {
if item.Item != nil && item.Item.GetFocusable().HasFocus() {
return true
@ -198,6 +228,9 @@ func (f *Flex) HasFocus() bool {
// GetChildren returns all primitives that have been added.
func (f *Flex) GetChildren() []Primitive {
f.Lock()
defer f.Unlock()
children := make([]Primitive, len(f.items))
for i, item := range f.items {
children[i] = item.Item

116
form.go