ui/frame_pack.go
Noah Petherbridge 4ba563d48d Supervisor, Frame Pack and Misc Fixes
* Button: do not call MoveTo inside of Compute().
* Label: do not call MoveTo inside of Compute().
* MainWindow: add OnLoop callback function support so you can run custom code
  each loop and react to the event.State before the UI updates.
* Supervisor: locate widgets using AbsolutePosition() instead of w.Point()
  to play nice with Frame and Window packed widgets.
* Widget interface: rename Adopt() to SetParent() which makes more sense for
  what the function actually does.
* Window: set itself as the parent of the body Frame so that the Supervisor
  can locate widgets anywhere inside a window's frames.

Frame packing fixes:

* Widgets with Expand:true grow their space with ResizeAuto to preserve the
  FixedSize() boolean, instead of being hard-resized to fill the Frame.
* Widgets that Fill their space are resized with ResizeAuto too.

Outstanding bugs:

* Labels don't expand (window title bars, etc.)
2019-12-29 00:00:03 -08:00

337 lines
8.3 KiB
Go

package ui
import (
"git.kirsle.net/go/render"
)
// Pack provides configuration fields for Frame.Pack().
type Pack struct {
// Side of the parent to anchor the position to, like N, SE, W. Default
// is Center.
Side Side
// If the widget is smaller than its allocated space, grow the widget
// to fill its space in the Frame.
Fill bool
FillX bool
FillY bool
Padding int // Equal padding on X and Y.
PadX int
PadY int
Expand bool // Widget should grow its allocated space to better fill the parent.
}
// Pack a widget along a side of the frame.
func (w *Frame) Pack(child Widget, config ...Pack) {
var C Pack
if len(config) > 0 {
C = config[0]
}
// Initialize the pack list for this side?
if _, ok := w.packs[C.Side]; !ok {
w.packs[C.Side] = []packedWidget{}
}
// Padding: if the user only provided Padding add it to both
// the X and Y value. If the user additionally provided the X
// and Y value, it will add to the base padding as you'd expect.
C.PadX += C.Padding
C.PadY += C.Padding
// Fill: true implies both directions.
if C.Fill {
C.FillX = true
C.FillY = true
}
// Adopt the child widget so it can access the Frame.
child.SetParent(w)
w.packs[C.Side] = append(w.packs[C.Side], packedWidget{
widget: child,
pack: C,
})
w.widgets = append(w.widgets, child)
}
// computePacked processes all the Pack layout widgets in the Frame.
func (w *Frame) computePacked(e render.Engine) {
var (
frameSize = w.BoxSize()
// maxWidth and maxHeight are always the computed minimum dimensions
// that the Frame must be to contain all of its children. If the Frame
// was configured with an explicit Size, the Frame will be that Size,
// but we still calculate how much space the widgets _actually_ take
// so we can expand them to fill remaining space in fixed size Frames.
maxWidth int
maxHeight int
visited = []packedWidget{}
expanded = []packedWidget{}
)
// Iterate through all directions and compute how much space to
// reserve to contain all of their widgets.
for side := SideMin; side <= SideMax; side++ {
if _, ok := w.packs[side]; !ok {
continue
}
var (
x int
y int
yDirection = 1
xDirection = 1
)
if side.IsSouth() {
y = frameSize.H - w.BoxThickness(4)
yDirection = -1
} else if side.IsEast() {
x = frameSize.W - w.BoxThickness(4)
xDirection = -1
}
for _, packedWidget := range w.packs[side] {
child := packedWidget.widget
pack := packedWidget.pack
child.Compute(e)
if child.Hidden() {
continue
}
x += pack.PadX * xDirection
y += pack.PadY * yDirection
var (
// point = child.Point()
size = child.Size()
yStep = y * yDirection
xStep = x * xDirection
)
if xStep+size.W+(pack.PadX*2) > maxWidth {
maxWidth = xStep + size.W + (pack.PadX * 2)
}
if yStep+size.H+(pack.PadY*2) > maxHeight {
maxHeight = yStep + size.H + (pack.PadY * 2)
}
if side.IsSouth() {
y -= size.H - pack.PadY
}
if side.IsEast() {
x -= size.W - pack.PadX
}
// NOTE: we place the child's position relative to the Frame's
// position. So a child placed at the top/left of the Frame gets
// an x,y near zero regardless of the Frame's position.
child.MoveTo(render.NewPoint(x, y))
if side.IsNorth() {
y += size.H + pack.PadY
}
if side.IsWest() {
x += size.W + pack.PadX
}
visited = append(visited, packedWidget)
if pack.Expand { // TODO: don't fuck with children of fixed size
expanded = append(expanded, packedWidget)
}
}
}
// If we have extra space in the Frame and any expanding widgets, let the
// expanding widgets grow and share the remaining space.
computedSize := render.NewRect(maxWidth, maxHeight)
if len(expanded) > 0 && !frameSize.IsZero() { // && frameSize.Bigger(computedSize) {
// Divy up the size available.
growBy := render.Rect{
W: ((frameSize.W - computedSize.W) / len(expanded)) - w.BoxThickness(4),
H: ((frameSize.H - computedSize.H) / len(expanded)) - w.BoxThickness(4),
}
for _, pw := range expanded {
// Grow the widget but maintain its auto-size flag, in case the widget
// was not given an explicit size before.
size := pw.widget.Size()
pw.widget.ResizeAuto(render.Rect{
W: size.W + growBy.W,
H: size.H + growBy.H,
})
pw.widget.Compute(e)
}
}
// If we're not using a fixed Frame size, use the dynamically computed one.
if !w.FixedSize() {
frameSize = render.NewRect(maxWidth, maxHeight)
} else {
// If either of the sizes were left zero, use the dynamically computed one.
if frameSize.W == 0 {
frameSize.W = maxWidth
}
if frameSize.H == 0 {
frameSize.H = maxHeight
}
}
// Rescan all the widgets in this side to re-center them
// in their space.
innerFrameSize := render.NewRect(
frameSize.W-w.BoxThickness(2),
frameSize.H-w.BoxThickness(2),
)
for _, pw := range visited {
var (
child = pw.widget
pack = pw.pack
point = child.Point()
size = child.Size()
resize = size
resized bool
moved bool
)
if pack.Side.IsNorth() || pack.Side.IsSouth() {
// Aligned to the top or bottom. If the widget Fills horizontally,
// resize it so its Width matches the frame's Width.
if pack.FillX && resize.W < innerFrameSize.W {
resize.W = innerFrameSize.W - w.BoxThickness(2) // TODO: child.BoxThickness instead??
resized = true
}
// If it does not Fill horizontally and there is extra horizontal
// space, center the widget inside the space. TODO: Anchor option
// could align the widget to the left or right instead of center.
if resize.W < innerFrameSize.W-w.BoxThickness(4) {
if pack.Side.IsCenter() {
point.X = (innerFrameSize.W / 2) - (resize.W / 2)
} else if pack.Side.IsWest() {
point.X = pack.PadX
} else if pack.Side.IsEast() {
point.X = innerFrameSize.W - resize.W - pack.PadX
}
moved = true
}
} else if pack.Side.IsWest() || pack.Side.IsEast() {
// Similar logic to the above, but widget is packed against the
// left or right edge. Handle vertical Fill to grow the widget.
if pack.FillY && resize.H < innerFrameSize.H {
resize.H = innerFrameSize.H - w.BoxThickness(2) // TODO: child.BoxThickness instead??
resized = true
}
// Vertically align the widgets.
if resize.H < innerFrameSize.H {
if pack.Side.IsMiddle() {
point.Y = (innerFrameSize.H / 2) - (resize.H / 2) // - w.BoxThickness(1)
} else if pack.Side.IsNorth() {
point.Y = pack.PadY // - w.BoxThickness(4)
} else if pack.Side.IsSouth() {
point.Y = innerFrameSize.H - resize.H - pack.PadY
}
moved = true
}
} else {
panic("unsupported pack.Side")
}
if resized && size != resize {
child.ResizeAuto(resize)
child.Compute(e)
}
if moved {
child.MoveTo(point)
}
}
// TODO: the Frame should ResizeAuto so it doesn't mark fixedSize=true.
// Currently there's a bug where frames will grow when the window grows but
// never shrink again when the window shrinks.
// if !w.FixedSize() {
w.Resize(render.NewRect(
frameSize.W-w.BoxThickness(2),
frameSize.H-w.BoxThickness(2),
))
// }
}
// Side is a cardinal direction.
type Side uint8
// Side values.
const (
Center Side = iota
N
NE
E
SE
S
SW
W
NW
)
// Range of Side values.
const (
SideMin = Center
SideMax = NW
)
// IsNorth returns if the side is N, NE or NW.
func (a Side) IsNorth() bool {
return a == N || a == NE || a == NW
}
// IsSouth returns if the side is S, SE or SW.
func (a Side) IsSouth() bool {
return a == S || a == SE || a == SW
}
// IsEast returns if the side is E, NE or SE.
func (a Side) IsEast() bool {
return a == E || a == NE || a == SE
}
// IsWest returns if the side is W, NW or SW.
func (a Side) IsWest() bool {
return a == W || a == NW || a == SW
}
// IsCenter returns if the side is Center, N or S, to determine
// whether to align text as centered for North/South sides.
func (a Side) IsCenter() bool {
return a == Center || a == N || a == S
}
// IsMiddle returns if the side is Center, E or W, to determine
// whether to align text as middled for East/West sides.
func (a Side) IsMiddle() bool {
return a == Center || a == W || a == E
}
type packLayout struct {
widgets []packedWidget
}
type packedWidget struct {
widget Widget
pack Pack
fill uint8
}
// packedWidget.fill values
const (
fillNone uint8 = iota
fillX
fillY
fillBoth
)