diff --git a/newctrl/container_windows.go b/newctrl/container_windows.go index a9552cc..61a7296 100644 --- a/newctrl/container_windows.go +++ b/newctrl/container_windows.go @@ -57,6 +57,10 @@ func (c *container) hide() { C.ShowWindow(c.hwnd, C.SW_HIDE) } +func (c *container) parent() *controlParent { + return &controlParent{c.hwnd} +) + //export storeContainerHWND func storeContainerHWND(data unsafe.Pointer, hwnd C.HWND) { c := (*container)(data) diff --git a/newctrl/grid.go b/newctrl/grid.go new file mode 100644 index 0000000..0400ec0 --- /dev/null +++ b/newctrl/grid.go @@ -0,0 +1,424 @@ +// 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) +} + +// 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 + container *container + + 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{}, + container: newContainer(), + } +} + +// 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, + } + control.setParent(g.container.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.container.setParent(p) +} + +// 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 + } + + // -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 + // 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 += 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 && 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 += d.ypadding + } + g.controls[i].finalheight += rowheights[y] + } + cury += rowheights[y] + d.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 { + current = nil + 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 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 (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)*d.xpadding, + rowheight + (g.ymax-1)*d.ypadding +} diff --git a/newctrl/simplegrid.go b/newctrl/simplegrid.go new file mode 100644 index 0000000..2e8767e --- /dev/null +++ b/newctrl/simplegrid.go @@ -0,0 +1,221 @@ +// 25 february 2014 + +package ui + +import ( + "fmt" +) + +// A SimpleGrid 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 SimpleGrid 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 SimpleGrid 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 SimpleGrid are given in (row,column) form with (0,0) being the top-left cell. +// +// As a special rule, to ensure proper appearance, non-standalone Labels are automatically made filling. +type SimpleGrid interface { + Control + + // SetFilling marks the given Control of the SimpleGrid as filling its cell instead of staying at its preferred size. + // It panics if the given coordinate is invalid. + SetFilling(row int, column int) + + // SetStretchy marks the given Control of the SimpleGrid as stretchy. + // Stretchy implies filling. + // Only one control can be stretchy per SimpleGrid; calling SetStretchy multiple times merely changes which control is stretchy (preserving the previous filling value). + // It panics if the given coordinate is invalid. + SetStretchy(row int, column int) +} + +type simpleGrid struct { + controls [][]Control + filling [][]bool + stretchyrow, stretchycol int + stretchyfill bool + widths, heights [][]int // caches to avoid reallocating each time + rowheights, colwidths []int + container *container +} + +// NewSimpleGrid creates a new SimpleGrid with the given Controls. +// NewSimpleGrid 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. +// NewSimpleGrid panics if not given a full grid of Controls. +// Example: +// grid := NewSimpleGrid(3, +// control00, control01, control02, +// control10, control11, control12, +// control20, control21, control22) +func NewSimpleGrid(nPerRow int, controls ...Control) SimpleGrid { + if len(controls)%nPerRow != 0 { + panic(fmt.Errorf("incomplete simpleGrid given to NewSimpleGrid() (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] + if l, ok := controls[i].(Label); ok && !l.isStandalone() { + cf[row][x] = true + } + i++ + } + } + g := &simpleGrid{ + controls: cc, + filling: cf, + stretchyrow: -1, + stretchycol: -1, + widths: cw, + heights: ch, + rowheights: make([]int, nRows), + colwidths: make([]int, nPerRow), + container: newContainer(), + } + p := g.container.parent() + for _, cc := range g.cc { + for _, c := range cc { + c.setParent(p) + } + } + return g +} + +func (g *simpleGrid) SetFilling(row int, column int) { + 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 SimpleGrid.SetFilling()", row, column)) + } + g.filling[row][column] = true +} + +func (g *simpleGrid) SetStretchy(row int, column int) { + 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 SimpleGrid.SetStretchy()", row, column)) + } + if g.stretchyrow != -1 || g.stretchycol != -1 { + g.filling[g.stretchyrow][g.stretchycol] = g.stretchyfill + } + g.stretchyrow = row + g.stretchycol = column + g.stretchyfill = g.filling[g.stretchyrow][g.stretchycol] // save previous value in case it changes later + g.filling[g.stretchyrow][g.stretchycol] = true +} + +func (g *simpleGrid) setParent(parent *controlParent) { + g.container.setParent(parent) +} + +func (g *simpleGrid) resize(x int, y int, width int, height int, d *sizing) { + max := func(a int, b int) int { + if a > b { + return a + } + return b + } + + if len(g.controls) == 0 { + return + } + // 0) inset the available rect by the needed padding and reset x/y for children + width -= (len(g.colwidths) - 1) * d.xpadding + height -= (len(g.rowheights) - 1) * d.ypadding + x = 0 + y = 0 + // 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] + } + c.resize(x, y, w, h, d) + x += g.colwidths[col] + d.xpadding + } + x = startx + y += g.rowheights[row] + d.ypadding + } +} + +// filling and stretchy are ignored for preferred size calculation +func (g *simpleGrid) preferredSize(d *sizing) (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 +} diff --git a/newctrl/stack.go b/newctrl/stack.go new file mode 100644 index 0000000..053cb5b --- /dev/null +++ b/newctrl/stack.go @@ -0,0 +1,195 @@ +// 13 february 2014 + +package ui + +import ( + "fmt" +) + +type orientation bool + +const ( + horizontal orientation = false + vertical orientation = true +) + +// A Stack stacks controls horizontally or vertically within the Stack's parent. +// A horizontal Stack gives all controls the same height and their preferred widths. +// A vertical Stack gives all controls the same width and their preferred heights. +// Any extra space at the end of a Stack is left blank. +// Some controls may be marked as "stretchy": when the Window they are in changes size, stretchy controls resize to take up the remaining space after non-stretchy controls are laid out. If multiple controls are marked stretchy, they are alloted equal distribution of the remaining space. +type Stack interface { + Control + + // SetStretchy marks a control in a Stack as stretchy. + // It panics if index is out of range. + SetStretchy(index int) +} + +type stack struct { + orientation orientation + controls []Control + stretchy []bool + width, height []int // caches to avoid reallocating these each time + container *container +} + +func newStack(o orientation, controls ...Control) Stack { + s := &stack{ + orientation: o, + controls: controls, + stretchy: make([]bool, len(controls)), + width: make([]int, len(controls)), + height: make([]int, len(controls)), + container: newContainer(), + } + p := s.container.parent() + for _, c := range s.controls { + c.setParent(p) + } +} + +// NewHorizontalStack creates a new Stack that arranges the given Controls horizontally. +func NewHorizontalStack(controls ...Control) Stack { + return newStack(horizontal, controls...) +} + +// NewVerticalStack creates a new Stack that arranges the given Controls vertically. +func NewVerticalStack(controls ...Control) Stack { + return newStack(vertical, controls...) +} + +func (s *stack) SetStretchy(index int) { + if index < 0 || index > len(s.stretchy) { + panic(fmt.Errorf("index %d out of range in Stack.SetStretchy()", index)) + } + s.stretchy[index] = true +} + +func (s *stack) setParent(parent *controlParent) { + s.container.setParent(parent) +} + +func (s *stack) resize(x int, y int, width int, height int, d *sizing) { + var stretchywid, stretchyht int + + s.container.resize(x, y, width, height, d) + if len(s.controls) == 0 { // do nothing if there's nothing to do + return + } + // 0) inset the available rect by the needed padding and reset the x/y coordinates for the children + if s.orientation == horizontal { + width -= (len(s.controls) - 1) * d.xpadding + } else { + height -= (len(s.controls) - 1) * d.ypadding + } + x = 0 + y = 0 + // 1) get height and width of non-stretchy controls; figure out how much space is alloted to stretchy controls + stretchywid = width + stretchyht = height + nStretchy := 0 + for i, c := range s.controls { + if s.stretchy[i] { + nStretchy++ + continue + } + w, h := c.preferredSize(d) + if s.orientation == horizontal { // all controls have same height + s.width[i] = w + s.height[i] = height + stretchywid -= w + } else { // all controls have same width + s.width[i] = width + s.height[i] = h + stretchyht -= h + } + } + // 2) figure out size of stretchy controls + if nStretchy != 0 { + if s.orientation == horizontal { // split rest of width + stretchywid /= nStretchy + } else { // split rest of height + stretchyht /= nStretchy + } + } + for i := range s.controls { + if !s.stretchy[i] { + continue + } + s.width[i] = stretchywid + s.height[i] = stretchyht + } + // 3) now actually place controls + for i, c := range s.controls { + as := c.resize(x, y, s.width[i], s.height[i], d) + if s.orientation == horizontal { + x += s.width[i] + d.xpadding + } else { + y += s.height[i] + d.ypadding + } + } + return +} + +// The preferred size of a Stack is the sum of the preferred sizes of non-stretchy controls + (the number of stretchy controls * the largest preferred size among all stretchy controls). +func (s *stack) preferredSize(d *sizing) (width int, height int) { + max := func(a int, b int) int { + if a > b { + return a + } + return b + } + + var nStretchy int + var maxswid, maxsht int + + if len(s.controls) == 0 { // no controls, so return emptiness + return 0, 0 + } + if s.orientation == horizontal { + width = (len(s.controls) - 1) * d.xpadding + } else { + height = (len(s.controls) - 1) * d.ypadding + } + for i, c := range s.controls { + w, h := c.preferredSize(d) + if s.stretchy[i] { + nStretchy++ + maxswid = max(maxswid, w) + maxsht = max(maxsht, h) + } + if s.orientation == horizontal { // max vertical size + if !s.stretchy[i] { + width += w + } + height = max(height, h) + } else { + width = max(width, w) + if !s.stretchy[i] { + height += h + } + } + } + if s.orientation == horizontal { + width += nStretchy * maxswid + } else { + height += nStretchy * maxsht + } + return +} + +// TODO the below needs to be changed + +// Space returns a null Control intended for padding layouts with blank space. +// It appears to its owner as a Control of 0x0 size. +// +// For a Stack, Space can be used to insert spaces in the beginning or middle of Stacks (Stacks by nature handle spaces at the end themselves). In order for this to work properly, make the Space stretchy. +// +// For a SimpleGrid, Space can be used to have an empty cell. A stretchy Grid cell with a Space can be used to anchor the perimeter of a Grid to the respective Window edges without making one of the other controls stretchy instead (leaving empty space in the Window otherwise). Otherwise, you do not need to do anything special for the Space to work (though remember that an entire row or column of Spaces will appear as having height or width zero, respectively, unless one is marked as stretchy). +// +// The value returned from Space() is guaranteed to be unique. +func Space() Control { + // Grid's rules require this to be unique on every call + return newStack(horizontal) +}