// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package tooey
import (
"github.com/gdamore/tcell/v2"
"github.com/gdamore/tcell/v2/encoding"
)
var scrn tcell.Screen
// Init initializes the screen and sets default styling
func Init() error {
// This says it is deprecated and you only need to import the package
// but autoimport removes unused imports so...
encoding.Register()
s, err := tcell.NewScreen()
if err != nil {
return err
}
if err = s.Init(); err != nil {
return err
}
defaultStyle := tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite)
s.SetStyle(defaultStyle)
s.Clear()
scrn = s
return nil
}
// InitSim is just to support testing
func InitSim() error {
s := tcell.NewSimulationScreen("")
if err := s.Init(); err != nil {
return err
}
defaultStyle := tcell.StyleDefault.Background(tcell.ColorBlack).Foreground(tcell.ColorWhite)
s.SetStyle(defaultStyle)
s.Clear()
scrn = s
return nil
}
// Close is refactor of close
func Close() {
maybePanic := recover()
scrn.Fini()
if maybePanic != nil {
panic(maybePanic)
}
}
// GetRootScreen returns the root screen tooey writes to
func GetRootScreen() tcell.Screen {
return scrn
}
// PollEvents returns a poll of events for the
// root screen
func PollEvents() tcell.Event {
return scrn.PollEvent()
}
// DrawableDimensions is the same as TerminalDimensions -1 to represent visibly drawable space in
// most terminals
func DrawableDimensions() (int, int) {
width, height := TerminalDimensions()
return width - 1, height - 1
}
// Terminal dimensions returns an aggregate dimension for the terminal
// but it often is clipped on the right and buttom
// Use DrawableDimensions to get visible terminal dimensions
func TerminalDimensions() (int, int) {
scrn.Sync()
width, height := scrn.Size()
return width, height
}
// Sync ...
func Sync() {
scrn.Sync()
}
// Clear the global screen
func Clear() {
scrn.Clear()
}
package tooey
import "github.com/gdamore/tcell/v2"
// NewDefaultBorder returns a border with the default character set
func NewDefaultBorder(theme *Theme) *Border {
if theme == nil {
theme = DefaultTheme
}
return &Border{
Enabled: true,
Theme: theme,
Chars: theme.Chars,
Left: true,
Top: true,
Right: true,
Bottom: true,
}
}
// Border contains the definition and drawing logic
// of an element border
type Border struct {
Enabled bool
Theme *Theme
Chars *Chars
Left bool
Top bool
Right bool
Bottom bool
}
// SetChars sets the char map for borders
func (b *Border) SetChars(chars *Chars) {
b.Chars = chars
}
// Draw the borders for the given rect to the given tcell.Screen
func (b *Border) Draw(s tcell.Screen, rect *Rectangle) {
if b.Enabled {
for col := rect.X1(); col <= rect.X2(); col++ {
if b.Top {
s.SetContent(col, rect.Y1(), b.Chars.HLine, nil, b.Theme.Border.Style)
}
if b.Bottom {
s.SetContent(col, rect.Y2(), b.Chars.HLine, nil, b.Theme.Border.Style)
}
}
for row := rect.Y1(); row <= rect.Y2(); row++ {
if b.Left {
s.SetContent(rect.X1(), row, b.Chars.VLine, nil, b.Theme.Border.Style)
}
if b.Right {
s.SetContent(rect.X2(), row, b.Chars.VLine, nil, b.Theme.Border.Style)
}
}
// Patch corners as necessary
if !rect.ZeroSize() {
if b.Top && b.Left {
s.SetContent(rect.X1(), rect.Y1(), b.Chars.ULCorner, nil, b.Theme.Border.Style)
}
if b.Top && b.Right {
s.SetContent(rect.X2(), rect.Y1(), b.Chars.URCorner, nil, b.Theme.Border.Style)
}
if b.Left && b.Bottom {
s.SetContent(rect.X1(), rect.Y2(), b.Chars.LLCorner, nil, b.Theme.Border.Style)
}
if b.Bottom && b.Right {
s.SetContent(rect.X2(), rect.Y2(), b.Chars.LRCorner, nil, b.Theme.Border.Style)
}
}
}
}
package tooey
import (
"github.com/gdamore/tcell/v2"
)
type FlexDirection uint
const (
FlexColumn FlexDirection = iota
FlexColumnReverse // NOT IMPLEMENTED
FlexRow
FlexRowReverse // NOT IMPLEMENTED
)
// NewContainer returns a new default configuration Container
//
// A theme can be passed at this time or nil for defaults or to configure later
func NewContainer(theme *Theme) *Container {
return &Container{
Direction: FlexRow,
Element: *NewElement(theme),
Theme: theme,
}
}
// Container is an element that holds other elements within
type Container struct {
Element
Direction FlexDirection
Children []ContainerChild
Theme *Theme
}
// ContainerChild which represents a member of the flex
// Grow is added up then divided by the total grow
// to find the ratio of space each child will consume
type ContainerChild struct {
Drawable bool
Contents interface{}
Grow float64
}
// NewFlexChild produces a ContainerChild which can be Wrapped
func NewFlexChild(grow float64, i ...interface{}) ContainerChild {
_, ok := i[0].(Drawable)
child := i[0]
if !ok {
child = i
}
return ContainerChild{
Drawable: ok,
Contents: child,
Grow: grow,
}
}
// Wrap embeds the given objects within the container
// using a top-level container that will fill it's available space
func (c *Container) Wrap(children ...interface{}) {
child := ContainerChild{
Drawable: false,
Contents: children,
Grow: 1.0,
}
c.RecursiveWrap(child)
}
// RecursiveWrap wraps a tree of children
func (c *Container) RecursiveWrap(child ContainerChild) {
if child.Drawable {
c.Children = append(c.Children, child)
} else {
children := InterfaceSlice(child.Contents)
for i := 0; i < len(children); i++ {
if children[i] == nil {
continue
}
ch, _ := children[i].(ContainerChild)
c.RecursiveWrap(ch)
}
}
}
// DrawFlexRow will draw the contents as a flexible row
func (c *Container) DrawFlexRow(s tcell.Screen) {
totalFlex := 0.0
for _, child := range c.Children {
totalFlex += child.Grow
}
width := float64(c.GetInnerRect().Dx())
lastPosition := c.InnerX1()
for _, child := range c.Children {
childRatio := child.Grow / totalFlex // mult by available width
childWidth := width * childRatio
drawableChild := child.Contents.(Drawable)
x := lastPosition
y := c.InnerY1()
w := int(childWidth)
h := c.InnerY2()
if x+w > c.GetInnerRect().Dx() {
w--
}
drawableChild.SetRect(x, y, x+w, h)
drawableChild.Lock()
drawableChild.Draw(s)
drawableChild.Unlock()
lastPosition = x + w + 1
}
}
// DrawFlexColumn will draw the contents as a flexible column
func (c *Container) DrawFlexColumn(s tcell.Screen) {
totalFlex := c.calcFlex()
height := float64(c.GetInnerRect().Dy())
lastPosition := c.InnerY1()
for _, child := range c.Children {
childRatio := child.Grow / totalFlex
childHeight := height * childRatio
drawableChild := child.Contents.(Drawable)
x := c.InnerX1()
y := lastPosition
w := c.InnerX2()
h := int(childHeight)
if y+h > c.GetInnerRect().Dy() {
h--
}
drawableChild.SetRect(x, y, w, y+h)
drawableChild.Lock()
drawableChild.Draw(s)
drawableChild.Unlock()
lastPosition = y + h + 1
}
}
// Draw draws the row or col flex and their children
func (c *Container) Draw(s tcell.Screen) {
c.Element.Draw(s)
switch c.Direction {
case FlexColumn:
c.DrawFlexColumn(s)
case FlexColumnReverse:
panic("FlexColumnReverse not yet implemented")
case FlexRow:
c.DrawFlexRow(s)
case FlexRowReverse:
panic("FlexRowReverse not yet implemented")
default:
panic("No flex direction selected")
}
}
// calcFlex just adds up the flex Grows across the container's children
func (c *Container) calcFlex() float64 {
totalFlex := 0.0
for _, child := range c.Children {
totalFlex += child.Grow
}
return totalFlex
}
package tooey
import (
"github.com/gdamore/tcell/v2"
)
// NewElement returns a stable empty Element ready to be modified
func NewElement(theme *Theme) *Element {
if theme == nil {
theme = DefaultTheme
}
e := &Element{
Rectangle: NewRectangle(nil),
Border: NewDefaultBorder(theme),
Title: NewTitle(theme),
Theme: theme,
}
return e
}
// Element is the base drawable struct inherited by most widgets
// Element manages size, positin, inner drawable space
// All other ui elements will inherit
type Element struct {
Rectangle
Theme *Theme
Border *Border
Title *Title
}
// SetTheme will set the theme of the element
func (e *Element) SetTheme(theme *Theme) {
e.Theme = theme
e.Border.Theme = theme
e.Title.Theme = theme
}
// SetBorderCharacters allows you to set the border characters
// for an element without touching the theme
func (e *Element) SetBorderCharacters(chars *Chars) {
e.Border.SetChars(chars)
}
// Draw call on the element to write to the tcell.Screen
func (e *Element) Draw(s tcell.Screen) {
// Draw body of the element
for y := e.Y1(); y < e.Y2(); y++ {
for x := e.X1(); x < e.X2(); x++ {
s.SetContent(x, y, ' ', nil, e.Theme.Element.Style)
}
}
// Draw the border
e.Border.Draw(s, &e.Rectangle)
// Draw the title
e.Title.Draw(s, &e.Rectangle)
}
package tooey
// Padding provides an inset from an outer edge
type Padding struct {
Left int
Top int
Right int
Bottom int
}
// NewDefaultPadding returns a global padding of 1 all around
// which can account for a basic border
func NewDefaultPadding() *Padding {
return &Padding{
Left: 1,
Top: 1,
Right: 1,
Bottom: 1,
}
}
// NewTitlePadding returns a left & right padding of 2 to give the title
// more room to breathe
func NewTitlePadding() *Padding {
return &Padding{
Left: 2,
Top: 1,
Right: 2,
Bottom: 1,
}
}
// NewPadding returns an empty padding
func NewPadding() *Padding {
return &Padding{
Left: 0,
Top: 0,
Right: 0,
Bottom: 0,
}
}
package tooey
import (
"image"
"sync"
)
func NewRectangle(padding *Padding) Rectangle {
if padding == nil {
padding = NewDefaultPadding()
}
return Rectangle{
Padding: padding,
}
}
// Rectangle is a convenience extension of the stdlib
// image.Rectangle
type Rectangle struct {
image.Rectangle
// Padding should only affect objects within a rectangle
// not the outer bounds of the rectangle
Padding *Padding
sync.Mutex
}
// SetRect defines the boundaries of the rectangle
func (r *Rectangle) SetRect(x1 int, y1 int, x2 int, y2 int) {
r.Rectangle = image.Rect(x1, y1, x2, y2)
}
// GetRect returns the current underlying image.Rectangle
func (r *Rectangle) GetRect() image.Rectangle {
return r.Rectangle
}
// GetInnerRect returns the bounds of the inner padded rectangle
func (r *Rectangle) GetInnerRect() image.Rectangle {
return image.Rect(r.InnerX1(), r.InnerY1(), r.InnerX2(), r.InnerY2())
}
// X1 returns the rectangle's Min.X point
func (r *Rectangle) X1() int {
return r.Min.X
}
// X2 returns the rectangle's Max.X point
func (r *Rectangle) X2() int {
return r.Max.X
}
// Y1 returns the rectangle's Min.Y point
func (r *Rectangle) Y1() int {
return r.Min.Y
}
// Y2 returns the rectangle's Max.Y point
func (r *Rectangle) Y2() int {
return r.Max.Y
}
// InnerX1 returns X1 with padding
func (r *Rectangle) InnerX1() int {
return r.X1() + r.Padding.Left
}
// InnerX2 returns X2 with padding
func (r *Rectangle) InnerX2() int {
return r.X2() - r.Padding.Right
}
// InnerY1 returns Y1 with padding
func (r *Rectangle) InnerY1() int {
return r.Y1() + r.Padding.Top
}
// InnerY2 returns Y2 with padding
func (r *Rectangle) InnerY2() int {
return r.Y2() - r.Padding.Bottom
}
// DrawableWidth returns the max width of the rectangle minus padding
func (r *Rectangle) DrawableWidth() int {
return r.X2() - r.X1() - r.Padding.Left - r.Padding.Right
}
// DrawableHeight returns the max height of the rectangle minux padding
func (r *Rectangle) DrawableHeight() int {
return r.Y2() - r.Y1() - r.Padding.Top - r.Padding.Bottom
}
// ZeroSize returns true if the rectangle has no size
func (r *Rectangle) ZeroSize() bool {
if r.Y1() == r.Y2() && r.X1() == r.X2() {
return true
} else {
return false
}
}
// Copyright 2017 Zack Guo <zack.y.guo@gmail.com>. All rights reserved.
// Use of this source code is governed by a MIT license that can
// be found in the LICENSE file.
package tooey
import (
"image"
"sync"
"github.com/gdamore/tcell/v2"
)
// Drawable represents a renderable item
type Drawable interface {
GetRect() image.Rectangle
GetInnerRect() image.Rectangle
// SetRect x1, y1, x2, y2
SetRect(int, int, int, int)
Draw(tcell.Screen)
SetTheme(*Theme)
sync.Locker
}
// Render locks and draws the passed Drawables
func Render(items ...Drawable) {
for _, item := range items {
item.Lock()
item.Draw(scrn)
item.Unlock()
}
scrn.Show()
}
package tooey
import (
"github.com/gdamore/tcell/v2"
)
const (
DefaultULCorner = tcell.RuneULCorner
DefaultURCorner = tcell.RuneURCorner
DefaultLLCorner = tcell.RuneLLCorner
DefaultLRCorner = tcell.RuneLRCorner
DefaultHLine = tcell.RuneHLine
DefaultVLine = tcell.RuneVLine
)
// Chars is to enable theming border characters
type Chars struct {
HLine rune
VLine rune
ULCorner rune
URCorner rune
LLCorner rune
LRCorner rune
}
// NewDefaultChars returns the default character set
// for borders
func NewDefaultChars() *Chars {
return &Chars{
HLine: DefaultHLine,
VLine: DefaultVLine,
ULCorner: DefaultULCorner,
URCorner: DefaultURCorner,
LLCorner: DefaultLLCorner,
LRCorner: DefaultLRCorner,
}
}
var DefaultStylized = &Chars{
HLine: DefaultHLine,
VLine: DefaultVLine,
ULCorner: '╒',
URCorner: DefaultURCorner,
LLCorner: DefaultLLCorner,
LRCorner: DefaultLRCorner,
}
// RoundedBarBorderChars is like the Default border but the
// corners are rounded
var RoundedBarBorderChars = &Chars{
HLine: DefaultHLine,
VLine: DefaultVLine,
ULCorner: '╭',
URCorner: '╮',
LLCorner: '╰',
LRCorner: '╯',
}
// CornersOnlyBorderChars will only render corners
// with triangle ascii
var CornersOnlyBorderChars = &Chars{
HLine: ' ',
VLine: ' ',
ULCorner: '◤',
URCorner: '◥',
LLCorner: '◣',
LRCorner: '◢',
}
// DoubleBarBorder is like the Default border but with
// double bars for more dramatic effect // rune(2550)
var DoubleBarBorderChars = &Chars{
HLine: '═',
VLine: '║',
ULCorner: '╔',
URCorner: '╗',
LLCorner: '╚',
LRCorner: '╝',
}
/*
Theme is a bundle of Styles for different elements, subelements, and widgets
If Inherit is true in the theme, then when a theme is set it will propagate the Default
down to the others
*/
type Theme struct {
Default Style
Element Style
Border Style
Title Style
Text Style
Chars *Chars
}
// DefaultTheme is a basic white foreground and black background for all elements
var DefaultTheme = &Theme{
Default: StyleDefault,
Element: StyleClear,
Border: StyleDefault,
Title: StyleDefault,
Text: StyleDefault,
Chars: NewDefaultChars(),
}
package tooey
import (
"fmt"
"strings"
"github.com/gdamore/tcell/v2"
)
// NewTitle returns a basic empty title
func NewTitle(theme *Theme) *Title {
if theme == nil {
theme = DefaultTheme
}
return &Title{
Padding: NewTitlePadding(),
Theme: theme,
}
}
// Title represents a rendered title in the header of an Element
type Title struct {
Content string
Padding *Padding
Theme *Theme
}
// Draw the title
func (t *Title) Draw(s tcell.Screen, rect *Rectangle) {
if len(t.Content) == 0 {
return
}
w := rect.DrawableWidth()
//draw := TrimString(t.Content, w-1)
row := rect.Y1()
col := rect.X1() + rect.Padding.Left
leftPad := ""
if t.Padding.Left > 0 {
leftPad = strings.Repeat(" ", t.Padding.Left)
}
rightPad := ""
if t.Padding.Right > 0 {
rightPad = strings.Repeat(" ", t.Padding.Right)
}
draw := TrimString(t.Content, w-len(leftPad)-len(rightPad)-1)
draw = fmt.Sprintf("%s%s%s", leftPad, draw, rightPad)
for _, r := range draw {
s.SetContent(col, row, r, nil, t.Theme.Title.Style)
col++
if col+1 > rect.X2()-rect.Padding.Right {
// add ... at some point
break
}
}
}
package tooey
import (
"fmt"
"math"
"reflect"
"unicode"
"github.com/davecgh/go-spew/spew"
rw "github.com/mattn/go-runewidth"
)
// InterfaceSlice takes an []interface{} represented as an interface{} and converts it
// https://stackoverflow.com/questions/12753805/type-converting-slices-of-interfaces-in-go
func InterfaceSlice(slice interface{}) []interface{} {
s := reflect.ValueOf(slice)
if s.Kind() != reflect.Slice {
panic(fmt.Sprintf("InterfaceSlice() given a non-slice type: %v [%v]", s.Kind(), spew.Sdump(slice)))
}
ret := make([]interface{}, s.Len())
for i := 0; i < s.Len(); i++ {
ret[i] = s.Index(i).Interface()
}
return ret
}
// TrimString trims a string to a max length and adds '…' to the end if it was trimmed.
func TrimString(s string, w int) string {
if w <= 0 {
return ""
}
if rw.StringWidth(s) > w {
return rw.Truncate(s, w, string(ELLIPSES))
}
return s
}
// ShiftRuneSliceRight takes a []rune and shifts everything right, wrapping the right
// most character back to the left most position
func ShiftRuneSliceRight(slice []rune) []rune {
if len(slice) < 1 {
return slice
}
contentLength := len(slice)
newSlice := make([]rune, 0)
newSlice = append(newSlice, slice[contentLength-1])
for i, r := range slice {
if i == contentLength-1 {
break
}
newSlice = append(newSlice, r)
}
return newSlice
}
// ShiftRuneSliceLeft takes a []rune and shifts everything left, wrapping the
// right most character back to the right most position
func ShiftRuneSliceLeft(slice []rune) []rune {
if len(slice) < 1 {
return slice
}
newSlice := append(slice[1:], slice[0])
return newSlice
}
// ShiftRuneWhitespaceToLeft takes a []rune and moves all whitespace to the left
func ShiftRuneWhitespaceToLeft(slice []rune) []rune {
if len(slice) < 1 {
return slice
}
contentLength := len(slice)
newSlice := slice
if !ContainsNonWhitespace(slice) {
return slice
}
for {
if unicode.IsSpace(newSlice[contentLength-1]) {
newSlice = ShiftRuneSliceRight(newSlice)
} else {
break
}
}
return newSlice
}
// ShiftRuneWhitespaceToRight takes a []rune and moves all whitespace to the right
func ShiftRuneWhitespaceToRight(slice []rune) []rune {
if !ContainsNonWhitespace(slice) || len(slice) < 1 {
return slice
}
newSlice := slice
for {
if unicode.IsSpace(newSlice[0]) {
newSlice = ShiftRuneSliceLeft(newSlice)
} else {
break
}
}
return newSlice
}
/*
SpreadWhitespaceAcrossSliceInterior takes a []rune
and attempts to distribute it's whitespace across the
width of the slice interior but not at the outside edges
Take the string "abc def gh ij "
"abc_def___gh_ij___" would try to make "abc___def__gh___ij"
"__abc_def___gh_ij" would try to make "abc__def__gh___ij"
*/
func SpreadWhitespaceAcrossSliceInterior(slice []rune) []rune {
wordCount := CountWordsInRuneSlice(slice)
if !ContainsNonWhitespace(slice) || len(slice) < 1 || wordCount < 2 {
return slice
}
// Shift all right whitespace to the left
newSlice := ShiftRuneWhitespaceToLeft(slice)
// Move left whitespace inwards
newSlice = NormalizeLeftWhitespace(newSlice)
//newSlice = NormalizeLeftWhitespace(newSlice)
return newSlice
}
// CheckWhichPositionHasFewest returns index of the position with the lowest
// count
func CheckWhichPositionHasFewest(positions []int) int {
lowestIndex := 0
lowestCount, _ := GetMaxIntFromSlice(positions)
if lowestCount == 0 {
// TODO: figure out what to do here, tighten this up
// I mean it works for now but will we find a condition when it doesn't
return 0
}
for i, count := range positions {
if count < lowestCount {
lowestCount = count
lowestIndex = i
}
}
return lowestIndex
}
func NormalizeLeftWhitespace(slice []rune) []rune {
wordCount := CountWordsInRuneSlice(slice)
if !ContainsNonWhitespace(slice) || len(slice) < 1 || wordCount < 2 {
return slice
}
newSlice := slice
position := make([]int, wordCount)
OUTER:
for {
insideWord := false
//interior := false
if unicode.IsSpace(newSlice[0]) {
wordIndex := 0
for i, r := range newSlice {
bestIndex := CheckWhichPositionHasFewest(position)
// range through the runes and detect words
// place left spaces throughout the rune slice between words interior
if !unicode.IsSpace(r) {
if !insideWord {
insideWord = true
}
} else {
if insideWord {
insideWord = false
wordIndex++
}
}
if i == len(newSlice) {
// if we reached the end without finding a spot
// give up continue
break OUTER
}
if bestIndex == wordIndex {
if unicode.IsSpace(r) {
newSlice = newSlice[1:]
newSlice = append(newSlice[:i+1], newSlice[i:]...)
newSlice[i] = rune(' ')
position[wordIndex]++
break
}
}
}
} else {
break
}
}
return newSlice
}
// CountWordsInRuneSlice counts how many blocks of non-whitespace characters
// are inside the rune
//
// This can be used to traverse whitespace between the left and right boundaries
// use count-1 for right bound to prevent traversing outside of the word bounds
func CountWordsInRuneSlice(slice []rune) int {
count := 0
insideWord := false
for _, r := range slice {
if !unicode.IsSpace(r) {
if !insideWord {
insideWord = true
count++
}
} else {
if insideWord {
insideWord = false
}
}
}
return count
}
// CountWhiteSpace returns a count of the number of space characters in a []rune
func CountWhiteSpace(slice []rune) int {
count := 0
for _, r := range slice {
if unicode.IsSpace(r) {
count++
}
}
return count
}
// ContainsNonWhitespace returns true if a given rune slice has any non-whitespace
// runes
func ContainsNonWhitespace(slice []rune) bool {
for _, r := range slice {
if !unicode.IsSpace(r) {
return true
}
}
return false
}
func SelectColor(colors []Color, index int) Color {
return colors[index%len(colors)]
}
func SelectStyle(styles []Style, index int) Style {
return styles[index%len(styles)]
}
// Math ------------------------------------------------------------------------
func SumIntSlice(slice []int) int {
sum := 0
for _, val := range slice {
sum += val
}
return sum
}
func SumFloat64Slice(data []float64) float64 {
sum := 0.0
for _, v := range data {
sum += v
}
return sum
}
func GetMaxIntFromSlice(slice []int) (int, error) {
if len(slice) == 0 {
return 0, fmt.Errorf("cannot get max value from empty slice")
}
var max int
for _, val := range slice {
if val > max {
max = val
}
}
return max, nil
}
func GetMaxFloat64FromSlice(slice []float64) (float64, error) {
if len(slice) == 0 {
return 0, fmt.Errorf("cannot get max value from empty slice")
}
var max float64
for _, val := range slice {
if val > max {
max = val
}
}
return max, nil
}
func GetMaxFloat64From2dSlice(slices [][]float64) (float64, error) {
if len(slices) == 0 {
return 0, fmt.Errorf("cannot get max value from empty slice")
}
var max float64
for _, slice := range slices {
for _, val := range slice {
if val > max {
max = val
}
}
}
return max, nil
}
func RoundFloat64(x float64) float64 {
return math.Floor(x + 0.5)
}
func FloorFloat64(x float64) float64 {
return math.Floor(x)
}
func AbsInt(x int) int {
if x >= 0 {
return x
}
return -x
}
func MinFloat64(x, y float64) float64 {
if x < y {
return x
}
return y
}
func MaxFloat64(x, y float64) float64 {
if x > y {
return x
}
return y
}
func MaxInt(x, y int) int {
if x > y {
return x
}
return y
}
func MinInt(x, y int) int {
if x < y {
return x
}
return y
}
// TCell implementation of paragraph
//
// Charles <asciifaceman> Corbett 2023
//
//
package widgets
import (
"fmt"
"image"
"unicode"
"github.com/asciifaceman/tooey"
"github.com/gdamore/tcell/v2"
"github.com/mattn/go-runewidth"
)
// NewText returns a basic empty *Text
func NewText(theme *tooey.Theme) *Text {
if theme == nil {
theme = tooey.DefaultTheme
}
return &Text{
Element: *tooey.NewElement(theme),
Wrap: true,
Theme: theme,
}
}
// Text represents a simple styled block of text with wrapping
// paragraph capabilities
type Text struct {
tooey.Element
Content string
Theme *tooey.Theme
Wrap bool
}
// SetTheme sets the theme for the Text and it's underlying Element
func (t *Text) SetTheme(theme *tooey.Theme) {
t.Theme = theme
t.Element.SetTheme(theme)
}
// DrawableRect returns a set of ints representing
// the calculated drawable space based on all padding
// and positioning
//
// Returns image.Rectangle
func (t *Text) DrawableRect() image.Rectangle {
// Calculate rect for drawable text
minX1 := t.InnerX1() + t.Padding.Left
minX2 := t.InnerX2() // - t.Padding.Right
minY1 := t.InnerY1() + t.Padding.Top
minY2 := t.InnerY2() // - t.Padding.Bottom
contentRect := image.Rect(minX1, minY1, minX2, minY2)
return contentRect
}
// Draw draws the text to its given Rect taking into account
// left and right padding, wrapping, etc
func (t *Text) Draw(s tcell.Screen) {
t.Element.Draw(s)
sw, sh := tooey.DrawableDimensions()
contentCellLength := len([]rune(t.Content))
if contentCellLength == 0 {
// If the string is empty let's exit
// early and save cycles
return
}
contentRect := t.DrawableRect()
if t.Wrap {
alignedContent := make([][]rune, sh)
for y := 0; y < sh; y++ {
alignedContent[y] = make([]rune, sw)
}
switch t.Theme.Text.Align {
case tooey.AlignLeft:
alignedContent = t.ProcessLeftAlignment(contentRect)
case tooey.AlignCenter:
alignedContent = t.ProcessCenterAlignment(contentRect)
case tooey.AlignRight:
alignedContent = t.ProcessRightAlignment(contentRect)
case tooey.AlignFull:
alignedContent = t.ProcessFullAlignment(contentRect)
}
// aligned content is a precalculated grid so
// we don't need to worry about placement logic here
for y := 0; y < contentRect.Dy(); y++ {
for x := 0; x < contentRect.Dx(); x++ {
draw := alignedContent[y][x]
// handle zero width chars
var comb []rune
w := runewidth.RuneWidth(draw)
if w == 0 {
comb = []rune{draw}
draw = ' '
w = 1
}
s.SetContent(x+contentRect.Min.X, y+contentRect.Min.Y, draw, comb, t.Theme.Text.Style)
}
}
} else {
processed := t.ProcessUnwrapped(contentRect.Min.X, sw-contentRect.Min.X)
y := contentRect.Min.Y
for x := contentRect.Min.X; x < sw; x++ {
draw := processed[x]
// handle zero width chars
var comb []rune
w := runewidth.RuneWidth(draw)
if w == 0 {
comb = []rune{draw}
draw = ' '
w = 1
}
s.SetContent(x, y, draw, comb, t.Theme.Text.Style)
}
}
}
// FillRuneBuffer prefills a buffer of size image.Rectangle for
// filling drawable text areas
//
// This is unfortunately destructive and prevents things like overlapping
// which may or may not be desirable but it was the way I could think of
// to prepare a buffer for managing alignment of text within a text space
func (t *Text) FillRuneBuffer(r image.Rectangle) [][]rune {
buf2 := make([][]rune, r.Dy())
for y := 0; y < r.Dy(); y++ {
buf2[y] = make([]rune, r.Dx())
}
for y := 0; y < r.Dy(); y++ {
for x := 0; x < r.Dx(); x++ {
buf2[y][x] = rune(' ')
}
}
return buf2
}
// ProcessLeftAlignment accepts a space of image.Rectangle and processes
// Text.Content within that space to be left justified, and attempt to
// wrap word-aware when possible. If the word will fit on a line by itself
// it will wrap, otherwise it will wrap mid-word.
//
// returns [y][x]rune
func (t *Text) ProcessLeftAlignment(r image.Rectangle) [][]rune {
content := []rune(t.Content)
contentLength := len(content)
processed := t.FillRuneBuffer(r)
// if the entire content's length is less than the width of
// the image.Rectangle just spew it really quick and return early
if contentLength < r.Dx() {
for x := 0; x < contentLength; x++ {
processed[0][x] = content[x]
}
return processed
}
offset := 0
var previousRune rune
for y := 0; y < r.Dy(); y++ {
for x := 0; x < r.Dx(); x++ {
// Attempt to wrap early if a word won't fit
// but only if it fits in the drawable width to start with
if !unicode.IsSpace(content[offset]) {
if unicode.IsSpace(previousRune) {
var word []rune
word = append(word, content[offset])
for i := offset + 1; i < contentLength; i++ {
if unicode.IsSpace(content[i]) {
break
}
word = append(word, content[i])
}
if len(word) > (r.Dx()-x) && len(word) < (r.Dx()) {
break
}
}
}
processed[y][x] = content[offset]
previousRune = content[offset]
offset++
if offset == contentLength {
return processed
}
}
block := processed[y]
block = tooey.ShiftRuneWhitespaceToRight(block)
processed[y] = block
}
return processed
}
// ProcessRightAlignment accepts a space of image.Rectangle and processes
// Text.Content within that space to be right justified and attempt to
// wrap word-aware when possible. If the word will fit on a line by itself
// it will wrap, otherwise it will wrap mid-word
//
// returns [y][x]rune
func (t *Text) ProcessRightAlignment(r image.Rectangle) [][]rune {
leftAligned := t.ProcessLeftAlignment(r)
for y := 0; y < r.Dy(); y++ {
block := leftAligned[y]
right := tooey.ShiftRuneWhitespaceToLeft(block)
leftAligned[y] = right
}
return leftAligned
}
// ProcessCenterAlignment ...
func (t *Text) ProcessCenterAlignment(r image.Rectangle) [][]rune {
leftAligned := t.ProcessLeftAlignment(r)
for y := 0; y < r.Dy(); y++ {
block := leftAligned[y]
full := tooey.SpreadWhitespaceAcrossSliceInterior(block)
leftAligned[y] = full
}
return leftAligned
}
// ProcessFullAlignment ...
func (t *Text) ProcessFullAlignment(r image.Rectangle) [][]rune {
leftAligned := t.ProcessLeftAlignment(r)
for y := 0; y < r.Dy(); y++ {
block := leftAligned[y]
full := tooey.SpreadWhitespaceAcrossSliceInterior(block)
leftAligned[y] = full
}
return leftAligned
}
// Draw ...
func (t *Text) Draw2(s tcell.Screen) {
t.Element.Draw(s)
row := t.Rectangle.Min.Y + t.Padding.Top
col := t.Rectangle.Min.X + t.Padding.Left
wrapped := "&"
previousRune := rune(wrapped[0])
var currentWord string
for i, r := range t.Content {
// Lookahead
if !unicode.IsSpace(r) {
if unicode.IsSpace(previousRune) {
INNER:
for ix := i; i < len(t.Content); i++ {
runeIter := rune(t.Content[ix])
if unicode.IsSpace(runeIter) {
break INNER
}
currentWord = fmt.Sprintf("%s%v", currentWord, runeIter)
}
// look ahead to find word
if len([]rune(currentWord)) > (t.Rectangle.X2()-t.Padding.Right)-col {
wrapped := "&"
previousRune = rune(wrapped[0])
row++
col = t.Rectangle.X1() + t.Padding.Left
continue
}
}
} else {
// if alignment is full make sure we aren't starting a new line on a space
if t.Theme.Text.Align == tooey.AlignFull {
previousRune = r
continue
}
// see if I am at the beginning of the width and
// skip spaces
}
// if character is a newline advance row and continue
if fmt.Sprintf("%v", r) == "\n" {
previousRune = r
row++
col = t.Rectangle.X1() + t.Padding.Left
continue
}
// Write the cell
s.SetContent(col, row, r, nil, t.Theme.Title.Style)
col++
previousRune = r
if t.Wrap {
if col > t.Rectangle.Max.X-t.Padding.Right {
// Wordwrap
row++
col = t.Rectangle.Min.X + t.Padding.Left
}
if row > t.Rectangle.Max.Y {
// gobble the remainder
break
}
}
}
}
// ProcessUnwrapped needs rewritten...
//
// returns [y][x]rune
func (t *Text) ProcessUnwrapped(x int, maxWidth int) map[int]rune {
draw := t.Content
if runewidth.StringWidth(t.Content) > maxWidth {
draw = runewidth.Truncate(t.Content, maxWidth, string(tooey.ELLIPSES))
}
processed := map[int]rune{}
col := x
for _, r := range draw {
processed[x] = r
col++
}
return processed
}