clui/window.go

837 lines
16 KiB
Go

package clui
import (
"fmt"
"github.com/VladimirMarkelov/termbox-go"
xs "github.com/huandu/xstrings"
"log"
"os"
"strings"
)
// Internal representation of Window object that keeps and manages control inside it.
type view struct {
posX, posY int
width, height int
minW, minH int // min size constraints
maxW, maxH int // max size constraints
id WinId
title string
borderStyle BorderStyle
icons BorderIcon
enabled bool
active bool
canvas *FrameBuffer
controls []Control
parent *Composer
lastCtrlId WinId //last Id used for control
originals map[WinId]Coord
originalWidth int
originalHeight int
// pack support
layout LayoutType
pack PackType
lockUpdate bool // it is true while new controls are adding
padSide, padTop int
padX, padY int
scale int
children []WinId
packer Packer
lastX, lastY int
currWidth, currHeight int
// helpers
logger *log.Logger
}
func NewView(composer *Composer, id WinId, posX, posY, width, height int, title string) *view {
d := new(view)
d.minW, d.minH = 10, 5
if width < d.minW {
width = d.minW
}
if height < d.minH {
height = d.minH
}
d.SetTitle(title)
d.SetSize(width, height)
d.SetPos(posX, posY)
d.SetEnabled(true)
d.controls = make([]Control, 0)
d.originals = make(map[WinId]Coord)
d.parent = composer
d.id = id
d.active = false
d.originalWidth = width
d.originalHeight = height
d.lastCtrlId = 0
d.children = make([]WinId, 0)
d.layout = LayoutManual
d.lastX, d.lastY = -1, -1
d.currWidth, d.currHeight = -1, -1
d.padSide, d.padTop, d.padX, d.padY = 0, 0, 1, 0
d.borderStyle = BorderSingle
file, _ := os.OpenFile("debug.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
d.logger = log.New(file, fmt.Sprintf("WND[%v]", title), log.Ldate|log.Ltime|log.Lshortfile)
return d
}
func (d *view) SetTitle(title string) {
d.title = title
}
func (d *view) GetTitle() string {
return d.title
}
func (d *view) SetSize(width, height int) {
if width > 1000 || width < d.minW {
panic(fmt.Sprintf("Invalid width: %v", width))
}
if height > 200 || height < d.minH {
panic(fmt.Sprintf("Invalid height: %v", height))
}
d.width = width
d.height = height
d.canvas = NewFrameBuffer(width, height)
}
func (d *view) SetPos(x, y int) {
d.posX = x
d.posY = y
}
func (d *view) GetId() WinId {
return d.id
}
func (d *view) GetPos() (int, int) {
return d.posX, d.posY
}
func (d *view) GetSize() (int, int) {
return d.width, d.height
}
func (d *view) GetBorderStyle() BorderStyle {
return d.borderStyle
}
func (d *view) DrawControls() {
if d.packer != nil {
d.packer.Redraw(d)
}
for _, ctrl := range d.controls {
if ctrl.GetVisible() {
ctrl.Redraw(d)
}
}
}
func (d *view) DrawDecoration() {
if d.canvas == nil {
return
}
tm := d.parent.GetThemeManager()
fg, bg := ColorWhite, ColorBlack
if d.active {
fg = tm.GetSysColor(ColorActiveText)
bg = tm.GetSysColor(ColorViewBack)
} else {
fg = tm.GetSysColor(ColorInactiveText)
bg = tm.GetSysColor(ColorViewBack)
}
d.canvas.DrawBorder(d, tm, fg, bg)
d.canvas.DrawBorderIcons(d, tm, fg, bg)
d.canvas.DrawTitle(d, fg, bg)
}
func (d *view) Redraw() {
if d.canvas == nil {
return
}
tm := d.parent.GetThemeManager()
bg := tm.GetSysColor(ColorViewBack)
d.canvas.Clear(bg)
d.DrawDecoration()
d.DrawControls()
}
func (d *view) GetBorderIcons() BorderIcon {
return d.icons
}
func (d *view) SetBorderIcons(icons BorderIcon) {
if d.icons != icons {
d.icons = icons
d.DrawDecoration()
}
}
func (d *view) isInside(screenX, screenY int) bool {
if screenX >= d.posX && screenX < d.posX+d.width && screenY >= d.posY && screenY < d.posY+d.height {
return true
} else {
return false
}
}
func (d *view) GetScreenSymbol(screenX, screenY int) (Symbol, bool) {
if !d.isInside(screenX, screenY) {
return Symbol{ch: ' '}, false
} else {
return d.canvas.GetSymbol(screenX-d.posX, screenY-d.posY)
}
}
func (d *view) GetEnabled() bool {
return d.enabled
}
func (d *view) SetEnabled(enable bool) {
d.enabled = enable
}
func (d *view) GetActive() bool {
return d.active
}
func (d *view) SetActive(active bool) {
d.active = active
}
func (d *view) HitTest(screenX, screenY int) HitResult {
if !d.isInside(screenX, screenY) {
return HitOutside
}
if screenX == d.posX {
if screenY == d.posY {
return HitTopLeft
} else if screenY == d.posY+d.height-1 {
return HitBottomLeft
} else {
return HitLeftBorder
}
}
if screenX == d.posX+d.width-1 {
if screenY == d.posY {
return HitTopRight
} else if screenY == d.posY+d.height-1 {
return HitBottomRight
} else {
return HitRightBorder
}
}
if screenY == d.posY+d.height-1 {
return HitBottomBorder
}
if screenY == d.posY {
dx := -3
if d.icons&IconClose != 0 {
if screenX == d.posX+d.width+dx {
return HitButtonClose
}
dx--
}
if d.icons&IconBottom != 0 {
if screenX == d.posX+d.width+dx {
return HitButtonBottom
}
dx--
}
return HitHeader
}
return HitInside
}
func (d *view) GetConstraints() (int, int) {
if d.packer != nil {
minW, minH := d.packer.GetConstraints()
if minW < d.minW {
minW = d.minW
}
if minH < d.minH {
minH = d.minH
}
return minW, minH
}
return d.minW, d.minH
}
func (d *view) AddControl(control Control) WinId {
d.controls = append(d.controls, control)
id := control.GetId()
x, y := control.GetPos()
w, h := control.GetSize()
c := Coord{x: x, y: y, w: w, h: h}
d.originals[id] = c
d.logger.Printf("Control %v got id %v (%vx%v)", control.GetText(), id, w, h)
return id
}
func (d *view) RemoveControl(control Control) {
id := control.GetId()
_, ok := d.originals[id]
if ok {
delete(d.originals, id)
}
newList := make([]Control, 0)
for _, ctrl := range d.controls {
if ctrl.GetId() != id {
newList = append(newList, ctrl)
}
}
d.controls = newList
if len(d.children) > 0 {
newKids := make([]WinId, 0)
for _, cid := range d.children {
if cid != id {
newKids = append(newKids, cid)
}
}
d.children = newKids
}
}
func (d *view) GetControl(id WinId) Control {
for _, ctrl := range d.controls {
if ctrl.GetId() == id {
return ctrl
}
}
return nil
}
// ------------ Canvas methods ---------------
func (d *view) DrawText(x, y, w int, text string, fg, bg Color) {
if text == "" || w < 1 {
return
}
if xs.Len(text) > w {
text = xs.Slice(text, 0, w)
}
d.canvas.DrawText(d, x, y, text, fg, bg)
}
func (d *view) DrawVerticalText(x, y, h int, text string, fg, bg Color) {
if text == "" || h < 1 {
return
}
for idx, r := range text {
if idx >= h {
break
}
d.canvas.DrawText(d, x, y+idx, string(r), fg, bg)
}
}
func (d *view) DrawAlignedText(x, y, w int, text string, fg, bg Color, align Align) {
if text == "" || w < 1 {
return
}
length := xs.Len(text)
if length < w {
if align == AlignCenter {
d.DrawText(x+int((w-length)/2), y, w, text, fg, bg)
} else if align == AlignLeft {
d.DrawText(x, y, w, text, fg, bg)
} else {
d.DrawText(x+w-length, y, length, text, fg, bg)
}
} else {
str := ""
if align == AlignCenter {
dx := int((length - w) / 2)
str = xs.Slice(text, dx, dx+w)
} else if align == AlignLeft {
str = xs.Slice(text, 0, w)
} else {
str = xs.Slice(text, length-w, -1)
}
d.DrawText(x, y, w, str, fg, bg)
}
}
func (d *view) DrawRune(x, y int, r rune, fg, bg Color) {
d.canvas.DrawText(d, x, y, string(r), fg, bg)
}
func (d *view) DrawFrame(x, y, width, height int, bs BorderStyle, fg, bg Color) {
if bs == BorderNone {
return
}
tm := d.parent.GetThemeManager()
var cH, cV, cUL, cUR, cDL, cDR rune
if bs == BorderSingle {
cH = tm.GetSysObject(ObjSingleBorderHLine)
cV = tm.GetSysObject(ObjSingleBorderVLine)
cUL = tm.GetSysObject(ObjSingleBorderULCorner)
cUR = tm.GetSysObject(ObjSingleBorderURCorner)
cDL = tm.GetSysObject(ObjSingleBorderDLCorner)
cDR = tm.GetSysObject(ObjSingleBorderDRCorner)
} else {
cH = tm.GetSysObject(ObjDoubleBorderHLine)
cV = tm.GetSysObject(ObjDoubleBorderVLine)
cUL = tm.GetSysObject(ObjDoubleBorderULCorner)
cUR = tm.GetSysObject(ObjDoubleBorderURCorner)
cDL = tm.GetSysObject(ObjDoubleBorderDLCorner)
cDR = tm.GetSysObject(ObjDoubleBorderDRCorner)
}
if width > 1 && height > 1 {
d.DrawRune(x, y, cUL, fg, bg)
d.DrawRune(x, y+height-1, cDL, fg, bg)
d.DrawRune(x+width-1, y, cUR, fg, bg)
d.DrawRune(x+width-1, y+height-1, cDR, fg, bg)
for dx := 1; dx < width-1; dx++ {
d.DrawRune(x+dx, y, cH, fg, bg)
d.DrawRune(x+dx, y+height-1, cH, fg, bg)
}
for dy := 1; dy < height-1; dy++ {
d.DrawRune(x, y+dy, cV, fg, bg)
d.DrawRune(x+width-1, y+dy, cV, fg, bg)
}
} else if width == 1 {
for dy := 0; dy < height; dy++ {
d.DrawRune(x, y+dy, cV, fg, bg)
}
} else if height == 1 {
for dx := 0; dx < width; dx++ {
d.DrawRune(x+dx, y, cH, fg, bg)
}
}
}
func (d *view) ClearRect(x, y, w, h int, bg Color) {
if w < 1 || h < 1 {
return
}
s := strings.Repeat(" ", w)
for i := y; i < y+h; i++ {
d.canvas.DrawText(d, x, i, s, ColorWhite, bg)
}
}
func (d *view) SetCursorPos(control Control, x, y int) {
if !d.active {
return
}
xc, yc := -1, -1
wc, hc := 0, 0
for _, ctrl := range d.controls {
if ctrl.GetId() == control.GetId() {
xc, yc = ctrl.GetPos()
wc, hc = ctrl.GetSize()
break
}
}
if xc >= 0 && yc >= 0 && x >= 0 && x < wc && y >= 0 && y < hc {
wx, wy := d.mapViewToScreen(xc, yc)
d.parent.SetCursorPos(wx+x, wy+y)
}
}
//---------------- internal -----------------------
func (d *view) mapViewToScreen(x, y int) (int, int) {
wx, wy := d.GetPos()
bs := d.GetBorderStyle()
if bs != BorderNone {
wx++
wy++
}
return x + wx, y + wy
}
func (d *view) mapScreenToView(x, y int) (int, int) {
wx, wy := d.GetPos()
bs := d.GetBorderStyle()
if bs != BorderNone {
wx++
wy++
}
return x - wx, y - wy
}
func (d *view) deactivateControls() {
for _, ctrl := range d.controls {
ctrl.SetActive(false)
}
}
func (d *view) getActiveControl() Control {
for _, ctrl := range d.controls {
if ctrl.GetActive() {
return ctrl
}
}
return nil
}
func (d *view) activateNextControl(forward bool) bool {
if len(d.controls) == 0 {
return false
}
idx := -1
for i := 0; i < len(d.controls); i++ {
if d.controls[i].GetActive() {
idx = i
break
}
}
if idx == -1 && forward {
idx = len(d.controls)
}
var newidx, inc int
if forward {
newidx = idx + 1
inc = 1
} else {
newidx = idx - 1
inc = -1
}
for {
if newidx == idx {
break
}
if newidx < 0 {
newidx = len(d.controls) - 1
}
if newidx >= len(d.controls) {
newidx = 0
}
if d.controls[newidx].GetTabStop() && d.controls[newidx].GetVisible() && d.controls[newidx].GetEnabled() {
break
}
newidx += inc
}
if idx == newidx {
return false
} else {
d.ActivateControl(d.controls[newidx])
return true
}
}
func (d *view) ActivateControl(control Control) bool {
id := control.GetId()
activated := false
for _, ctrl := range d.controls {
if ctrl.GetId() == id {
activated = true
if !ctrl.GetActive() {
event := Event{Type: EventActivate, X: 1}
ctrl.ProcessEvent(event)
}
ctrl.SetActive(true)
} else {
if ctrl.GetActive() {
event := Event{Type: EventActivate, X: 0}
ctrl.ProcessEvent(event)
}
ctrl.SetActive(false)
}
}
return activated
}
func (d *view) controlAtPos(screenX, screenY int) Control {
posX, posY := d.mapScreenToView(screenX, screenY)
for id := len(d.controls) - 1; id >= 0; id-- {
ctrl := d.controls[id]
if ctrl.GetVisible() {
w, h := ctrl.GetSize()
x, y := ctrl.GetPos()
if posX >= x && posX < x+w && posY >= y && posY < y+h {
return ctrl
}
}
}
return nil
}
func (d *view) recalculateManual() {
winW, winH := d.GetSize()
for _, ctrl := range d.controls {
anchor := ctrl.GetAnchors()
if anchor == AnchorNone {
continue
}
orig, ok := d.originals[ctrl.GetId()]
if !ok {
d.logger.Printf("No originals for %v", ctrl.GetId())
continue
}
newX, newY := orig.x, orig.y
newW, newH := orig.w, orig.h
dx := winW - d.originalWidth
dy := winH - d.originalHeight
if anchor&AnchorRight != 0 && anchor&AnchorLeft == 0 {
// right side align
newX += dx
}
if anchor&AnchorRight != 0 && anchor&AnchorLeft != 0 {
// full width
newW = orig.w + dx
}
if anchor&AnchorBottom != 0 && anchor&AnchorTop == 0 {
// bottom align
newY += dy
}
if anchor&AnchorTop != 0 && anchor&AnchorBottom != 0 {
// full width
newH = orig.h + dy
}
if newH > 0 && newW > 0 && newX >= 0 && newY >= 0 {
ctrl.SetPos(newX, newY)
ctrl.SetSize(newW, newH)
}
}
}
func (d *view) recalculateDynamic() {
if d.packer != nil {
newW, newH := d.GetSize()
oldW, oldH := d.GetConstraints()
dx, dy := newW-oldW, newH-oldH
d.packer.ResizeChidren(dx, dy)
}
}
func (d *view) recalculateControls() {
if d.pack == PackFixed {
d.recalculateManual()
} else {
d.recalculateDynamic()
d.packer.RepositionChildren()
}
}
func (d *view) hideAllExtraControls() {
for _, ctrl := range d.controls {
ctrl.HideChildren()
}
}
func (d *view) ProcessEvent(ev Event) bool {
switch ev.Type {
case EventKey, EventMouseScroll, EventMouseClick, EventMousePress, EventMouseRelease, EventMouseMove, EventMouse:
if ev.Type == EventKey && ev.Key == termbox.KeyTab {
d.activateNextControl(ev.Mod&termbox.ModShift == 0)
return true
}
if ev.Type == EventMouse || ev.Type == EventMouseClick {
cunder := d.controlAtPos(ev.X, ev.Y)
if cunder == nil {
return true
}
d.ActivateControl(cunder)
}
ctrl := d.getActiveControl()
x, y := ev.X, ev.Y
copyEv := ev
if ev.Type != EventMouseScroll {
copyEv.X, copyEv.Y = d.mapScreenToView(x, y)
}
if ctrl != nil {
ctrl.ProcessEvent(copyEv)
}
case EventActivate:
if ev.X == 0 {
d.parent.SetCursorPos(-1, -1)
}
case EventResize:
d.hideAllExtraControls()
d.recalculateControls()
}
return true
}
func (d *view) SendEvent(ev InternalEvent) {
// now just send to composer
ev.view = d.GetId()
d.parent.SendEvent(ev)
}
func (d *view) Theme() *ThemeManager {
return d.parent.GetThemeManager()
}
func (d *view) GetNextControlId() WinId {
d.lastCtrlId++
id := d.lastCtrlId
return id
}
// ----- Packer ----------------------
func (d *view) AddPack(pt PackType) Packer {
if d.packer != nil {
panic("View already has a packer")
}
if len(d.controls) > 0 {
panic("Cannot enable pack mode if a packer contains any control")
}
if pt != PackFixed {
d.layout = LayoutDynamic
d.pack = pt
pid := d.GetNextControlId()
d.packer = NewContainer(d, nil, pid, 0, 0, d.width, d.height, Props{})
d.packer.SetPackType(pt)
}
d.lockUpdate = true
return d.packer
}
func (d *view) PackEnd() (int, int) {
if d.packer == nil || d.pack == PackFixed {
panic("PackEnd can be used only if any dynamic packer is created before")
}
d.lockUpdate = false
w, h := d.CalculateSize()
// add window border
w += 2
h += 2
d.logger.Printf("Pack ends with %vx%v", w, h)
if w > 0 && h > 0 && (w > d.minW || h > d.minH) {
d.SetConstraints(w, h)
}
d.recalculateControls()
return w, h
}
func (d *view) CalculateSize() (int, int) {
if d.packer != nil {
return d.packer.CalculateSize()
}
return -1, -1
}
func (d *view) GetLayout() LayoutType {
return d.layout
}
func (d *view) GetNextPosition() (int, int) {
return d.lastX, d.lastY
}
func (d *view) SetNextPosition(x, y int) {
d.lastX, d.lastY = x, y
}
func (d *view) SetConstraints(w, h int) {
if w >= 10 {
d.minW = w
}
if h >= 5 {
d.minH = h
}
if d.width < w || d.height < h {
d.SetSize(w, h)
}
}
func (d *view) SetPaddings(pSide, pTop, pX, pY int) {
if len(d.children) > 0 {
panic("Cannot change padding if a child is added")
}
if pSide != DoNotChange {
d.padSide = pSide
}
if pTop != DoNotChange {
d.padTop = pTop
}
if pX != DoNotChange {
d.padX = pX
}
if pY != DoNotChange {
d.padY = pSide
}
}
func (d *view) GetPaddings() (int, int, int, int) {
return d.padSide, d.padTop, d.padX, d.padY
}
func (d *view) GetScale() int {
return d.scale
}
func (d *view) SetScale(scale int) {
// do nothing - does not make sense to set scale for view
}
//-------------- debug -----------------------------
func (d *view) Logger() *log.Logger {
return d.logger
}