2014-10-15 11:57:08 -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, in any direction.
// it can also have Controls spanning multiple rows and columns.
//
// Each Control in a Grid has associated "expansion" and "alignment" values in both the X and Y direction.
// Expansion determines whether all cells in the same row/column are given whatever space is left over after figuring out how big the rest of the Grid should be.
// Alignment determines the position of a Control relative to its cell after computing the above.
// The special alignment Fill can be used to grow a Control to fit its cell.
// Note that expansion and alignment are independent variables.
// For more information on expansion and alignment, read https://developer.gnome.org/gtk3/unstable/ch28s02.html.
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.
// The effect of overlapping spanning Controls is also undefined.
// Add panics if either xspan or yspan are zero or negative.
Add ( control Control , nextTo Control , side Side , xexpand bool , xalign Align , yexpand bool , yalign Align , xspan int , yspan int )
}
// 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 (
West Side = iota
East
North
South
nSides
)
type grid struct {
controls [ ] gridCell
indexof map [ Control ] int
prev int
container * container
xmax int
ymax int
}
type gridCell struct {
control Control
xexpand bool
xalign Align
yexpand bool
yalign Align
xspan int
yspan int
x int
y int
finalx int
finaly int
finalwidth int
finalheight int
prefwidth int
prefheight int
}
// NewGrid creates a new Grid with no Controls.
func NewGrid ( ) Grid {
return & grid {
indexof : map [ Control ] int { } ,
container : newContainer ( ) ,
}
}
// ensures that all (x, y) pairs are 0-based
// also computes g.xmax/g.ymax
func ( g * grid ) reorigin ( ) {
xmin := 0
ymin := 0
for i := range g . controls {
if g . controls [ i ] . x < xmin {
xmin = g . controls [ i ] . x
}
if g . controls [ i ] . y < ymin {
ymin = g . controls [ i ] . y
}
}
xmin = - xmin
ymin = - ymin
g . xmax = 0
g . ymax = 0
for i := range g . controls {
g . controls [ i ] . x += xmin
g . controls [ i ] . y += ymin
if g . xmax < g . controls [ i ] . x + g . controls [ i ] . xspan {
g . xmax = g . controls [ i ] . x + g . controls [ i ] . xspan
}
if g . ymax < g . controls [ i ] . y + g . controls [ i ] . yspan {
g . ymax = g . controls [ i ] . y + g . controls [ i ] . yspan
}
}
}
func ( g * grid ) Add ( control Control , nextTo Control , side Side , xexpand bool , xalign Align , yexpand bool , yalign Align , xspan int , yspan int ) {
if xspan <= 0 || yspan <= 0 {
panic ( fmt . Errorf ( "invalid span %dx%d given to Grid.Add()" , xspan , yspan ) )
}
cell := gridCell {
control : control ,
xexpand : xexpand ,
xalign : xalign ,
yexpand : yexpand ,
yalign : yalign ,
xspan : xspan ,
yspan : yspan ,
}
control . setParent ( g . container . parent ( ) )
// if this is the first control, just add it in directly
if len ( g . controls ) != 0 {
next := g . prev
if nextTo != nil {
next = g . indexof [ nextTo ]
}
switch side {
case West :
cell . x = g . controls [ next ] . x - cell . xspan
cell . y = g . controls [ next ] . y
case North :
cell . x = g . controls [ next ] . x
cell . y = g . controls [ next ] . y - cell . yspan
case East :
cell . x = g . controls [ next ] . x + g . controls [ next ] . xspan
cell . y = g . controls [ next ] . y
case South :
cell . x = g . controls [ next ] . x
cell . y = g . controls [ next ] . y + g . controls [ next ] . yspan
default :
panic ( fmt . Errorf ( "invalid side %d in Grid.Add()" , side ) )
}
}
g . controls = append ( g . controls , cell )
g . prev = len ( g . controls ) - 1
g . indexof [ control ] = g . prev
g . reorigin ( )
}
func ( g * grid ) setParent ( p * controlParent ) {
g . container . setParent ( p )
}
// builds the topological cell grid; also makes colwidths and rowheights
func ( g * grid ) mkgrid ( ) ( gg [ ] [ ] int , colwidths [ ] int , rowheights [ ] int ) {
gg = make ( [ ] [ ] int , g . ymax )
for y := 0 ; y < g . ymax ; y ++ {
gg [ y ] = make ( [ ] int , g . xmax )
for x := 0 ; x < g . xmax ; x ++ {
gg [ y ] [ x ] = - 1
}
}
for i := range g . controls {
for y := g . controls [ i ] . y ; y < g . controls [ i ] . y + g . controls [ i ] . yspan ; y ++ {
for x := g . controls [ i ] . x ; x < g . controls [ i ] . x + g . controls [ i ] . xspan ; x ++ {
gg [ y ] [ x ] = i
}
}
}
return gg , make ( [ ] int , g . xmax ) , make ( [ ] int , g . ymax )
}
func ( g * grid ) resize ( x int , y int , width int , height int , d * sizing ) {
2014-10-16 15:20:09 -05:00
g . container . resize ( x , y , width , height , d )
2014-10-15 11:57:08 -05:00
if len ( g . controls ) == 0 {
// nothing to do
return
}
// -1) discount padding from width/height
width -= ( g . xmax - 1 ) * d . xpadding
height -= ( g . ymax - 1 ) * d . ypadding
// 0) build necessary data structures
gg , colwidths , rowheights := g . mkgrid ( )
xexpand := make ( [ ] bool , g . xmax )
yexpand := make ( [ ] bool , g . ymax )
// 1) compute colwidths and rowheights before handling expansion
// we only count non-spanning controls to avoid weirdness
for y := 0 ; y < len ( gg ) ; y ++ {
for x := 0 ; x < len ( gg [ y ] ) ; x ++ {
i := gg [ y ] [ x ]
if i == - 1 {
continue
}
w , h := g . controls [ i ] . control . preferredSize ( d )
if g . controls [ i ] . xspan == 1 {
if colwidths [ x ] < w {
colwidths [ x ] = w
}
}
if g . controls [ i ] . yspan == 1 {
if rowheights [ y ] < h {
rowheights [ y ] = h
}
}
// save these for step 6
g . controls [ i ] . prefwidth = w
g . controls [ i ] . prefheight = h
}
}
// 2) figure out which rows/columns expand but not span
// we need to know which expanding rows/columns don't span before we can handle the ones that do
for i := range g . controls {
if g . controls [ i ] . xexpand && g . controls [ i ] . xspan == 1 {
xexpand [ g . controls [ i ] . x ] = true
}
if g . controls [ i ] . yexpand && g . controls [ i ] . yspan == 1 {
yexpand [ g . controls [ i ] . y ] = true
}
}
// 2) figure out which rows/columns expand that do span
// the way we handle this is simple: if none of the spanned rows/columns expand, make all rows/columns expand
for i := range g . controls {
if g . controls [ i ] . xexpand && g . controls [ i ] . xspan != 1 {
do := true
for x := g . controls [ i ] . x ; x < g . controls [ i ] . x + g . controls [ i ] . xspan ; x ++ {
if xexpand [ x ] {
do = false
break
}
}
if do {
for x := g . controls [ i ] . x ; x < g . controls [ i ] . x + g . controls [ i ] . xspan ; x ++ {
xexpand [ x ] = true
}
}
}
if g . controls [ i ] . yexpand && g . controls [ i ] . yspan != 1 {
do := true
for y := g . controls [ i ] . y ; y < g . controls [ i ] . y + g . controls [ i ] . yspan ; y ++ {
if yexpand [ y ] {
do = false
break
}
}
if do {
for y := g . controls [ i ] . y ; y < g . controls [ i ] . y + g . controls [ i ] . yspan ; y ++ {
yexpand [ y ] = true
}
}
}
}
// 4) compute and assign expanded widths/heights
nxexpand := 0
nyexpand := 0
for x , expand := range xexpand {
if expand {
nxexpand ++
} else {
width -= colwidths [ x ]
}
}
for y , expand := range yexpand {
if expand {
nyexpand ++
} else {
height -= rowheights [ y ]
}
}
for x , expand := range xexpand {
if expand {
colwidths [ x ] = width / nxexpand
}
}
for y , expand := range yexpand {
if expand {
rowheights [ y ] = height / nyexpand
}
}
// 5) reset the final coordinates for the next step
for i := range g . controls {
g . controls [ i ] . finalx = 0
g . controls [ i ] . finaly = 0
g . controls [ i ] . finalwidth = 0
g . controls [ i ] . finalheight = 0
}
// 6) compute cell positions and sizes
for y := 0 ; y < g . ymax ; y ++ {
curx := 0
prev := - 1
for x := 0 ; x < g . xmax ; x ++ {
i := gg [ y ] [ x ]
if i != - 1 && y == g . controls [ i ] . y { // don't repeat this step if the control spans vertically
if i != prev {
g . controls [ i ] . finalx = curx
} else {
g . controls [ i ] . finalwidth += d . xpadding
}
g . controls [ i ] . finalwidth += colwidths [ x ]
}
curx += colwidths [ x ] + d . xpadding
prev = i
}
}
for x := 0 ; x < g . xmax ; x ++ {
cury := 0
prev := - 1
for y := 0 ; y < g . ymax ; y ++ {
i := gg [ y ] [ x ]
if i != - 1 && x == g . controls [ i ] . x { // don't repeat this step if the control spans horizontally
if i != prev {
g . controls [ i ] . finaly = cury
} else {
g . controls [ i ] . finalheight += d . ypadding
}
g . controls [ i ] . finalheight += rowheights [ y ]
}
cury += rowheights [ y ] + d . ypadding
prev = i
}
}
// 7) everything as it stands now is set for xalign == Fill yalign == Fill; set the correct alignments
// this is why we saved prefwidth/prefheight above
for i := range g . controls {
if g . controls [ i ] . xalign != Fill {
switch g . controls [ i ] . xalign {
case RightBottom :
g . controls [ i ] . finalx += g . controls [ i ] . finalwidth - g . controls [ i ] . prefwidth
case Center :
g . controls [ i ] . finalx += ( g . controls [ i ] . finalwidth - g . controls [ i ] . prefwidth ) / 2
}
g . controls [ i ] . finalwidth = g . controls [ i ] . prefwidth // for all three
}
if g . controls [ i ] . yalign != Fill {
switch g . controls [ i ] . yalign {
case RightBottom :
g . controls [ i ] . finaly += g . controls [ i ] . finalheight - g . controls [ i ] . prefheight
case Center :
g . controls [ i ] . finaly += ( g . controls [ i ] . finalheight - g . controls [ i ] . prefheight ) / 2
}
g . controls [ i ] . finalheight = g . controls [ i ] . prefheight // for all three
}
}
// 8) and FINALLY we draw
for _ , ycol := range gg {
for _ , i := range ycol {
if i != - 1 { // treat empty cells like spaces
g . controls [ i ] . control . resize (
g . controls [ i ] . finalx + x , g . controls [ i ] . finaly + y ,
g . controls [ i ] . finalwidth , g . controls [ i ] . finalheight , d )
}
}
}
2014-10-15 12:30:35 -05:00
return
2014-10-15 11:57:08 -05:00
}
func ( g * grid ) preferredSize ( d * sizing ) ( width , height int ) {
if len ( g . controls ) == 0 {
// nothing to do
return 0 , 0
}
// 0) build necessary data structures
gg , colwidths , rowheights := g . mkgrid ( )
// 1) compute colwidths and rowheights before handling expansion
// TODO put this in its own function (but careful about the spanning calculation in allocate())
for y := 0 ; y < len ( gg ) ; y ++ {
for x := 0 ; x < len ( gg [ y ] ) ; x ++ {
i := gg [ y ] [ x ]
if i == - 1 {
continue
}
w , h := g . controls [ i ] . control . preferredSize ( d )
// allot equal space in the presence of spanning to keep things sane
if colwidths [ x ] < w / g . controls [ i ] . xspan {
colwidths [ x ] = w / g . controls [ i ] . xspan
}
if rowheights [ y ] < h / g . controls [ i ] . yspan {
rowheights [ y ] = h / g . controls [ i ] . yspan
}
// save these for step 6
g . controls [ i ] . prefwidth = w
g . controls [ i ] . prefheight = h
}
}
// 2) compute total column width/row height
colwidth := 0
rowheight := 0
for _ , w := range colwidths {
colwidth += w
}
for _ , h := range rowheights {
rowheight += h
}
// and that's it; just account for padding
return colwidth + ( g . xmax - 1 ) * d . xpadding ,
rowheight + ( g . ymax - 1 ) * d . ypadding
}