1
0
mirror of https://github.com/mum4k/termdash.git synced 2025-04-27 13:48:49 +08:00

Merge pull request #166 from mum4k/padding-and-margin

Containers now support padding and margin
This commit is contained in:
Jakub Sobon 2019-03-03 03:03:02 -05:00 committed by GitHub
commit aa688e223e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1388 additions and 40 deletions

View File

@ -14,4 +14,4 @@ script:
- diff -u <(echo -n) <(./internal/scripts/autogen_licences.sh .)
- diff -u <(echo -n) <(golint ./...)
after_success:
- ./scripts/coverage.sh
- ./internal/scripts/coverage.sh

View File

@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added
- Test coverage for data only packages.
- Containers now support margin around them and padding of their content.
### Changed

View File

@ -86,12 +86,18 @@ func New(t terminalapi.Terminal, opts ...Option) (*Container, error) {
if err := applyOptions(root, opts...); err != nil {
return nil, err
}
ar, err := root.opts.margin.apply(root.area)
if err != nil {
return nil, err
}
root.area = ar
return root, nil
}
// newChild creates a new child container of the given parent.
func newChild(parent *Container, area image.Rectangle) *Container {
return &Container{
func newChild(parent *Container, area image.Rectangle, opts []Option) (*Container, error) {
child := &Container{
parent: parent,
term: parent.term,
focusTracker: parent.focusTracker,
@ -99,6 +105,16 @@ func newChild(parent *Container, area image.Rectangle) *Container {
opts: newOptions(parent.opts),
mu: parent.mu,
}
if err := applyOptions(child, opts...); err != nil {
return nil, err
}
ar, err := child.opts.margin.apply(child.area)
if err != nil {
return nil, err
}
child.area = ar
return child, nil
}
// hasBorder determines if this container has a border.
@ -129,9 +145,13 @@ func (c *Container) widgetArea() (image.Rectangle, error) {
return image.ZR, nil
}
adjusted := c.usable()
padded, err := c.opts.padding.apply(c.usable())
if err != nil {
return image.ZR, err
}
wOpts := c.opts.widget.Options()
adjusted := padded
if maxX := wOpts.MaximumSize.X; maxX > 0 && adjusted.Dx() > maxX {
adjusted.Max.X -= adjusted.Dx() - maxX
}
@ -142,17 +162,20 @@ func (c *Container) widgetArea() (image.Rectangle, error) {
if wOpts.Ratio.X > 0 && wOpts.Ratio.Y > 0 {
adjusted = area.WithRatio(adjusted, wOpts.Ratio)
}
adjusted, err := alignfor.Rectangle(c.usable(), adjusted, c.opts.hAlign, c.opts.vAlign)
aligned, err := alignfor.Rectangle(padded, adjusted, c.opts.hAlign, c.opts.vAlign)
if err != nil {
return image.ZR, err
}
return adjusted, nil
return aligned, nil
}
// split splits the container's usable area into child areas.
// Panics if the container isn't configured for a split.
func (c *Container) split() (image.Rectangle, image.Rectangle, error) {
ar := c.usable()
ar, err := c.opts.padding.apply(c.usable())
if err != nil {
return image.ZR, image.ZR, err
}
if c.opts.split == splitTypeVertical {
return area.VSplit(ar, c.opts.splitPercent)
}
@ -160,23 +183,31 @@ func (c *Container) split() (image.Rectangle, image.Rectangle, error) {
}
// createFirst creates and returns the first sub container of this container.
func (c *Container) createFirst() (*Container, error) {
func (c *Container) createFirst(opts []Option) error {
ar, _, err := c.split()
if err != nil {
return nil, err
return err
}
c.first = newChild(c, ar)
return c.first, nil
first, err := newChild(c, ar, opts)
if err != nil {
return err
}
c.first = first
return nil
}
// createSecond creates and returns the second sub container of this container.
func (c *Container) createSecond() (*Container, error) {
func (c *Container) createSecond(opts []Option) error {
_, ar, err := c.split()
if err != nil {
return nil, err
return err
}
c.second = newChild(c, ar)
return c.second, nil
second, err := newChild(c, ar, opts)
if err != nil {
return err
}
c.second = second
return nil
}
// Draw draws this container and all of its sub containers.

View File

@ -83,6 +83,326 @@ func TestNew(t *testing.T) {
wantContainerErr bool
want func(size image.Point) *faketerm.Terminal
}{
{
desc: "fails on MarginTop too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginTop(-1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginTopPercent too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginTopPercent(-1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginTopPercent too high",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginTopPercent(101))
},
wantContainerErr: true,
},
{
desc: "fails when both MarginTop and MarginTopPercent specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginTop(1), MarginTopPercent(1))
},
wantContainerErr: true,
},
{
desc: "fails when both MarginTopPercent and MarginTop specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginTopPercent(1), MarginTop(1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginRight too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginRight(-1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginRightPercent too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginRightPercent(-1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginRightPercent too high",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginRightPercent(101))
},
wantContainerErr: true,
},
{
desc: "fails when both MarginRight and MarginRightPercent specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginRight(1), MarginRightPercent(1))
},
wantContainerErr: true,
},
{
desc: "fails when both MarginRightPercent and MarginRight specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginRightPercent(1), MarginRight(1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginBottom too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginBottom(-1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginBottomPercent too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginBottomPercent(-1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginBottomPercent too high",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginBottomPercent(101))
},
wantContainerErr: true,
},
{
desc: "fails when both MarginBottom and MarginBottomPercent specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginBottom(1), MarginBottomPercent(1))
},
wantContainerErr: true,
},
{
desc: "fails when both MarginBottomPercent and MarginBottom specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginBottomPercent(1), MarginBottom(1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginLeft too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginLeft(-1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginLeftPercent too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginLeftPercent(-1))
},
wantContainerErr: true,
},
{
desc: "fails on MarginLeftPercent too high",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginLeftPercent(101))
},
wantContainerErr: true,
},
{
desc: "fails when both MarginLeft and MarginLeftPercent specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginLeft(1), MarginLeftPercent(1))
},
wantContainerErr: true,
},
{
desc: "fails when both MarginLeftPercent and MarginLeft specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, MarginLeftPercent(1), MarginLeft(1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingTop too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingTop(-1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingTopPercent too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingTopPercent(-1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingTopPercent too high",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingTopPercent(101))
},
wantContainerErr: true,
},
{
desc: "fails when both PaddingTop and PaddingTopPercent specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingTop(1), PaddingTopPercent(1))
},
wantContainerErr: true,
},
{
desc: "fails when both PaddingTopPercent and PaddingTop specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingTopPercent(1), PaddingTop(1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingRight too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingRight(-1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingRightPercent too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingRightPercent(-1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingRightPercent too high",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingRightPercent(101))
},
wantContainerErr: true,
},
{
desc: "fails when both PaddingRight and PaddingRightPercent specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingRight(1), PaddingRightPercent(1))
},
wantContainerErr: true,
},
{
desc: "fails when both PaddingRightPercent and PaddingRight specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingRightPercent(1), PaddingRight(1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingBottom too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingBottom(-1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingBottomPercent too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingBottomPercent(-1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingBottomPercent too high",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingBottomPercent(101))
},
wantContainerErr: true,
},
{
desc: "fails when both PaddingBottom and PaddingBottomPercent specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingBottom(1), PaddingBottomPercent(1))
},
wantContainerErr: true,
},
{
desc: "fails when both PaddingBottomPercent and PaddingBottom specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingBottomPercent(1), PaddingBottom(1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingLeft too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingLeft(-1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingLeftPercent too low",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingLeftPercent(-1))
},
wantContainerErr: true,
},
{
desc: "fails on PaddingLeftPercent too high",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingLeftPercent(101))
},
wantContainerErr: true,
},
{
desc: "fails when both PaddingLeft and PaddingLeftPercent specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingLeft(1), PaddingLeftPercent(1))
},
wantContainerErr: true,
},
{
desc: "fails when both PaddingLeftPercent and PaddingLeft specified",
termSize: image.Point{10, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(ft, PaddingLeftPercent(1), PaddingLeft(1))
},
wantContainerErr: true,
},
{
desc: "empty container",
termSize: image.Point{10, 10},
@ -526,7 +846,17 @@ func TestNew(t *testing.T) {
t.Fatalf("Draw => unexpected error: %v", err)
}
if diff := faketerm.Diff(tc.want(tc.termSize), got); diff != "" {
var want *faketerm.Terminal
if tc.want != nil {
want = tc.want(tc.termSize)
} else {
w, err := faketerm.New(tc.termSize)
if err != nil {
t.Fatalf("faketerm.New => unexpected error: %v", err)
}
want = w
}
if diff := faketerm.Diff(want, got); diff != "" {
t.Errorf("Draw => %v", diff)
}
})

View File

@ -33,7 +33,11 @@ func drawTree(c *Container) error {
root := rootCont(c)
size := root.term.Size()
root.area = image.Rect(0, 0, size.X, size.Y)
ar, err := root.opts.margin.apply(image.Rect(0, 0, size.X, size.Y))
if err != nil {
return err
}
root.area = ar
preOrder(root, &errStr, visitFunc(func(c *Container) error {
first, second, err := c.split()
@ -41,11 +45,19 @@ func drawTree(c *Container) error {
return err
}
if c.first != nil {
c.first.area = first
ar, err := c.first.opts.margin.apply(first)
if err != nil {
return err
}
c.first.area = ar
}
if c.second != nil {
c.second.area = second
ar, err := c.second.opts.margin.apply(second)
if err != nil {
return err
}
c.second.area = ar
}
return drawCont(c)
}))

View File

@ -64,6 +64,305 @@ func TestDrawWidget(t *testing.T) {
return ft
},
},
{
desc: "absolute margin on root container",
termSize: image.Point{20, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Border(linestyle.Light),
MarginTop(1),
MarginRight(2),
MarginBottom(3),
MarginLeft(4),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(image.Rect(4, 1, 18, 7))
// Container border.
testdraw.MustBorder(
cvs,
cvs.Area(),
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
)
testcanvas.MustApply(cvs, ft)
return ft
},
},
{
desc: "relative margin on root container",
termSize: image.Point{20, 20},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Border(linestyle.Light),
MarginTopPercent(10),
MarginRightPercent(20),
MarginBottomPercent(50),
MarginLeftPercent(40),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(image.Rect(8, 2, 16, 10))
// Container border.
testdraw.MustBorder(
cvs,
cvs.Area(),
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
)
testcanvas.MustApply(cvs, ft)
return ft
},
},
{
desc: "draws vertical sub-containers with margin",
termSize: image.Point{20, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Border(linestyle.Light),
SplitVertical(
Left(
Border(linestyle.Double),
MarginTop(1),
MarginRight(2),
MarginBottom(3),
MarginLeft(4),
),
Right(
Border(linestyle.Double),
MarginTop(3),
MarginRight(4),
MarginBottom(1),
MarginLeft(2),
),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
// Outer container border.
testdraw.MustBorder(
cvs,
cvs.Area(),
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
)
// Borders around the sub-containers.
testdraw.MustBorder(
cvs,
image.Rect(5, 2, 8, 6),
draw.BorderLineStyle(linestyle.Double),
)
testdraw.MustBorder(
cvs,
image.Rect(12, 4, 15, 8),
draw.BorderLineStyle(linestyle.Double),
)
testcanvas.MustApply(cvs, ft)
return ft
},
},
{
desc: "draws horizontal sub-containers with margin",
termSize: image.Point{20, 20},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Border(linestyle.Light),
SplitHorizontal(
Top(
Border(linestyle.Double),
MarginTop(1),
MarginRight(2),
MarginBottom(3),
MarginLeft(4),
),
Bottom(
Border(linestyle.Double),
MarginTop(3),
MarginRight(4),
MarginBottom(1),
MarginLeft(2),
),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
// Outer container border.
testdraw.MustBorder(
cvs,
cvs.Area(),
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
)
// Borders around the sub-containers.
testdraw.MustBorder(
cvs,
image.Rect(5, 2, 17, 7),
draw.BorderLineStyle(linestyle.Double),
)
testdraw.MustBorder(
cvs,
image.Rect(3, 13, 15, 18),
draw.BorderLineStyle(linestyle.Double),
)
testcanvas.MustApply(cvs, ft)
return ft
},
},
{
desc: "draws padded widget, absolute padding",
termSize: image.Point{20, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Border(linestyle.Light),
PlaceWidget(fakewidget.New(widgetapi.Options{})),
PaddingTop(1),
PaddingRight(2),
PaddingBottom(3),
PaddingLeft(4),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
// Container border.
testdraw.MustBorder(
cvs,
cvs.Area(),
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
)
wAr := image.Rect(5, 2, 17, 6)
wCvs := testcanvas.MustNew(wAr)
// Fake widget border.
fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
testcanvas.MustCopyTo(wCvs, cvs)
testcanvas.MustApply(cvs, ft)
return ft
},
},
{
desc: "draws padded widget, relative padding",
termSize: image.Point{20, 20},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Border(linestyle.Light),
PlaceWidget(fakewidget.New(widgetapi.Options{})),
PaddingTopPercent(10),
PaddingRightPercent(30),
PaddingBottomPercent(20),
PaddingLeftPercent(20),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
// Container border.
testdraw.MustBorder(
cvs,
cvs.Area(),
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
)
wAr := image.Rect(4, 2, 14, 16)
wCvs := testcanvas.MustNew(wAr)
// Fake widget border.
fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
testcanvas.MustCopyTo(wCvs, cvs)
testcanvas.MustApply(cvs, ft)
return ft
},
},
{
desc: "draws padded sub-containers",
termSize: image.Point{20, 10},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
PaddingTop(1),
PaddingRight(2),
PaddingBottom(3),
PaddingLeft(4),
Border(linestyle.Light),
SplitVertical(
Left(Border(linestyle.Double)),
Right(Border(linestyle.Double)),
),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
// Outer container border.
testdraw.MustBorder(
cvs,
cvs.Area(),
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
)
// Borders around the sub-containers.
testdraw.MustBorder(
cvs,
image.Rect(5, 2, 11, 6),
draw.BorderLineStyle(linestyle.Double),
)
testdraw.MustBorder(
cvs,
image.Rect(11, 2, 17, 6),
draw.BorderLineStyle(linestyle.Double),
)
testcanvas.MustApply(cvs, ft)
return ft
},
},
{
desc: "draws with both padding and margin enabled",
termSize: image.Point{30, 20},
container: func(ft *faketerm.Terminal) (*Container, error) {
return New(
ft,
Border(linestyle.Light),
PlaceWidget(fakewidget.New(widgetapi.Options{})),
PaddingTop(1),
PaddingRight(2),
PaddingBottom(3),
PaddingLeft(4),
MarginTop(1),
MarginRight(2),
MarginBottom(3),
MarginLeft(4),
)
},
want: func(size image.Point) *faketerm.Terminal {
ft := faketerm.MustNew(size)
cvs := testcanvas.MustNew(ft.Area())
// Container border.
testdraw.MustBorder(
cvs,
image.Rect(4, 1, 28, 17),
draw.BorderCellOpts(cell.FgColor(cell.ColorYellow)),
)
wAr := image.Rect(9, 3, 25, 13)
wCvs := testcanvas.MustNew(wAr)
// Fake widget border.
fakewidget.MustDraw(ft, wCvs, widgetapi.Options{})
testcanvas.MustCopyTo(wCvs, cvs)
testcanvas.MustApply(cvs, ft)
return ft
},
},
{
desc: "draws widget with container border and title aligned on the left",
termSize: image.Point{9, 5},

View File

@ -18,9 +18,11 @@ package container
import (
"fmt"
"image"
"github.com/mum4k/termdash/align"
"github.com/mum4k/termdash/cell"
"github.com/mum4k/termdash/internal/area"
"github.com/mum4k/termdash/linestyle"
"github.com/mum4k/termdash/widgetapi"
)
@ -63,6 +65,61 @@ type options struct {
border linestyle.LineStyle
borderTitle string
borderTitleHAlign align.Horizontal
// padding is a space reserved between the outer edge of the container and
// its content (the widget or other sub-containers).
padding padding
// margin is a space reserved on the outside of the container.
margin margin
}
// margin stores the configured margin for the container.
// For each margin direction, only one of the percentage or cells is set.
type margin struct {
topCells int
topPerc int
rightCells int
rightPerc int
bottomCells int
bottomPerc int
leftCells int
leftPerc int
}
// apply applies the configured margin to the area.
func (p *margin) apply(ar image.Rectangle) (image.Rectangle, error) {
switch {
case p.topCells != 0 || p.rightCells != 0 || p.bottomCells != 0 || p.leftCells != 0:
return area.Shrink(ar, p.topCells, p.rightCells, p.bottomCells, p.leftCells)
case p.topPerc != 0 || p.rightPerc != 0 || p.bottomPerc != 0 || p.leftPerc != 0:
return area.ShrinkPercent(ar, p.topPerc, p.rightPerc, p.bottomPerc, p.leftPerc)
}
return ar, nil
}
// padding stores the configured padding for the container.
// For each padding direction, only one of the percentage or cells is set.
type padding struct {
topCells int
topPerc int
rightCells int
rightPerc int
bottomCells int
bottomPerc int
leftCells int
leftPerc int
}
// apply applies the configured padding to the area.
func (p *padding) apply(ar image.Rectangle) (image.Rectangle, error) {
switch {
case p.topCells != 0 || p.rightCells != 0 || p.bottomCells != 0 || p.leftCells != 0:
return area.Shrink(ar, p.topCells, p.rightCells, p.bottomCells, p.leftCells)
case p.topPerc != 0 || p.rightPerc != 0 || p.bottomPerc != 0 || p.leftPerc != 0:
return area.ShrinkPercent(ar, p.topPerc, p.rightPerc, p.bottomPerc, p.leftPerc)
}
return ar, nil
}
// inherited contains options that are inherited by child containers.
@ -146,19 +203,10 @@ func SplitVertical(l LeftOption, r RightOption, opts ...SplitOption) Option {
}
}
f, err := c.createFirst()
if err != nil {
if err := c.createFirst(l.lOpts()); err != nil {
return err
}
if err := applyOptions(f, l.lOpts()...); err != nil {
return err
}
s, err := c.createSecond()
if err != nil {
return err
}
return applyOptions(s, r.rOpts()...)
return c.createSecond(r.rOpts())
})
}
@ -175,19 +223,11 @@ func SplitHorizontal(t TopOption, b BottomOption, opts ...SplitOption) Option {
}
}
f, err := c.createFirst()
if err != nil {
return err
}
if err := applyOptions(f, t.tOpts()...); err != nil {
if err := c.createFirst(t.tOpts()); err != nil {
return err
}
s, err := c.createSecond()
if err != nil {
return err
}
return applyOptions(s, b.bOpts()...)
return c.createSecond(b.bOpts())
})
}
@ -203,6 +243,278 @@ func PlaceWidget(w widgetapi.Widget) Option {
})
}
// MarginTop sets reserved space outside of the container at its top.
// The provided number is the absolute margin in cells and must be zero or a
// positive integer. Only one of MarginTop or MarginTopPercent can be specified.
func MarginTop(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid MarginTop(%d), must be in range %d <= value", cells, min)
}
if c.opts.margin.topPerc > 0 {
return fmt.Errorf("cannot specify both MarginTop(%d) and MarginTopPercent(%d)", cells, c.opts.margin.topPerc)
}
c.opts.margin.topCells = cells
return nil
})
}
// MarginRight sets reserved space outside of the container at its right.
// The provided number is the absolute margin in cells and must be zero or a
// positive integer. Only one of MarginRight or MarginRightPercent can be specified.
func MarginRight(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid MarginRight(%d), must be in range %d <= value", cells, min)
}
if c.opts.margin.rightPerc > 0 {
return fmt.Errorf("cannot specify both MarginRight(%d) and MarginRightPercent(%d)", cells, c.opts.margin.rightPerc)
}
c.opts.margin.rightCells = cells
return nil
})
}
// MarginBottom sets reserved space outside of the container at its bottom.
// The provided number is the absolute margin in cells and must be zero or a
// positive integer. Only one of MarginBottom or MarginBottomPercent can be specified.
func MarginBottom(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid MarginBottom(%d), must be in range %d <= value", cells, min)
}
if c.opts.margin.bottomPerc > 0 {
return fmt.Errorf("cannot specify both MarginBottom(%d) and MarginBottomPercent(%d)", cells, c.opts.margin.bottomPerc)
}
c.opts.margin.bottomCells = cells
return nil
})
}
// MarginLeft sets reserved space outside of the container at its left.
// The provided number is the absolute margin in cells and must be zero or a
// positive integer. Only one of MarginLeft or MarginLeftPercent can be specified.
func MarginLeft(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid MarginLeft(%d), must be in range %d <= value", cells, min)
}
if c.opts.margin.leftPerc > 0 {
return fmt.Errorf("cannot specify both MarginLeft(%d) and MarginLeftPercent(%d)", cells, c.opts.margin.leftPerc)
}
c.opts.margin.leftCells = cells
return nil
})
}
// MarginTopPercent sets reserved space outside of the container at its top.
// The provided number is a relative margin defined as percentage of the container's height.
// Only one of MarginTop or MarginTopPercent can be specified.
// The value must be in range 0 <= value <= 100.
func MarginTopPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid MarginTopPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.margin.topCells > 0 {
return fmt.Errorf("cannot specify both MarginTopPercent(%d) and MarginTop(%d)", perc, c.opts.margin.topCells)
}
c.opts.margin.topPerc = perc
return nil
})
}
// MarginRightPercent sets reserved space outside of the container at its right.
// The provided number is a relative margin defined as percentage of the container's height.
// Only one of MarginRight or MarginRightPercent can be specified.
// The value must be in range 0 <= value <= 100.
func MarginRightPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid MarginRightPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.margin.rightCells > 0 {
return fmt.Errorf("cannot specify both MarginRightPercent(%d) and MarginRight(%d)", perc, c.opts.margin.rightCells)
}
c.opts.margin.rightPerc = perc
return nil
})
}
// MarginBottomPercent sets reserved space outside of the container at its bottom.
// The provided number is a relative margin defined as percentage of the container's height.
// Only one of MarginBottom or MarginBottomPercent can be specified.
// The value must be in range 0 <= value <= 100.
func MarginBottomPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid MarginBottomPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.margin.bottomCells > 0 {
return fmt.Errorf("cannot specify both MarginBottomPercent(%d) and MarginBottom(%d)", perc, c.opts.margin.bottomCells)
}
c.opts.margin.bottomPerc = perc
return nil
})
}
// MarginLeftPercent sets reserved space outside of the container at its left.
// The provided number is a relative margin defined as percentage of the container's height.
// Only one of MarginLeft or MarginLeftPercent can be specified.
// The value must be in range 0 <= value <= 100.
func MarginLeftPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid MarginLeftPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.margin.leftCells > 0 {
return fmt.Errorf("cannot specify both MarginLeftPercent(%d) and MarginLeft(%d)", perc, c.opts.margin.leftCells)
}
c.opts.margin.leftPerc = perc
return nil
})
}
// PaddingTop sets reserved space between container and the top side of its widget.
// The widget's area size is decreased to accommodate the padding.
// The provided number is the absolute padding in cells and must be zero or a
// positive integer. Only one of PaddingTop or PaddingTopPercent can be specified.
func PaddingTop(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid PaddingTop(%d), must be in range %d <= value", cells, min)
}
if c.opts.padding.topPerc > 0 {
return fmt.Errorf("cannot specify both PaddingTop(%d) and PaddingTopPercent(%d)", cells, c.opts.padding.topPerc)
}
c.opts.padding.topCells = cells
return nil
})
}
// PaddingRight sets reserved space between container and the right side of its widget.
// The widget's area size is decreased to accommodate the padding.
// The provided number is the absolute padding in cells and must be zero or a
// positive integer. Only one of PaddingRight or PaddingRightPercent can be specified.
func PaddingRight(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid PaddingRight(%d), must be in range %d <= value", cells, min)
}
if c.opts.padding.rightPerc > 0 {
return fmt.Errorf("cannot specify both PaddingRight(%d) and PaddingRightPercent(%d)", cells, c.opts.padding.rightPerc)
}
c.opts.padding.rightCells = cells
return nil
})
}
// PaddingBottom sets reserved space between container and the bottom side of its widget.
// The widget's area size is decreased to accommodate the padding.
// The provided number is the absolute padding in cells and must be zero or a
// positive integer. Only one of PaddingBottom or PaddingBottomPercent can be specified.
func PaddingBottom(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid PaddingBottom(%d), must be in range %d <= value", cells, min)
}
if c.opts.padding.bottomPerc > 0 {
return fmt.Errorf("cannot specify both PaddingBottom(%d) and PaddingBottomPercent(%d)", cells, c.opts.padding.bottomPerc)
}
c.opts.padding.bottomCells = cells
return nil
})
}
// PaddingLeft sets reserved space between container and the left side of its widget.
// The widget's area size is decreased to accommodate the padding.
// The provided number is the absolute padding in cells and must be zero or a
// positive integer. Only one of PaddingLeft or PaddingLeftPercent can be specified.
func PaddingLeft(cells int) Option {
return option(func(c *Container) error {
if min := 0; cells < min {
return fmt.Errorf("invalid PaddingLeft(%d), must be in range %d <= value", cells, min)
}
if c.opts.padding.leftPerc > 0 {
return fmt.Errorf("cannot specify both PaddingLeft(%d) and PaddingLeftPercent(%d)", cells, c.opts.padding.leftPerc)
}
c.opts.padding.leftCells = cells
return nil
})
}
// PaddingTopPercent sets reserved space between container and the top side of
// its widget. The widget's area size is decreased to accommodate the padding.
// The provided number is a relative padding defined as percentage of the
// container's height. The value must be in range 0 <= value <= 100.
// Only one of PaddingTop or PaddingTopPercent can be specified.
func PaddingTopPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid PaddingTopPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.padding.topCells > 0 {
return fmt.Errorf("cannot specify both PaddingTopPercent(%d) and PaddingTop(%d)", perc, c.opts.padding.topCells)
}
c.opts.padding.topPerc = perc
return nil
})
}
// PaddingRightPercent sets reserved space between container and the right side of
// its widget. The widget's area size is decreased to accommodate the padding.
// The provided number is a relative padding defined as percentage of the
// container's width. The value must be in range 0 <= value <= 100.
// Only one of PaddingRight or PaddingRightPercent can be specified.
func PaddingRightPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid PaddingRightPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.padding.rightCells > 0 {
return fmt.Errorf("cannot specify both PaddingRightPercent(%d) and PaddingRight(%d)", perc, c.opts.padding.rightCells)
}
c.opts.padding.rightPerc = perc
return nil
})
}
// PaddingBottomPercent sets reserved space between container and the bottom side of
// its widget. The widget's area size is decreased to accommodate the padding.
// The provided number is a relative padding defined as percentage of the
// container's height. The value must be in range 0 <= value <= 100.
// Only one of PaddingBottom or PaddingBottomPercent can be specified.
func PaddingBottomPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid PaddingBottomPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.padding.bottomCells > 0 {
return fmt.Errorf("cannot specify both PaddingBottomPercent(%d) and PaddingBottom(%d)", perc, c.opts.padding.bottomCells)
}
c.opts.padding.bottomPerc = perc
return nil
})
}
// PaddingLeftPercent sets reserved space between container and the left side of
// its widget. The widget's area size is decreased to accommodate the padding.
// The provided number is a relative padding defined as percentage of the
// container's width. The value must be in range 0 <= value <= 100.
// Only one of PaddingLeft or PaddingLeftPercent can be specified.
func PaddingLeftPercent(perc int) Option {
return option(func(c *Container) error {
if min, max := 0, 100; perc < min || perc > max {
return fmt.Errorf("invalid PaddingLeftPercent(%d), must be in range %d <= value <= %d", perc, min, max)
}
if c.opts.padding.leftCells > 0 {
return fmt.Errorf("cannot specify both PaddingLeftPercent(%d) and PaddingLeft(%d)", perc, c.opts.padding.leftCells)
}
c.opts.padding.leftPerc = perc
return nil
})
}
// AlignHorizontal sets the horizontal alignment for the widget placed in the
// container. Has no effect if the container contains no widget.
// Defaults to alignment in the center.

View File

@ -121,3 +121,60 @@ func WithRatio(area image.Rectangle, ratio image.Point) image.Rectangle {
ratio.Y*fact+area.Min.Y,
)
}
// Shrink returns a new area whose size is reduced by the specified amount of
// cells. Can return a zero area if there is no space left in the area.
// The values must be zero or positive integers.
func Shrink(area image.Rectangle, topCells, rightCells, bottomCells, leftCells int) (image.Rectangle, error) {
for _, v := range []struct {
name string
value int
}{
{"topCells", topCells},
{"rightCells", rightCells},
{"bottomCells", bottomCells},
{"leftCells", leftCells},
} {
if min := 0; v.value < min {
return image.ZR, fmt.Errorf("invalid %s(%d), must be in range %d <= value", v.name, v.value, min)
}
}
shrinked := area
shrinked.Min.X, _ = numbers.MinMaxInts([]int{shrinked.Min.X + leftCells, shrinked.Max.X})
_, shrinked.Max.X = numbers.MinMaxInts([]int{shrinked.Max.X - rightCells, shrinked.Min.X})
shrinked.Min.Y, _ = numbers.MinMaxInts([]int{shrinked.Min.Y + topCells, shrinked.Max.Y})
_, shrinked.Max.Y = numbers.MinMaxInts([]int{shrinked.Max.Y - bottomCells, shrinked.Min.Y})
if shrinked.Dx() == 0 || shrinked.Dy() == 0 {
return image.ZR, nil
}
return shrinked, nil
}
// ShrinkPercent returns a new area whose size is reduced by percentage of its
// width or height. Can return a zero area if there is no space left in the area.
// The topPerc and bottomPerc indicate the percentage of area's height.
// The rightPerc and leftPerc indicate the percentage of area's width.
// The percentages must be in range 0 <= v <= 100.
func ShrinkPercent(area image.Rectangle, topPerc, rightPerc, bottomPerc, leftPerc int) (image.Rectangle, error) {
for _, v := range []struct {
name string
value int
}{
{"topPerc", topPerc},
{"rightPerc", rightPerc},
{"bottomPerc", bottomPerc},
{"leftPerc", leftPerc},
} {
if min, max := 0, 100; v.value < min || v.value > max {
return image.ZR, fmt.Errorf("invalid %s(%d), must be in range %d <= value <= %d", v.name, v.value, min, max)
}
}
top := area.Dy() * topPerc / 100
bottom := area.Dy() * bottomPerc / 100
right := area.Dx() * rightPerc / 100
left := area.Dx() * leftPerc / 100
return Shrink(area, top, right, bottom, left)
}

View File

@ -398,3 +398,306 @@ func TestWithRatio(t *testing.T) {
})
}
}
func TestShrink(t *testing.T) {
tests := []struct {
desc string
area image.Rectangle
top, right, bottom, left int
want image.Rectangle
wantErr bool
}{
{
desc: "fails for negative top",
area: image.Rect(0, 0, 1, 1),
top: -1,
right: 0,
bottom: 0,
left: 0,
wantErr: true,
},
{
desc: "fails for negative right",
area: image.Rect(0, 0, 1, 1),
top: 0,
right: -1,
bottom: 0,
left: 0,
wantErr: true,
},
{
desc: "fails for negative bottom",
area: image.Rect(0, 0, 1, 1),
top: 0,
right: 0,
bottom: -1,
left: 0,
wantErr: true,
},
{
desc: "fails for negative left",
area: image.Rect(0, 0, 1, 1),
top: 0,
right: 0,
bottom: 0,
left: -1,
wantErr: true,
},
{
desc: "area unchanged when all zero",
area: image.Rect(7, 8, 9, 10),
top: 0,
right: 0,
bottom: 0,
left: 0,
want: image.Rect(7, 8, 9, 10),
},
{
desc: "shrinks top",
area: image.Rect(7, 8, 17, 18),
top: 1,
right: 0,
bottom: 0,
left: 0,
want: image.Rect(7, 9, 17, 18),
},
{
desc: "zero area when top too large",
area: image.Rect(7, 8, 17, 18),
top: 10,
right: 0,
bottom: 0,
left: 0,
want: image.ZR,
},
{
desc: "shrinks bottom",
area: image.Rect(7, 8, 17, 18),
top: 0,
right: 0,
bottom: 1,
left: 0,
want: image.Rect(7, 8, 17, 17),
},
{
desc: "zero area when bottom too large",
area: image.Rect(7, 8, 17, 18),
top: 0,
right: 0,
bottom: 10,
left: 0,
want: image.ZR,
},
{
desc: "zero area when top and bottom cross",
area: image.Rect(7, 8, 17, 18),
top: 5,
right: 0,
bottom: 5,
left: 0,
want: image.ZR,
},
{
desc: "zero area when top and bottom overrun",
area: image.Rect(7, 8, 17, 18),
top: 50,
right: 0,
bottom: 50,
left: 0,
want: image.ZR,
},
{
desc: "shrinks right",
area: image.Rect(7, 8, 17, 18),
top: 0,
right: 1,
bottom: 0,
left: 0,
want: image.Rect(7, 8, 16, 18),
},
{
desc: "zero area when right too large",
area: image.Rect(7, 8, 17, 18),
top: 0,
right: 10,
bottom: 0,
left: 0,
want: image.ZR,
},
{
desc: "shrinks left",
area: image.Rect(7, 8, 17, 18),
top: 0,
right: 0,
bottom: 0,
left: 1,
want: image.Rect(8, 8, 17, 18),
},
{
desc: "zero area when left too large",
area: image.Rect(7, 8, 17, 18),
top: 0,
right: 0,
bottom: 0,
left: 10,
want: image.ZR,
},
{
desc: "zero area when right and left cross",
area: image.Rect(7, 8, 17, 18),
top: 0,
right: 5,
bottom: 0,
left: 5,
want: image.ZR,
},
{
desc: "zero area when right and left overrun",
area: image.Rect(7, 8, 17, 18),
top: 0,
right: 50,
bottom: 0,
left: 50,
want: image.ZR,
},
{
desc: "shrinks from all sides",
area: image.Rect(7, 8, 17, 18),
top: 1,
right: 2,
bottom: 3,
left: 4,
want: image.Rect(11, 9, 15, 15),
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := Shrink(tc.area, tc.top, tc.right, tc.bottom, tc.left)
if (err != nil) != tc.wantErr {
t.Errorf("Shrink => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("Shrink => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}
func TestShrinkPercent(t *testing.T) {
tests := []struct {
desc string
area image.Rectangle
top, right, bottom, left int
want image.Rectangle
wantErr bool
}{
{
desc: "fails on top too low",
top: -1,
wantErr: true,
},
{
desc: "fails on top too high",
top: 101,
wantErr: true,
},
{
desc: "fails on right too low",
right: -1,
wantErr: true,
},
{
desc: "fails on right too high",
right: 101,
wantErr: true,
},
{
desc: "fails on bottom too low",
bottom: -1,
wantErr: true,
},
{
desc: "fails on bottom too high",
bottom: 101,
wantErr: true,
},
{
desc: "fails on left too low",
left: -1,
wantErr: true,
},
{
desc: "fails on left too high",
left: 101,
wantErr: true,
},
{
desc: "shrinks to zero area for top too large",
area: image.Rect(0, 0, 100, 100),
top: 100,
want: image.ZR,
},
{
desc: "shrinks to zero area for bottom too large",
area: image.Rect(0, 0, 100, 100),
bottom: 100,
want: image.ZR,
},
{
desc: "shrinks to zero area top and bottom that meet",
area: image.Rect(0, 0, 100, 100),
top: 50,
bottom: 50,
want: image.ZR,
},
{
desc: "shrinks to zero area for right too large",
area: image.Rect(0, 0, 100, 100),
right: 100,
want: image.ZR,
},
{
desc: "shrinks to zero area for left too large",
area: image.Rect(0, 0, 100, 100),
left: 100,
want: image.ZR,
},
{
desc: "shrinks to zero area right and left that meet",
area: image.Rect(0, 0, 100, 100),
right: 50,
left: 50,
want: image.ZR,
},
{
desc: "shrinks from all sides",
area: image.Rect(0, 0, 100, 100),
top: 10,
right: 20,
bottom: 30,
left: 40,
want: image.Rect(40, 10, 80, 70),
},
}
for _, tc := range tests {
t.Run(tc.desc, func(t *testing.T) {
got, err := ShrinkPercent(tc.area, tc.top, tc.right, tc.bottom, tc.left)
if (err != nil) != tc.wantErr {
t.Errorf("ShrinkPercent => unexpected error: %v, wantErr: %v", err, tc.wantErr)
}
if err != nil {
return
}
if diff := pretty.Compare(tc.want, got); diff != "" {
t.Errorf("ShrinkPercent => unexpected diff (-want, +got):\n%s", diff)
}
})
}
}

View File

@ -80,6 +80,7 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
container.Top(container.PlaceWidget(spGreen)),
container.Bottom(container.PlaceWidget(spRed)),
),
container.MarginLeft(1),
),
),
),
@ -148,10 +149,12 @@ func layout(ctx context.Context, t terminalapi.Terminal) (*container.Container,
container.Left(
container.PlaceWidget(leftB),
container.AlignHorizontal(align.HorizontalRight),
container.PaddingRight(1),
),
container.Right(
container.PlaceWidget(rightB),
container.AlignHorizontal(align.HorizontalLeft),
container.PaddingLeft(1),
),
),
),