521 lines
12 KiB
Go
521 lines
12 KiB
Go
package clui
|
|
|
|
import (
|
|
xs "github.com/huandu/xstrings"
|
|
term "github.com/nsf/termbox-go"
|
|
"strings"
|
|
)
|
|
|
|
type attr struct {
|
|
text term.Attribute
|
|
back term.Attribute
|
|
}
|
|
|
|
type rect struct {
|
|
x, y, w, h int
|
|
}
|
|
|
|
/*
|
|
Canvas is a 'graphical' engine to draw primitives.
|
|
*/
|
|
type Canvas struct {
|
|
width int
|
|
height int
|
|
textColor term.Attribute
|
|
backColor term.Attribute
|
|
clipX int
|
|
clipY int
|
|
clipW int
|
|
clipH int
|
|
attrStack []attr
|
|
clipStack []rect
|
|
}
|
|
|
|
var (
|
|
canvas *Canvas
|
|
)
|
|
|
|
func initCanvas() bool {
|
|
err := term.Init()
|
|
if err != nil {
|
|
return false
|
|
}
|
|
term.SetInputMode(term.InputEsc | term.InputMouse)
|
|
|
|
canvas = new(Canvas)
|
|
Reset()
|
|
|
|
return true
|
|
}
|
|
|
|
// PushAttributes saves the current back and fore colors. Useful when used with
|
|
// PopAttributes: you can save colors then change them to anything you like and
|
|
// as the final step just restore original colors
|
|
func PushAttributes() {
|
|
p := attr{text: canvas.textColor, back: canvas.backColor}
|
|
canvas.attrStack = append(canvas.attrStack, p)
|
|
}
|
|
|
|
// PopAttributes restores saved with PushAttributes colors. Function does
|
|
// nothing if there is no saved colors
|
|
func PopAttributes() {
|
|
if len(canvas.attrStack) == 0 {
|
|
return
|
|
}
|
|
a := canvas.attrStack[len(canvas.attrStack)-1]
|
|
canvas.attrStack = canvas.attrStack[:len(canvas.attrStack)-1]
|
|
SetTextColor(a.text)
|
|
SetBackColor(a.back)
|
|
}
|
|
|
|
// PushClip saves the current clipping window
|
|
func PushClip() {
|
|
c := rect{x: canvas.clipX, y: canvas.clipY, w: canvas.clipW, h: canvas.clipH}
|
|
canvas.clipStack = append(canvas.clipStack, c)
|
|
}
|
|
|
|
// PopClip restores saved with PushClip clipping window
|
|
func PopClip() {
|
|
if len(canvas.clipStack) == 0 {
|
|
return
|
|
}
|
|
c := canvas.clipStack[len(canvas.clipStack)-1]
|
|
canvas.clipStack = canvas.clipStack[:len(canvas.clipStack)-1]
|
|
SetClipRect(c.x, c.y, c.w, c.h)
|
|
}
|
|
|
|
// Reset reinitializes canvas: set clipping rectangle to the whole
|
|
// terminal window, clears clip and color saved data, sets colors
|
|
// to default ones
|
|
func Reset() {
|
|
canvas.width, canvas.height = term.Size()
|
|
canvas.clipX, canvas.clipY = 0, 0
|
|
canvas.clipW, canvas.clipH = canvas.width, canvas.height
|
|
canvas.textColor = ColorWhite
|
|
canvas.backColor = ColorBlack
|
|
|
|
canvas.attrStack = make([]attr, 0)
|
|
canvas.clipStack = make([]rect, 0)
|
|
}
|
|
|
|
// InClipRect returns true if x and y position is inside current clipping
|
|
// rectangle
|
|
func InClipRect(x, y int) bool {
|
|
return x >= canvas.clipX && y >= canvas.clipY &&
|
|
x < canvas.clipX+canvas.clipW &&
|
|
y < canvas.clipY+canvas.clipH
|
|
}
|
|
|
|
func clip(x, y, w, h int) (cx int, cy int, cw int, ch int) {
|
|
if x+w < canvas.clipX || x > canvas.clipX+canvas.clipW ||
|
|
y+h < canvas.clipY || y > canvas.clipY+canvas.clipH {
|
|
return 0, 0, 0, 0
|
|
}
|
|
|
|
if x < canvas.clipX {
|
|
w = w - (canvas.clipX - x)
|
|
x = canvas.clipX
|
|
}
|
|
if y < canvas.clipY {
|
|
h = h - (canvas.clipY - y)
|
|
y = canvas.clipY
|
|
}
|
|
if x+w > canvas.clipX+canvas.clipW {
|
|
w = canvas.clipW - (x - canvas.clipX)
|
|
}
|
|
if y+h > canvas.clipY+canvas.clipH {
|
|
h = canvas.clipH - (y - canvas.clipY)
|
|
}
|
|
|
|
return x, y, w, h
|
|
}
|
|
|
|
// Flush makes termbox to draw everything to screen
|
|
func Flush() {
|
|
term.Flush()
|
|
}
|
|
|
|
// SetSize sets the new Canvas size. If new size does not
|
|
// equal old size then Canvas is recreated and cleared
|
|
// with default colors. Both Canvas width and height must
|
|
// be greater than 2
|
|
func SetScreenSize(width int, height int) {
|
|
if canvas.width == width && canvas.height == height {
|
|
return
|
|
}
|
|
|
|
canvas.width = width
|
|
canvas.height = height
|
|
|
|
canvas.clipStack = make([]rect, 0)
|
|
SetClipRect(0, 0, width, height)
|
|
}
|
|
|
|
// Size returns current Canvas size
|
|
func ScreenSize() (width int, height int) {
|
|
return canvas.width, canvas.height
|
|
}
|
|
|
|
// SetCursorPos sets text caret position. Used by controls like EditField
|
|
func SetCursorPos(x int, y int) {
|
|
term.SetCursor(x, y)
|
|
}
|
|
|
|
// PutChar sets value for the Canvas cell: rune and its colors. Returns result of
|
|
// operation: e.g, if the symbol position is outside Canvas the operation fails
|
|
// and the function returns false
|
|
func PutChar(x, y int, r rune) bool {
|
|
if InClipRect(x, y) {
|
|
term.SetCell(x, y, r, canvas.textColor, canvas.backColor)
|
|
return true
|
|
}
|
|
|
|
return false
|
|
}
|
|
|
|
func putCharUnsafe(x, y int, r rune) {
|
|
term.SetCell(x, y, r, canvas.textColor, canvas.backColor)
|
|
}
|
|
|
|
// Symbol returns the character and its attributes by its coordinates
|
|
func Symbol(x, y int) (term.Cell, bool) {
|
|
if x >= 0 && x < canvas.width && y >= 0 && y < canvas.height {
|
|
cells := term.CellBuffer()
|
|
return cells[y*canvas.width+x], true
|
|
}
|
|
return term.Cell{Ch: ' '}, false
|
|
}
|
|
|
|
// SetTextColor changes current text color
|
|
func SetTextColor(clr term.Attribute) {
|
|
canvas.textColor = clr
|
|
}
|
|
|
|
// SetBackColor changes current background color
|
|
func SetBackColor(clr term.Attribute) {
|
|
canvas.backColor = clr
|
|
}
|
|
|
|
func TextColor() term.Attribute {
|
|
return canvas.textColor
|
|
}
|
|
|
|
func BackColor() term.Attribute {
|
|
return canvas.backColor
|
|
}
|
|
|
|
// SetClipRect defines a new clipping rect. Maybe useful with PopClip and
|
|
// PushClip functions
|
|
func SetClipRect(x, y, w, h int) {
|
|
if x < 0 {
|
|
x = 0
|
|
}
|
|
if y < 0 {
|
|
y = 0
|
|
}
|
|
if x+w > canvas.width {
|
|
w = canvas.width - x
|
|
}
|
|
if y+h > canvas.height {
|
|
h = canvas.height - h
|
|
}
|
|
|
|
canvas.clipX = x
|
|
canvas.clipY = y
|
|
canvas.clipW = w
|
|
canvas.clipH = h
|
|
}
|
|
|
|
// ClipRect returns the current clipping rectangle
|
|
func ClipRect() (x int, y int, w int, h int) {
|
|
return canvas.clipX, canvas.clipY, canvas.clipW, canvas.clipH
|
|
}
|
|
|
|
// DrawHorizontalLine draws the part of the horizontal line that is inside
|
|
// current clipping rectangle
|
|
func DrawHorizontalLine(x, y, w int, r rune) {
|
|
x, y, w, _ = clip(x, y, w, 1)
|
|
if w == 0 {
|
|
return
|
|
}
|
|
|
|
for i := x; i < x+w; i++ {
|
|
putCharUnsafe(i, y, r)
|
|
}
|
|
}
|
|
|
|
// DrawVerticalLine draws the part of the vertical line that is inside current
|
|
// clipping rectangle
|
|
func DrawVerticalLine(x, y, h int, r rune) {
|
|
x, y, _, h = clip(x, y, 1, h)
|
|
if h == 0 {
|
|
return
|
|
}
|
|
|
|
for i := y; i < y+h; i++ {
|
|
putCharUnsafe(x, i, r)
|
|
}
|
|
}
|
|
|
|
// DrawText draws the part of text that is inside the current clipping
|
|
// rectangle. DrawText always paints colorized string. If you want to draw
|
|
// raw string then use DrawRawText function
|
|
func DrawText(x, y int, text string) {
|
|
PushAttributes()
|
|
defer PopAttributes()
|
|
|
|
defText, defBack := TextColor(), BackColor()
|
|
firstdrawn := InClipRect(x, y)
|
|
|
|
parser := NewColorParser(text, defText, defBack)
|
|
elem := parser.NextElement()
|
|
for elem.Type != ElemEndOfText {
|
|
if elem.Type == ElemPrintable {
|
|
SetTextColor(elem.Fg)
|
|
SetBackColor(elem.Bg)
|
|
drawn := PutChar(x, y, elem.Ch)
|
|
x += 1
|
|
if firstdrawn && !drawn {
|
|
break
|
|
}
|
|
}
|
|
|
|
elem = parser.NextElement()
|
|
}
|
|
}
|
|
|
|
// DrawRawText draws the part of text that is inside the current clipping
|
|
// rectangle. DrawRawText always paints string as is - no color changes.
|
|
// If you want to draw string with color changing commands included then
|
|
// use DrawText function
|
|
func DrawRawText(x, y int, text string) {
|
|
cx, cy, cw, ch := ClipRect()
|
|
if x >= cx+cw || y < cy || y >= cy+ch {
|
|
return
|
|
}
|
|
|
|
length := xs.Len(text)
|
|
if x+length < cx {
|
|
return
|
|
}
|
|
|
|
if x < cx {
|
|
text = xs.Slice(text, cx-x, -1)
|
|
length = length - (cx - x)
|
|
x = cx
|
|
}
|
|
text = CutText(text, cw)
|
|
|
|
dx := 0
|
|
for _, ch := range text {
|
|
putCharUnsafe(x+dx, y, ch)
|
|
dx++
|
|
}
|
|
}
|
|
|
|
// DrawTextVertical draws the part of text that is inside the current clipping
|
|
// rectangle. DrawTextVertical always paints colorized string. If you want to draw
|
|
// raw string then use DrawRawTextVertical function
|
|
func DrawTextVertical(x, y int, text string) {
|
|
PushAttributes()
|
|
defer PopAttributes()
|
|
|
|
defText, defBack := TextColor(), BackColor()
|
|
firstdrawn := InClipRect(x, y)
|
|
|
|
parser := NewColorParser(text, defText, defBack)
|
|
elem := parser.NextElement()
|
|
for elem.Type != ElemEndOfText {
|
|
if elem.Type == ElemPrintable {
|
|
SetTextColor(elem.Fg)
|
|
SetBackColor(elem.Bg)
|
|
drawn := PutChar(x, y, elem.Ch)
|
|
y += 1
|
|
if firstdrawn && !drawn {
|
|
break
|
|
}
|
|
}
|
|
|
|
elem = parser.NextElement()
|
|
}
|
|
}
|
|
|
|
// DrawRawTextVertical draws the part of text that is inside the current clipping
|
|
// rectangle. DrawRawTextVertical always paints string as is - no color changes.
|
|
// If you want to draw string with color changing commands included then
|
|
// use DrawTextVertical function
|
|
func DrawRawTextVertical(x, y int, text string) {
|
|
cx, cy, cw, ch := ClipRect()
|
|
if y >= cy+ch || x < cx || x >= cx+cw {
|
|
return
|
|
}
|
|
|
|
length := xs.Len(text)
|
|
if y+length < cy {
|
|
return
|
|
}
|
|
|
|
if y < cy {
|
|
text = xs.Slice(text, cy-y, -1)
|
|
length = length - (cy - y)
|
|
y = cy
|
|
}
|
|
text = CutText(text, ch)
|
|
|
|
dy := 0
|
|
for _, ch := range text {
|
|
putCharUnsafe(x, y+dy, ch)
|
|
dy++
|
|
}
|
|
}
|
|
|
|
// DrawFrame paints the frame without changing area inside it
|
|
func DrawFrame(x, y, w, h int, border BorderStyle) {
|
|
var chars string
|
|
if border == BorderThick {
|
|
chars = SysObject(ObjDoubleBorder)
|
|
} else {
|
|
chars = SysObject(ObjSingleBorder)
|
|
}
|
|
|
|
parts := []rune(chars)
|
|
H, V, UL, UR, DL, DR := parts[0], parts[1], parts[2], parts[3], parts[4], parts[5]
|
|
|
|
if InClipRect(x, y) {
|
|
putCharUnsafe(x, y, UL)
|
|
}
|
|
if InClipRect(x+w-1, y+h-1) {
|
|
putCharUnsafe(x+w-1, y+h-1, DR)
|
|
}
|
|
if InClipRect(x, y+h-1) {
|
|
putCharUnsafe(x, y+h-1, DL)
|
|
}
|
|
if InClipRect(x+w-1, y) {
|
|
putCharUnsafe(x+w-1, y, UR)
|
|
}
|
|
|
|
var xx, yy, ww, hh int
|
|
xx, yy, ww, _ = clip(x+1, y, w-2, 1)
|
|
if ww > 0 {
|
|
DrawHorizontalLine(xx, yy, ww, H)
|
|
}
|
|
xx, yy, ww, _ = clip(x+1, y+h-1, w-2, 1)
|
|
if ww > 0 {
|
|
DrawHorizontalLine(xx, yy, ww, H)
|
|
}
|
|
xx, yy, _, hh = clip(x, y+1, 1, h-2)
|
|
if hh > 0 {
|
|
DrawVerticalLine(xx, yy, hh, V)
|
|
}
|
|
xx, yy, _, hh = clip(x+w-1, y+1, 1, h-2)
|
|
if hh > 0 {
|
|
DrawVerticalLine(xx, yy, hh, V)
|
|
}
|
|
}
|
|
|
|
// DrawScrollBar displays a scrollbar. pos is the position of the thumb.
|
|
// The function detects direction of the scrollbar automatically: if w is greater
|
|
// than h then it draws horizontal scrollbar and vertical otherwise
|
|
func DrawScrollBar(x, y, w, h, pos int) {
|
|
xx, yy, ww, hh := clip(x, y, w, h)
|
|
if ww < 1 || hh < 1 {
|
|
return
|
|
}
|
|
|
|
PushAttributes()
|
|
defer PopAttributes()
|
|
|
|
fg, bg := RealColor(ColorDefault, ColorScrollText), RealColor(ColorDefault, ColorScrollBack)
|
|
// TODO: add thumb styling
|
|
// fgThumb, bgThumb := RealColor(ColorDefault, ColorThumbText), RealColor(ColorDefault, ColorThumbBack)
|
|
SetTextColor(fg)
|
|
SetBackColor(bg)
|
|
|
|
parts := []rune(SysObject(ObjScrollBar))
|
|
chLine, chThumb, chUp, chDown := parts[0], parts[1], parts[2], parts[3]
|
|
chLeft, chRight := parts[4], parts[5]
|
|
|
|
chStart, chEnd := chUp, chDown
|
|
var dx, dy int
|
|
if w > h {
|
|
chStart, chEnd = chLeft, chRight
|
|
dx = w - 1
|
|
dy = 0
|
|
} else {
|
|
dx = 0
|
|
dy = h - 1
|
|
}
|
|
|
|
if InClipRect(x, y) {
|
|
putCharUnsafe(x, y, chStart)
|
|
}
|
|
if InClipRect(x+dx, y+dy) {
|
|
putCharUnsafe(x+dx, y+dy, chEnd)
|
|
}
|
|
if xx == x && w > h {
|
|
xx = x + 1
|
|
ww--
|
|
}
|
|
if yy == y && w < h {
|
|
yy = y + 1
|
|
hh--
|
|
}
|
|
if xx+ww == x+w && w > h {
|
|
ww--
|
|
}
|
|
if yy+hh == y+h && w < h {
|
|
hh--
|
|
}
|
|
|
|
if w > h {
|
|
DrawHorizontalLine(xx, yy, ww, chLine)
|
|
} else {
|
|
DrawVerticalLine(xx, yy, hh, chLine)
|
|
}
|
|
|
|
if pos >= 0 {
|
|
if w > h {
|
|
if pos < w-2 && InClipRect(x+1+pos, y) {
|
|
putCharUnsafe(x+1+pos, y, chThumb)
|
|
}
|
|
} else {
|
|
if pos < h-2 && InClipRect(x, y+1+pos) {
|
|
putCharUnsafe(x, y+1+pos, chThumb)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// FillRect paints the area with r character using the current colors
|
|
func FillRect(x, y, w, h int, r rune) {
|
|
x, y, w, h = clip(x, y, w, h)
|
|
if w < 1 || y < -1 {
|
|
return
|
|
}
|
|
|
|
for yy := y; yy < y+h; yy++ {
|
|
for xx := x; xx < x+w; xx++ {
|
|
putCharUnsafe(xx, yy, r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TextExtent calculates the width and the height of the text
|
|
func TextExtent(text string) (int, int) {
|
|
if text == "" {
|
|
return 0, 0
|
|
}
|
|
parts := strings.Split(text, "\n")
|
|
h := len(parts)
|
|
w := 0
|
|
for _, p := range parts {
|
|
s := UnColorizeText(p)
|
|
l := len(s)
|
|
if l > w {
|
|
w = l
|
|
}
|
|
}
|
|
|
|
return h, w
|
|
}
|