diff --git a/tests/screen_test.go b/tests/screen_test.go index da3f979..a67726a 100644 --- a/tests/screen_test.go +++ b/tests/screen_test.go @@ -55,6 +55,46 @@ func TestFlowGroup(t *testing.T) { fmt.Println(err) } +func TestSeparatorGroup(t *testing.T) { + textView := views.NewTextView("hello world!") + textView.SetStyle(tui.StyleDefault.Background(tcell.ColorRed).Foreground(tcell.ColorBlack)) + + frameView := views.NewFrameView(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)) + + separatorGroup := views.NewSeparatorGroup(tui.Vertical) + separatorGroup.AppendView(frameView, 1) + separatorGroup.AppendView(growView, 1) + separatorGroup.AppendView(textView2, 1) + + screen, err := tui.NewScreen(separatorGroup) + if err != nil { + t.Error(err) + return + } + + 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())))) + } + + return true + } + + err = screen.Start() + fmt.Println(err) +} + func TestBorderGroup(t *testing.T) { topView := views.NewConstrainView(nil) topView.SetStyle(tui.StyleDefault.Background(tcell.ColorBlue)) diff --git a/types.go b/types.go index 7535e59..429b0e3 100644 --- a/types.go +++ b/types.go @@ -35,8 +35,22 @@ const ( type Side uint8 const ( - Top Side = iota - Bottom - Left - Right + SideTop Side = iota + SideBottom + SideLeft + SideRight +) + +type Anchor uint8 + +const ( + AnchorTopLeft Anchor = iota + AnchorTop + AnchorTopRight + AnchorLeft + AnchorCenter + AnchorRight + AnchorBottomLeft + AnchorBottom + AnchorBottomRight ) diff --git a/utils.go b/utils.go index 1ba456f..8a28863 100644 --- a/utils.go +++ b/utils.go @@ -87,3 +87,28 @@ func iff[T any](condition bool, trueValue, falseValue T) T { } return falseValue } + +func ConstrainBufferToAnchor(buf *ViewBuffer, anchor Anchor, width, height int) *ViewBuffer { + switch anchor { + default: + fallthrough + case AnchorTopLeft: + return buf.Sub(0, 0, width, height) + case AnchorTop: + return buf.Sub(buf.Width()/2-width/2, 0, width, height) + case AnchorTopRight: + return buf.Sub(buf.Width()-width, 0, width, height) + case AnchorLeft: + return buf.Sub(0, buf.Height()/2-height/2, width, height) + case AnchorCenter: + return buf.Sub(buf.Width()/2-width/2, buf.Height()/2-height/2, width, height) + case AnchorRight: + return buf.Sub(buf.Width()-width, buf.Height()/2-height/2, width, height) + case AnchorBottomLeft: + return buf.Sub(0, buf.Height()-height, width, height) + case AnchorBottom: + return buf.Sub(buf.Width()/2-width/2, buf.Height()-height, width, height) + case AnchorBottomRight: + return buf.Sub(buf.Width()-width, buf.Height()-height, width, height) + } +} diff --git a/view.go b/view.go index 09d30f4..1f2ad6b 100644 --- a/view.go +++ b/view.go @@ -10,7 +10,14 @@ type View interface { SetStyle(s Style) Style() Style + // Layout is usually called by the parent view to ask the view's preferred size. + // If the parent view does not care about its preferred size, it might not be called at all. + // Negative values indicate as much space as possible. Layout() (prefWidth, prefHeight int) + + // Draw is called for each view when it should print itself onto the screen. + // The parent view has full control of the ViewBuffer size + // and may or may not use the values returned from Layout() to set the size. Draw(buf *ViewBuffer) } diff --git a/views/bordergroup.go b/views/bordergroup.go index e4e8f06..0150a33 100644 --- a/views/bordergroup.go +++ b/views/bordergroup.go @@ -4,7 +4,7 @@ import ( "git.tordarus.net/Tordarus/tui" ) -// BorderGroup ia a tui.Group which places its children in a linear layout +// BorderGroup ia a tui.Group which places its children onto a given tui.Side type BorderGroup struct { tui.ViewTmpl views map[Slot]tui.View diff --git a/views/flowgroup.go b/views/flowgroup.go index 181911f..d255499 100644 --- a/views/flowgroup.go +++ b/views/flowgroup.go @@ -39,6 +39,15 @@ 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) RemoveView(v tui.View) { + for index, view := range g.Views() { + if view == v { + g.views = append(g.views[:index], g.views[index:]...) + return + } + } +} + func (g *FlowGroup) Draw(buf *tui.ViewBuffer) { g.ViewTmpl.Draw(buf) diff --git a/views/frameview.go b/views/frameview.go new file mode 100644 index 0000000..f4e3437 --- /dev/null +++ b/views/frameview.go @@ -0,0 +1,35 @@ +package views + +import "git.tordarus.net/Tordarus/tui" + +// FrameView is a tui.Wrapper which draws its view preferably with preferred size on its tui.Anchor point +type FrameView struct { + tui.WrapperTmpl + Anchor tui.Anchor +} + +var _ tui.View = &FrameView{} + +func NewFrameView(view tui.View) *FrameView { + v := new(FrameView) + v.SetView(view) + v.Anchor = tui.AnchorCenter + return v +} + +func (g *FrameView) Draw(buf *tui.ViewBuffer) { + g.ViewTmpl.Draw(buf) + + w, h := g.View().Layout() + w = iff(w >= 0, w, buf.Width()) + h = iff(h >= 0, h, buf.Height()) + g.View().Draw(tui.ConstrainBufferToAnchor(buf, g.Anchor, w, h)) +} + +func (v *FrameView) Layout() (prefWidth, prefHeight int) { + return -1, -1 +} + +func (v *FrameView) Style() tui.Style { + return v.ViewTmpl.Style() +} diff --git a/views/marginview.go b/views/marginview.go index 4ef9fdc..b231dc7 100644 --- a/views/marginview.go +++ b/views/marginview.go @@ -18,10 +18,10 @@ func NewMarginView(view tui.View) *MarginView { } 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] + x := g.Margin[tui.SideLeft] + y := g.Margin[tui.SideTop] + w := buf.Width() - x - g.Margin[tui.SideRight] + h := buf.Height() - y - g.Margin[tui.SideBottom] g.ViewTmpl.Draw(buf) g.View().Draw(buf.Sub(x, y, w, h)) @@ -29,8 +29,8 @@ func (g *MarginView) Draw(buf *tui.ViewBuffer) { 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) + w = iff(w > 0, w+v.Margin[tui.SideLeft]+v.Margin[tui.SideRight], w) + h = iff(h > 0, h+v.Margin[tui.SideTop]+v.Margin[tui.SideBottom], h) return w, h } @@ -40,9 +40,9 @@ func (v *MarginView) Style() tui.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, + tui.SideTop: top, + tui.SideRight: right, + tui.SideBottom: bottom, + tui.SideLeft: left, } } diff --git a/views/separatorgroup.go b/views/separatorgroup.go new file mode 100644 index 0000000..0ad0374 --- /dev/null +++ b/views/separatorgroup.go @@ -0,0 +1,111 @@ +package views + +import ( + "git.tordarus.net/Tordarus/tui" +) + +// SeperatorGroup ia a tui.Group which separates +type SeperatorGroup struct { + tui.ViewTmpl + views []tui.View + + gravity map[tui.View]int + gravitySum int + + Orientation tui.Orientation +} + +var _ tui.Group = &SeperatorGroup{} + +func NewSeparatorGroup(orientation tui.Orientation) *SeperatorGroup { + return &SeperatorGroup{ + views: make([]tui.View, 0), + gravity: map[tui.View]int{}, + Orientation: orientation, + } +} + +func (g *SeperatorGroup) Views() []tui.View { + return g.views[:] +} + +func (g *SeperatorGroup) AppendView(v tui.View, gravity int) { + g.views = append(g.views, v) + g.gravitySum += gravity + g.gravity[v] = gravity +} + +func (g *SeperatorGroup) PrependView(v tui.View, gravity int) { + g.views = append([]tui.View{v}, g.views...) + g.gravitySum += gravity + g.gravity[v] = gravity +} + +func (g *SeperatorGroup) InsertView(v tui.View, index int, gravity int) { + g.views = append(g.views[:index], append([]tui.View{v}, g.views[index:]...)...) + g.gravitySum += gravity + g.gravity[v] = gravity +} + +func (g *SeperatorGroup) SetGravity(v tui.View, gravity int) { + for _, view := range g.Views() { + if view == v { + g.gravitySum += gravity - g.gravity[v] + g.gravity[v] = gravity + return + } + } +} + +func (g *SeperatorGroup) RemoveView(v tui.View) { + for index, view := range g.Views() { + if view == v { + g.views = append(g.views[:index], g.views[index:]...) + g.gravitySum -= g.gravity[v] + delete(g.gravity, v) + return + } + } +} + +func (g *SeperatorGroup) View(slot Slot) tui.View { + return g.views[slot] +} + +func (g *SeperatorGroup) Draw(buf *tui.ViewBuffer) { + g.ViewTmpl.Draw(buf) + + if g.Orientation == tui.Horizontal { + x := 0 + for _, v := range g.Views() { + viewGravity := g.gravity[v] + percentage := float64(viewGravity) / float64(g.gravitySum) + width := int(percentage * float64(buf.Width())) + v.Draw(buf.Sub(x, 0, width, buf.Height())) + x += width + } + } else if g.Orientation == tui.Vertical { + y := 0 + for _, v := range g.Views() { + viewGravity := g.gravity[v] + percentage := float64(viewGravity) / float64(g.gravitySum) + height := int(percentage * float64(buf.Height())) + v.Draw(buf.Sub(0, y, buf.Width(), height)) + y += height + } + } + +} + +func (g *SeperatorGroup) Layout() (prefWidth, prefHeight int) { + return -1, -1 +} + +func (g *SeperatorGroup) OnKeyPressed(event *tui.KeyEvent) (consumed bool) { + for _, view := range g.Views() { + if view.OnKeyPressed(event) { + return true + } + } + return false +} diff --git a/views/textview.go b/views/textview.go index 7d23a9b..6be4400 100644 --- a/views/textview.go +++ b/views/textview.go @@ -13,6 +13,7 @@ type TextView struct { var _ tui.View = &TextView{} func (v *TextView) Draw(buf *tui.ViewBuffer) { + v.ViewTmpl.Draw(buf) tui.WriteMultiLineString(buf, v.Text, v.Style(), 0, 0) }