clui/base_control.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)
}