Migrated the layout containers. No margins yet. If all goes well, though, this should be enough...

This commit is contained in:
Pietro Gagliardi 2014-10-15 12:57:08 -04:00
parent d6b7a0d8c2
commit 6b2dac4200
4 changed files with 844 additions and 0 deletions

View File

@ -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)

424
newctrl/grid.go Normal file
View File

@ -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
}

221
newctrl/simplegrid.go Normal file
View File

@ -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
}

195
newctrl/stack.go Normal file
View File

@ -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)
}