2014-08-31 17:59:55 -05:00
// 31 august 2014
package ui
import (
"fmt"
)
// Grid is a Control that arranges other Controls in a grid.
// Grid is a very powerful container: it can position and size each Control in several ways and can (and must) have Controls added to it at any time.
// [TODO it can also have Controls spanning multiple rows and columns.]
type Grid interface {
Control
// Add adds a Control to the Grid.
// If this is the first Control in the Grid, it is merely added; nextTo should be nil.
// Otherwise, it is placed relative to nextTo.
// If nextTo is nil, it is placed next to the previously added Control,
// The effect of adding the same Control multiple times is undefined, as is the effect of adding a Control next to one not present in the Grid.
2014-08-31 23:36:20 -05:00
Add ( control Control , nextTo Control , side Side , xexpand bool , xalign Align , yexpand bool , yalign Align )
2014-08-31 17:59:55 -05:00
}
// Align represents the alignment of a Control in its cell of a Grid.
type Align uint
const (
LeftTop Align = iota
Center
RightBottom
Fill
)
// Side represents a side of a Control to add other Controls to a Grid to.
type Side uint
const (
// this arrangement is important
// it makes finding the opposite side as easy as ^ 1
West Side = iota
East
North
South
nSides
)
type grid struct {
controls map [ Control ] * gridCell
prev Control
parent * controlParent
// for allocate() and preferredSize()
xoff , yoff int
xmax , ymax int
2014-08-31 23:36:20 -05:00
grid [ ] [ ] Control
2014-08-31 17:59:55 -05:00
}
type gridCell struct {
2014-08-31 23:36:20 -05:00
xexpand bool
xalign Align
yexpand bool
yalign Align
2014-08-31 17:59:55 -05:00
neighbors [ nSides ] Control
// for allocate() and preferredSize()
gridx int
gridy int
2014-08-31 23:56:33 -05:00
xoff int
yoff int
2014-08-31 17:59:55 -05:00
width int
height int
visited bool
}
// NewGrid creates a new Grid with no Controls.
func NewGrid ( ) Grid {
return & grid {
controls : map [ Control ] * gridCell { } ,
}
}
2014-08-31 23:36:20 -05:00
func ( g * grid ) Add ( control Control , nextTo Control , side Side , xexpand bool , xalign Align , yexpand bool , yalign Align ) {
2014-08-31 17:59:55 -05:00
cell := & gridCell {
2014-08-31 23:36:20 -05:00
xexpand : xexpand ,
xalign : xalign ,
yexpand : yexpand ,
yalign : yalign ,
2014-08-31 17:59:55 -05:00
}
// if this is the first control, just add it in directly
if len ( g . controls ) != 0 {
if nextTo == nil {
nextTo = g . prev
}
next := g . controls [ nextTo ]
// squeeze any control previously on the same side out of the way
temp := next . neighbors [ side ]
next . neighbors [ side ] = control
cell . neighbors [ side ] = temp
cell . neighbors [ side ^ 1 ] = nextTo // doubly-link
}
g . controls [ control ] = cell
g . prev = control
if g . parent != nil {
control . setParent ( g . parent )
}
}
func ( g * grid ) setParent ( p * controlParent ) {
g . parent = p
for c , _ := range g . controls {
c . setParent ( g . parent )
}
}
func ( g * grid ) trasverse ( c Control , x int , y int ) {
cell := g . controls [ c ]
if cell . visited {
return
}
cell . visited = true
cell . gridx = x
cell . gridy = y
if x < g . xoff {
g . xoff = x
}
if y < g . yoff {
g . yoff = y
}
if cell . neighbors [ West ] != nil {
g . trasverse ( cell . neighbors [ West ] , x - 1 , y )
}
if cell . neighbors [ North ] != nil {
g . trasverse ( cell . neighbors [ North ] , x , y - 1 )
}
if cell . neighbors [ East ] != nil {
g . trasverse ( cell . neighbors [ East ] , x + 1 , y )
}
if cell . neighbors [ South ] != nil {
g . trasverse ( cell . neighbors [ South ] , x , y + 1 )
}
}
func ( g * grid ) buildGrid ( ) {
// thanks to http://programmers.stackexchange.com/a/254968/147812
// before we do anything, reset the visited bits
for _ , cell := range g . controls {
cell . visited = false
}
// we first mark the previous control as the origin...
g . xoff = 0
g . yoff = 0
g . trasverse ( g . prev , 0 , 0 ) // start at the last control added
// now we need to make all offsets zero-based
g . xoff = - g . xoff
g . yoff = - g . yoff
g . xmax = 0
g . ymax = 0
for _ , cell := range g . controls {
cell . gridx += g . xoff
cell . gridy += g . yoff
if cell . gridx > g . xmax {
g . xmax = cell . gridx
}
if cell . gridy > g . ymax {
g . ymax = cell . gridy
}
}
// g.xmax and g.ymax are the last valid index; make them one over to make everything work
g . xmax ++
g . ymax ++
// and finally build the matrix
2014-08-31 23:36:20 -05:00
g . grid = make ( [ ] [ ] Control , g . ymax )
2014-08-31 17:59:55 -05:00
for y := 0 ; y < g . ymax ; y ++ {
2014-08-31 23:36:20 -05:00
g . grid [ y ] = make ( [ ] Control , g . xmax )
// the elements are assigned below for efficiency
2014-08-31 17:59:55 -05:00
}
}
func ( g * grid ) allocate ( x int , y int , width int , height int , d * sizing ) ( allocations [ ] * allocation ) {
if len ( g . controls ) == 0 {
// nothing to do
return nil
}
// 1) compute the resultant grid
g . buildGrid ( )
width -= d . xpadding * g . xmax
height -= d . ypadding * g . ymax
2014-08-31 23:36:20 -05:00
// 2) for every control, set the width of each cell of its column/height of each cell of its row to the largest such
colwidths := make ( [ ] int , g . xmax )
rowheights := make ( [ ] int , g . ymax )
colxexpand := make ( [ ] bool , g . xmax )
rowyexpand := make ( [ ] bool , g . ymax )
2014-08-31 17:59:55 -05:00
for c , cell := range g . controls {
width , height := c . preferredSize ( d )
cell . width = width
cell . height = height
2014-08-31 23:36:20 -05:00
if colwidths [ cell . gridx ] < width {
colwidths [ cell . gridx ] = width
2014-08-31 17:59:55 -05:00
}
2014-08-31 23:36:20 -05:00
if rowheights [ cell . gridy ] < height {
rowheights [ cell . gridy ] = height
2014-08-31 17:59:55 -05:00
}
2014-08-31 23:36:20 -05:00
if cell . xexpand {
colxexpand [ cell . gridx ] = true
2014-08-31 17:59:55 -05:00
}
2014-08-31 23:36:20 -05:00
if cell . yexpand {
rowyexpand [ cell . gridy ] = true
2014-08-31 17:59:55 -05:00
}
2014-08-31 23:36:20 -05:00
g . grid [ cell . gridy ] [ cell . gridx ] = c
2014-08-31 17:59:55 -05:00
}
// 3) distribute the remaining space equally to expanding cells, adjusting widths and heights as needed
2014-08-31 23:36:20 -05:00
nexpand := 0
for x , expand := range colxexpand {
if expand {
nexpand ++
2014-08-31 17:59:55 -05:00
} else { // column width known; subtract it
2014-08-31 23:36:20 -05:00
width -= colwidths [ x ]
2014-08-31 17:59:55 -05:00
}
}
2014-08-31 23:36:20 -05:00
if nexpand > 0 {
w := width / nexpand
for x , expand := range colxexpand {
if expand {
colwidths [ x ] = w
2014-08-31 17:59:55 -05:00
}
}
}
2014-08-31 23:36:20 -05:00
nexpand = 0
for y , expand := range rowyexpand {
if expand {
nexpand ++
} else { // row height known; subtract it
height -= rowheights [ y ]
2014-08-31 17:59:55 -05:00
}
}
2014-08-31 23:36:20 -05:00
if nexpand > 0 {
h := height / nexpand
for y , expand := range rowyexpand {
if expand {
rowheights [ y ] = h
2014-08-31 17:59:55 -05:00
}
}
}
// all right, now we have the size of each cell
// 4) handle alignment
for _ , cell := range g . controls {
2014-08-31 23:56:33 -05:00
cell . xoff = 0
switch cell . xalign {
case LeftTop :
// do nothing; this is the default
case Center :
2014-09-01 09:04:48 -05:00
cell . xoff = ( colwidths [ cell . gridx ] - cell . width ) / 2
2014-08-31 23:56:33 -05:00
case RightBottom :
cell . xoff = colwidths [ cell . gridx ] - cell . width
case Fill :
cell . width = colwidths [ cell . gridx ]
default :
panic ( fmt . Errorf ( "invalid xalign %d in Grid.allocate()" , cell . xalign ) )
2014-08-31 17:59:55 -05:00
}
2014-09-01 09:11:41 -05:00
cell . yoff = 0
2014-08-31 23:56:33 -05:00
switch cell . yalign {
case LeftTop :
// do nothing; this is the default
case Center :
2014-09-01 09:11:41 -05:00
cell . yoff = ( rowheights [ cell . gridy ] - cell . height ) / 2
2014-08-31 23:56:33 -05:00
case RightBottom :
2014-09-01 09:11:41 -05:00
cell . yoff = rowheights [ cell . gridy ] - cell . height
2014-08-31 23:56:33 -05:00
case Fill :
cell . height = rowheights [ cell . gridy ]
default :
panic ( fmt . Errorf ( "invalid yalign %d in Grid.allocate()" , cell . yalign ) )
2014-08-31 17:59:55 -05:00
}
}
// 5) draw
var current * allocation
startx := x
for row , xcol := range g . grid {
current = nil
2014-08-31 23:36:20 -05:00
for col , c := range xcol {
cell := g . controls [ c ]
2014-09-01 09:11:41 -05:00
as := c . allocate ( x + cell . xoff , y + cell . yoff , cell . width , cell . height , d )
2014-08-31 17:59:55 -05:00
if current != nil { // connect first left to first right
2014-08-31 23:36:20 -05:00
current . neighbor = c
2014-08-31 17:59:55 -05:00
}
if len ( as ) != 0 {
current = as [ 0 ] // next left is first subwidget
} else {
current = nil // spaces don't have allocation data
}
allocations = append ( allocations , as ... )
2014-08-31 23:36:20 -05:00
x += colwidths [ col ] + d . xpadding
2014-08-31 17:59:55 -05:00
}
x = startx
2014-08-31 23:36:20 -05:00
y += rowheights [ row ] + d . ypadding
2014-08-31 17:59:55 -05:00
}
return allocations
}
func ( g * grid ) preferredSize ( d * sizing ) ( width , height int ) {
if len ( g . controls ) == 0 {
// nothing to do
return 0 , 0
}
// 1) compute the resultant grid
g . buildGrid ( )
// 2) for every control (including those that don't expand), set the width of each cell of its column/height of each cell of its row to the largest such
2014-08-31 23:36:20 -05:00
colwidths := make ( [ ] int , g . xmax )
rowheights := make ( [ ] int , g . ymax )
2014-08-31 17:59:55 -05:00
for c , cell := range g . controls {
width , height := c . preferredSize ( d )
2014-08-31 23:36:20 -05:00
cell . width = width
cell . height = height
if colwidths [ cell . gridx ] < width {
colwidths [ cell . gridx ] = width
2014-08-31 17:59:55 -05:00
}
2014-08-31 23:36:20 -05:00
if rowheights [ cell . gridy ] < height {
rowheights [ cell . gridy ] = height
2014-08-31 17:59:55 -05:00
}
}
2014-08-31 23:36:20 -05:00
// 3) and sum the widths and heights
2014-08-31 17:59:55 -05:00
maxx := 0
2014-08-31 23:36:20 -05:00
for _ , x := range colwidths {
maxx += x
}
maxy := 0
for _ , y := range rowheights {
maxy += y
2014-08-31 17:59:55 -05:00
}
// and that's it really; just discount the padding
return maxx + ( g . xmax - 1 ) * d . xpadding ,
maxy + ( g . ymax - 1 ) * d . ypadding
}
func ( g * grid ) commitResize ( a * allocation , d * sizing ) {
// do nothing; needed to satisfy Control
}
func ( g * grid ) getAuxResizeInfo ( d * sizing ) {
// do nothing; needed to satisfy Control
}