diff --git a/container/container.go b/container/container.go index 6070efe..fc41caf 100644 --- a/container/container.go +++ b/container/container.go @@ -180,14 +180,26 @@ func (c *Container) split() (image.Rectangle, image.Rectangle, error) { } if c.opts.splitFixed > DefaultSplitFixed { if c.opts.split == splitTypeVertical { + if c.opts.splitReversed { + return area.VSplitCellsReversed(ar, c.opts.splitFixed) + } return area.VSplitCells(ar, c.opts.splitFixed) } + if c.opts.splitReversed { + return area.HSplitCellsReversed(ar, c.opts.splitFixed) + } return area.HSplitCells(ar, c.opts.splitFixed) } if c.opts.split == splitTypeVertical { + if c.opts.splitReversed { + return area.VSplitReversed(ar, c.opts.splitPercent) + } return area.VSplit(ar, c.opts.splitPercent) } + if c.opts.splitReversed { + return area.HSplitReversed(ar, c.opts.splitPercent) + } return area.HSplit(ar, c.opts.splitPercent) } diff --git a/container/container_test.go b/container/container_test.go index f16c315..efe2af5 100644 --- a/container/container_test.go +++ b/container/container_test.go @@ -713,7 +713,33 @@ func TestNew(t *testing.T) { }, }, { - desc: "horizontal unequal split", + desc: "horizontal, reversed unequal split", + termSize: image.Point{10, 20}, + container: func(ft *faketerm.Terminal) (*Container, error) { + return New( + ft, + SplitHorizontal( + Top( + Border(linestyle.Light), + ), + Bottom( + Border(linestyle.Light), + ), + SplitPercentFromEnd(20), + ), + ) + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 16)) + testdraw.MustBorder(cvs, image.Rect(0, 16, 10, 20)) + testcanvas.MustApply(cvs, ft) + return ft + }, + }, + { + desc: "horizontal fixed splits", termSize: image.Point{10, 20}, container: func(ft *faketerm.Terminal) (*Container, error) { return New( @@ -738,6 +764,32 @@ func TestNew(t *testing.T) { return ft }, }, + { + desc: "horizontal, reversed fixed splits", + termSize: image.Point{10, 20}, + container: func(ft *faketerm.Terminal) (*Container, error) { + return New( + ft, + SplitHorizontal( + Top( + Border(linestyle.Light), + ), + Bottom( + Border(linestyle.Light), + ), + SplitFixedFromEnd(4), + ), + ) + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + testdraw.MustBorder(cvs, image.Rect(0, 0, 10, 16)) + testdraw.MustBorder(cvs, image.Rect(0, 16, 10, 20)) + testcanvas.MustApply(cvs, ft) + return ft + }, + }, { desc: "horizontal split, parent and children have borders", termSize: image.Point{10, 10}, @@ -864,6 +916,32 @@ func TestNew(t *testing.T) { return ft }, }, + { + desc: "vertical, reversed unequal split", + termSize: image.Point{20, 10}, + container: func(ft *faketerm.Terminal) (*Container, error) { + return New( + ft, + SplitVertical( + Left( + Border(linestyle.Light), + ), + Right( + Border(linestyle.Light), + ), + SplitPercentFromEnd(20), + ), + ) + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + testdraw.MustBorder(cvs, image.Rect(0, 0, 16, 10)) + testdraw.MustBorder(cvs, image.Rect(16, 0, 20, 10)) + testcanvas.MustApply(cvs, ft) + return ft + }, + }, { desc: "vertical fixed splits", termSize: image.Point{20, 10}, @@ -890,6 +968,32 @@ func TestNew(t *testing.T) { return ft }, }, + { + desc: "vertical, reversed fixed splits", + termSize: image.Point{20, 10}, + container: func(ft *faketerm.Terminal) (*Container, error) { + return New( + ft, + SplitVertical( + Left( + Border(linestyle.Light), + ), + Right( + Border(linestyle.Light), + ), + SplitFixedFromEnd(4), + ), + ) + }, + want: func(size image.Point) *faketerm.Terminal { + ft := faketerm.MustNew(size) + cvs := testcanvas.MustNew(ft.Area()) + testdraw.MustBorder(cvs, image.Rect(0, 0, 16, 10)) + testdraw.MustBorder(cvs, image.Rect(16, 0, 20, 10)) + testcanvas.MustApply(cvs, ft) + return ft + }, + }, { desc: "vertical split, parent and children have borders", termSize: image.Point{10, 10}, diff --git a/container/options.go b/container/options.go index 1fa718a..764429f 100644 --- a/container/options.go +++ b/container/options.go @@ -108,9 +108,10 @@ type options struct { inherited inherited // split identifies how is this container split. - split splitType - splitPercent int - splitFixed int + split splitType + splitReversed bool + splitPercent int + splitFixed int // widget is the widget in the container. // A container can have either two sub containers (left and right) or a @@ -247,10 +248,11 @@ func newOptions(parent *options) *options { inherited: inherited{ focusedColor: cell.ColorYellow, }, - hAlign: align.HorizontalCenter, - vAlign: align.VerticalMiddle, - splitPercent: DefaultSplitPercent, - splitFixed: DefaultSplitFixed, + hAlign: align.HorizontalCenter, + vAlign: align.VerticalMiddle, + splitReversed: DefaultSplitReversed, + splitPercent: DefaultSplitPercent, + splitFixed: DefaultSplitFixed, } if parent != nil { opts.global = parent.global @@ -281,13 +283,17 @@ func (so splitOption) setSplit(opts *options) error { return so(opts) } +// DefaultSplitReversed is the default value for the SplitReversed option. +const DefaultSplitReversed = false + // DefaultSplitPercent is the default value for the SplitPercent option. const DefaultSplitPercent = 50 // DefaultSplitFixed is the default value for the SplitFixed option. const DefaultSplitFixed = -1 -// SplitPercent sets the relative size of the split as percentage of the available space. +// SplitPercent sets the relative size of the split as percentage of the +// available space. // When using SplitVertical, the provided size is applied to the new left // container, the new right container gets the reminder of the size. // When using SplitHorizontal, the provided size is applied to the new top @@ -304,6 +310,25 @@ func SplitPercent(p int) SplitOption { }) } +// SplitPercentFromEnd sets the relative size of the split as percentage of the +// available space. +// When using SplitVertical, the provided size is applied to the new right +// container, the new left container gets the reminder of the size. +// When using SplitHorizontal, the provided size is applied to the new bottom +// container, the new top container gets the reminder of the size. +// The provided value must be a positive number in the range 0 < p < 100. +// If not provided, defaults to using SplitPercent with DefaultSplitPercent. +func SplitPercentFromEnd(p int) SplitOption { + return splitOption(func(opts *options) error { + if min, max := 0, 100; p <= min || p >= max { + return fmt.Errorf("invalid split percentage %d, must be in range %d < p < %d", p, min, max) + } + opts.splitReversed = true + opts.splitPercent = p + return nil + }) +} + // SplitFixed sets the size of the first container to be a fixed value // and makes the second container take up the remaining space. // When using SplitVertical, the provided size is applied to the new left @@ -311,8 +336,9 @@ func SplitPercent(p int) SplitOption { // When using SplitHorizontal, the provided size is applied to the new top // container, the new bottom container gets the reminder of the size. // The provided value must be a positive number in the range 0 <= cells. -// If SplitFixed() is not specified, it defaults to SplitPercent() and its given value. -// Only one of SplitFixed() and SplitPercent() can be specified per container. +// If SplitFixed* or SplitPercent* is not specified, it defaults to +// SplitPercent() and its given value. +// Only one SplitFixed* or SplitPercent* may be specified per container. func SplitFixed(cells int) SplitOption { return splitOption(func(opts *options) error { if cells < 0 { @@ -323,6 +349,27 @@ func SplitFixed(cells int) SplitOption { }) } +// SplitFixedFromEnd sets the size of the second container to be a fixed value +// and makes the first container take up the remaining space. +// When using SplitVertical, the provided size is applied to the new right +// container, the new left container gets the reminder of the size. +// When using SplitHorizontal, the provided size is applied to the new bottom +// container, the new top container gets the reminder of the size. +// The provided value must be a positive number in the range 0 <= cells. +// If SplitFixed* or SplitPercent* is not specified, it defaults to +// SplitPercent() and its given value. +// Only one SplitFixed* or SplitPercent* may be specified per container. +func SplitFixedFromEnd(cells int) SplitOption { + return splitOption(func(opts *options) error { + if cells < 0 { + return fmt.Errorf("invalid fixed value %d, must be in range %d <= cells", cells, 0) + } + opts.splitFixed = cells + opts.splitReversed = true + return nil + }) +} + // SplitVertical splits the container along the vertical axis into two sub // containers. The use of this option removes any widget placed at this // container, containers with sub containers cannot contain widgets. diff --git a/private/area/area.go b/private/area/area.go index 34b21a1..94fceca 100644 --- a/private/area/area.go +++ b/private/area/area.go @@ -38,92 +38,210 @@ func FromSize(size image.Point) (image.Rectangle, error) { return image.Rect(0, 0, size.X, size.Y), nil } -// HSplit returns two new areas created by splitting the provided area at the -// specified percentage of its width. The percentage must be in the range -// 0 <= heightPerc <= 100. +// hSplit returns two new areas created by splitting the provided area at the +// specified percentage of its height, applying the percentage to the top or +// bottom area, depending on the reversed flag. The percentage must be in the +// range 0 <= heightPerc <= 100. // Can return zero size areas. -func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { +func hSplit(area image.Rectangle, heightPerc int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) { if min, max := 0, 100; heightPerc < min || heightPerc > max { return image.ZR, image.ZR, fmt.Errorf("invalid heightPerc %d, must be in range %d <= heightPerc <= %d", heightPerc, min, max) } + height := area.Dy() * heightPerc / 100 - top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height) + + if reversed { + top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Max.Y-height) + bottom = image.Rect(area.Min.X, area.Max.Y-height, area.Max.X, area.Max.Y) + } else { + top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+height) + bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y) + } + if top.Dy() == 0 { top = image.ZR } - bottom = image.Rect(area.Min.X, area.Min.Y+height, area.Max.X, area.Max.Y) if bottom.Dy() == 0 { bottom = image.ZR } + return top, bottom, nil } -// VSplit returns two new areas created by splitting the provided area at the -// specified percentage of its width. The percentage must be in the range -// 0 <= widthPerc <= 100. +// HSplit returns two new areas created by splitting the provided area at the +// specified percentage of its height, applying the percentage to the top area. +// The percentage must be in the range 0 <= heightPerc <= 100. // Can return zero size areas. -func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { +func HSplit(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { + return hSplit(area, heightPerc, false) +} + +// HSplitReversed returns two new areas created by splitting the provided area +// at the specified percentage of its height, applying the percentage to the +// bottom area. The percentage must be in the range 0 <= heightPerc <= 100. +// Can return zero size areas. +func HSplitReversed(area image.Rectangle, heightPerc int) (top image.Rectangle, bottom image.Rectangle, err error) { + return hSplit(area, heightPerc, true) +} + +// vSplit returns two new areas created by splitting the provided area at the +// specified percentage of its width, applying the percentage to the left or +// right area, depending on the reversed flag. The percentage must be in the +// range 0 <= widthPerc <= 100. +// Can return zero size areas. +func vSplit(area image.Rectangle, widthPerc int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) { if min, max := 0, 100; widthPerc < min || widthPerc > max { return image.ZR, image.ZR, fmt.Errorf("invalid widthPerc %d, must be in range %d <= widthPerc <= %d", widthPerc, min, max) } + width := area.Dx() * widthPerc / 100 - left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y) + + if reversed { + left = image.Rect(area.Min.X, area.Min.Y, area.Max.X-width, area.Max.Y) + right = image.Rect(area.Max.X-width, area.Min.Y, area.Max.X, area.Max.Y) + } else { + left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+width, area.Max.Y) + right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y) + } + if left.Dx() == 0 { left = image.ZR } - right = image.Rect(area.Min.X+width, area.Min.Y, area.Max.X, area.Max.Y) if right.Dx() == 0 { right = image.ZR } + return left, right, nil } -// VSplitCells returns two new areas created by splitting the provided area -// after the specified amount of cells of its width. The number of cells must -// be a zero or a positive integer. Providing a zero returns left=image.ZR, +// VSplit returns two new areas created by splitting the provided area at the +// specified percentage of its width, applying the percentage to the left area. +// The percentage must be in the range 0 <= widthPerc <= 100. +// Can return zero size areas. +func VSplit(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplit(area, widthPerc, false) +} + +// VSplitReversed returns two new areas created by splitting the provided area +// at the specified percentage of its width, applying the percentage to the +// right area. The percentage must be in the range 0 <= widthPerc <= 100. +// Can return zero size areas. +func VSplitReversed(area image.Rectangle, widthPerc int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplit(area, widthPerc, true) +} + +// vSplitCells returns two new areas created by splitting the provided area +// after the specified amount of cells of its width, applied to the left or +// right area, depending on the reversed flag. The number of cells must be a +// zero or a positive integer. Providing a zero returns left=image.ZR, // right=area. Providing a number equal or larger to area's width returns // left=area, right=image.ZR. -func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { +func vSplitCells(area image.Rectangle, cells int, reversed bool) (left image.Rectangle, right image.Rectangle, err error) { if min := 0; cells < min { return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) } if cells == 0 { + if reversed { + return area, image.ZR, nil + } return image.ZR, area, nil } width := area.Dx() if cells >= width { + if reversed { + return image.ZR, area, nil + } return area, image.ZR, nil } - left = image.Rect(area.Min.X, area.Min.Y, area.Min.X+cells, area.Max.Y) - right = image.Rect(area.Min.X+cells, area.Min.Y, area.Max.X, area.Max.Y) + splitX := area.Min.X + if reversed { + splitX = area.Max.X - cells + } else { + splitX = area.Min.X + cells + } + + left = image.Rect(area.Min.X, area.Min.Y, splitX, area.Max.Y) + right = image.Rect(splitX, area.Min.Y, area.Max.X, area.Max.Y) + return left, right, nil } -// HSplitCells returns two new areas created by splitting the provided area -// after the specified amount of cells of its height. The number of cells must -// be a zero or a positive integer. Providing a zero returns top=image.ZR, +// VSplitCells returns two new areas created by splitting the provided area +// after the specified amount of cells of its width, as applied to the left +// area. The number of cells must be a zero or a positive integer. Providing a +// zero returns left=image.ZR, right=area. Providing a number equal or larger to +// area's width returns left=area, right=image.ZR. +func VSplitCells(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplitCells(area, cells, false) +} + +// VSplitCellsReversed returns two new areas created by splitting the provided +// area after the specified amount of cells of its width, as applied to the +// right area. The number of cells must be a zero or a positive integer. +// Providing a zero returns left=image.ZR, right=area. Providing a number equal +// or larger to area's width returns left=area, right=image.ZR. +func VSplitCellsReversed(area image.Rectangle, cells int) (left image.Rectangle, right image.Rectangle, err error) { + return vSplitCells(area, cells, true) +} + +// hSplitCells returns two new areas created by splitting the provided area +// after the specified amount of cells of its height, applied to the top or +// bottom area, depending on the reversed flag. The number of cells must be a +// zero or a positive integer. Providing a zero returns top=image.ZR, // bottom=area. Providing a number equal or larger to area's height returns // top=area, bottom=image.ZR. -func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) { +func hSplitCells(area image.Rectangle, cells int, reversed bool) (top image.Rectangle, bottom image.Rectangle, err error) { if min := 0; cells < min { return image.ZR, image.ZR, fmt.Errorf("invalid cells %d, must be a positive integer", cells) } if cells == 0 { + if reversed { + return area, image.ZR, nil + } return image.ZR, area, nil } height := area.Dy() if cells >= height { + if reversed { + return image.ZR, area, nil + } return area, image.ZR, nil } - top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, area.Min.Y+cells) - bottom = image.Rect(area.Min.X, area.Min.Y+cells, area.Max.X, area.Max.Y) + splitY := area.Min.Y + if reversed { + splitY = area.Max.Y - cells + } else { + splitY = area.Min.Y + cells + } + + top = image.Rect(area.Min.X, area.Min.Y, area.Max.X, splitY) + bottom = image.Rect(area.Min.X, splitY, area.Max.X, area.Max.Y) + return top, bottom, nil } +// HSplitCells returns two new areas created by splitting the provided area +// after the specified amount of cells of its height, as applied to the top +// area. The number of cells must be a zero or a positive integer. Providing a +// zero returns top=image.ZR, bottom=area. Providing a number equal or larger to +// area's height returns top=area, bottom=image.ZR. +func HSplitCells(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) { + return hSplitCells(area, cells, false) +} + +// HSplitCellsReversed returns two new areas created by splitting the provided +// area after the specified amount of cells of its height, as applied to the +// bottom area. The number of cells must be a zero or a positive integer. +// Providing a zero returns top=area, bottom=image.ZR. Providing a number equal +// or larger to area's height returns top=image.ZR, bottom=area. +func HSplitCellsReversed(area image.Rectangle, cells int) (top image.Rectangle, bottom image.Rectangle, err error) { + return hSplitCells(area, cells, true) +} + // ExcludeBorder returns a new area created by subtracting a border around the // provided area. Return the zero area if there isn't enough space to exclude // the border. diff --git a/private/area/area_test.go b/private/area/area_test.go index f4a46eb..fcf2ae4 100644 --- a/private/area/area_test.go +++ b/private/area/area_test.go @@ -198,6 +198,87 @@ func TestHSplit(t *testing.T) { } } +func TestHSplitReversed(t *testing.T) { + tests := []struct { + desc string + area image.Rectangle + heightPerc int + wantTop image.Rectangle + wantBot image.Rectangle + wantErr bool + }{ + { + desc: "fails on heightPerc too small", + area: image.Rect(1, 1, 2, 2), + heightPerc: -1, + wantErr: true, + }, + { + desc: "fails on heightPerc too large", + area: image.Rect(1, 1, 2, 2), + heightPerc: 101, + wantErr: true, + }, + { + desc: "zero area to begin with", + area: image.ZR, + heightPerc: 50, + wantTop: image.ZR, + wantBot: image.ZR, + }, + { + desc: "splitting results in zero height area on the bottom", + area: image.Rect(1, 1, 2, 2), + heightPerc: 0, + wantTop: image.Rect(1, 1, 2, 2), + wantBot: image.ZR, + }, + { + desc: "splitting results in 100 height area on the top", + area: image.Rect(1, 1, 2, 2), + heightPerc: 100, + wantTop: image.ZR, + wantBot: image.Rect(1, 1, 2, 2), + }, + { + desc: "splits area with even height", + area: image.Rect(1, 1, 3, 3), + heightPerc: 50, + wantTop: image.Rect(1, 1, 3, 2), + wantBot: image.Rect(1, 2, 3, 3), + }, + { + desc: "splits area with odd height", + area: image.Rect(1, 1, 4, 4), + heightPerc: 50, + wantTop: image.Rect(1, 1, 4, 3), + wantBot: image.Rect(1, 3, 4, 4), + }, + { + desc: "splits to unequal areas", + area: image.Rect(0, 0, 4, 4), + heightPerc: 25, + wantTop: image.Rect(0, 0, 4, 3), + wantBot: image.Rect(0, 3, 4, 4), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + gotTop, gotBot, err := HSplitReversed(tc.area, tc.heightPerc) + if (err != nil) != tc.wantErr { + t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr) + } + if diff := pretty.Compare(tc.wantTop, gotTop); diff != "" { + t.Errorf("HSplit => first value unexpected diff (-want, +got):\n%s", diff) + } + if diff := pretty.Compare(tc.wantBot, gotBot); diff != "" { + t.Errorf("HSplit => second value unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + func TestVSplit(t *testing.T) { tests := []struct { desc string @@ -282,6 +363,90 @@ func TestVSplit(t *testing.T) { } } +func TestVSplitReversed(t *testing.T) { + tests := []struct { + desc string + area image.Rectangle + widthPerc int + wantLeft image.Rectangle + wantRight image.Rectangle + wantErr bool + }{ + { + desc: "fails on widthPerc too small", + area: image.Rect(1, 1, 2, 2), + widthPerc: -1, + wantErr: true, + }, + { + desc: "fails on widthPerc too large", + area: image.Rect(1, 1, 2, 2), + widthPerc: 101, + wantErr: true, + }, + { + desc: "zero area to begin with", + area: image.ZR, + widthPerc: 50, + wantLeft: image.ZR, + wantRight: image.ZR, + }, + { + desc: "splitting results in zero width area on the right", + area: image.Rect(1, 1, 2, 2), + widthPerc: 0, + wantLeft: image.Rect(1, 1, 2, 2), + wantRight: image.ZR, + }, + { + desc: "splitting results in zero width area on the left", + area: image.Rect(1, 1, 2, 2), + widthPerc: 100, + wantLeft: image.ZR, + wantRight: image.Rect(1, 1, 2, 2), + }, + { + desc: "splits area with even width", + area: image.Rect(1, 1, 3, 3), + widthPerc: 50, + wantLeft: image.Rect(1, 1, 2, 3), + wantRight: image.Rect(2, 1, 3, 3), + }, + { + desc: "splits area with odd width", + area: image.Rect(1, 1, 4, 4), + widthPerc: 50, + wantLeft: image.Rect(1, 1, 3, 4), + wantRight: image.Rect(3, 1, 4, 4), + }, + { + desc: "splits to unequal areas", + area: image.Rect(0, 0, 4, 4), + widthPerc: 25, + wantLeft: image.Rect(0, 0, 3, 4), + wantRight: image.Rect(3, 0, 4, 4), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + gotLeft, gotRight, err := VSplitReversed(tc.area, tc.widthPerc) + if (err != nil) != tc.wantErr { + t.Errorf("VSplit => unexpected error:%v, wantErr:%v", err, tc.wantErr) + } + if err != nil { + return + } + if diff := pretty.Compare(tc.wantLeft, gotLeft); diff != "" { + t.Errorf("VSplit => left value unexpected diff (-want, +got):\n%s", diff) + } + if diff := pretty.Compare(tc.wantRight, gotRight); diff != "" { + t.Errorf("VSplit => right value unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + func TestVSplitCells(t *testing.T) { tests := []struct { desc string @@ -367,6 +532,91 @@ func TestVSplitCells(t *testing.T) { } } +func TestVSplitCellsReversed(t *testing.T) { + tests := []struct { + desc string + area image.Rectangle + cells int + wantLeft image.Rectangle + wantRight image.Rectangle + wantErr bool + }{ + { + desc: "fails on negative cells", + area: image.Rect(1, 1, 2, 2), + cells: -1, + wantErr: true, + }, + { + desc: "returns area as left on cells too large", + area: image.Rect(1, 1, 2, 2), + cells: 2, + wantLeft: image.ZR, + wantRight: image.Rect(1, 1, 2, 2), + }, + { + desc: "returns area as left on cells equal area width", + area: image.Rect(1, 1, 2, 2), + cells: 1, + wantLeft: image.ZR, + wantRight: image.Rect(1, 1, 2, 2), + }, + { + desc: "returns area as right on zero cells", + area: image.Rect(1, 1, 2, 2), + cells: 0, + wantRight: image.ZR, + wantLeft: image.Rect(1, 1, 2, 2), + }, + { + desc: "zero area to begin with", + area: image.ZR, + cells: 0, + wantLeft: image.ZR, + wantRight: image.ZR, + }, + { + desc: "splits area with even width", + area: image.Rect(1, 1, 3, 3), + cells: 1, + wantLeft: image.Rect(1, 1, 2, 3), + wantRight: image.Rect(2, 1, 3, 3), + }, + { + desc: "splits area with odd width", + area: image.Rect(1, 1, 4, 4), + cells: 1, + wantLeft: image.Rect(1, 1, 3, 4), + wantRight: image.Rect(3, 1, 4, 4), + }, + { + desc: "splits to unequal areas", + area: image.Rect(0, 0, 4, 4), + cells: 3, + wantLeft: image.Rect(0, 0, 1, 4), + wantRight: image.Rect(1, 0, 4, 4), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + gotLeft, gotRight, err := VSplitCellsReversed(tc.area, tc.cells) + if (err != nil) != tc.wantErr { + t.Errorf("VSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr) + } + if err != nil { + return + } + if diff := pretty.Compare(tc.wantLeft, gotLeft); diff != "" { + t.Errorf("VSplitCells => left value unexpected diff (-want, +got):\n%s", diff) + } + if diff := pretty.Compare(tc.wantRight, gotRight); diff != "" { + t.Errorf("VSplitCells => right value unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + func TestHSplitCells(t *testing.T) { tests := []struct { desc string @@ -452,6 +702,91 @@ func TestHSplitCells(t *testing.T) { } } +func TestHSplitCellsReversed(t *testing.T) { + tests := []struct { + desc string + area image.Rectangle + cells int + wantTop image.Rectangle + wantBottom image.Rectangle + wantErr bool + }{ + { + desc: "fails on negative cells", + area: image.Rect(1, 1, 2, 2), + cells: -1, + wantErr: true, + }, + { + desc: "returns area as bottom on cells too large", + area: image.Rect(1, 1, 2, 2), + cells: 2, + wantTop: image.ZR, + wantBottom: image.Rect(1, 1, 2, 2), + }, + { + desc: "returns area as bottom on cells equal area width", + area: image.Rect(1, 1, 2, 2), + cells: 1, + wantTop: image.ZR, + wantBottom: image.Rect(1, 1, 2, 2), + }, + { + desc: "returns area as top on zero cells", + area: image.Rect(1, 1, 2, 2), + cells: 0, + wantBottom: image.ZR, + wantTop: image.Rect(1, 1, 2, 2), + }, + { + desc: "zero area to begin with", + area: image.ZR, + cells: 0, + wantTop: image.ZR, + wantBottom: image.ZR, + }, + { + desc: "splits area with even height", + area: image.Rect(1, 1, 3, 3), + cells: 1, + wantTop: image.Rect(1, 1, 3, 2), + wantBottom: image.Rect(1, 2, 3, 3), + }, + { + desc: "splits area with odd width", + area: image.Rect(1, 1, 4, 4), + cells: 1, + wantTop: image.Rect(1, 1, 4, 3), + wantBottom: image.Rect(1, 3, 4, 4), + }, + { + desc: "splits to unequal areas", + area: image.Rect(0, 0, 4, 4), + cells: 3, + wantTop: image.Rect(0, 0, 4, 1), + wantBottom: image.Rect(0, 1, 4, 4), + }, + } + + for _, tc := range tests { + t.Run(tc.desc, func(t *testing.T) { + gotTop, gotBottom, err := HSplitCellsReversed(tc.area, tc.cells) + if (err != nil) != tc.wantErr { + t.Errorf("HSplitCells => unexpected error:%v, wantErr:%v", err, tc.wantErr) + } + if err != nil { + return + } + if diff := pretty.Compare(tc.wantTop, gotTop); diff != "" { + t.Errorf("HSplitCells => left value unexpected diff (-want, +got):\n%s", diff) + } + if diff := pretty.Compare(tc.wantBottom, gotBottom); diff != "" { + t.Errorf("HSplitCells => right value unexpected diff (-want, +got):\n%s", diff) + } + }) + } +} + func TestExcludeBorder(t *testing.T) { tests := []struct { desc string