From dfa00f5fe319298a3ed5f7113cbc8cc641d6a59b Mon Sep 17 00:00:00 2001 From: Timon Ringwald Date: Sat, 2 Apr 2022 13:01:41 +0200 Subject: [PATCH] more views --- draw_buffer.go | 27 +++++++- events.go | 3 + go.mod | 4 +- go.sum | 7 +- screen.go | 27 ++++++-- tests/screen_test.go | 81 ++++++++++++++++++++-- types.go | 29 ++++++++ utils.go | 66 ++++++++++++++++-- view.go | 19 ++---- viewgroup.go | 3 - views/bordergroup.go | 149 +++++++++++++++++++++++++++++++++++++++++ views/borderview.go | 93 +++++++++++++++++++++++++ views/constrainview.go | 29 ++++++++ views/coordgroup.go | 49 ++++++++++++++ views/eventview.go | 27 -------- views/flowgroup.go | 114 +++++++++++++++++++++++++++++++ views/growview.go | 20 ++++++ views/marginview.go | 48 +++++++++++++ views/textview.go | 5 ++ views/utils.go | 86 ++++++++++++++++++++++++ viewtmpl.go | 40 ++++------- wrappertmpl.go | 49 +++++--------- 22 files changed, 848 insertions(+), 127 deletions(-) delete mode 100644 viewgroup.go create mode 100644 views/bordergroup.go create mode 100644 views/borderview.go create mode 100644 views/constrainview.go create mode 100644 views/coordgroup.go delete mode 100644 views/eventview.go create mode 100644 views/flowgroup.go create mode 100644 views/growview.go create mode 100644 views/marginview.go create mode 100644 views/utils.go diff --git a/draw_buffer.go b/draw_buffer.go index 29a168e..82b886a 100644 --- a/draw_buffer.go +++ b/draw_buffer.go @@ -5,8 +5,31 @@ import ( ) func drawBuffer(scr tcell.Screen, buf *ViewBuffer) { - buf.ForEach(func(x, y int, rn Rune) { - scr.SetContent(x, y, rn.Rn, nil, rn.Style) + // 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) +} diff --git a/events.go b/events.go index fea5151..8f40df2 100644 --- a/events.go +++ b/events.go @@ -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) } diff --git a/go.mod b/go.mod index a5519c9..363173b 100644 --- a/go.mod +++ b/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 ) diff --git a/go.sum b/go.sum index 01c1580..0369fb0 100644 --- a/go.sum +++ b/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= diff --git a/screen.go b/screen.go index b0a5e67..ba9e39c 100644 --- a/screen.go +++ b/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) } diff --git a/tests/screen_test.go b/tests/screen_test.go index 5c0bd0b..da3f979 100644 --- a/tests/screen_test.go +++ b/tests/screen_test.go @@ -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 } diff --git a/types.go b/types.go index cf30060..7535e59 100644 --- a/types.go +++ b/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 +) diff --git a/utils.go b/utils.go index 9d2d87f..1ba456f 100644 --- a/utils.go +++ b/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 +} diff --git a/view.go b/view.go index 7912478..09d30f4 100644 --- a/view.go +++ b/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 diff --git a/viewgroup.go b/viewgroup.go deleted file mode 100644 index b37474c..0000000 --- a/viewgroup.go +++ /dev/null @@ -1,3 +0,0 @@ -package tui - -// TODO GroupTmpl diff --git a/views/bordergroup.go b/views/bordergroup.go new file mode 100644 index 0000000..e4e8f06 --- /dev/null +++ b/views/bordergroup.go @@ -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 +) diff --git a/views/borderview.go b/views/borderview.go new file mode 100644 index 0000000..8174f66 --- /dev/null +++ b/views/borderview.go @@ -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: '║', + } +} diff --git a/views/constrainview.go b/views/constrainview.go new file mode 100644 index 0000000..5f0010d --- /dev/null +++ b/views/constrainview.go @@ -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 +} diff --git a/views/coordgroup.go b/views/coordgroup.go new file mode 100644 index 0000000..2fdc5dd --- /dev/null +++ b/views/coordgroup.go @@ -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 +} diff --git a/views/eventview.go b/views/eventview.go deleted file mode 100644 index 5f5d3a9..0000000 --- a/views/eventview.go +++ /dev/null @@ -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) -} diff --git a/views/flowgroup.go b/views/flowgroup.go new file mode 100644 index 0000000..181911f --- /dev/null +++ b/views/flowgroup.go @@ -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 +} diff --git a/views/growview.go b/views/growview.go new file mode 100644 index 0000000..27bdeda --- /dev/null +++ b/views/growview.go @@ -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 +} diff --git a/views/marginview.go b/views/marginview.go new file mode 100644 index 0000000..4ef9fdc --- /dev/null +++ b/views/marginview.go @@ -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, + } +} diff --git a/views/textview.go b/views/textview.go index 1b16b14..7d23a9b 100644 --- a/views/textview.go +++ b/views/textview.go @@ -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) +} diff --git a/views/utils.go b/views/utils.go new file mode 100644 index 0000000..52ea38e --- /dev/null +++ b/views/utils.go @@ -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 +} diff --git a/viewtmpl.go b/viewtmpl.go index fdfc120..a050c34 100644 --- a/viewtmpl.go +++ b/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()) -} - -func (v *ViewTmpl) Foreground() Color { - if v.foreground == nil { - return tcell.ColorDefault + if v.style == nil { + return StyleDefault } - 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 } diff --git a/wrappertmpl.go b/wrappertmpl.go index 3786b1f..64c2c36 100644 --- a/wrappertmpl.go +++ b/wrappertmpl.go @@ -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} }