summaryrefslogtreecommitdiff
path: root/prevlib/grid.go
diff options
context:
space:
mode:
Diffstat (limited to 'prevlib/grid.go')
-rw-r--r--prevlib/grid.go245
1 files changed, 245 insertions, 0 deletions
diff --git a/prevlib/grid.go b/prevlib/grid.go
new file mode 100644
index 0000000..bc58a00
--- /dev/null
+++ b/prevlib/grid.go
@@ -0,0 +1,245 @@
+// 25 february 2014
+
+package ui
+
+import (
+ "fmt"
+)
+
+// A Grid arranges Controls in a two-dimensional grid.
+// The height of each row and the width of each column is the maximum preferred height and width (respectively) of all the controls in that row or column (respectively).
+// Controls are aligned to the top left corner of each cell.
+// All Controls in a Grid maintain their preferred sizes by default; if a Control is marked as being "filling", it will be sized to fill its cell.
+// Even if a Control is marked as filling, its preferred size is used to calculate cell sizes.
+// One Control can be marked as "stretchy": when the Window containing the Grid is resized, the cell containing that Control resizes to take any remaining space; its row and column are adjusted accordingly (so other filling controls in the same row and column will fill to the new height and width, respectively).
+// A stretchy Control implicitly fills its cell.
+// All cooridnates in a Grid are given in (row,column) form with (0,0) being the top-left cell.
+type Grid struct {
+ created bool
+ controls [][]Control
+ filling [][]bool
+ stretchyrow, stretchycol int
+ widths, heights [][]int // caches to avoid reallocating each time
+ rowheights, colwidths []int
+}
+
+// NewGrid creates a new Grid with the given Controls.
+// NewGrid needs to know the number of Controls in a row (alternatively, the number of columns); it will determine the number in a column from the number of Controls given.
+// NewGrid panics if not given a full grid of Controls.
+// Example:
+// grid := NewGrid(3,
+// control00, control01, control02,
+// control10, control11, control12,
+// control20, control21, control22)
+func NewGrid(nPerRow int, controls ...Control) *Grid {
+ if len(controls)%nPerRow != 0 {
+ panic(fmt.Errorf("incomplete grid given to NewGrid() (not enough controls to evenly divide %d controls into rows of %d controls each)", len(controls), nPerRow))
+ }
+ nRows := len(controls) / nPerRow
+ cc := make([][]Control, nRows)
+ cf := make([][]bool, nRows)
+ cw := make([][]int, nRows)
+ ch := make([][]int, nRows)
+ i := 0
+ for row := 0; row < nRows; row++ {
+ cc[row] = make([]Control, nPerRow)
+ cf[row] = make([]bool, nPerRow)
+ cw[row] = make([]int, nPerRow)
+ ch[row] = make([]int, nPerRow)
+ for x := 0; x < nPerRow; x++ {
+ cc[row][x] = controls[i]
+ i++
+ }
+ }
+ return &Grid{
+ controls: cc,
+ filling: cf,
+ stretchyrow: -1,
+ stretchycol: -1,
+ widths: cw,
+ heights: ch,
+ rowheights: make([]int, nRows),
+ colwidths: make([]int, nPerRow),
+ }
+}
+
+// SetFilling marks the given Control of the Grid as filling its cell instead of staying at its preferred size.
+// This function cannot be called after the Window that contains the Grid has been created.
+// It panics if the given coordinate is invalid.
+func (g *Grid) SetFilling(row int, column int) {
+ if g.created {
+ panic(fmt.Errorf("Grid.SetFilling() called after window create"))
+ }
+ if row < 0 || column < 0 || row > len(g.filling) || column > len(g.filling[row]) {
+ panic(fmt.Errorf("coordinate (%d,%d) out of range passed to Grid.SetFilling()", row, column))
+ }
+ g.filling[row][column] = true
+}
+
+// SetStretchy marks the given Control of the Grid as stretchy.
+// Stretchy implies filling.
+// Only one control can be stretchy per Grid; calling SetStretchy multiple times merely changes which control is stretchy.
+// This function cannot be called after the Window that contains the Grid has been created.
+// It panics if the given coordinate is invalid.
+func (g *Grid) SetStretchy(row int, column int) {
+ if g.created {
+ panic(fmt.Errorf("Grid.SetFilling() called after window create"))
+ }
+ if row < 0 || column < 0 || row > len(g.filling) || column > len(g.filling[row]) {
+ panic(fmt.Errorf("coordinate (%d,%d) out of range passed to Grid.SetStretchy()", row, column))
+ }
+ g.stretchyrow = row
+ g.stretchycol = column
+ // don't set filling here in case we call SetStretchy() multiple times; the filling is committed in make() below
+}
+
+func (g *Grid) make(window *sysData) error {
+ // commit filling for the stretchy control now (see SetStretchy() above)
+ if g.stretchyrow != -1 && g.stretchycol != -1 {
+ g.filling[g.stretchyrow][g.stretchycol] = true
+ } else if (g.stretchyrow == -1 && g.stretchycol != -1) || // sanity check
+ (g.stretchyrow != -1 && g.stretchycol == -1) {
+ panic(fmt.Errorf("internal inconsistency in Grid: stretchy (%d,%d) impossible (one component, not both, is -1/no stretchy control) in Grid.make()", g.stretchyrow, g.stretchycol))
+ }
+ for row, xcol := range g.controls {
+ for col, c := range xcol {
+ err := c.make(window)
+ if err != nil {
+ return fmt.Errorf("error adding control (%d,%d) to Grid: %v", row, col, err)
+ }
+ }
+ }
+ g.created = true
+ return nil
+}
+
+func (g *Grid) allocate(x int, y int, width int, height int, d *sysSizeData) (allocations []*allocation) {
+ max := func(a int, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+ }
+
+ var current *allocation // for neighboring
+
+ // TODO return if nControls == 0?
+ // before we do anything, steal the margin so nested Stacks/Grids don't double down
+ xmargin := d.xmargin
+ ymargin := d.ymargin
+ d.xmargin = 0
+ d.ymargin = 0
+ // 0) inset the available rect by the margins and needed padding
+ x += xmargin
+ y += ymargin
+ width -= xmargin * 2
+ height -= ymargin * 2
+ width -= (len(g.colwidths) - 1) * d.xpadding
+ height -= (len(g.rowheights) - 1) * d.ypadding
+ // 1) clear data structures
+ for i := range g.rowheights {
+ g.rowheights[i] = 0
+ }
+ for i := range g.colwidths {
+ g.colwidths[i] = 0
+ }
+ // 2) get preferred sizes; compute row/column sizes
+ for row, xcol := range g.controls {
+ for col, c := range xcol {
+ w, h := c.preferredSize(d)
+ g.widths[row][col] = w
+ g.heights[row][col] = h
+ g.rowheights[row] = max(g.rowheights[row], h)
+ g.colwidths[col] = max(g.colwidths[col], w)
+ }
+ }
+ // 3) handle the stretchy control
+ if g.stretchyrow != -1 && g.stretchycol != -1 {
+ for i, w := range g.colwidths {
+ if i != g.stretchycol {
+ width -= w
+ }
+ }
+ for i, h := range g.rowheights {
+ if i != g.stretchyrow {
+ height -= h
+ }
+ }
+ g.colwidths[g.stretchycol] = width
+ g.rowheights[g.stretchyrow] = height
+ }
+ // 4) draw
+ startx := x
+ for row, xcol := range g.controls {
+ current = nil // reset on new columns
+ for col, c := range xcol {
+ w := g.widths[row][col]
+ h := g.heights[row][col]
+ if g.filling[row][col] {
+ w = g.colwidths[col]
+ h = g.rowheights[row]
+ }
+ as := c.allocate(x, y, w, h, d)
+ if current != nil { // connect first left to first right
+ current.neighbor = 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.colwidths[col] + d.xpadding
+ }
+ x = startx
+ y += g.rowheights[row] + d.ypadding
+ }
+ return
+}
+
+// filling and stretchy are ignored for preferred size calculation
+// We don't consider the margins here, but will need to if Window.SizeToFit() is ever made a thing.
+func (g *Grid) preferredSize(d *sysSizeData) (width int, height int) {
+ max := func(a int, b int) int {
+ if a > b {
+ return a
+ }
+ return b
+ }
+
+ width -= (len(g.colwidths) - 1) * d.xpadding
+ height -= (len(g.rowheights) - 1) * d.ypadding
+ // 1) clear data structures
+ for i := range g.rowheights {
+ g.rowheights[i] = 0
+ }
+ for i := range g.colwidths {
+ g.colwidths[i] = 0
+ }
+ // 2) get preferred sizes; compute row/column sizes
+ for row, xcol := range g.controls {
+ for col, c := range xcol {
+ w, h := c.preferredSize(d)
+ g.widths[row][col] = w
+ g.heights[row][col] = h
+ g.rowheights[row] = max(g.rowheights[row], h)
+ g.colwidths[col] = max(g.colwidths[col], w)
+ }
+ }
+ // 3) now compute
+ for _, w := range g.colwidths {
+ width += w
+ }
+ for _, h := range g.rowheights {
+ height += h
+ }
+ return width, height
+}
+
+func (g *Grid) commitResize(c *allocation, d *sysSizeData) {
+ // this is to satisfy Control; nothing to do here
+}
+
+func (g *Grid) getAuxResizeInfo(d *sysSizeData) {
+ // this is to satisfy Control; nothing to do here
+}