// 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) // Padded and SetPadded get and set whether the controls of the Grid have padding between them. // The size of the padding is platform-dependent. Padded() bool SetPadded(padded bool) } // 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 padded bool 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) Padded() bool { return g.padded } func (g *grid) SetPadded(padded bool) { g.padded = padded } 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) { g.container.resize(x, y, width, height, d) if len(g.controls) == 0 { // nothing to do return } // -2) get this Grid's padding xpadding := d.xpadding ypadding := d.ypadding if !g.padded { xpadding = 0 ypadding = 0 } // -1) discount padding from width/height width -= (g.xmax - 1) * xpadding height -= (g.ymax - 1) * 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 += xpadding } g.controls[i].finalwidth += colwidths[x] } curx += colwidths[x] + 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 += ypadding } g.controls[i].finalheight += rowheights[y] } cury += rowheights[y] + 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) } } } return } func (g *grid) preferredSize(d *sizing) (width, height int) { if len(g.controls) == 0 { // nothing to do return 0, 0 } // -1) get this Grid's padding xpadding := d.xpadding ypadding := d.ypadding if !g.padded { xpadding = 0 ypadding = 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) * xpadding, rowheight + (g.ymax-1) * ypadding } func (g *grid) nTabStops() int { n := 0 for _, c := range g.controls { n += c.control.nTabStops() } return n }