package layouts import "math" import "image" import "git.tebibyte.media/tomo/tomo" var _ tomo.Layout = new(Grid) // 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. // // 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, } } } 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 { if y == 0 { return 0 } return int(math.Ceil(float64(x) / float64(y))) } func (this *Grid) RecommendedHeight (hints tomo.LayoutHints, boxes []tomo.Box, width int) int { return this.MinimumSize(hints, boxes).Y } func (this *Grid) RecommendedWidth (hints tomo.LayoutHints, boxes []tomo.Box, height int) int { return this.MinimumSize(hints, boxes).X }