andlabs-ui/stack.go

220 lines
6.5 KiB
Go
Raw Normal View History

2014-02-13 16:04:57 -06:00
// 13 february 2014
package ui
2014-02-13 16:04:57 -06:00
import (
"fmt"
"sync"
)
type orientation bool
2014-02-13 16:04:57 -06:00
const (
horizontal orientation = false
2014-06-10 10:12:32 -05:00
vertical orientation = true
2014-02-13 16:04:57 -06:00
)
// 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.
2014-02-25 14:38:04 -06:00
// 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.
2014-02-13 16:04:57 -06:00
type Stack struct {
2014-06-10 10:12:32 -05:00
lock sync.Mutex
created bool
orientation orientation
controls []Control
stretchy []bool
width, height []int // caches to avoid reallocating these each time
2014-02-13 16:04:57 -06:00
}
func newStack(o orientation, controls ...Control) *Stack {
2014-02-13 16:04:57 -06:00
return &Stack{
2014-06-10 10:12:32 -05:00
orientation: o,
controls: controls,
stretchy: make([]bool, len(controls)),
width: make([]int, len(controls)),
height: make([]int, len(controls)),
2014-02-13 16:04:57 -06:00
}
}
// 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) {
s.lock.Lock()
defer s.lock.Unlock()
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 {
s.lock.Lock()
defer s.lock.Unlock()
2014-02-15 14:38:41 -06:00
for i, c := range s.controls {
err := c.make(window)
2014-02-13 16:04:57 -06:00
if err != nil {
return fmt.Errorf("error adding control %d to Stack: %v", i, err)
2014-02-13 16:04:57 -06:00
}
}
2014-02-14 19:41:36 -06:00
s.created = true
2014-02-13 16:04:57 -06:00
return nil
}
func (s *Stack) allocate(x int, y int, width int, height int, d *sysSizeData) (allocations []*allocation) {
2014-02-24 09:42:58 -06:00
var stretchywid, stretchyht int
var current *allocation // for neighboring
2014-02-13 16:04:57 -06:00
2014-06-10 10:12:32 -05:00
if len(s.controls) == 0 { // do nothing if there's nothing to do
return nil
2014-02-13 16:04:57 -06:00
}
// 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
x += xmargin
y += ymargin
width -= xmargin * 2
height -= ymargin * 2
2014-02-24 09:42:58 -06:00
// 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)
2014-06-10 10:12:32 -05:00
if s.orientation == horizontal { // all controls have same height
2014-02-24 09:42:58 -06:00
s.width[i] = w
s.height[i] = height
stretchywid -= w
2014-06-10 10:12:32 -05:00
} else { // all controls have same width
2014-02-24 09:42:58 -06:00
s.width[i] = width
s.height[i] = h
stretchyht -= h
}
2014-02-13 16:04:57 -06:00
}
2014-02-24 09:42:58 -06:00
// 2) figure out size of stretchy controls
if nStretchy != 0 {
2014-06-10 10:12:32 -05:00
if s.orientation == horizontal { // split rest of width
2014-02-24 09:42:58 -06:00
stretchywid /= nStretchy
2014-06-10 10:12:32 -05:00
} else { // split rest of height
2014-02-24 09:42:58 -06:00
stretchyht /= nStretchy
}
}
for i := range s.controls {
2014-02-24 09:42:58 -06:00
if !s.stretchy[i] {
continue
}
s.width[i] = stretchywid
s.height[i] = stretchyht
2014-02-24 09:42:58 -06:00
}
// 3) now actually place controls
2014-02-15 14:38:41 -06:00
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 {
2014-02-24 09:42:58 -06:00
x += s.width[i]
} else {
y += s.height[i]
2014-02-13 16:04:57 -06:00
}
}
return allocations
2014-02-13 16:04:57 -06:00
}
// 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 *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
2014-06-10 10:12:32 -05:00
if len(s.controls) == 0 { // no controls, so return emptiness
return 0, 0
}
for i, c := range s.controls {
w, h := c.preferredSize(d)
if s.stretchy[i] {
nStretchy++
maxswid = max(maxswid, w)
maxsht = max(maxsht, h)
}
2014-06-10 10:12:32 -05:00
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
}
2014-02-25 14:38:04 -06:00
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.
2014-02-25 14:38:04 -06:00
func Space() Control {
return space
2014-02-25 14:38:04 -06:00
}
// 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)