Add Window and WindowManager

This commit is contained in:
Trevor Slocum 2020-09-24 08:09:55 -07:00
parent 97f450fc34
commit e20e58147f
10 changed files with 441 additions and 40 deletions

View File

@ -1,5 +1,6 @@
v1.5.0 (WIP)
- Add scroll bar to TextView
- Add Window and WindowManager
- Add focus-driven style options
- Add InputField autocomplete style options
- Add arrow symbol to DropDown

View File

@ -18,12 +18,13 @@ Available widgets:
- __Input forms__ (including __input/password fields__, __drop-down selections__, __checkboxes__, and __buttons__)
- Navigable multi-color __text views__
- Selectable __lists__ with __context menus__
- Modal __dialogs__
- Horizontal and vertical __progress bars__
- __Grid__, __Flexbox__ and __page layouts__
- Sophisticated navigable __table views__
- Flexible __tree views__
- Selectable __lists__ with __context menus__
- __Grid__, __Flexbox__ and __page layouts__
- Modal __message windows__
- Horizontal and vertical __progress bars__
- Draggable and resizable __windows__
- An __application__ wrapper
Widgets may be customized and extended to suit any application.

View File

@ -57,6 +57,7 @@ func main() {
TreeView,
Flex,
Grid,
Window,
End,
}

View File

@ -0,0 +1,39 @@
package main
import (
"gitlab.com/tslocum/cview"
)
const loremIpsumText = "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."
// Window returns the window page.
func Window(nextSlide func()) (title string, content cview.Primitive) {
wm := cview.NewWindowManager()
list := cview.NewList().
AddItem(cview.NewListItem("Item #1")).
AddItem(cview.NewListItem("Item #2")).
AddItem(cview.NewListItem("Item #3")).
AddItem(cview.NewListItem("Item #4")).
AddItem(cview.NewListItem("Item #5")).
AddItem(cview.NewListItem("Item #6")).
AddItem(cview.NewListItem("Item #7")).
ShowSecondaryText(false)
loremIpsum := cview.NewTextView().SetText(loremIpsumText)
w1 := cview.NewWindow(list).
SetPosition(2, 2).
SetSize(10, 7)
w2 := cview.NewWindow(loremIpsum).
SetPosition(7, 4).
SetSize(12, 12)
w1.SetTitle("List")
w2.SetTitle("Lorem Ipsum")
wm.Add(w1, w2)
return "Window", wm
}

30
doc.go
View File

@ -4,6 +4,21 @@ Package cview implements rich widgets for terminal based user interfaces.
See the demos folder and the example application provided with the
NewApplication documentation for usage examples.
Types
This package is built on top of tcell, which provides the types necessary to
create a terminal-based application (e.g. EventKey). For information on
inherited types see the tcell documentation.
tcell: https://github.com/gdamore/tcell
Base Primitive
Widgets must implement the Primitive interface. All widgets embed the base
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.
Widgets
The following widgets are available:
@ -30,21 +45,6 @@ The following widgets are available:
Widgets may be used without an application created via NewApplication, allowing
them to be integrated into any tcell-based application.
Base Primitive
Widgets must implement the Primitive interface. All widgets embed the base
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 built on top of tcell, which provides the types necessary to
create a terminal-based application (e.g. EventKey). For information on
inherited types see the tcell documentation.
tcell: https://github.com/gdamore/tcell
Concurrency
All functions may be called concurrently (they are thread-safe). When called

6
go.mod
View File

@ -3,10 +3,10 @@ module gitlab.com/tslocum/cview
go 1.12
require (
github.com/gdamore/tcell/v2 v2.0.0-dev.0.20200908121250-0c5e1e1720f1
github.com/gdamore/tcell/v2 v2.0.0-dev.0.20200921183946-6c71be68010f
github.com/lucasb-eyer/go-colorful v1.0.3
github.com/mattn/go-runewidth v0.0.9
github.com/rivo/uniseg v0.1.0
gitlab.com/tslocum/cbind v0.1.2-0.20200826214515-b5f2c6a8711a
golang.org/x/sys v0.0.0-20200908134130-d2e65c121b96 // indirect
gitlab.com/tslocum/cbind v0.1.2
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d // indirect
)

12
go.sum
View File

@ -1,8 +1,8 @@
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell/v2 v2.0.0-dev/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.0.0-dev.0.20200908121250-0c5e1e1720f1 h1:ec/DAe6ms4fBkpSHObVDYU4N/w6Swd929zkN01g8ozY=
github.com/gdamore/tcell/v2 v2.0.0-dev.0.20200908121250-0c5e1e1720f1/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/gdamore/tcell/v2 v2.0.0-dev.0.20200921183946-6c71be68010f h1:fUyor2CR18vdlU4gjQhIjQErJwtia5OAvS/2U3hkJlI=
github.com/gdamore/tcell/v2 v2.0.0-dev.0.20200921183946-6c71be68010f/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
github.com/lucasb-eyer/go-colorful v1.0.3 h1:QIbQXiugsb+q10B+MI+7DI1oQLdmnep86tWFlaaUAac=
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
@ -10,13 +10,13 @@ github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/Qd
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
github.com/rivo/uniseg v0.1.0 h1:+2KBaVoUmb9XzDsrx/Ct0W/EYOSFf/nWTauy++DprtY=
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
gitlab.com/tslocum/cbind v0.1.2-0.20200826214515-b5f2c6a8711a h1:6u2QDDcKdeFhyHT/srxPDzLfDJqwKTgy1v+3209LYBY=
gitlab.com/tslocum/cbind v0.1.2-0.20200826214515-b5f2c6a8711a/go.mod h1:HfB7qAhHSZbn1rFK8M9SvSN5NG6ScAg/3h3iE6xdeeI=
gitlab.com/tslocum/cbind v0.1.2 h1:ptDjO7WeOl1HglprsK18L8I9JeRkmtuBoBBaYw/6/Ow=
gitlab.com/tslocum/cbind v0.1.2/go.mod h1:HfB7qAhHSZbn1rFK8M9SvSN5NG6ScAg/3h3iE6xdeeI=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6 h1:DvY3Zkh7KabQE/kfzMvYvKirSiguP9Q/veMtkYyf0o8=
golang.org/x/sys v0.0.0-20200826173525-f9321e4c35a6/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200908134130-d2e65c121b96 h1:gJciq3lOg0eS9fSZJcoHfv7q1BfC6cJfnmSSKL1yu3Q=
golang.org/x/sys v0.0.0-20200908134130-d2e65c121b96/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d h1:L/IKR6COd7ubZrs2oTnTi73IhgqJ71c9s80WsQnh0Es=
golang.org/x/sys v0.0.0-20200923182605-d9f96fdee20d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3 h1:cokOdA+Jmi5PJGXLlLllQSgYigAEfHXJAERHVMaCc2k=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

View File

@ -22,21 +22,25 @@ type Theme struct {
ContrastBackgroundColor tcell.Color // Background color for contrasting elements.
MoreContrastBackgroundColor tcell.Color // Background color for even more contrasting elements.
// Scroll bar
ScrollBarColor tcell.Color // Scroll bar color.
// Context menu
ContextMenuPaddingTop int // Top padding.
ContextMenuPaddingBottom int // Bottom padding.
ContextMenuPaddingLeft int // Left padding.
ContextMenuPaddingRight int // Right padding.
// Check box
CheckBoxCheckedRune rune
// Context menu
ContextMenuPaddingTop int
ContextMenuPaddingBottom int
ContextMenuPaddingLeft int
ContextMenuPaddingRight int
// Drop down
DropDownAbbreviationChars string // The chars to show when the option's text gets shortened.
DropDownSymbol rune // The symbol to draw at the end of the field.
// Scroll bar
ScrollBarColor tcell.Color
// Window
WindowMinWidth int
WindowMinHeight int
}
// Styles defines the appearance of an application. The default is for a black
@ -58,15 +62,18 @@ var Styles = Theme{
ContrastBackgroundColor: tcell.ColorBlue.TrueColor(),
MoreContrastBackgroundColor: tcell.ColorGreen.TrueColor(),
ScrollBarColor: tcell.ColorWhite.TrueColor(),
CheckBoxCheckedRune: 'X',
ContextMenuPaddingTop: 0,
ContextMenuPaddingBottom: 0,
ContextMenuPaddingLeft: 1,
ContextMenuPaddingRight: 1,
CheckBoxCheckedRune: 'X',
DropDownAbbreviationChars: "...",
DropDownSymbol: '▼',
ScrollBarColor: tcell.ColorWhite.TrueColor(),
WindowMinWidth: 4,
WindowMinHeight: 3,
}

160
window.go Normal file
View File

@ -0,0 +1,160 @@
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// Window is a draggable, resizable frame around a primitive.
type Window struct {
*Box
primitive Primitive
x, y int
width, height int
fullscreen bool
dragX, dragY int
dragWX, dragWY int
sync.RWMutex
}
// NewWindow returns a new window around the given primitive.
func NewWindow(primitive Primitive) *Window {
w := &Window{
Box: NewBox(),
primitive: primitive,
dragWX: -1,
dragWY: -1,
}
w.Box.focus = w
return w
}
// SetPosition sets the position of the window.
func (w *Window) SetPosition(x, y int) *Window {
w.Lock()
defer w.Unlock()
w.x, w.y = x, y
return w
}
// SetSize sets the size of the window.
func (w *Window) SetSize(width, height int) *Window {
w.Lock()
defer w.Unlock()
w.width, w.height = width, height
return w
}
// SetFullscreen sets the flag indicating whether or not the the window should
// be drawn fullscreen.
func (w *Window) SetFullscreen(fullscreen bool) *Window {
w.Lock()
defer w.Unlock()
w.fullscreen = fullscreen
return w
}
// Focus is called when this primitive receives focus.
func (w *Window) Focus(delegate func(p Primitive)) {
w.Lock()
defer w.Unlock()
w.Box.Focus(delegate)
w.primitive.Focus(delegate)
}
// Blur is called when this primitive loses focus.
func (w *Window) Blur() {
w.Lock()
defer w.Unlock()
w.Box.Blur()
w.primitive.Blur()
}
// HasFocus returns whether or not this primitive has focus.
func (w *Window) HasFocus() bool {
w.RLock()
defer w.RUnlock()
focusable := w.primitive.GetFocusable()
if focusable != nil {
return focusable.HasFocus()
}
return w.Box.HasFocus()
}
// Draw draws this primitive onto the screen.
func (w *Window) Draw(screen tcell.Screen) {
w.RLock()
defer w.RUnlock()
w.Box.Draw(screen)
x, y, width, height := w.GetInnerRect()
w.primitive.SetRect(x, y, width, height)
w.primitive.Draw(screen)
}
// InputHandler returns the handler for this primitive.
func (w *Window) InputHandler() func(event *tcell.EventKey, setFocus func(p Primitive)) {
return w.primitive.InputHandler()
}
// MouseHandler returns the mouse handler for this primitive.
func (w *Window) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return w.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !w.InRect(event.Position()) {
return false, nil
}
if action == MouseLeftDown || action == MouseMiddleDown || action == MouseRightDown {
setFocus(w)
}
if action == MouseLeftDown {
x, y, width, height := w.GetRect()
mouseX, mouseY := event.Position()
leftEdge := mouseX == x
rightEdge := mouseX == x+width-1
bottomEdge := mouseY == y+height-1
topEdge := mouseY == y
if mouseY >= y && mouseY <= y+height-1 {
if leftEdge {
w.dragX = -1
} else if rightEdge {
w.dragX = 1
}
}
if mouseX >= x && mouseX <= x+width-1 {
if bottomEdge {
w.dragY = -1
} else if topEdge {
if leftEdge || rightEdge {
w.dragY = 1
} else {
w.dragWX = mouseX - x
w.dragWY = mouseY - y
}
}
}
}
_, capture = w.primitive.MouseHandler()(action, event, setFocus)
return true, capture
})
}

192
windowmanager.go Normal file
View File

@ -0,0 +1,192 @@
package cview
import (
"sync"
"github.com/gdamore/tcell/v2"
)
// WindowManager provides an area which windows may be added to.
type WindowManager struct {
*Box
windows []*Window
sync.RWMutex
}
// NewWindowManager returns a new window manager.
func NewWindowManager() *WindowManager {
return &WindowManager{
Box: NewBox(),
}
}
// Add adds a window to the manager.
func (wm *WindowManager) Add(w ...*Window) {
wm.Lock()
defer wm.Unlock()
wm.windows = append(wm.windows, w...)
}
// Clear removes all windows from the manager.
func (wm *WindowManager) Clear() {
wm.Lock()
defer wm.Unlock()
wm.windows = nil
}
// Focus is called when this primitive receives focus.
func (wm *WindowManager) Focus(delegate func(p Primitive)) {
wm.Lock()
defer wm.Unlock()
if len(wm.windows) == 0 {
return
}
wm.windows[len(wm.windows)-1].Focus(delegate)
}
// HasFocus returns whether or not this primitive has focus.
func (wm *WindowManager) HasFocus() bool {
wm.RLock()
defer wm.RUnlock()
for _, w := range wm.windows {
if w.HasFocus() {
return true
}
}
return false
}
// Draw draws this primitive onto the screen.
func (wm *WindowManager) Draw(screen tcell.Screen) {
wm.RLock()
defer wm.RUnlock()
x, y, width, height := wm.GetInnerRect()
var hasFullScreen bool
for _, w := range wm.windows {
if !w.fullscreen {
continue
}
w.SetBorder(false)
w.SetRect(x, y+1, width, height-1)
w.Draw(screen)
hasFullScreen = true
}
if hasFullScreen {
return
}
for _, w := range wm.windows {
w.SetBorder(true)
w.SetRect(x+w.x, x+w.y, w.width, w.height)
w.Draw(screen)
}
}
// MouseHandler returns the mouse handler for this primitive.
func (wm *WindowManager) MouseHandler() func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
return wm.WrapMouseHandler(func(action MouseAction, event *tcell.EventMouse, setFocus func(p Primitive)) (consumed bool, capture Primitive) {
if !wm.InRect(event.Position()) {
return false, nil
}
if action == MouseMove {
x, y, _, _ := wm.GetInnerRect()
mouseX, mouseY := event.Position()
for _, w := range wm.windows {
if w.dragWX != -1 || w.dragWY != -1 {
offsetX := w.x - (mouseX - x)
offsetY := w.y - (mouseY - y)
w.x -= offsetX + w.dragWX
w.y -= offsetY + w.dragWY
consumed = true
}
if w.dragX != 0 {
if w.dragX == -1 {
offsetX := w.x - (mouseX - x)
if w.width+offsetX >= Styles.WindowMinWidth {
w.x -= offsetX
w.width += offsetX
}
} else {
offsetX := mouseX - (x + w.x + w.width)
if w.width+offsetX >= Styles.WindowMinWidth {
w.width += offsetX
}
}
consumed = true
}
if w.dragY != 0 {
if w.dragY == -1 {
offsetY := mouseY - (y + w.y + w.height)
if w.height+offsetY >= Styles.WindowMinHeight {
w.height += offsetY
}
} else {
offsetY := w.y - (mouseY - y)
if w.height+offsetY >= Styles.WindowMinHeight {
w.y -= offsetY
w.height += offsetY
}
}
consumed = true
}
}
} else if action == MouseLeftUp {
for _, w := range wm.windows {
w.dragX, w.dragY = 0, 0
w.dragWX, w.dragWY = -1, -1
}
}
// Focus window on mousedown
var (
focusWindow *Window
focusWindowIndex int
)
for i := len(wm.windows) - 1; i >= 0; i-- {
if wm.windows[i].InRect(event.Position()) {
focusWindow = wm.windows[i]
focusWindowIndex = i
break
}
}
if focusWindow != nil {
if action == MouseLeftDown || action == MouseMiddleDown || action == MouseRightDown {
for _, w := range wm.windows {
if w != focusWindow {
w.Blur()
}
}
wm.windows = append(append(wm.windows[:focusWindowIndex], wm.windows[focusWindowIndex+1:]...), focusWindow)
}
return focusWindow.MouseHandler()(action, event, setFocus)
}
return consumed, nil
})
}