doodle/ui/frame_pack.go
Noah Petherbridge 5956863996 Menu Toolbar for Editor + Shell Prompts + Theme
* Added a "menu toolbar" to the top of the Edit Mode with useful buttons
  that work: New Level, New Doodad (same thing), Save, Save as, Open.
* Added ability for the dev console to prompt the user for a question,
  which opens the console automatically. "Save", "Save as" and "Load"
  ask for their filenames this way.
* Started groundwork for theming the app. The palette window is a light
  brown with an orange title bar, the Menu Toolbar has a black
  background, etc.
* Added support for multiple fonts instead of just monospace. DejaVu
  Sans (normal and bold) are used now for most labels and window titles,
  respectively. The dev console uses DejaVu Sans Mono as before.
* Update ui.Label to accept PadX and PadY separately instead of only
  having the Padding option which did both.
* Improvements to Frame packing algorithm.
* Set the SDL draw mode to BLEND so we can use alpha colors properly,
  so now the dev console is semi-translucent.
2018-08-11 17:30:00 -07:00

315 lines
7.5 KiB
Go

package ui
import "git.kirsle.net/apps/doodle/render"
// 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 widgets.
maxWidth int32
maxHeight int32
visited = []packedWidget{}
expanded = []packedWidget{}
)
// Iterate through all anchored directions and compute how much space to
// reserve to contain all of their widgets.
for anchor := AnchorMin; anchor <= AnchorMax; anchor++ {
if _, ok := w.packs[anchor]; !ok {
continue
}
var (
x int32
y int32
yDirection int32 = 1
xDirection int32 = 1
)
if anchor.IsSouth() {
y = frameSize.H
yDirection = -1 - w.BoxThickness(2) // parent + child BoxThickness(1) = 2
} else if anchor == E {
x = frameSize.W
xDirection = -1 // - w.BoxThickness(2)
}
for _, packedWidget := range w.packs[anchor] {
child := packedWidget.widget
pack := packedWidget.pack
child.Compute(e)
x += pack.PadX
y += pack.PadY
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 anchor.IsSouth() {
y -= size.H + pack.PadY
}
if anchor.IsEast() {
x -= size.W + pack.PadX
}
child.MoveTo(render.NewPoint(x, y))
if anchor.IsNorth() {
y += size.H + pack.PadY
}
if anchor == W {
x += size.W + pack.PadX
}
visited = append(visited, packedWidget)
if pack.Expand {
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) / int32(len(expanded))), // - w.BoxThickness(2),
H: ((frameSize.H - computedSize.H) / int32(len(expanded))), // - w.BoxThickness(2),
}
for _, pw := range expanded {
pw.widget.ResizeBy(growBy)
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 anchor 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 w.String() == "Frame<Row0; 3 widgets>" {
log.Debug("%s>%s: pack.FillX=%d resize=%s innerFrameSize=%s", w, child, pack.FillX, resize, innerFrameSize)
}
if pack.Anchor.IsNorth() || pack.Anchor.IsSouth() {
if pack.FillX && resize.W < innerFrameSize.W {
resize.W = innerFrameSize.W - w.BoxThickness(2)
resized = true
}
if resize.W < innerFrameSize.W-w.BoxThickness(4) {
if pack.Anchor.IsCenter() {
point.X = (innerFrameSize.W / 2) - (resize.W / 2)
} else if pack.Anchor.IsWest() {
point.X = pack.PadX
} else if pack.Anchor.IsEast() {
point.X = innerFrameSize.W - resize.W - pack.PadX
}
moved = true
}
} else if pack.Anchor.IsWest() || pack.Anchor.IsEast() {
if pack.FillY && resize.H < innerFrameSize.H {
resize.H = innerFrameSize.H - w.BoxThickness(2) // BoxThickness(2) for parent + child
// point.Y -= (w.BoxThickness(4) + child.BoxThickness(2))
moved = true
resized = true
}
// Vertically align the widgets.
if resize.H < innerFrameSize.H {
if pack.Anchor.IsMiddle() {
point.Y = (innerFrameSize.H / 2) - (resize.H / 2) - w.BoxThickness(1)
} else if pack.Anchor.IsNorth() {
point.Y = pack.PadY - w.BoxThickness(4)
} else if pack.Anchor.IsSouth() {
point.Y = innerFrameSize.H - resize.H - pack.PadY
}
moved = true
}
} else {
log.Error("unsupported pack.Anchor")
}
if resized && size != resize {
// log.Debug("%s/%s: resize to: %s", w, child, resize)
child.Resize(resize)
child.Compute(e)
}
if moved {
child.MoveTo(point)
}
}
// if !w.FixedSize() {
w.Resize(render.NewRect(
frameSize.W-w.BoxThickness(2),
frameSize.H-w.BoxThickness(2),
))
// }
}
// 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.
Anchor Anchor
// 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 int32 // Equal padding on X and Y.
PadX int32
PadY int32
Expand bool // Widget should grow its allocated space to better fill the parent.
}
// Anchor is a cardinal direction.
type Anchor uint8
// Anchor values.
const (
Center Anchor = iota
N
NE
E
SE
S
SW
W
NW
)
// Range of Anchor values.
const (
AnchorMin = Center
AnchorMax = NW
)
// IsNorth returns if the anchor is N, NE or NW.
func (a Anchor) IsNorth() bool {
return a == N || a == NE || a == NW
}
// IsSouth returns if the anchor is S, SE or SW.
func (a Anchor) IsSouth() bool {
return a == S || a == SE || a == SW
}
// IsEast returns if the anchor is E, NE or SE.
func (a Anchor) IsEast() bool {
return a == E || a == NE || a == SE
}
// IsWest returns if the anchor is W, NW or SW.
func (a Anchor) IsWest() bool {
return a == W || a == NW || a == SW
}
// IsCenter returns if the anchor is Center, N or S, to determine
// whether to align text as centered for North/South anchors.
func (a Anchor) IsCenter() bool {
return a == Center || a == N || a == S
}
// IsMiddle returns if the anchor is Center, E or W, to determine
// whether to align text as middled for East/West anchors.
func (a Anchor) IsMiddle() bool {
return a == Center || a == W || a == E
}
// 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 anchor?
if _, ok := w.packs[C.Anchor]; !ok {
w.packs[C.Anchor] = []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
}
w.packs[C.Anchor] = append(w.packs[C.Anchor], packedWidget{
widget: child,
pack: C,
})
w.widgets = append(w.widgets, child)
}
type packLayout struct {
widgets []packedWidget
}
type packedWidget struct {
widget Widget
pack Pack
fill uint8
}
// packedWidget.fill values
const (
fillNone uint8 = iota
fillX
fillY
fillBoth
)