summaryrefslogtreecommitdiff
path: root/grid.go
diff options
context:
space:
mode:
authorPietro Gagliardi <[email protected]>2014-08-31 18:59:55 -0400
committerPietro Gagliardi <[email protected]>2014-08-31 18:59:55 -0400
commit29a764199f1696bf1214ec0c1c30cf4b8eb807dc (patch)
tree6d9ee79d00dfe1bc1a19e9c44f17b8518197ed38 /grid.go
parentc9301665858c650bacbf8b0fdc0de7760470da34 (diff)
Added initial implementation of GtkGrid-style Grid.
Diffstat (limited to 'grid.go')
-rw-r--r--grid.go391
1 files changed, 391 insertions, 0 deletions
diff --git a/grid.go b/grid.go
new file mode 100644
index 0000000..f0e32a8
--- /dev/null
+++ b/grid.go
@@ -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
+}