summaryrefslogtreecommitdiff
path: root/prev/grid.go
diff options
context:
space:
mode:
Diffstat (limited to 'prev/grid.go')
-rw-r--r--prev/grid.go477
1 files changed, 477 insertions, 0 deletions
diff --git a/prev/grid.go b/prev/grid.go
new file mode 100644
index 0000000..d778c5f
--- /dev/null
+++ b/prev/grid.go
@@ -0,0 +1,477 @@
+// 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
+ parent *controlParent
+ 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{},
+ }
+}
+
+// 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) Padded() bool {
+ return g.padded
+}
+
+func (g *grid) SetPadded(padded bool) {
+ g.padded = padded
+}
+
+func (g *grid) setParent(p *controlParent) {
+ g.parent = p
+ for _, c := range g.controls {
+ c.control.setParent(g.parent)
+ }
+}
+
+func (g *grid) containerShow() {
+ for _, c := range g.controls {
+ c.control.containerShow()
+ }
+}
+
+func (g *grid) containerHide() {
+ for _, c := range g.controls {
+ c.control.containerHide()
+ }
+}
+
+// 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) {
+ 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
+}