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.
2014-09-01 09:18:22 -05:00
// 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.
2014-09-03 17:30:38 -05:00
// it can also have Controls spanning multiple rows and columns.
2014-09-01 09:18:22 -05:00
//
// Each Control in a Grid has associated "expansion" and "alignment" values in both the X and Y direction.
2014-09-01 09:20:01 -05:00
// 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.
2014-09-01 09:18:22 -05:00
// 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.
2014-08-31 17:59:55 -05:00
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.
2014-09-01 09:20:58 -05:00
// If nextTo is nil, it is placed next to the previously added Control.
2014-08-31 17:59:55 -05:00
// 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-09-03 17:30:38 -05:00
// 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 )
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 (
West Side = iota
East
North
South
nSides
)
type grid struct {
2014-09-03 16:59:20 -05:00
controls [ ] gridCell
indexof map [ Control ] int
prev int
2014-08-31 17:59:55 -05:00
parent * controlParent
2014-09-03 16:59:20 -05:00
xmax int
ymax int
2014-08-31 17:59:55 -05:00
}
type gridCell struct {
2014-09-03 16:59:20 -05:00
control Control
2014-08-31 23:36:20 -05:00
xexpand bool
xalign Align
yexpand bool
yalign Align
2014-09-03 16:59:20 -05:00
xspan int
yspan int
x int
y int
finalx int
finaly int
finalwidth int
finalheight int
prefwidth int
prefheight int
2014-08-31 17:59:55 -05:00
}
// NewGrid creates a new Grid with no Controls.
func NewGrid ( ) Grid {
return & grid {
2014-09-03 16:59:20 -05:00
indexof : map [ Control ] int { } ,
}
}
// 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
}
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 17:30:38 -05:00
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 ) )
}
2014-09-03 16:59:20 -05:00
cell := gridCell {
control : control ,
2014-08-31 23:36:20 -05:00
xexpand : xexpand ,
xalign : xalign ,
yexpand : yexpand ,
yalign : yalign ,
2014-09-03 17:30:38 -05:00
xspan : xspan ,
yspan : yspan ,
2014-09-03 16:59:20 -05:00
}
if g . parent != nil {
control . setParent ( g . parent )
2014-08-31 17:59:55 -05:00
}
// if this is the first control, just add it in directly
if len ( g . controls ) != 0 {
2014-09-03 16:59:20 -05:00
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 ) )
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 16:59:20 -05:00
g . controls = append ( g . controls , cell )
g . prev = len ( g . controls ) - 1
g . indexof [ control ] = g . prev
g . reorigin ( )
2014-08-31 17:59:55 -05:00
}
func ( g * grid ) setParent ( p * controlParent ) {
g . parent = p
2014-09-03 16:59:20 -05:00
for i := range g . controls {
g . controls [ i ] . control . setParent ( g . parent )
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 16:59:20 -05:00
// 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
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 16:59:20 -05:00
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
}
}
2014-08-31 17:59:55 -05:00
}
2014-09-03 16:59:20 -05:00
return gg , make ( [ ] int , g . xmax ) , make ( [ ] int , g . ymax )
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
}
2014-09-03 16:59:20 -05:00
// -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
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
2014-08-31 17:59:55 -05:00
}
2014-09-03 16:59:20 -05:00
}
// 2) figure out which columns expand
// we only mark the first row/column of a spanning cell as expanding to prevent unexpected behavior
for i := range g . controls {
if g . controls [ i ] . xexpand {
xexpand [ g . controls [ i ] . x ] = true
2014-08-31 17:59:55 -05:00
}
2014-09-03 16:59:20 -05:00
if g . controls [ i ] . yexpand {
yexpand [ g . controls [ i ] . y ] = true
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 17:26:49 -05:00
// 3) compute and assign expanded widths/heights
nxexpand := 0
nyexpand := 0
2014-09-03 16:59:20 -05:00
for x , expand := range xexpand {
2014-09-03 17:26:49 -05:00
if expand {
nxexpand ++
} else {
2014-08-31 23:36:20 -05:00
width -= colwidths [ x ]
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 16:59:20 -05:00
for y , expand := range yexpand {
2014-09-03 17:26:49 -05:00
if expand {
nyexpand ++
} else {
2014-09-03 16:59:20 -05:00
height -= rowheights [ y ]
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 17:07:27 -05:00
for x , expand := range xexpand {
2014-08-31 23:36:20 -05:00
if expand {
2014-09-03 16:59:20 -05:00
colwidths [ x ] = width / nxexpand
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 16:59:20 -05:00
for y , expand := range yexpand {
if expand {
rowheights [ y ] = height / nyexpand
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 17:07:27 -05:00
2014-09-03 16:59:20 -05:00
// 4) 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
}
2014-08-31 17:59:55 -05:00
2014-09-03 16:59:20 -05:00
// 5) compute cell positions and widths
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 {
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
2014-08-31 17:59:55 -05:00
}
2014-09-03 16:59:20 -05:00
}
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 {
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
2014-08-31 17:59:55 -05:00
}
}
2014-09-03 16:59:20 -05:00
// 6) 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
2014-09-03 11:10:03 -05:00
}
2014-09-03 16:59:20 -05:00
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
2014-09-03 11:10:03 -05:00
}
}
2014-09-03 16:59:20 -05:00
// 7) and FINALLY we draw
2014-08-31 17:59:55 -05:00
var current * allocation
2014-09-03 16:59:20 -05:00
for _ , ycol := range gg {
2014-08-31 17:59:55 -05:00
current = nil
2014-09-03 16:59:20 -05:00
for _ , i := range ycol {
if i != - 1 { // treat empty cells like spaces
as := g . controls [ i ] . control . allocate (
g . controls [ i ] . finalx + x , g . controls [ i ] . finaly + y ,
g . controls [ i ] . finalwidth , g . controls [ i ] . finalheight , d )
if current != nil { // connect first left to first right
current . neighbor = g . controls [ i ] . control
2014-09-01 12:31:39 -05:00
}
2014-09-03 16:59:20 -05:00
if len ( as ) != 0 {
current = as [ 0 ] // next left is first subwidget
2014-09-01 12:31:39 -05:00
} else {
2014-09-03 16:59:20 -05:00
current = nil // spaces don't have allocation data
2014-09-01 12:31:39 -05:00
}
2014-09-03 16:59:20 -05:00
allocations = append ( allocations , as ... )
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
}
2014-09-03 16:59:20 -05:00
// 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
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
2014-08-31 17:59:55 -05:00
}
}
2014-08-31 23:36:20 -05:00
2014-09-03 16:59:20 -05:00
// 2) compute total column width/row height
colwidth := 0
rowheight := 0
for _ , w := range colwidths {
colwidth += w
2014-08-31 23:36:20 -05:00
}
2014-09-03 16:59:20 -05:00
for _ , h := range rowheights {
rowheight += h
2014-08-31 17:59:55 -05:00
}
2014-09-03 16:59:20 -05:00
// and that's it; just account for padding
return colwidth + ( g . xmax - 1 ) * d . xpadding ,
rowheight + ( g . ymax - 1 ) * d . ypadding
2014-08-31 17:59:55 -05:00
}
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
}