more views
This commit is contained in:
parent
d335211770
commit
dfa00f5fe3
@ -5,8 +5,31 @@ import (
|
||||
)
|
||||
|
||||
func drawBuffer(scr tcell.Screen, buf *ViewBuffer) {
|
||||
buf.ForEach(func(x, y int, rn Rune) {
|
||||
// buf.ForEach(func(x, y int, rn Rune) {
|
||||
// scr.SetContent(x, y, rn.Rn, nil, rn.Style)
|
||||
// })
|
||||
|
||||
buf.ForEachLine(func(y int, content []Rune) {
|
||||
for x := 0; x < buf.Width(); x++ {
|
||||
rn := content[x]
|
||||
if rn.Rn >= '─' && rn.Rn <= '╿' {
|
||||
scr.SetContent(x, y, rn.Rn, []rune{content[x+1].Rn}, rn.Style)
|
||||
x++
|
||||
} else {
|
||||
scr.SetContent(x, y, rn.Rn, nil, rn.Style)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
scr.Show()
|
||||
}
|
||||
|
||||
func truncateBuffer(buf *ViewBuffer, w, h int) *ViewBuffer {
|
||||
if w < 0 {
|
||||
w = buf.Width()
|
||||
}
|
||||
if h < 0 {
|
||||
h = buf.Height()
|
||||
}
|
||||
return buf.Sub(0, 0, w, h)
|
||||
}
|
||||
|
@ -1,5 +1,8 @@
|
||||
package tui
|
||||
|
||||
type Events interface {
|
||||
|
||||
// KeyPressed is called every time a key or key-combination is pressed.
|
||||
// If KeyPressed returns true, the event will not be passed onto child views
|
||||
OnKeyPressed(event *KeyEvent) (consumed bool)
|
||||
}
|
||||
|
4
go.mod
4
go.mod
@ -3,8 +3,9 @@ module git.tordarus.net/Tordarus/tui
|
||||
go 1.18
|
||||
|
||||
require (
|
||||
git.tordarus.net/Tordarus/buf2d v1.1.0
|
||||
git.tordarus.net/Tordarus/buf2d v1.1.1
|
||||
github.com/gdamore/tcell v1.4.0
|
||||
golang.org/x/text v0.3.7
|
||||
)
|
||||
|
||||
require (
|
||||
@ -12,5 +13,4 @@ require (
|
||||
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.7 // indirect
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect
|
||||
golang.org/x/text v0.3.0 // indirect
|
||||
)
|
||||
|
7
go.sum
7
go.sum
@ -1,5 +1,5 @@
|
||||
git.tordarus.net/Tordarus/buf2d v1.1.0 h1:rIZjD7yeX5XK2D1h75ET5Og0u/NQF3eVonnC5aaqVkQ=
|
||||
git.tordarus.net/Tordarus/buf2d v1.1.0/go.mod h1:XXPpS8nQK0gUI0ki7ftV/qlprsGCRWFVSD4ybvDuUL8=
|
||||
git.tordarus.net/Tordarus/buf2d v1.1.1 h1:rYvQ2YveqogCoKy5andQxuORPusWbUhpnqJhzVkTlRs=
|
||||
git.tordarus.net/Tordarus/buf2d v1.1.1/go.mod h1:XXPpS8nQK0gUI0ki7ftV/qlprsGCRWFVSD4ybvDuUL8=
|
||||
github.com/gdamore/encoding v1.0.0 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU=
|
||||
@ -10,5 +10,6 @@ github.com/mattn/go-runewidth v0.0.7 h1:Ei8KR0497xHyKJPAv59M1dkC+rOZCMBJ+t3fZ+tw
|
||||
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
|
||||
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
|
27
screen.go
27
screen.go
@ -10,8 +10,12 @@ import (
|
||||
|
||||
type Screen struct {
|
||||
scr tcell.Screen
|
||||
buf *ViewBuffer
|
||||
stopCh chan error
|
||||
Root View
|
||||
|
||||
// KeyPressed is called every time a key or key-combination is pressed.
|
||||
KeyPressed func(event *KeyEvent) (consumed bool)
|
||||
}
|
||||
|
||||
func NewScreen(root View) (*Screen, error) {
|
||||
@ -37,10 +41,7 @@ func (s *Screen) eventloop() {
|
||||
case *tcell.EventResize:
|
||||
go s.Redraw()
|
||||
case *tcell.EventKey:
|
||||
go func() {
|
||||
s.Root.OnKeyPressed(event)
|
||||
s.Redraw()
|
||||
}()
|
||||
go s.onKeyPressed(event)
|
||||
default:
|
||||
s.StopWithError(errors.New(fmt.Sprintf("%#v", event)))
|
||||
}
|
||||
@ -67,9 +68,21 @@ func (s *Screen) StopWithError(err error) {
|
||||
s.stopCh <- err
|
||||
}
|
||||
|
||||
func (s *Screen) onKeyPressed(event *KeyEvent) {
|
||||
if s.KeyPressed == nil || !s.KeyPressed(event) {
|
||||
s.Root.OnKeyPressed(event)
|
||||
}
|
||||
s.Redraw()
|
||||
}
|
||||
|
||||
func (s *Screen) Redraw() {
|
||||
w, h := s.scr.Size()
|
||||
buf := buf2d.NewBuffer(w, h, DefaultRune)
|
||||
s.Root.Draw(buf)
|
||||
drawBuffer(s.scr, buf)
|
||||
|
||||
if s.buf == nil || s.buf.Width() != w || s.buf.Height() != h {
|
||||
s.buf = buf2d.NewBuffer(w, h, DefaultRune)
|
||||
}
|
||||
|
||||
rw, rh := s.Root.Layout()
|
||||
s.Root.Draw(truncateBuffer(s.buf, rw, rh))
|
||||
drawBuffer(s.scr, s.buf)
|
||||
}
|
||||
|
@ -10,21 +10,90 @@ import (
|
||||
"github.com/gdamore/tcell"
|
||||
)
|
||||
|
||||
func TestScreen(t *testing.T) {
|
||||
textView := views.NewTextView("hello world")
|
||||
eventView := views.NewEventView(textView)
|
||||
screen, err := tui.NewScreen(eventView)
|
||||
func TestFlowGroup(t *testing.T) {
|
||||
textView := views.NewTextView("hello world!")
|
||||
textView.SetStyle(tui.StyleDefault.Background(tcell.ColorRed).Foreground(tcell.ColorBlack))
|
||||
|
||||
marginView := views.NewMarginView(textView)
|
||||
marginView.SetMargin(3, 1, 1, 0)
|
||||
|
||||
//borderView := views.NewBorderView(textView)
|
||||
|
||||
textView2 := views.NewTextView("Hi!")
|
||||
textView2.SetStyle(tui.StyleDefault.Background(tcell.ColorBlue).Foreground(tcell.ColorYellow))
|
||||
|
||||
growView := views.NewGrowView()
|
||||
growView.SetStyle(tui.StyleDefault.Background(tcell.ColorGreen))
|
||||
|
||||
growView2 := views.NewGrowView()
|
||||
growView2.SetStyle(tui.StyleDefault.Background(tcell.ColorYellow))
|
||||
|
||||
flowGroup := views.NewFlowGroup(tui.Vertical)
|
||||
flowGroup.AppendViews(marginView, growView, textView2)
|
||||
|
||||
constrainView := views.NewConstrainView(flowGroup)
|
||||
constrainView.SetStyle(tui.StyleDefault.Background(tcell.ColorPurple))
|
||||
constrainView.Constrain(-1, -1)
|
||||
|
||||
screen, err := tui.NewScreen(constrainView)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
eventView.KeyPressed = func(event *tui.KeyEvent) (consumed bool) {
|
||||
screen.KeyPressed = func(event *tui.KeyEvent) (consumed bool) {
|
||||
textView.Text = event.When().String()
|
||||
|
||||
if event.Key() == tcell.KeyCtrlC {
|
||||
screen.StopWithError(errors.New(fmt.Sprintf("key: %#v | rune: %s", event.Key(), string(event.Rune()))))
|
||||
}
|
||||
|
||||
//textView.Text = event.When().String()
|
||||
return true
|
||||
}
|
||||
|
||||
err = screen.Start()
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
func TestBorderGroup(t *testing.T) {
|
||||
topView := views.NewConstrainView(nil)
|
||||
topView.SetStyle(tui.StyleDefault.Background(tcell.ColorBlue))
|
||||
topView.Constrain(10, 10)
|
||||
|
||||
bottomView := views.NewConstrainView(nil)
|
||||
bottomView.SetStyle(tui.StyleDefault.Background(tcell.ColorRed))
|
||||
bottomView.Constrain(10, 10)
|
||||
|
||||
leftView := views.NewConstrainView(nil)
|
||||
leftView.SetStyle(tui.StyleDefault.Background(tcell.ColorYellow))
|
||||
leftView.Constrain(10, 10)
|
||||
|
||||
rightView := views.NewConstrainView(nil)
|
||||
rightView.SetStyle(tui.StyleDefault.Background(tcell.ColorGreen))
|
||||
rightView.Constrain(10, 10)
|
||||
|
||||
centerView := views.NewConstrainView(nil)
|
||||
centerView.SetStyle(tui.StyleDefault.Background(tcell.ColorPurple))
|
||||
centerView.Constrain(10, 10)
|
||||
|
||||
borderGroup := views.NewBorderGroup()
|
||||
borderGroup.SetStyle(tui.StyleDefault.Background(tcell.ColorPurple))
|
||||
borderGroup.SetView(topView, views.Top)
|
||||
borderGroup.SetView(bottomView, views.Bottom)
|
||||
borderGroup.SetView(leftView, views.Left)
|
||||
borderGroup.SetView(rightView, views.Right)
|
||||
borderGroup.SetView(centerView, views.Center)
|
||||
|
||||
screen, err := tui.NewScreen(borderGroup)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
screen.KeyPressed = func(event *tui.KeyEvent) (consumed bool) {
|
||||
if event.Key() == tcell.KeyCtrlC {
|
||||
screen.StopWithError(errors.New(fmt.Sprintf("key: %#v | rune: %s", event.Key(), string(event.Rune()))))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
|
29
types.go
29
types.go
@ -11,3 +11,32 @@ type Style = tcell.Style
|
||||
type Color = tcell.Color
|
||||
|
||||
var StyleDefault Style = tcell.StyleDefault
|
||||
|
||||
type Point struct {
|
||||
X, Y int
|
||||
}
|
||||
|
||||
type Size struct {
|
||||
Width, Height int
|
||||
}
|
||||
|
||||
type Dimension struct {
|
||||
Point
|
||||
Size
|
||||
}
|
||||
|
||||
type Orientation uint8
|
||||
|
||||
const (
|
||||
Horizontal Orientation = iota
|
||||
Vertical
|
||||
)
|
||||
|
||||
type Side uint8
|
||||
|
||||
const (
|
||||
Top Side = iota
|
||||
Bottom
|
||||
Left
|
||||
Right
|
||||
)
|
||||
|
66
utils.go
66
utils.go
@ -2,30 +2,88 @@ package tui
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/width"
|
||||
)
|
||||
|
||||
// WriteString writes a whole string to the buffer at position (x,y)
|
||||
// no word wrap is applied at all. If the string does not fit, it will be truncated
|
||||
func WriteString(b *ViewBuffer, str string, style Style, x, y int) {
|
||||
func WriteString(b *ViewBuffer, str string, style Style, x, y int) (width int) {
|
||||
dx := x
|
||||
for _, r := range str {
|
||||
if dx >= b.Width() {
|
||||
return
|
||||
}
|
||||
|
||||
b.Set(dx, y, Rune{r, style})
|
||||
dx++
|
||||
dx += runeWidth(r)
|
||||
}
|
||||
return dx - x
|
||||
}
|
||||
|
||||
// WriteMultiLineString writes a multi-line string to the buffer at position (x,y)
|
||||
// no word wrap is applied at all. If a line does not fit horizontally, it will be truncated
|
||||
// All lines which do not fit vertically will be truncated as well
|
||||
func WriteMultiLineString(b *ViewBuffer, str string, style Style, x, y int) {
|
||||
func WriteMultiLineString(b *ViewBuffer, str string, style Style, x, y int) (maxLineWidth, lineCount int) {
|
||||
lines := strings.Split(str, "\n")
|
||||
for dy, line := range lines {
|
||||
if dy >= b.Height() {
|
||||
return
|
||||
}
|
||||
WriteString(b, line, style, x, y+dy)
|
||||
lineWidth := WriteString(b, line, style, x, y+dy)
|
||||
maxLineWidth = max(maxLineWidth, lineWidth)
|
||||
}
|
||||
return maxLineWidth, len(lines)
|
||||
}
|
||||
|
||||
// MeasureString measures how much horizontal space str consumes when drawn to a buffer
|
||||
func MeasureString(str string) (width int) {
|
||||
dx := 0
|
||||
for _, r := range str {
|
||||
dx += runeWidth(r)
|
||||
}
|
||||
return dx
|
||||
}
|
||||
|
||||
// MeasureString measures how much horizontal and vertical space str consumes when drawn to a buffer
|
||||
func MeasureMultiLineString(str string) (maxLineWidth, lineCount int) {
|
||||
lines := strings.Split(str, "\n")
|
||||
for _, line := range lines {
|
||||
lineWidth := MeasureString(line)
|
||||
maxLineWidth = max(maxLineWidth, lineWidth)
|
||||
}
|
||||
return maxLineWidth, len(lines)
|
||||
}
|
||||
|
||||
func runeWidth(r rune) int {
|
||||
//fmt.Println(r, width.LookupRune(r).Kind())
|
||||
switch width.LookupRune(r).Kind() {
|
||||
case width.EastAsianFullwidth:
|
||||
fallthrough
|
||||
case width.EastAsianWide:
|
||||
return 2
|
||||
default:
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
func min(x, y int) int {
|
||||
if x < y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func max(x, y int) int {
|
||||
if x > y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func iff[T any](condition bool, trueValue, falseValue T) T {
|
||||
if condition {
|
||||
return trueValue
|
||||
}
|
||||
return falseValue
|
||||
}
|
||||
|
19
view.go
19
view.go
@ -1,34 +1,27 @@
|
||||
package tui
|
||||
|
||||
// View defines the behavior of any element displayable on screen
|
||||
// View defines the behavior of any element displayable on screen.
|
||||
// To define custom Views, it is recommended to add ViewTmpl
|
||||
// as the promoted anonymous field for your custom View struct.
|
||||
// It implements the View interface with useful default behavior
|
||||
type View interface {
|
||||
Events
|
||||
|
||||
SetForeground(color Color)
|
||||
Foreground() Color
|
||||
|
||||
SetBackground(color Color)
|
||||
Background() Color
|
||||
|
||||
SetStyle(s Style)
|
||||
Style() Style
|
||||
|
||||
Draw(*ViewBuffer)
|
||||
Layout() (prefWidth, prefHeight int)
|
||||
Draw(buf *ViewBuffer)
|
||||
}
|
||||
|
||||
// Group defines the behavior of a View which can hold multiple sub views
|
||||
// To define custom Groups, it is recommended to add GroupTmpl
|
||||
// as the promoted anonymous field for your custom Wrapper struct.
|
||||
// It implements the Group interface with useful default behavior
|
||||
type Group interface {
|
||||
View
|
||||
|
||||
Children() []View
|
||||
Views() []View
|
||||
}
|
||||
|
||||
// Wrapper defines the behavior of a GroupView which can hold exactly one sub view
|
||||
// Wrapper defines the behavior of a GroupView which can hold exactly one sub view.
|
||||
// To define custom Wrappers, it is recommended to add WrapperTmpl
|
||||
// as the promoted anonymous field for your custom Wrapper struct.
|
||||
// It implements the Wrapper interface with useful default behavior
|
||||
|
@ -1,3 +0,0 @@
|
||||
package tui
|
||||
|
||||
// TODO GroupTmpl
|
149
views/bordergroup.go
Normal file
149
views/bordergroup.go
Normal file
@ -0,0 +1,149 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"git.tordarus.net/Tordarus/tui"
|
||||
)
|
||||
|
||||
// BorderGroup ia a tui.Group which places its children in a linear layout
|
||||
type BorderGroup struct {
|
||||
tui.ViewTmpl
|
||||
views map[Slot]tui.View
|
||||
horizontalLayout *LayoutResult
|
||||
verticalLayout *LayoutResult
|
||||
}
|
||||
|
||||
var _ tui.Group = &BorderGroup{}
|
||||
|
||||
func NewBorderGroup() *BorderGroup {
|
||||
return &BorderGroup{
|
||||
views: map[Slot]tui.View{},
|
||||
}
|
||||
}
|
||||
|
||||
func (g *BorderGroup) Views() []tui.View {
|
||||
s := make([]tui.View, 0, len(g.views))
|
||||
for _, view := range g.views {
|
||||
s = append(s, view)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (g *BorderGroup) SetView(v tui.View, slot Slot) {
|
||||
g.views[slot] = v
|
||||
}
|
||||
|
||||
func (g *BorderGroup) View(slot Slot) tui.View {
|
||||
return g.views[slot]
|
||||
}
|
||||
|
||||
func (g *BorderGroup) Draw(buf *tui.ViewBuffer) {
|
||||
g.ViewTmpl.Draw(buf)
|
||||
|
||||
if g.verticalLayout == nil {
|
||||
g.Layout()
|
||||
}
|
||||
verticalLayout := g.verticalLayout
|
||||
|
||||
if g.horizontalLayout == nil {
|
||||
g.Layout()
|
||||
}
|
||||
horizontalLayout := g.horizontalLayout
|
||||
|
||||
remainingVerticalSpacePerView := (buf.Height() - verticalLayout.Sum.Height)
|
||||
if verticalLayout.VerticalNegativeCount > 0 {
|
||||
remainingVerticalSpacePerView /= verticalLayout.VerticalNegativeCount
|
||||
}
|
||||
|
||||
remainingHorizontalSpacePerView := (buf.Width() - horizontalLayout.Sum.Width)
|
||||
if horizontalLayout.HorizontalNegativeCount > 0 {
|
||||
remainingHorizontalSpacePerView /= horizontalLayout.HorizontalNegativeCount
|
||||
}
|
||||
|
||||
fitsVertically := buf.Height() >= verticalLayout.Sum.Height
|
||||
fitsHorizontally := buf.Width() >= horizontalLayout.Sum.Width
|
||||
|
||||
var topHeight int
|
||||
var bottomHeight int
|
||||
var leftWidth int
|
||||
var rightWidth int
|
||||
|
||||
if view, ok := g.views[Top]; ok {
|
||||
_, topHeight = view.Layout()
|
||||
|
||||
if fitsVertically {
|
||||
topHeight = iff(topHeight < 0, remainingVerticalSpacePerView, topHeight)
|
||||
} else {
|
||||
topHeight = int(float64(buf.Height()) * float64(topHeight) / float64(verticalLayout.Sum.Height))
|
||||
}
|
||||
|
||||
view.Draw(buf.Sub(0, 0, buf.Width(), topHeight))
|
||||
}
|
||||
|
||||
if view, ok := g.views[Bottom]; ok {
|
||||
_, bottomHeight = view.Layout()
|
||||
|
||||
if fitsVertically {
|
||||
bottomHeight = iff(bottomHeight < 0, remainingVerticalSpacePerView, bottomHeight)
|
||||
} else {
|
||||
bottomHeight = int(float64(buf.Height()) * float64(bottomHeight) / float64(verticalLayout.Sum.Height))
|
||||
}
|
||||
|
||||
view.Draw(buf.Sub(0, buf.Height()-bottomHeight, buf.Width(), bottomHeight))
|
||||
}
|
||||
|
||||
if view, ok := g.views[Left]; ok {
|
||||
leftWidth, _ = view.Layout()
|
||||
|
||||
if fitsHorizontally {
|
||||
leftWidth = iff(leftWidth < 0, remainingHorizontalSpacePerView, leftWidth)
|
||||
} else {
|
||||
leftWidth = int(float64(buf.Width()) * float64(leftWidth) / float64(horizontalLayout.Sum.Width))
|
||||
}
|
||||
|
||||
view.Draw(buf.Sub(0, topHeight, leftWidth, buf.Height()-topHeight-bottomHeight))
|
||||
}
|
||||
|
||||
if view, ok := g.views[Right]; ok {
|
||||
rightWidth, _ = view.Layout()
|
||||
|
||||
if fitsHorizontally {
|
||||
rightWidth = iff(rightWidth < 0, remainingHorizontalSpacePerView, rightWidth)
|
||||
} else {
|
||||
rightWidth = int(float64(buf.Width()) * float64(rightWidth) / float64(horizontalLayout.Sum.Width))
|
||||
}
|
||||
|
||||
view.Draw(buf.Sub(buf.Width()-rightWidth, topHeight, rightWidth, buf.Height()-topHeight-bottomHeight))
|
||||
}
|
||||
|
||||
if view, ok := g.views[Center]; ok {
|
||||
view.Draw(buf.Sub(leftWidth, topHeight, buf.Width()-leftWidth-rightWidth, buf.Height()-topHeight-bottomHeight))
|
||||
}
|
||||
|
||||
g.verticalLayout = nil
|
||||
g.horizontalLayout = nil
|
||||
}
|
||||
|
||||
func (g *BorderGroup) Layout() (prefWidth, prefHeight int) {
|
||||
g.verticalLayout = CalculateLayoutResult([]tui.View{g.View(Top), g.View(Center), g.View(Bottom)})
|
||||
g.horizontalLayout = CalculateLayoutResult([]tui.View{g.View(Left), g.View(Center), g.View(Right)})
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func (g *BorderGroup) OnKeyPressed(event *tui.KeyEvent) (consumed bool) {
|
||||
for _, view := range g.Views() {
|
||||
if view.OnKeyPressed(event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
type Slot uint8
|
||||
|
||||
const (
|
||||
Top Slot = iota
|
||||
Bottom
|
||||
Left
|
||||
Right
|
||||
Center
|
||||
)
|
93
views/borderview.go
Normal file
93
views/borderview.go
Normal file
@ -0,0 +1,93 @@
|
||||
package views
|
||||
|
||||
import "git.tordarus.net/Tordarus/tui"
|
||||
|
||||
// BorderView is a tui.Wrapper which draws an ASCII border around its view.
|
||||
// Be aware that box drawing characters must share the same tui.Style with the next character.
|
||||
// This can lead to color artifacts when using BorderView with colored styles.
|
||||
type BorderView struct {
|
||||
tui.WrapperTmpl
|
||||
Border BorderBox
|
||||
}
|
||||
|
||||
var _ tui.View = &BorderView{}
|
||||
|
||||
func NewBorderView(view tui.View) *BorderView {
|
||||
v := new(BorderView)
|
||||
v.SetView(view)
|
||||
v.Border = ThinBorder()
|
||||
return v
|
||||
}
|
||||
|
||||
func (g *BorderView) Draw(buf *tui.ViewBuffer) {
|
||||
g.View().Draw(buf.Sub(1, 1, buf.Width()-2, buf.Height()-2))
|
||||
|
||||
buf.Set(0, 0, tui.Rune{Rn: g.Border.TopLeft, Style: g.Style()})
|
||||
buf.Set(buf.Width()-1, 0, tui.Rune{Rn: g.Border.TopRight, Style: g.Style()})
|
||||
|
||||
buf.Set(0, buf.Height()-1, tui.Rune{Rn: g.Border.BottomLeft, Style: g.Style()})
|
||||
buf.Set(buf.Width()-1, buf.Height()-1, tui.Rune{Rn: g.Border.BottomRight, Style: g.Style()})
|
||||
|
||||
for x := 1; x < buf.Width()-1; x++ {
|
||||
buf.Set(x, 0, tui.Rune{Rn: g.Border.Horizontal, Style: g.Style()})
|
||||
buf.Set(x, buf.Height()-1, tui.Rune{Rn: g.Border.Horizontal, Style: g.Style()})
|
||||
}
|
||||
|
||||
for y := 1; y < buf.Height()-1; y++ {
|
||||
buf.Set(0, y, tui.Rune{Rn: g.Border.Vertical, Style: g.Style()})
|
||||
buf.Set(buf.Width()-1, y, tui.Rune{Rn: g.Border.Vertical, Style: g.Style()})
|
||||
}
|
||||
}
|
||||
|
||||
func (v *BorderView) Layout() (prefWidth, prefHeight int) {
|
||||
w, h := v.View().Layout()
|
||||
w = iff(w > 0, w+2, w)
|
||||
h = iff(h > 0, h+2, h)
|
||||
return w, h
|
||||
}
|
||||
|
||||
func (v *BorderView) Style() tui.Style {
|
||||
return v.ViewTmpl.Style()
|
||||
}
|
||||
|
||||
type BorderBox struct {
|
||||
TopLeft rune
|
||||
TopRight rune
|
||||
BottomLeft rune
|
||||
BottomRight rune
|
||||
Horizontal rune
|
||||
Vertical rune
|
||||
}
|
||||
|
||||
func ThickBorder() BorderBox {
|
||||
return BorderBox{
|
||||
TopLeft: '┏',
|
||||
TopRight: '┓',
|
||||
BottomLeft: '┗',
|
||||
BottomRight: '┛',
|
||||
Horizontal: '━',
|
||||
Vertical: '┃',
|
||||
}
|
||||
}
|
||||
|
||||
func ThinBorder() BorderBox {
|
||||
return BorderBox{
|
||||
TopLeft: '┌',
|
||||
TopRight: '┐',
|
||||
BottomLeft: '└',
|
||||
BottomRight: '┘',
|
||||
Horizontal: '─',
|
||||
Vertical: '│',
|
||||
}
|
||||
}
|
||||
|
||||
func DoubleBorder() BorderBox {
|
||||
return BorderBox{
|
||||
TopLeft: '╔',
|
||||
TopRight: '╗',
|
||||
BottomLeft: '╚',
|
||||
BottomRight: '╝',
|
||||
Horizontal: '═',
|
||||
Vertical: '║',
|
||||
}
|
||||
}
|
29
views/constrainview.go
Normal file
29
views/constrainview.go
Normal file
@ -0,0 +1,29 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"git.tordarus.net/Tordarus/tui"
|
||||
)
|
||||
|
||||
// ConstrainView is a tui.Wrapper which constrains the dimensions of its View
|
||||
type ConstrainView struct {
|
||||
tui.WrapperTmpl
|
||||
MaxWidth int
|
||||
MaxHeight int
|
||||
}
|
||||
|
||||
var _ tui.View = &ConstrainView{}
|
||||
|
||||
func NewConstrainView(view tui.View) *ConstrainView {
|
||||
v := new(ConstrainView)
|
||||
v.SetView(view)
|
||||
v.Constrain(-1, -1)
|
||||
return v
|
||||
}
|
||||
|
||||
func (v *ConstrainView) Constrain(maxWidth, maxHeight int) {
|
||||
v.MaxWidth, v.MaxHeight = maxWidth, maxHeight
|
||||
}
|
||||
|
||||
func (v *ConstrainView) Layout() (prefWidth, prefHeight int) {
|
||||
return v.MaxWidth, v.MaxHeight
|
||||
}
|
49
views/coordgroup.go
Normal file
49
views/coordgroup.go
Normal file
@ -0,0 +1,49 @@
|
||||
package views
|
||||
|
||||
import "git.tordarus.net/Tordarus/tui"
|
||||
|
||||
// CoordGroup is a tui.Group which places its children on predefined coordinates
|
||||
type CoordGroup struct {
|
||||
tui.ViewTmpl
|
||||
views map[tui.View]tui.Dimension
|
||||
}
|
||||
|
||||
var _ tui.Group = &CoordGroup{}
|
||||
|
||||
func NewCoordGroup() *CoordGroup {
|
||||
return &CoordGroup{
|
||||
views: map[tui.View]tui.Dimension{},
|
||||
}
|
||||
}
|
||||
|
||||
func (g *CoordGroup) Views() []tui.View {
|
||||
s := make([]tui.View, 0, len(g.views))
|
||||
for v := range g.views {
|
||||
s = append(s, v)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// SetView places v at the given coordinates with the given dimensions.
|
||||
// v will be added to g's children if not already added before
|
||||
func (g *CoordGroup) SetView(v tui.View, x, y, width, height int) {
|
||||
g.views[v] = tui.Dimension{Point: tui.Point{X: x, Y: y}, Size: tui.Size{Width: width, Height: height}}
|
||||
}
|
||||
func (g *CoordGroup) Draw(buf *tui.ViewBuffer) {
|
||||
for v, d := range g.views {
|
||||
v.Draw(buf.Sub(d.X, d.Y, d.Width, d.Height))
|
||||
}
|
||||
}
|
||||
|
||||
func (v *CoordGroup) Layout() (prefWidth, prefHeight int) {
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func (g *CoordGroup) OnKeyPressed(event *tui.KeyEvent) (consumed bool) {
|
||||
for _, view := range g.Views() {
|
||||
if view.OnKeyPressed(event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"git.tordarus.net/Tordarus/tui"
|
||||
)
|
||||
|
||||
type EventView struct {
|
||||
tui.WrapperTmpl
|
||||
|
||||
View tui.View
|
||||
KeyPressed func(event *tui.KeyEvent) (consumed bool)
|
||||
}
|
||||
|
||||
func NewEventView(view tui.View) *EventView {
|
||||
return &EventView{View: view}
|
||||
}
|
||||
|
||||
func (v *EventView) OnKeyPressed(event *tui.KeyEvent) (consumed bool) {
|
||||
if v.KeyPressed != nil {
|
||||
return v.KeyPressed(event)
|
||||
}
|
||||
return v.ViewTmpl.OnKeyPressed(event)
|
||||
}
|
||||
|
||||
func (v *EventView) Draw(buf *tui.ViewBuffer) {
|
||||
v.View.Draw(buf)
|
||||
}
|
114
views/flowgroup.go
Normal file
114
views/flowgroup.go
Normal file
@ -0,0 +1,114 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"git.tordarus.net/Tordarus/tui"
|
||||
)
|
||||
|
||||
// FlowGroup ia a tui.Group which places its children in a linear layout
|
||||
type FlowGroup struct {
|
||||
tui.ViewTmpl
|
||||
views []tui.View
|
||||
lastLayoutPhase *LayoutResult
|
||||
|
||||
// Orientation defines in which direction the children will be placed
|
||||
Orientation tui.Orientation
|
||||
}
|
||||
|
||||
var _ tui.Group = &FlowGroup{}
|
||||
|
||||
func NewFlowGroup(orientation tui.Orientation) *FlowGroup {
|
||||
return &FlowGroup{
|
||||
views: make([]tui.View, 0),
|
||||
Orientation: orientation,
|
||||
}
|
||||
}
|
||||
|
||||
func (g *FlowGroup) Views() []tui.View {
|
||||
return g.views[:]
|
||||
}
|
||||
|
||||
func (g *FlowGroup) AppendViews(v ...tui.View) {
|
||||
g.views = append(g.views, v...)
|
||||
}
|
||||
|
||||
func (g *FlowGroup) PrependViews(v ...tui.View) {
|
||||
g.views = append(v, g.views...)
|
||||
}
|
||||
|
||||
func (g *FlowGroup) InsertView(v tui.View, index int) {
|
||||
g.views = append(g.views[:index], append([]tui.View{v}, g.views[index:]...)...)
|
||||
}
|
||||
|
||||
func (g *FlowGroup) Draw(buf *tui.ViewBuffer) {
|
||||
g.ViewTmpl.Draw(buf)
|
||||
|
||||
if g.lastLayoutPhase == nil {
|
||||
g.Layout()
|
||||
}
|
||||
layout := g.lastLayoutPhase
|
||||
|
||||
if g.Orientation == tui.Horizontal {
|
||||
remainingSpacePerView := buf.Width() - layout.Sum.Width
|
||||
if layout.HorizontalNegativeCount > 0 {
|
||||
remainingSpacePerView /= layout.HorizontalNegativeCount
|
||||
}
|
||||
|
||||
x := 0
|
||||
for _, view := range g.views {
|
||||
size := layout.Sizes[view]
|
||||
|
||||
size.Height = iff(size.Height < 0, buf.Height(), size.Height)
|
||||
if size.Width < 0 {
|
||||
size.Width = iff(layout.Sum.Width > buf.Width(), 0, remainingSpacePerView)
|
||||
}
|
||||
|
||||
view.Draw(buf.Sub(x, 0, size.Width, size.Height))
|
||||
x += size.Width
|
||||
}
|
||||
} else if g.Orientation == tui.Vertical {
|
||||
remainingSpacePerView := buf.Height() - layout.Sum.Height
|
||||
if layout.VerticalNegativeCount > 0 {
|
||||
remainingSpacePerView /= layout.VerticalNegativeCount
|
||||
}
|
||||
|
||||
y := 0
|
||||
for _, view := range g.views {
|
||||
size := layout.Sizes[view]
|
||||
|
||||
size.Width = iff(size.Width < 0, buf.Width(), size.Width)
|
||||
if size.Height < 0 {
|
||||
size.Height = iff(layout.Sum.Height > buf.Height(), 0, remainingSpacePerView)
|
||||
}
|
||||
|
||||
view.Draw(buf.Sub(0, y, size.Width, size.Height))
|
||||
y += size.Height
|
||||
}
|
||||
}
|
||||
|
||||
g.lastLayoutPhase = nil
|
||||
}
|
||||
|
||||
func (g *FlowGroup) Layout() (prefWidth, prefHeight int) {
|
||||
layout := CalculateLayoutResult(g.Views())
|
||||
g.lastLayoutPhase = layout
|
||||
|
||||
if g.Orientation == tui.Horizontal {
|
||||
prefWidth = iff(layout.HorizontalNegativeCount == 0, layout.Sum.Width, -1)
|
||||
prefHeight = iff(layout.VerticalNegativeCount == 0, layout.Max.Height, -1)
|
||||
} else if g.Orientation == tui.Vertical {
|
||||
prefWidth = iff(layout.HorizontalNegativeCount == 0, layout.Max.Width, -1)
|
||||
prefHeight = iff(layout.VerticalNegativeCount == 0, layout.Sum.Width, -1)
|
||||
}
|
||||
|
||||
layout.Pref = tui.Size{Width: prefWidth, Height: prefHeight}
|
||||
return
|
||||
}
|
||||
|
||||
func (g *FlowGroup) OnKeyPressed(event *tui.KeyEvent) (consumed bool) {
|
||||
for _, view := range g.Views() {
|
||||
if view.OnKeyPressed(event) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
20
views/growview.go
Normal file
20
views/growview.go
Normal file
@ -0,0 +1,20 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"git.tordarus.net/Tordarus/tui"
|
||||
)
|
||||
|
||||
// GrowView is a tui.View which always demands all available space
|
||||
type GrowView struct {
|
||||
tui.ViewTmpl
|
||||
}
|
||||
|
||||
var _ tui.View = &GrowView{}
|
||||
|
||||
func NewGrowView() *GrowView {
|
||||
return &GrowView{}
|
||||
}
|
||||
|
||||
func (v *GrowView) Layout() (prefWidth, prefHeight int) {
|
||||
return -1, -1
|
||||
}
|
48
views/marginview.go
Normal file
48
views/marginview.go
Normal file
@ -0,0 +1,48 @@
|
||||
package views
|
||||
|
||||
import "git.tordarus.net/Tordarus/tui"
|
||||
|
||||
// MarginView is a tui.Wrapper which applies margin around its view
|
||||
type MarginView struct {
|
||||
tui.WrapperTmpl
|
||||
Margin map[tui.Side]int
|
||||
}
|
||||
|
||||
var _ tui.View = &MarginView{}
|
||||
|
||||
func NewMarginView(view tui.View) *MarginView {
|
||||
v := new(MarginView)
|
||||
v.SetView(view)
|
||||
v.SetMargin(0, 0, 0, 0)
|
||||
return v
|
||||
}
|
||||
|
||||
func (g *MarginView) Draw(buf *tui.ViewBuffer) {
|
||||
x := g.Margin[tui.Left]
|
||||
y := g.Margin[tui.Top]
|
||||
w := buf.Width() - x - g.Margin[tui.Right]
|
||||
h := buf.Height() - y - g.Margin[tui.Bottom]
|
||||
|
||||
g.ViewTmpl.Draw(buf)
|
||||
g.View().Draw(buf.Sub(x, y, w, h))
|
||||
}
|
||||
|
||||
func (v *MarginView) Layout() (prefWidth, prefHeight int) {
|
||||
w, h := v.View().Layout()
|
||||
w = iff(w > 0, w+v.Margin[tui.Left]+v.Margin[tui.Right], w)
|
||||
h = iff(h > 0, h+v.Margin[tui.Top]+v.Margin[tui.Bottom], h)
|
||||
return w, h
|
||||
}
|
||||
|
||||
func (v *MarginView) Style() tui.Style {
|
||||
return v.ViewTmpl.Style()
|
||||
}
|
||||
|
||||
func (v *MarginView) SetMargin(top, right, bottom, left int) {
|
||||
v.Margin = map[tui.Side]int{
|
||||
tui.Top: top,
|
||||
tui.Right: right,
|
||||
tui.Bottom: bottom,
|
||||
tui.Left: left,
|
||||
}
|
||||
}
|
@ -4,6 +4,7 @@ import (
|
||||
"git.tordarus.net/Tordarus/tui"
|
||||
)
|
||||
|
||||
// TextView is a tui.View which prints text
|
||||
type TextView struct {
|
||||
tui.ViewTmpl
|
||||
Text string
|
||||
@ -20,3 +21,7 @@ func NewTextView(text string) *TextView {
|
||||
Text: text,
|
||||
}
|
||||
}
|
||||
|
||||
func (v *TextView) Layout() (prefWidth, prefHeight int) {
|
||||
return tui.MeasureMultiLineString(v.Text)
|
||||
}
|
||||
|
86
views/utils.go
Normal file
86
views/utils.go
Normal file
@ -0,0 +1,86 @@
|
||||
package views
|
||||
|
||||
import (
|
||||
"math"
|
||||
|
||||
"git.tordarus.net/Tordarus/tui"
|
||||
)
|
||||
|
||||
func min(x, y int) int {
|
||||
if x < y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func max(x, y int) int {
|
||||
if x > y {
|
||||
return x
|
||||
}
|
||||
return y
|
||||
}
|
||||
|
||||
func iff[T any](condition bool, trueValue, falseValue T) T {
|
||||
if condition {
|
||||
return trueValue
|
||||
}
|
||||
return falseValue
|
||||
}
|
||||
|
||||
type LayoutResult struct {
|
||||
Sizes map[tui.View]tui.Size
|
||||
|
||||
Sum tui.Size
|
||||
Min tui.Size
|
||||
Max tui.Size
|
||||
Pref tui.Size
|
||||
|
||||
Count int
|
||||
|
||||
VerticalNegativeCount int
|
||||
HorizontalNegativeCount int
|
||||
}
|
||||
|
||||
func CalculateLayoutResult(views []tui.View) *LayoutResult {
|
||||
result := &LayoutResult{
|
||||
Sizes: map[tui.View]tui.Size{},
|
||||
|
||||
Sum: tui.Size{Width: 0, Height: 0},
|
||||
Min: tui.Size{Width: math.MaxInt, Height: math.MaxInt},
|
||||
Max: tui.Size{Width: -1, Height: -1},
|
||||
|
||||
Count: 0,
|
||||
|
||||
VerticalNegativeCount: 0,
|
||||
HorizontalNegativeCount: 0,
|
||||
}
|
||||
|
||||
for _, view := range views {
|
||||
if view == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
result.Count++
|
||||
|
||||
width, height := view.Layout()
|
||||
result.Sizes[view] = tui.Size{Width: width, Height: height}
|
||||
|
||||
if width > 0 {
|
||||
result.Sum.Width += width
|
||||
result.Min.Width = min(result.Min.Width, width)
|
||||
result.Max.Width = max(result.Max.Width, width)
|
||||
} else if width < 0 {
|
||||
result.HorizontalNegativeCount++
|
||||
}
|
||||
|
||||
if height > 0 {
|
||||
result.Sum.Height += height
|
||||
result.Min.Height = min(result.Min.Height, height)
|
||||
result.Max.Height = max(result.Max.Height, height)
|
||||
} else if height < 0 {
|
||||
result.VerticalNegativeCount++
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
40
viewtmpl.go
40
viewtmpl.go
@ -1,44 +1,30 @@
|
||||
package tui
|
||||
|
||||
import "github.com/gdamore/tcell"
|
||||
|
||||
type ViewTmpl struct {
|
||||
foreground *Color
|
||||
background *Color
|
||||
style *Style
|
||||
}
|
||||
|
||||
var _ View = &ViewTmpl{}
|
||||
|
||||
func (v *ViewTmpl) Draw(buf *ViewBuffer) {
|
||||
buf.Fill(DefaultRune)
|
||||
buf.Fill(Rune{' ', v.Style()})
|
||||
}
|
||||
|
||||
func (v *ViewTmpl) Layout() (prefWidth, prefHeight int) {
|
||||
return -1, -1
|
||||
}
|
||||
|
||||
func (v *ViewTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
|
||||
return false
|
||||
}
|
||||
|
||||
func (v *ViewTmpl) SetStyle(s Style) {
|
||||
v.style = &s
|
||||
}
|
||||
|
||||
func (v *ViewTmpl) Style() Style {
|
||||
return StyleDefault.Background(v.Background()).Foreground(v.Foreground())
|
||||
if v.style == nil {
|
||||
return StyleDefault
|
||||
}
|
||||
|
||||
func (v *ViewTmpl) Foreground() Color {
|
||||
if v.foreground == nil {
|
||||
return tcell.ColorDefault
|
||||
}
|
||||
return *v.foreground
|
||||
}
|
||||
|
||||
func (v *ViewTmpl) SetForeground(color Color) {
|
||||
v.foreground = &color
|
||||
}
|
||||
|
||||
func (v *ViewTmpl) Background() Color {
|
||||
if v.background == nil {
|
||||
return tcell.ColorDefault
|
||||
}
|
||||
return *v.background
|
||||
}
|
||||
|
||||
func (v *ViewTmpl) SetBackground(color Color) {
|
||||
v.background = &color
|
||||
return *v.style
|
||||
}
|
||||
|
@ -1,7 +1,5 @@
|
||||
package tui
|
||||
|
||||
import "github.com/gdamore/tcell"
|
||||
|
||||
type WrapperTmpl struct {
|
||||
ViewTmpl
|
||||
view View
|
||||
@ -17,6 +15,13 @@ func (v *WrapperTmpl) Draw(buf *ViewBuffer) {
|
||||
}
|
||||
}
|
||||
|
||||
func (v *WrapperTmpl) Layout() (prefWidth, prefHeight int) {
|
||||
if v.view != nil {
|
||||
return v.view.Layout()
|
||||
}
|
||||
return v.ViewTmpl.Layout()
|
||||
}
|
||||
|
||||
func (v *WrapperTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
|
||||
if v.view != nil {
|
||||
return v.view.OnKeyPressed(event)
|
||||
@ -24,6 +29,14 @@ func (v *WrapperTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
|
||||
return v.ViewTmpl.OnKeyPressed(event)
|
||||
}
|
||||
|
||||
func (v *WrapperTmpl) SetStyle(s Style) {
|
||||
if v.view != nil {
|
||||
v.view.SetStyle(s)
|
||||
return
|
||||
}
|
||||
v.ViewTmpl.SetStyle(s)
|
||||
}
|
||||
|
||||
func (v *WrapperTmpl) Style() Style {
|
||||
if v.view != nil {
|
||||
return v.view.Style()
|
||||
@ -31,37 +44,7 @@ func (v *WrapperTmpl) Style() Style {
|
||||
return v.ViewTmpl.Style()
|
||||
}
|
||||
|
||||
func (v *WrapperTmpl) Foreground() Color {
|
||||
if v.view != nil {
|
||||
return v.view.Foreground()
|
||||
}
|
||||
return v.ViewTmpl.Foreground()
|
||||
}
|
||||
|
||||
func (v *WrapperTmpl) SetForeground(color Color) {
|
||||
if v.view != nil {
|
||||
v.view.SetForeground(color)
|
||||
} else {
|
||||
v.ViewTmpl.SetForeground(color)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *WrapperTmpl) Background() Color {
|
||||
if v.background == nil {
|
||||
return tcell.ColorDefault
|
||||
}
|
||||
return *v.background
|
||||
}
|
||||
|
||||
func (v *WrapperTmpl) SetBackground(color Color) {
|
||||
if v.view != nil {
|
||||
v.view.SetBackground(color)
|
||||
} else {
|
||||
v.ViewTmpl.SetBackground(color)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *WrapperTmpl) Children() []View {
|
||||
func (v *WrapperTmpl) Views() []View {
|
||||
return []View{v.view}
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user