andlabs-ui/grid.go

409 lines
10 KiB
Go
Raw Normal View History

// 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.
// 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.
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.
// 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
parent *controlParent
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{},
}
}
// 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,
}
if g.parent != nil {
control.setParent(g.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.parent = p
for i := range g.controls {
g.controls[i].control.setParent(g.parent)
}
}
// 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) allocate(x int, y int, width int, height int, d *sizing) (allocations []*allocation) {
if len(g.controls) == 0 {
// nothing to do
return nil
}
// -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
}
}
// 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
}
if g.controls[i].yexpand {
yexpand[g.controls[i].y] = true
}
}
// 3) 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
}
}
// 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
}
// 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
}
}
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
}
}
// 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
}
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
}
}
// 7) and FINALLY we draw
var current *allocation
for _, ycol := range gg {
current = nil
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
}
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...)
}
}
}
return allocations
}
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
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
}
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
}