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:
commit
aa688e223e
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
|
@ -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)
|
||||
}))
|
||||
|
@ -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},
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
Loading…
x
Reference in New Issue
Block a user