diff options
Diffstat (limited to 'grid.go')
| -rw-r--r-- | grid.go | 391 |
1 files changed, 391 insertions, 0 deletions
@@ -0,0 +1,391 @@ +// 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. +// [TODO it can also have Controls spanning multiple rows and columns.] +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. + Add(control Control, nextTo Control, side Side, hexpand bool, halign Align, vexpand bool, valign Align) +} + +// 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 ( + // this arrangement is important + // it makes finding the opposite side as easy as ^ 1 + West Side = iota + East + North + South + nSides +) + +type grid struct { + controls map[Control]*gridCell + prev Control + parent *controlParent + + // for allocate() and preferredSize() + xoff, yoff int + xmax, ymax int + grid [][]gridCellAllocation +} + +type gridCell struct { + hexpand bool + halign Align + vexpand bool + valign Align + neighbors [nSides]Control + + // for allocate() and preferredSize() + gridx int + gridy int + width int + height int + visited bool +} + +// NewGrid creates a new Grid with no Controls. +func NewGrid() Grid { + return &grid{ + controls: map[Control]*gridCell{}, + } +} + +func (g *grid) Add(control Control, nextTo Control, side Side, hexpand bool, halign Align, vexpand bool, valign Align) { + cell := &gridCell{ + hexpand: hexpand, + halign: halign, + vexpand: vexpand, + valign: valign, + } + // if this is the first control, just add it in directly + if len(g.controls) != 0 { + if nextTo == nil { + nextTo = g.prev + } + next := g.controls[nextTo] + // squeeze any control previously on the same side out of the way + temp := next.neighbors[side] + next.neighbors[side] = control + cell.neighbors[side] = temp + cell.neighbors[side ^ 1] = nextTo // doubly-link + } + g.controls[control] = cell + g.prev = control + if g.parent != nil { + control.setParent(g.parent) + } +} + +func (g *grid) setParent(p *controlParent) { + g.parent = p + for c, _ := range g.controls { + c.setParent(g.parent) + } +} + +func (g *grid) trasverse(c Control, x int, y int) { + cell := g.controls[c] + if cell.visited { + return + } + cell.visited = true + cell.gridx = x + cell.gridy = y + if x < g.xoff { + g.xoff = x + } + if y < g.yoff { + g.yoff = y + } + if cell.neighbors[West] != nil { + g.trasverse(cell.neighbors[West], x - 1, y) + } + if cell.neighbors[North] != nil { + g.trasverse(cell.neighbors[North], x, y - 1) + } + if cell.neighbors[East] != nil { + g.trasverse(cell.neighbors[East], x + 1, y) + } + if cell.neighbors[South] != nil { + g.trasverse(cell.neighbors[South], x, y + 1) + } +} + +type gridCellAllocation struct { + width int + height int + c Control +} + +func (g *grid) buildGrid() { + // thanks to http://programmers.stackexchange.com/a/254968/147812 + // before we do anything, reset the visited bits + for _, cell := range g.controls { + cell.visited = false + } + // we first mark the previous control as the origin... + g.xoff = 0 + g.yoff = 0 + g.trasverse(g.prev, 0, 0) // start at the last control added + // now we need to make all offsets zero-based + g.xoff = -g.xoff + g.yoff = -g.yoff + g.xmax = 0 + g.ymax = 0 + for _, cell := range g.controls { + cell.gridx += g.xoff + cell.gridy += g.yoff + if cell.gridx > g.xmax { + g.xmax = cell.gridx + } + if cell.gridy > g.ymax { + g.ymax = cell.gridy + } + } + // g.xmax and g.ymax are the last valid index; make them one over to make everything work + g.xmax++ + g.ymax++ + // and finally build the matrix + g.grid = make([][]gridCellAllocation, g.ymax) + for y := 0; y < g.ymax; y++ { + g.grid[y] = make([]gridCellAllocation, g.xmax) + // the field c is assigned below for efficiency + } +} + +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) compute the resultant grid + g.buildGrid() + width -= d.xpadding * g.xmax + height -= d.ypadding * g.ymax + + // 2) for every control that doesn't expand, set the width of each cell of its column/height of each cell of its row to the largest such + nhexpand := make([]bool, g.xmax) + nvexpand := make([]bool, g.ymax) + for c, cell := range g.controls { + width, height := c.preferredSize(d) + cell.width = width + cell.height = height + if !cell.hexpand { + g.grid[cell.gridy][cell.gridx].width = width + } else { + nhexpand[cell.gridx] = true + } + if !cell.vexpand { + g.grid[cell.gridy][cell.gridx].height = height + } else { + nvexpand[cell.gridy] = true + } + g.grid[cell.gridy][cell.gridx].c = c + } + // cells on the same row have the same height + for y := 0; y < g.ymax; y++ { + max := 0 + for x := 0; x < g.xmax; x++ { + if max < g.grid[y][x].height { + max = g.grid[y][x].height + } + } + for x := 0; x < g.xmax; x++ { + g.grid[y][x].height = max + } + } + // cells on the same column have the same width + for x := 0; x < g.xmax; x++ { + max := 0 + for y := 0; y < g.ymax; y++ { + if max < g.grid[y][x].width { + max = g.grid[y][x].width + } + } + for y := 0; y < g.ymax; y++ { + g.grid[y][x].width = max + } + } + + // 3) distribute the remaining space equally to expanding cells, adjusting widths and heights as needed + nh := 0 + for x, b := range nhexpand { + if b { + nh++ + } else { // column width known; subtract it + width -= g.grid[0][x].width + } + } + if nh > 0 { + h := width / nh + for y, b := range nhexpand { + if b { + for x := 0; x < g.xmax; x++ { + if g.grid[y][x].width < h { + g.grid[y][x].width = h + } + } + } + } + } + nv := 0 + for y, b := range nvexpand { + if b { + nv++ + } else { // column height known; subtract it + height -= g.grid[y][0].height + } + } + if nv > 0 { + v := height / nv + for x, b := range nvexpand { + if b { + for y := 0; y < g.ymax; y++ { + if g.grid[y][x].height < v { + g.grid[y][x].height = v + } + } + } + } + } + + // all right, now we have the size of each cell + + // 4) handle alignment + for _, cell := range g.controls { + if cell.hexpand { + switch cell.halign { + case LeftTop: + // do nothing; this is the default + case Center: + case RightBottom: + // TODO + case Fill: + cell.width = g.grid[cell.gridy][cell.gridx].width + default: + panic(fmt.Errorf("invalid halign %d in Grid.allocate()", cell.halign)) + } + } + if cell.vexpand { + switch cell.valign { + case LeftTop: + // do nothing; this is the default + case Center: + case RightBottom: + // TODO + case Fill: + cell.height = g.grid[cell.gridy][cell.gridx].height + default: + panic(fmt.Errorf("invalid valign %d in Grid.allocate()", cell.valign)) + } + } + } + + // 5) draw + var current *allocation + + startx := x + for row, xcol := range g.grid { + current = nil + for col, ca := range xcol { + cell := g.controls[ca.c] + as := ca.c.allocate(x, y, cell.width, cell.height, d) + if current != nil { // connect first left to first right + current.neighbor = ca.c + } + 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...) + x += g.grid[0][col].width + d.xpadding + } + x = startx + y += g.grid[row][0].height + d.ypadding + } + + return allocations +} + +func (g *grid) preferredSize(d *sizing) (width, height int) { + if len(g.controls) == 0 { + // nothing to do + return 0, 0 + } + + // 1) compute the resultant grid + g.buildGrid() + + // 2) for every control (including those that don't expand), set the width of each cell of its column/height of each cell of its row to the largest such + for c, cell := range g.controls { + width, height := c.preferredSize(d) + g.grid[cell.gridy][cell.gridx].width = width + g.grid[cell.gridy][cell.gridx].height = height + } + // cells on the same row have the same height + maxy := 0 + for y := 0; y < g.ymax; y++ { + max := 0 + for x := 0; x < g.xmax; x++ { + if max < g.grid[y][x].height { + max = g.grid[y][x].height + } + } + for x := 0; x < g.xmax; x++ { + g.grid[y][x].height = max + } + maxy += max + } + // cells on the same column have the same width + maxx := 0 + for x := 0; x < g.xmax; x++ { + max := 0 + for y := 0; y < g.ymax; y++ { + if max < g.grid[y][x].width { + max = g.grid[y][x].width + } + } + for y := 0; y < g.ymax; y++ { + g.grid[y][x].width = max + } + maxx += max + } + + // and that's it really; just discount the padding + return maxx + (g.xmax - 1) * d.xpadding, + maxy + (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 +} |
