2023-08-20 15:41:14 -06:00
|
|
|
package layouts
|
|
|
|
|
|
|
|
import "math"
|
|
|
|
import "image"
|
|
|
|
import "git.tebibyte.media/tomo/tomo"
|
|
|
|
|
2024-06-11 15:12:18 -06:00
|
|
|
var _ tomo.Layout = new(Grid)
|
2024-06-11 14:46:04 -06:00
|
|
|
|
2023-08-20 15:41:14 -06:00
|
|
|
// Grid is a layout that arranges boxes in a grid formation with distinct rows
|
|
|
|
// and columns. It is great for creating forms.
|
|
|
|
type Grid struct {
|
|
|
|
xExpand []bool
|
|
|
|
yExpand []bool
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewGrid creates a new grid layout. Rows and columns are specified as slices
|
|
|
|
// of booleans, where true means a row or column will expand and false means it
|
|
|
|
// will contract. Boxes are laid out left to right, then top to bottom. Boxes
|
|
|
|
// that go beyond the lengh of rows will be laid out according to columns, but
|
|
|
|
// they will not expand vertically.
|
2024-06-22 16:44:26 -06:00
|
|
|
//
|
|
|
|
// If you aren't sure how to use this constructor, here is an example:
|
|
|
|
//
|
|
|
|
// X0 X1 X2 Y0 Y1 Y2
|
|
|
|
// NewGrid(true, false, true)(false, true, true)
|
|
|
|
func NewGrid (columns ...bool) func (rows ...bool) *Grid {
|
|
|
|
return func (rows ...bool) *Grid {
|
|
|
|
return &Grid {
|
|
|
|
xExpand: columns,
|
|
|
|
yExpand: rows,
|
|
|
|
}
|
2023-08-20 15:41:14 -06:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *Grid) MinimumSize (hints tomo.LayoutHints, boxes []tomo.Box) image.Point {
|
|
|
|
cols, rows := this.minimums(boxes)
|
|
|
|
size := image.Pt (
|
|
|
|
(len(cols) - 1) * hints.Gap.X,
|
|
|
|
(len(rows) - 1) * hints.Gap.Y)
|
|
|
|
for _, width := range cols { size.X += width }
|
|
|
|
for _, height := range rows { size.Y += height }
|
|
|
|
return size
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *Grid) Arrange (hints tomo.LayoutHints, boxes []tomo.Box) {
|
|
|
|
xExpand := func (index int) bool {
|
|
|
|
return this.xExpand[index]
|
|
|
|
}
|
|
|
|
yExpand := func (index int) bool {
|
|
|
|
if index < len(this.yExpand) { return this.yExpand[index] }
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
cols, rows := this.minimums(boxes)
|
|
|
|
expand(hints, cols, hints.Bounds.Dx(), xExpand)
|
|
|
|
expand(hints, rows, hints.Bounds.Dy(), yExpand)
|
|
|
|
|
|
|
|
position := hints.Bounds.Min
|
|
|
|
for index, box := range boxes {
|
|
|
|
col, row := index % len(cols), index / len(cols)
|
|
|
|
box.SetBounds(image.Rectangle {
|
|
|
|
Min: position,
|
|
|
|
Max: position.Add(image.Pt(cols[col], rows[row])),
|
|
|
|
})
|
|
|
|
if col == len(cols) - 1 {
|
|
|
|
position.X = hints.Bounds.Min.X
|
|
|
|
position.Y += rows[row] + hints.Gap.Y
|
|
|
|
} else {
|
|
|
|
position.X += cols[col] + hints.Gap.X
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *Grid) minimums (boxes []tomo.Box) ([]int, []int) {
|
|
|
|
nCols, nRows := this.dimensions(boxes)
|
|
|
|
cols, rows := make([]int, nCols), make([]int, nRows)
|
|
|
|
|
|
|
|
for index, box := range boxes {
|
|
|
|
col, row := index % len(cols), index / len(cols)
|
|
|
|
minimum := box.MinimumSize()
|
|
|
|
if cols[col] < minimum.X {
|
|
|
|
cols[col] = minimum.X
|
|
|
|
}
|
|
|
|
if rows[row] < minimum.Y {
|
|
|
|
rows[row] = minimum.Y
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return cols, rows
|
|
|
|
}
|
|
|
|
|
|
|
|
func (this *Grid) dimensions (boxes []tomo.Box) (int, int) {
|
|
|
|
return len(this.xExpand), ceilDiv(len(boxes), len(this.xExpand))
|
|
|
|
}
|
|
|
|
|
|
|
|
func expand (hints tomo.LayoutHints, sizes []int, space int, expands func (int) bool) {
|
|
|
|
gaps := len(sizes) - 1
|
|
|
|
freeSpace := float64(space - hints.Gap.Y * gaps)
|
|
|
|
nExpanding := 0; for index, minimum := range sizes {
|
|
|
|
if expands(index) {
|
|
|
|
nExpanding ++
|
|
|
|
} else {
|
|
|
|
freeSpace -= float64(minimum)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
expandingSize := freeSpace / float64(nExpanding)
|
|
|
|
for index := range sizes {
|
|
|
|
if expands(index) {
|
|
|
|
sizes[index] = int(expandingSize)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func ceilDiv (x, y int) int {
|
2024-06-22 13:38:52 -06:00
|
|
|
if y == 0 { return 0 }
|
2023-08-20 15:41:14 -06:00
|
|
|
return int(math.Ceil(float64(x) / float64(y)))
|
|
|
|
}
|
2024-06-11 15:12:18 -06:00
|
|
|
|
|
|
|
func (this *Grid) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int {
|
2024-06-22 13:38:52 -06:00
|
|
|
return this.MinimumSize(hints, boxes).Y
|
2024-06-11 15:12:18 -06:00
|
|
|
}
|
|
|
|
|
|
|
|
func (this *Grid) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int {
|
2024-06-22 13:38:52 -06:00
|
|
|
return this.MinimumSize(hints, boxes).X
|
2024-06-11 15:12:18 -06:00
|
|
|
}
|