more views

This commit is contained in:
Timon Ringwald 2022-04-02 13:01:41 +02:00
parent d335211770
commit dfa00f5fe3
22 changed files with 848 additions and 127 deletions

View File

@ -5,8 +5,31 @@ import (
) )
func drawBuffer(scr tcell.Screen, buf *ViewBuffer) { 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.SetContent(x, y, rn.Rn, nil, rn.Style)
}
}
}) })
scr.Show() 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)
}

View File

@ -1,5 +1,8 @@
package tui package tui
type Events interface { 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) OnKeyPressed(event *KeyEvent) (consumed bool)
} }

4
go.mod
View File

@ -3,8 +3,9 @@ module git.tordarus.net/Tordarus/tui
go 1.18 go 1.18
require ( require (
git.tordarus.net/Tordarus/buf2d v1.1.0 git.tordarus.net/Tordarus/buf2d v1.1.1
github.com/gdamore/tcell v1.4.0 github.com/gdamore/tcell v1.4.0
golang.org/x/text v0.3.7
) )
require ( require (
@ -12,5 +13,4 @@ require (
github.com/lucasb-eyer/go-colorful v1.0.3 // indirect github.com/lucasb-eyer/go-colorful v1.0.3 // indirect
github.com/mattn/go-runewidth v0.0.7 // indirect github.com/mattn/go-runewidth v0.0.7 // indirect
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756 // indirect
golang.org/x/text v0.3.0 // indirect
) )

7
go.sum
View File

@ -1,5 +1,5 @@
git.tordarus.net/Tordarus/buf2d v1.1.0 h1:rIZjD7yeX5XK2D1h75ET5Og0u/NQF3eVonnC5aaqVkQ= git.tordarus.net/Tordarus/buf2d v1.1.1 h1:rYvQ2YveqogCoKy5andQxuORPusWbUhpnqJhzVkTlRs=
git.tordarus.net/Tordarus/buf2d v1.1.0/go.mod h1:XXPpS8nQK0gUI0ki7ftV/qlprsGCRWFVSD4ybvDuUL8= 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 h1:+7OoQ1Bc6eTm5niUzBa0Ctsh6JbMW6Ra+YNuAtDBdko=
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
github.com/gdamore/tcell v1.4.0 h1:vUnHwJRvcPQa3tzi+0QI4U9JINXYJlOz9yiaiPQ2wMU= 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= 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 h1:9nuHUbU8dRnRRfj9KjWUVrJeoexdbeMjttk6Oh1rD10=
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 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.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=

View File

@ -10,8 +10,12 @@ import (
type Screen struct { type Screen struct {
scr tcell.Screen scr tcell.Screen
buf *ViewBuffer
stopCh chan error stopCh chan error
Root View 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) { func NewScreen(root View) (*Screen, error) {
@ -37,10 +41,7 @@ func (s *Screen) eventloop() {
case *tcell.EventResize: case *tcell.EventResize:
go s.Redraw() go s.Redraw()
case *tcell.EventKey: case *tcell.EventKey:
go func() { go s.onKeyPressed(event)
s.Root.OnKeyPressed(event)
s.Redraw()
}()
default: default:
s.StopWithError(errors.New(fmt.Sprintf("%#v", event))) s.StopWithError(errors.New(fmt.Sprintf("%#v", event)))
} }
@ -67,9 +68,21 @@ func (s *Screen) StopWithError(err error) {
s.stopCh <- err 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() { func (s *Screen) Redraw() {
w, h := s.scr.Size() w, h := s.scr.Size()
buf := buf2d.NewBuffer(w, h, DefaultRune)
s.Root.Draw(buf) if s.buf == nil || s.buf.Width() != w || s.buf.Height() != h {
drawBuffer(s.scr, buf) 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)
} }

View File

@ -10,21 +10,90 @@ import (
"github.com/gdamore/tcell" "github.com/gdamore/tcell"
) )
func TestScreen(t *testing.T) { func TestFlowGroup(t *testing.T) {
textView := views.NewTextView("hello world") textView := views.NewTextView("hello world!")
eventView := views.NewEventView(textView) textView.SetStyle(tui.StyleDefault.Background(tcell.ColorRed).Foreground(tcell.ColorBlack))
screen, err := tui.NewScreen(eventView)
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 { if err != nil {
t.Error(err) t.Error(err)
return 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 { if event.Key() == tcell.KeyCtrlC {
screen.StopWithError(errors.New(fmt.Sprintf("key: %#v | rune: %s", event.Key(), string(event.Rune())))) 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 return true
} }

View File

@ -11,3 +11,32 @@ type Style = tcell.Style
type Color = tcell.Color type Color = tcell.Color
var StyleDefault Style = tcell.StyleDefault 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
)

View File

@ -2,30 +2,88 @@ package tui
import ( import (
"strings" "strings"
"golang.org/x/text/width"
) )
// WriteString writes a whole string to the buffer at position (x,y) // 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 // 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 dx := x
for _, r := range str { for _, r := range str {
if dx >= b.Width() { if dx >= b.Width() {
return return
} }
b.Set(dx, y, Rune{r, style}) 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) // 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 // 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 // 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") lines := strings.Split(str, "\n")
for dy, line := range lines { for dy, line := range lines {
if dy >= b.Height() { if dy >= b.Height() {
return 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
View File

@ -1,34 +1,27 @@
package tui 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 // To define custom Views, it is recommended to add ViewTmpl
// as the promoted anonymous field for your custom View struct. // as the promoted anonymous field for your custom View struct.
// It implements the View interface with useful default behavior // It implements the View interface with useful default behavior
type View interface { type View interface {
Events Events
SetForeground(color Color) SetStyle(s Style)
Foreground() Color
SetBackground(color Color)
Background() Color
Style() 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 // 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 { type Group interface {
View 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 // To define custom Wrappers, it is recommended to add WrapperTmpl
// as the promoted anonymous field for your custom Wrapper struct. // as the promoted anonymous field for your custom Wrapper struct.
// It implements the Wrapper interface with useful default behavior // It implements the Wrapper interface with useful default behavior

View File

@ -1,3 +0,0 @@
package tui
// TODO GroupTmpl

149
views/bordergroup.go Normal file
View 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
View 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
View 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
View 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
}

View File

@ -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
View 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
View 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
View 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,
}
}

View File

@ -4,6 +4,7 @@ import (
"git.tordarus.net/Tordarus/tui" "git.tordarus.net/Tordarus/tui"
) )
// TextView is a tui.View which prints text
type TextView struct { type TextView struct {
tui.ViewTmpl tui.ViewTmpl
Text string Text string
@ -20,3 +21,7 @@ func NewTextView(text string) *TextView {
Text: text, Text: text,
} }
} }
func (v *TextView) Layout() (prefWidth, prefHeight int) {
return tui.MeasureMultiLineString(v.Text)
}

86
views/utils.go Normal file
View 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
}

View File

@ -1,44 +1,30 @@
package tui package tui
import "github.com/gdamore/tcell"
type ViewTmpl struct { type ViewTmpl struct {
foreground *Color style *Style
background *Color
} }
var _ View = &ViewTmpl{} var _ View = &ViewTmpl{}
func (v *ViewTmpl) Draw(buf *ViewBuffer) { 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) { func (v *ViewTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
return false return false
} }
func (v *ViewTmpl) SetStyle(s Style) {
v.style = &s
}
func (v *ViewTmpl) Style() Style { func (v *ViewTmpl) Style() Style {
return StyleDefault.Background(v.Background()).Foreground(v.Foreground()) if v.style == nil {
return StyleDefault
} }
return *v.style
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
} }

View File

@ -1,7 +1,5 @@
package tui package tui
import "github.com/gdamore/tcell"
type WrapperTmpl struct { type WrapperTmpl struct {
ViewTmpl ViewTmpl
view View 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) { func (v *WrapperTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
if v.view != nil { if v.view != nil {
return v.view.OnKeyPressed(event) return v.view.OnKeyPressed(event)
@ -24,6 +29,14 @@ func (v *WrapperTmpl) OnKeyPressed(event *KeyEvent) (consumed bool) {
return v.ViewTmpl.OnKeyPressed(event) 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 { func (v *WrapperTmpl) Style() Style {
if v.view != nil { if v.view != nil {
return v.view.Style() return v.view.Style()
@ -31,37 +44,7 @@ func (v *WrapperTmpl) Style() Style {
return v.ViewTmpl.Style() return v.ViewTmpl.Style()
} }
func (v *WrapperTmpl) Foreground() Color { func (v *WrapperTmpl) Views() []View {
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 {
return []View{v.view} return []View{v.view}
} }