2014-02-25 17:21:58 -06:00
// 25 february 2014
2014-03-12 20:55:45 -05:00
2014-02-25 17:21:58 -06:00
package ui
import (
2014-02-25 17:40:36 -06:00
"fmt"
2014-02-25 17:21:58 -06:00
"sync"
)
// A Grid arranges Controls in a two-dimensional grid.
// The height of each row and the width of each column is the maximum preferred height and width (respectively) of all the controls in that row or column (respectively).
2014-02-25 18:05:01 -06:00
// Controls are aligned to the top left corner of each cell.
2014-02-25 20:54:13 -06:00
// All Controls in a Grid maintain their preferred sizes by default; if a Control is marked as being "filling", it will be sized to fill its cell.
// Even if a Control is marked as filling, its preferred size is used to calculate cell sizes.
2014-02-26 06:01:02 -06:00
// One Control can be marked as "stretchy": when the Window containing the Grid is resized, the cell containing that Control resizes to take any remaining space; its row and column are adjusted accordingly (so other filling controls in the same row and column will fill to the new height and width, respectively).
// A stretchy Control implicitly fills its cell.
2014-02-25 20:54:13 -06:00
// All cooridnates in a Grid are given in (row,column) form with (0,0) being the top-left cell.
2014-02-25 17:21:58 -06:00
type Grid struct {
2014-06-10 13:33:48 -05:00
lock sync . Mutex
created bool
controls [ ] [ ] Control
filling [ ] [ ] bool
stretchyrow , stretchycol int
2014-06-25 21:21:28 -05:00
widths , heights [ ] [ ] int // caches to avoid reallocating each time
2014-06-10 13:33:48 -05:00
rowheights , colwidths [ ] int
2014-02-25 17:21:58 -06:00
}
// NewGrid creates a new Grid with the given Controls.
// NewGrid needs to know the number of Controls in a row (alternatively, the number of columns); it will determine the number in a column from the number of Controls given.
// NewGrid panics if not given a full grid of Controls.
// Example:
// grid := NewGrid(3,
// control00, control01, control02,
// control10, control11, control12,
// control20, control21, control22)
func NewGrid ( nPerRow int , controls ... Control ) * Grid {
2014-06-10 13:33:48 -05:00
if len ( controls ) % nPerRow != 0 {
2014-02-25 17:21:58 -06:00
panic ( fmt . Errorf ( "incomplete grid given to NewGrid() (not enough controls to evenly divide %d controls into rows of %d controls each)" , len ( controls ) , nPerRow ) )
}
2014-02-25 17:40:36 -06:00
nRows := len ( controls ) / nPerRow
2014-02-25 17:21:58 -06:00
cc := make ( [ ] [ ] Control , nRows )
2014-02-25 20:54:13 -06:00
cf := make ( [ ] [ ] bool , nRows )
2014-02-25 17:21:58 -06:00
cw := make ( [ ] [ ] int , nRows )
2014-02-25 17:40:36 -06:00
ch := make ( [ ] [ ] int , nRows )
2014-02-25 17:21:58 -06:00
i := 0
for row := 0 ; row < nRows ; row ++ {
cc [ row ] = make ( [ ] Control , nPerRow )
2014-02-25 20:54:13 -06:00
cf [ row ] = make ( [ ] bool , nPerRow )
2014-02-25 17:21:58 -06:00
cw [ row ] = make ( [ ] int , nPerRow )
ch [ row ] = make ( [ ] int , nPerRow )
for x := 0 ; x < nPerRow ; x ++ {
cc [ row ] [ x ] = controls [ i ]
i ++
}
}
return & Grid {
2014-06-10 13:33:48 -05:00
controls : cc ,
filling : cf ,
stretchyrow : - 1 ,
stretchycol : - 1 ,
widths : cw ,
heights : ch ,
rowheights : make ( [ ] int , nRows ) ,
colwidths : make ( [ ] int , nPerRow ) ,
2014-02-25 17:21:58 -06:00
}
}
2014-03-11 11:01:25 -05:00
// SetFilling marks the given Control of the Grid as filling its cell instead of staying at its preferred size.
2014-02-25 20:54:13 -06:00
// This function cannot be called after the Window that contains the Grid has been created.
2014-03-11 11:01:25 -05:00
// It panics if the given coordinate is invalid.
2014-02-25 20:54:13 -06:00
func ( g * Grid ) SetFilling ( row int , column int ) {
g . lock . Lock ( )
defer g . lock . Unlock ( )
if g . created {
2014-03-11 11:01:25 -05:00
panic ( fmt . Errorf ( "Grid.SetFilling() called after window create" ) )
}
2014-03-11 11:25:52 -05:00
if row < 0 || column < 0 || row > len ( g . filling ) || column > len ( g . filling [ row ] ) {
2014-03-11 11:24:48 -05:00
panic ( fmt . Errorf ( "coordinate (%d,%d) out of range passed to Grid.SetFilling()" , row , column ) )
2014-02-25 20:54:13 -06:00
}
g . filling [ row ] [ column ] = true
}
2014-03-11 11:01:25 -05:00
// SetStretchy marks the given Control of the Grid as stretchy.
2014-02-26 06:01:02 -06:00
// Stretchy implies filling.
2014-03-11 11:01:25 -05:00
// Only one control can be stretchy per Grid; calling SetStretchy multiple times merely changes which control is stretchy.
2014-02-26 06:01:02 -06:00
// This function cannot be called after the Window that contains the Grid has been created.
2014-03-11 11:01:25 -05:00
// It panics if the given coordinate is invalid.
2014-02-26 06:01:02 -06:00
func ( g * Grid ) SetStretchy ( row int , column int ) {
g . lock . Lock ( )
defer g . lock . Unlock ( )
if g . created {
2014-03-11 11:01:25 -05:00
panic ( fmt . Errorf ( "Grid.SetFilling() called after window create" ) )
}
2014-03-11 11:25:52 -05:00
if row < 0 || column < 0 || row > len ( g . filling ) || column > len ( g . filling [ row ] ) {
2014-03-11 11:24:48 -05:00
panic ( fmt . Errorf ( "coordinate (%d,%d) out of range passed to Grid.SetStretchy()" , row , column ) )
2014-02-26 06:01:02 -06:00
}
g . stretchyrow = row
g . stretchycol = column
2014-04-29 09:10:28 -05:00
// don't set filling here in case we call SetStretchy() multiple times; the filling is committed in make() below
2014-02-26 06:01:02 -06:00
}
2014-02-25 17:21:58 -06:00
func ( g * Grid ) make ( window * sysData ) error {
g . lock . Lock ( )
defer g . lock . Unlock ( )
2014-04-29 09:10:28 -05:00
// commit filling for the stretchy control now (see SetStretchy() above)
if g . stretchyrow != - 1 && g . stretchycol != - 1 {
g . filling [ g . stretchyrow ] [ g . stretchycol ] = true
2014-06-10 13:33:48 -05:00
} else if ( g . stretchyrow == - 1 && g . stretchycol != - 1 ) || // sanity check
2014-04-29 09:10:28 -05:00
( g . stretchyrow != - 1 && g . stretchycol == - 1 ) {
panic ( fmt . Errorf ( "internal inconsistency in Grid: stretchy (%d,%d) impossible (one component, not both, is -1/no stretchy control) in Grid.make()" , g . stretchyrow , g . stretchycol ) )
}
2014-02-25 17:21:58 -06:00
for row , xcol := range g . controls {
2014-02-25 17:40:36 -06:00
for col , c := range xcol {
2014-02-25 17:21:58 -06:00
err := c . make ( window )
if err != nil {
return fmt . Errorf ( "error adding control (%d,%d) to Grid: %v" , row , col , err )
}
}
}
g . created = true
return nil
}
2014-06-25 22:05:29 -05:00
func ( g * Grid ) allocate ( x int , y int , width int , height int , d * sysSizeData ) ( allocations [ ] * allocation ) {
2014-02-25 17:21:58 -06:00
max := func ( a int , b int ) int {
if a > b {
return a
}
return b
}
2014-06-25 21:43:35 -05:00
var current * allocation // for neighboring
// before we do anything, steal the margin so nested Stacks/Grids don't double down
2014-06-25 22:05:29 -05:00
xmargin := d . xmargin
ymargin := d . ymargin
d . xmargin = 0
d . ymargin = 0
_ = xmargin
_ = ymargin
2014-02-25 17:21:58 -06:00
// 1) clear data structures
for i := range g . rowheights {
g . rowheights [ i ] = 0
}
for i := range g . colwidths {
g . colwidths [ i ] = 0
}
// 2) get preferred sizes; compute row/column sizes
for row , xcol := range g . controls {
for col , c := range xcol {
2014-06-25 21:43:35 -05:00
w , h := c . preferredSize ( d )
2014-02-25 17:40:36 -06:00
g . widths [ row ] [ col ] = w
2014-02-25 17:21:58 -06:00
g . heights [ row ] [ col ] = h
g . rowheights [ row ] = max ( g . rowheights [ row ] , h )
2014-02-25 17:40:36 -06:00
g . colwidths [ col ] = max ( g . colwidths [ col ] , w )
2014-02-25 17:21:58 -06:00
}
}
2014-02-26 06:01:02 -06:00
// 3) handle the stretchy control
2014-03-11 11:01:25 -05:00
if g . stretchyrow != - 1 && g . stretchycol != - 1 {
2014-02-26 06:01:02 -06:00
for i , w := range g . colwidths {
if i != g . stretchycol {
width -= w
}
}
for i , h := range g . rowheights {
if i != g . stretchyrow {
height -= h
}
}
g . colwidths [ g . stretchycol ] = width
g . rowheights [ g . stretchyrow ] = height
}
// 4) draw
2014-02-25 17:21:58 -06:00
startx := x
for row , xcol := range g . controls {
2014-06-25 21:43:35 -05:00
current = nil // reset on new columns
2014-02-25 17:21:58 -06:00
for col , c := range xcol {
2014-02-25 20:54:13 -06:00
w := g . widths [ row ] [ col ]
h := g . heights [ row ] [ col ]
if g . filling [ row ] [ col ] {
w = g . colwidths [ col ]
h = g . rowheights [ row ]
}
2014-06-25 22:21:57 -05:00
as := c . allocate ( x , y , w , h , d )
2014-06-25 21:43:35 -05:00
if current != nil { // connect first left to first right
current . neighbor = c
}
current = as [ 0 ] // next left is first subwidget
allocations = append ( allocations , as ... )
2014-02-25 17:40:36 -06:00
x += g . colwidths [ col ]
2014-02-25 17:21:58 -06:00
}
x = startx
2014-02-25 17:40:36 -06:00
y += g . rowheights [ row ]
2014-02-25 17:21:58 -06:00
}
2014-03-17 20:09:03 -05:00
return
2014-02-25 17:21:58 -06:00
}
2014-02-26 06:01:02 -06:00
// filling and stretchy are ignored for preferred size calculation
2014-06-25 21:43:35 -05:00
func ( g * Grid ) preferredSize ( d * sysSizeData ) ( width int , height int ) {
2014-02-25 17:21:58 -06:00
max := func ( a int , b int ) int {
if a > b {
return a
}
return b
}
// 1) clear data structures
for i := range g . rowheights {
g . rowheights [ i ] = 0
}
for i := range g . colwidths {
g . colwidths [ i ] = 0
}
// 2) get preferred sizes; compute row/column sizes
for row , xcol := range g . controls {
for col , c := range xcol {
2014-06-25 21:43:35 -05:00
w , h := c . preferredSize ( d )
2014-02-25 17:40:36 -06:00
g . widths [ row ] [ col ] = w
2014-02-25 17:21:58 -06:00
g . heights [ row ] [ col ] = h
g . rowheights [ row ] = max ( g . rowheights [ row ] , h )
2014-02-25 17:40:36 -06:00
g . colwidths [ col ] = max ( g . colwidths [ col ] , w )
2014-02-25 17:21:58 -06:00
}
}
// 3) now compute
for _ , w := range g . colwidths {
width += w
}
for _ , h := range g . rowheights {
height += h
}
2014-06-25 21:21:28 -05:00
return width , height
2014-02-25 17:21:58 -06:00
}
2014-06-25 21:43:35 -05:00
func ( g * Grid ) commitResize ( c * allocation , d * sysSizeData ) {
// this is to satisfy Control; nothing to do here
}
func ( g * Grid ) getAuxResizeInfo ( d * sysSizeData ) {
// this is to satisfy Control; nothing to do here
}