642 lines
11 KiB
Go
642 lines
11 KiB
Go
package clui
|
|
|
|
import (
|
|
term "github.com/nsf/termbox-go"
|
|
"sync"
|
|
"sync/atomic"
|
|
)
|
|
|
|
// BaseControl is a base for all visible controls.
|
|
// Every new control must inherit it or implement
|
|
// the same set of methods
|
|
type BaseControl struct {
|
|
refID int64
|
|
title string
|
|
x, y int
|
|
width, height int
|
|
minW, minH int
|
|
scale int
|
|
fg, bg term.Attribute
|
|
fgActive term.Attribute
|
|
bgActive term.Attribute
|
|
tabSkip bool
|
|
disabled bool
|
|
hidden bool
|
|
align Align
|
|
parent Control
|
|
inactive bool
|
|
modal bool
|
|
padX, padY int
|
|
gapX, gapY int
|
|
pack PackType
|
|
children []Control
|
|
mtx sync.RWMutex
|
|
onActive func(active bool)
|
|
style string
|
|
clipped bool
|
|
clipper *rect
|
|
}
|
|
|
|
var (
|
|
globalRefId int64
|
|
)
|
|
|
|
func nextRefId() int64 {
|
|
return atomic.AddInt64(&globalRefId, 1)
|
|
}
|
|
|
|
func NewBaseControl() BaseControl {
|
|
return BaseControl{refID: nextRefId()}
|
|
}
|
|
|
|
func (c *BaseControl) SetClipped(clipped bool) {
|
|
c.clipped = clipped
|
|
}
|
|
|
|
func (c *BaseControl) Clipped() bool {
|
|
return c.clipped
|
|
}
|
|
|
|
func (c *BaseControl) SetStyle(style string) {
|
|
c.style = style
|
|
}
|
|
|
|
func (c *BaseControl) Style() string {
|
|
return c.style
|
|
}
|
|
|
|
func (c *BaseControl) RefID() int64 {
|
|
return c.refID
|
|
}
|
|
|
|
func (c *BaseControl) Title() string {
|
|
return c.title
|
|
}
|
|
|
|
func (c *BaseControl) SetTitle(title string) {
|
|
c.title = title
|
|
}
|
|
|
|
func (c *BaseControl) Size() (widht int, height int) {
|
|
return c.width, c.height
|
|
}
|
|
|
|
func (c *BaseControl) SetSize(width, height int) {
|
|
if width < c.minW {
|
|
width = c.minW
|
|
}
|
|
if height < c.minH {
|
|
height = c.minH
|
|
}
|
|
|
|
if height != c.height || width != c.width {
|
|
c.height = height
|
|
c.width = width
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) Pos() (x int, y int) {
|
|
return c.x, c.y
|
|
}
|
|
|
|
func (c *BaseControl) SetPos(x, y int) {
|
|
if c.clipped && c.clipper != nil {
|
|
cx, cy, _, _ := c.Clipper()
|
|
px, py := c.Paddings()
|
|
|
|
distX := cx - c.x
|
|
distY := cy - c.y
|
|
|
|
c.clipper.x = x + px
|
|
c.clipper.y = y + py
|
|
|
|
c.x = (x - distX) + px
|
|
c.y = (y - distY) + py
|
|
} else {
|
|
c.x = x
|
|
c.y = y
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) applyConstraints() {
|
|
ww, hh := c.width, c.height
|
|
if ww < c.minW {
|
|
ww = c.minW
|
|
}
|
|
if hh < c.minH {
|
|
hh = c.minH
|
|
}
|
|
if hh != c.height || ww != c.width {
|
|
c.SetSize(ww, hh)
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) Constraints() (minw int, minh int) {
|
|
return c.minW, c.minH
|
|
}
|
|
|
|
func (c *BaseControl) SetConstraints(minw, minh int) {
|
|
c.minW = minw
|
|
c.minH = minh
|
|
c.applyConstraints()
|
|
}
|
|
|
|
func (c *BaseControl) Active() bool {
|
|
return !c.inactive
|
|
}
|
|
|
|
func (c *BaseControl) SetActive(active bool) {
|
|
c.inactive = !active
|
|
|
|
if c.onActive != nil {
|
|
c.onActive(active)
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) OnActive(fn func(active bool)) {
|
|
c.onActive = fn
|
|
}
|
|
|
|
func (c *BaseControl) TabStop() bool {
|
|
return !c.tabSkip
|
|
}
|
|
|
|
func (c *BaseControl) SetTabStop(tabstop bool) {
|
|
c.tabSkip = !tabstop
|
|
}
|
|
|
|
func (c *BaseControl) Enabled() bool {
|
|
c.mtx.RLock()
|
|
defer c.mtx.RUnlock()
|
|
|
|
return !c.disabled
|
|
}
|
|
|
|
func (c *BaseControl) SetEnabled(enabled bool) {
|
|
c.mtx.Lock()
|
|
defer c.mtx.Unlock()
|
|
|
|
c.disabled = !enabled
|
|
}
|
|
|
|
func (c *BaseControl) Visible() bool {
|
|
c.mtx.RLock()
|
|
defer c.mtx.RUnlock()
|
|
|
|
return !c.hidden
|
|
}
|
|
|
|
func (c *BaseControl) SetVisible(visible bool) {
|
|
c.mtx.Lock()
|
|
defer c.mtx.Unlock()
|
|
|
|
if visible == !c.hidden {
|
|
return
|
|
}
|
|
|
|
c.hidden = !visible
|
|
if c.parent == nil {
|
|
return
|
|
}
|
|
|
|
p := c.Parent()
|
|
for p.Parent() != nil {
|
|
p = p.Parent()
|
|
}
|
|
|
|
go func() {
|
|
if FindFirstActiveControl(c) != nil && !c.inactive {
|
|
PutEvent(Event{Type: EventKey, Key: term.KeyTab})
|
|
}
|
|
PutEvent(Event{Type: EventLayout, Target: p})
|
|
}()
|
|
}
|
|
|
|
func (c *BaseControl) Parent() Control {
|
|
return c.parent
|
|
}
|
|
|
|
func (c *BaseControl) SetParent(parent Control) {
|
|
if c.parent == nil {
|
|
c.parent = parent
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) Modal() bool {
|
|
return c.modal
|
|
}
|
|
|
|
func (c *BaseControl) SetModal(modal bool) {
|
|
c.modal = modal
|
|
}
|
|
|
|
func (c *BaseControl) Paddings() (px int, py int) {
|
|
return c.padX, c.padY
|
|
}
|
|
|
|
func (c *BaseControl) SetPaddings(px, py int) {
|
|
if px >= 0 {
|
|
c.padX = px
|
|
}
|
|
if py >= 0 {
|
|
c.padY = py
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) Gaps() (dx int, dy int) {
|
|
return c.gapX, c.gapY
|
|
}
|
|
|
|
func (c *BaseControl) SetGaps(dx, dy int) {
|
|
if dx >= 0 {
|
|
c.gapX = dx
|
|
}
|
|
if dy >= 0 {
|
|
c.gapY = dy
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) Pack() PackType {
|
|
return c.pack
|
|
}
|
|
|
|
func (c *BaseControl) SetPack(pack PackType) {
|
|
c.pack = pack
|
|
}
|
|
|
|
func (c *BaseControl) Scale() int {
|
|
return c.scale
|
|
}
|
|
|
|
func (c *BaseControl) SetScale(scale int) {
|
|
if scale >= 0 {
|
|
c.scale = scale
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) Align() Align {
|
|
return c.align
|
|
}
|
|
|
|
func (c *BaseControl) SetAlign(align Align) {
|
|
c.align = align
|
|
}
|
|
|
|
func (c *BaseControl) TextColor() term.Attribute {
|
|
return c.fg
|
|
}
|
|
|
|
func (c *BaseControl) SetTextColor(clr term.Attribute) {
|
|
c.fg = clr
|
|
}
|
|
|
|
func (c *BaseControl) BackColor() term.Attribute {
|
|
return c.bg
|
|
}
|
|
|
|
func (c *BaseControl) SetBackColor(clr term.Attribute) {
|
|
c.bg = clr
|
|
}
|
|
|
|
func (c *BaseControl) childCount() int {
|
|
cnt := 0
|
|
for _, child := range c.children {
|
|
if child.Visible() {
|
|
cnt++
|
|
}
|
|
}
|
|
|
|
return cnt
|
|
}
|
|
|
|
func (c *BaseControl) ResizeChildren() {
|
|
children := c.childCount()
|
|
if children == 0 {
|
|
return
|
|
}
|
|
|
|
fullWidth := c.width - 2*c.padX
|
|
fullHeight := c.height - 2*c.padY
|
|
if c.pack == Horizontal {
|
|
fullWidth -= (children - 1) * c.gapX
|
|
} else {
|
|
fullHeight -= (children - 1) * c.gapY
|
|
}
|
|
|
|
totalSc := c.ChildrenScale()
|
|
minWidth := 0
|
|
minHeight := 0
|
|
for _, child := range c.children {
|
|
if !child.Visible() {
|
|
continue
|
|
}
|
|
|
|
cw, ch := child.MinimalSize()
|
|
if c.pack == Horizontal {
|
|
minWidth += cw
|
|
} else {
|
|
minHeight += ch
|
|
}
|
|
}
|
|
|
|
aStep := 0
|
|
diff := fullWidth - minWidth
|
|
if c.pack == Vertical {
|
|
diff = fullHeight - minHeight
|
|
}
|
|
if totalSc > 0 {
|
|
aStep = int(float32(diff) / float32(totalSc))
|
|
}
|
|
|
|
for _, ctrl := range c.children {
|
|
if !ctrl.Visible() {
|
|
continue
|
|
}
|
|
|
|
tw, th := ctrl.MinimalSize()
|
|
sc := ctrl.Scale()
|
|
d := int(ctrl.Scale() * aStep)
|
|
if c.pack == Horizontal {
|
|
if sc != 0 {
|
|
if sc == totalSc {
|
|
tw += diff
|
|
d = diff
|
|
} else {
|
|
tw += d
|
|
}
|
|
}
|
|
th = fullHeight
|
|
} else {
|
|
if sc != 0 {
|
|
if sc == totalSc {
|
|
th += diff
|
|
d = diff
|
|
} else {
|
|
th += d
|
|
}
|
|
}
|
|
tw = fullWidth
|
|
}
|
|
diff -= d
|
|
totalSc -= sc
|
|
|
|
ctrl.SetSize(tw, th)
|
|
ctrl.ResizeChildren()
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) AddChild(control Control) {
|
|
if c.children == nil {
|
|
c.children = make([]Control, 1)
|
|
c.children[0] = control
|
|
} else {
|
|
if c.ChildExists(control) {
|
|
panic("Double adding a child")
|
|
}
|
|
|
|
c.children = append(c.children, control)
|
|
}
|
|
|
|
var ctrl Control
|
|
var mainCtrl Control
|
|
ctrl = c
|
|
for ctrl != nil {
|
|
ww, hh := ctrl.MinimalSize()
|
|
cw, ch := ctrl.Size()
|
|
if ww > cw || hh > ch {
|
|
if ww > cw {
|
|
cw = ww
|
|
}
|
|
if hh > ch {
|
|
ch = hh
|
|
}
|
|
ctrl.SetConstraints(cw, ch)
|
|
}
|
|
|
|
if ctrl.Parent() == nil {
|
|
mainCtrl = ctrl
|
|
}
|
|
ctrl = ctrl.Parent()
|
|
}
|
|
|
|
if mainCtrl != nil {
|
|
mainCtrl.ResizeChildren()
|
|
mainCtrl.PlaceChildren()
|
|
}
|
|
|
|
if c.clipped && c.clipper == nil {
|
|
c.setClipper()
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) Children() []Control {
|
|
child := make([]Control, len(c.children))
|
|
copy(child, c.children)
|
|
return child
|
|
}
|
|
|
|
func (c *BaseControl) ChildExists(control Control) bool {
|
|
if len(c.children) == 0 {
|
|
return false
|
|
}
|
|
|
|
for _, ctrl := range c.children {
|
|
if ctrl == control {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func (c *BaseControl) ChildrenScale() int {
|
|
if c.childCount() == 0 {
|
|
return c.scale
|
|
}
|
|
|
|
total := 0
|
|
for _, ctrl := range c.children {
|
|
if ctrl.Visible() {
|
|
total += ctrl.Scale()
|
|
}
|
|
}
|
|
|
|
return total
|
|
}
|
|
|
|
func (c *BaseControl) MinimalSize() (w int, h int) {
|
|
children := c.childCount()
|
|
if children == 0 {
|
|
return c.minW, c.minH
|
|
}
|
|
|
|
totalX := 2 * c.padX
|
|
totalY := 2 * c.padY
|
|
|
|
if c.pack == Vertical {
|
|
totalY += (children - 1) * c.gapY
|
|
} else {
|
|
totalX += (children - 1) * c.gapX
|
|
}
|
|
|
|
for _, ctrl := range c.children {
|
|
if ctrl.Clipped() {
|
|
continue
|
|
}
|
|
|
|
if !ctrl.Visible() {
|
|
continue
|
|
}
|
|
ww, hh := ctrl.MinimalSize()
|
|
if c.pack == Vertical {
|
|
totalY += hh
|
|
if ww+2*c.padX > totalX {
|
|
totalX = ww + 2*c.padX
|
|
}
|
|
} else {
|
|
totalX += ww
|
|
if hh+2*c.padY > totalY {
|
|
totalY = hh + 2*c.padY
|
|
}
|
|
}
|
|
}
|
|
|
|
if totalX < c.minW {
|
|
totalX = c.minW
|
|
}
|
|
if totalY < c.minH {
|
|
totalY = c.minH
|
|
}
|
|
|
|
return totalX, totalY
|
|
}
|
|
|
|
func (c *BaseControl) Draw() {
|
|
panic("BaseControl Draw Called")
|
|
}
|
|
|
|
func (c *BaseControl) DrawChildren() {
|
|
if c.hidden {
|
|
return
|
|
}
|
|
|
|
PushClip()
|
|
defer PopClip()
|
|
|
|
cp := ClippedParent(c)
|
|
var cTarget Control
|
|
|
|
cTarget = c
|
|
if cp != nil {
|
|
cTarget = cp
|
|
}
|
|
|
|
x, y, w, h := cTarget.Clipper()
|
|
SetClipRect(x, y, w, h)
|
|
|
|
for _, child := range c.children {
|
|
child.Draw()
|
|
}
|
|
}
|
|
|
|
func (c *BaseControl) Clipper() (int, int, int, int) {
|
|
clipped := ClippedParent(c)
|
|
|
|
if clipped == nil || (c.clipped && c.clipper != nil) {
|
|
return c.clipper.x, c.clipper.y, c.clipper.w, c.clipper.h
|
|
}
|
|
|
|
return CalcClipper(c)
|
|
}
|
|
|
|
func (c *BaseControl) setClipper() {
|
|
x, y, w, h := CalcClipper(c)
|
|
c.clipper = &rect{x: x, y: y, w: w, h: h}
|
|
}
|
|
|
|
func (c *BaseControl) HitTest(x, y int) HitResult {
|
|
if x > c.x && x < c.x+c.width-1 &&
|
|
y > c.y && y < c.y+c.height-1 {
|
|
return HitInside
|
|
}
|
|
|
|
if (x == c.x || x == c.x+c.width-1) &&
|
|
y >= c.y && y < c.y+c.height {
|
|
return HitBorder
|
|
}
|
|
|
|
if (y == c.y || y == c.y+c.height-1) &&
|
|
x >= c.x && x < c.x+c.width {
|
|
return HitBorder
|
|
}
|
|
|
|
return HitOutside
|
|
}
|
|
|
|
func (c *BaseControl) ProcessEvent(ev Event) bool {
|
|
return SendEventToChild(c, ev)
|
|
}
|
|
|
|
func (c *BaseControl) PlaceChildren() {
|
|
children := c.childCount()
|
|
if c.children == nil || children == 0 {
|
|
return
|
|
}
|
|
|
|
xx, yy := c.x+c.padX, c.y+c.padY
|
|
for _, ctrl := range c.children {
|
|
if !ctrl.Visible() {
|
|
continue
|
|
}
|
|
|
|
ctrl.SetPos(xx, yy)
|
|
ww, hh := ctrl.Size()
|
|
if c.pack == Vertical {
|
|
yy += c.gapY + hh
|
|
} else {
|
|
xx += c.gapX + ww
|
|
}
|
|
|
|
ctrl.PlaceChildren()
|
|
}
|
|
}
|
|
|
|
// ActiveColors return the attributes for the controls when it
|
|
// is active: text and background colors
|
|
func (c *BaseControl) ActiveColors() (term.Attribute, term.Attribute) {
|
|
return c.fgActive, c.bgActive
|
|
}
|
|
|
|
// SetActiveTextColor changes text color of the active control
|
|
func (c *BaseControl) SetActiveTextColor(clr term.Attribute) {
|
|
c.fgActive = clr
|
|
}
|
|
|
|
// SetActiveBackColor changes background color of the active control
|
|
func (c *BaseControl) SetActiveBackColor(clr term.Attribute) {
|
|
c.bgActive = clr
|
|
}
|
|
|
|
func (c *BaseControl) removeChild(control Control) {
|
|
children := []Control{}
|
|
|
|
for _, child := range c.children {
|
|
if child.RefID() == control.RefID() {
|
|
continue
|
|
}
|
|
|
|
children = append(children, child)
|
|
}
|
|
c.children = nil
|
|
|
|
for _, child := range children {
|
|
c.AddChild(child)
|
|
}
|
|
}
|
|
|
|
// Destroy removes an object from its parental chain
|
|
func (c *BaseControl) Destroy() {
|
|
c.parent.removeChild(c)
|
|
c.parent.SetConstraints(0, 0)
|
|
}
|