Copied stack.go and grid.go to a temporary restaging area; removed the margin code from both as we no longer need it there.
This commit is contained in:
parent
e679f8c9e4
commit
be8afd43c0
|
@ -0,0 +1,236 @@
|
||||||
|
// 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?
|
||||||
|
// 0) inset the available rect by the needed padding
|
||||||
|
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 in container if Window.SizeToFit() is ever made a thing. TODO
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,213 @@
|
||||||
|
// 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 struct {
|
||||||
|
created bool
|
||||||
|
orientation orientation
|
||||||
|
controls []Control
|
||||||
|
stretchy []bool
|
||||||
|
width, height []int // caches to avoid reallocating these each time
|
||||||
|
}
|
||||||
|
|
||||||
|
func newStack(o orientation, controls ...Control) *Stack {
|
||||||
|
return &Stack{
|
||||||
|
orientation: o,
|
||||||
|
controls: controls,
|
||||||
|
stretchy: make([]bool, len(controls)),
|
||||||
|
width: make([]int, len(controls)),
|
||||||
|
height: make([]int, len(controls)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStretchy marks a control in a Stack as stretchy. This cannot be called once the Window containing the Stack has been created.
|
||||||
|
// It panics if index is out of range.
|
||||||
|
func (s *Stack) SetStretchy(index int) {
|
||||||
|
if s.created {
|
||||||
|
panic("call to Stack.SetStretchy() after Stack has been created")
|
||||||
|
}
|
||||||
|
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) make(window *sysData) error {
|
||||||
|
for i, c := range s.controls {
|
||||||
|
err := c.make(window)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error adding control %d to Stack: %v", i, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.created = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stack) allocate(x int, y int, width int, height int, d *sysSizeData) (allocations []*allocation) {
|
||||||
|
var stretchywid, stretchyht int
|
||||||
|
var current *allocation // for neighboring
|
||||||
|
|
||||||
|
if len(s.controls) == 0 { // do nothing if there's nothing to do
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 0) inset the available rect by the needed padding
|
||||||
|
if s.orientation == horizontal {
|
||||||
|
width -= (len(s.controls) - 1) * d.xpadding
|
||||||
|
} else {
|
||||||
|
height -= (len(s.controls) - 1) * d.ypadding
|
||||||
|
}
|
||||||
|
// 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.allocate(x, y, s.width[i], s.height[i], d)
|
||||||
|
if s.orientation == horizontal { // no vertical neighbors
|
||||||
|
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...)
|
||||||
|
if s.orientation == horizontal {
|
||||||
|
x += s.width[i] + d.xpadding
|
||||||
|
} else {
|
||||||
|
y += s.height[i] + d.ypadding
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allocations
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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).
|
||||||
|
// We don't consider the margins here, but will need to in container if Window.SizeToFit() is ever made a thing. TODO
|
||||||
|
func (s *Stack) preferredSize(d *sysSizeData) (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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stack) commitResize(c *allocation, d *sysSizeData) {
|
||||||
|
// this is to satisfy Control; nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Stack) getAuxResizeInfo(d *sysSizeData) {
|
||||||
|
// this is to satisfy Control; nothing to do here
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// 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 Grid, 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() may or may not be unique.
|
||||||
|
func Space() Control {
|
||||||
|
return space
|
||||||
|
}
|
||||||
|
|
||||||
|
// As above, a Stack with no controls draws nothing and reports no errors; its parent will still size it properly if made stretchy.
|
||||||
|
var space Control = newStack(horizontal)
|
Loading…
Reference in New Issue