blob: 22bd019cddab1a61717fa1e26bd9417706d7f8c4 [file] [log] [blame]
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package flex provides a container widget that lays out its children
// following the CSS flexbox algorithm.
//
// As the shiny widget model does not provide all of the layout features
// of CSS, the flex package diverges in several ways. There is no item
// inline-axis, no item margins or padding to be accounted for, and the
// container size provided by the outer widget is taken as gospel and
// never expanded.
package flex
import (
"fmt"
"image"
"math"
"golang.org/x/exp/shiny/widget/node"
"golang.org/x/exp/shiny/widget/theme"
)
// Direction is the direction in which flex items are laid out.
//
// https://www.w3.org/TR/css-flexbox-1/#flex-direction-property
type Direction uint8
const (
Row Direction = iota
RowReverse
Column
ColumnReverse
)
// FlexWrap controls whether the container is single- or multi-line,
// and the direction in which the lines are laid out.
//
// https://www.w3.org/TR/css-flexbox-1/#flex-wrap-property
type FlexWrap uint8
const (
NoWrap FlexWrap = iota
Wrap
WrapReverse
)
// Justify aligns items along the main axis.
//
// https://www.w3.org/TR/css-flexbox-1/#justify-content-property
type Justify uint8
const (
JustifyStart Justify = iota // pack to start of line
JustifyEnd // pack to end of line
JustifyCenter // pack to center of line
JustifySpaceBetween // even spacing
JustifySpaceAround // even spacing, half-size on each end
)
// AlignItem aligns items along the cross axis.
//
// It is the 'align-items' property when applied to a Flex container,
// and the 'align-self' property when applied to an item in LayoutData.
//
// https://www.w3.org/TR/css-flexbox-1/#align-items-property
// https://www.w3.org/TR/css-flexbox-1/#propdef-align-self
type AlignItem uint8
const (
AlignItemAuto AlignItem = iota
AlignItemStart
AlignItemEnd
AlignItemCenter
AlignItemBaseline // TODO requires introducing inline-axis concept
AlignItemStretch
)
// AlignContent is the 'align-content' property.
// It aligns container lines when there is extra space on the cross-axis.
//
// https://www.w3.org/TR/css-flexbox-1/#align-content-property
type AlignContent uint8
const (
AlignContentStretch AlignContent = iota
AlignContentStart
AlignContentEnd
AlignContentCenter
AlignContentSpaceBetween
AlignContentSpaceAround
)
// Basis sets the base size of a flex item.
//
// A default basis of Auto means the flex container uses the
// MeasuredSize of an item. Otherwise a Definite Basis will
// override the MeasuredSize with BasisPx.
//
// TODO: do we (or will we )have a useful notion of Content in the
// widget layout model that is separate from MeasuredSize? If not,
// we could consider completely removing this concept from this
// flex implementation.
type Basis uint8
const (
Auto Basis = iota
Definite
)
// Flex is a container widget that lays out its children following the
// CSS flexbox algorithm.
type Flex struct {
node.ContainerEmbed
Direction Direction
Wrap FlexWrap
Justify Justify
AlignItems AlignItem
AlignContent AlignContent
}
// NewFlex returns a new Flex widget containing the given children.
func NewFlex(children ...node.Node) *Flex {
w := new(Flex)
w.Wrapper = w
for _, c := range children {
w.Insert(c, nil)
}
return w
}
func (w *Flex) Measure(t *theme.Theme, widthHint, heightHint int) {
// As Measure is a bottom-up calculation of natural size, we have no
// hint yet as to how we should flex. So we ignore Wrap, Justify,
// AlignItem, AlignContent.
for c := w.FirstChild; c != nil; c = c.NextSibling {
// TODO: pass down width/height hints?
c.Wrapper.Measure(t, node.NoHint, node.NoHint)
if d, ok := c.LayoutData.(LayoutData); ok {
_ = d
// TODO Measure
}
}
}
func (w *Flex) Layout(t *theme.Theme) {
var children []element
for c := w.FirstChild; c != nil; c = c.NextSibling {
children = append(children, element{
flexBaseSize: float64(w.flexBaseSize(c)),
n: c,
})
}
containerMainSize := float64(w.mainSize(w.Rect.Size()))
containerCrossSize := float64(w.crossSize(w.Rect.Size()))
// §9.3.5 collect children into flex lines
var lines []flexLine
if w.Wrap == NoWrap {
line := flexLine{child: make([]*element, len(children))}
for i := range children {
child := &children[i]
line.child[i] = child
line.mainSize += child.flexBaseSize
}
lines = []flexLine{line}
} else {
var line flexLine
for i := range children {
child := &children[i]
hypotheticalMainSize := w.clampSize(child.flexBaseSize, child.n)
if line.mainSize > 0 && line.mainSize+hypotheticalMainSize > containerMainSize {
lines = append(lines, line)
line = flexLine{}
}
line.child = append(line.child, child)
line.mainSize += hypotheticalMainSize
if d, ok := child.n.LayoutData.(LayoutData); ok && d.BreakAfter {
lines = append(lines, line)
line = flexLine{}
}
}
if len(line.child) > 0 || len(children) == 0 {
lines = append(lines, line)
}
if w.Wrap == WrapReverse {
for i := 0; i < len(lines)/2; i++ {
lines[i], lines[len(lines)-i-1] = lines[len(lines)-i-1], lines[i]
}
}
}
// §9.3.6 resolve flexible lengths (details in section §9.7)
for l := range lines {
line := &lines[l]
grow := line.mainSize < containerMainSize // §9.7.1
// §9.7.2 freeze inflexible children.
for _, child := range line.child {
mainSize := float64(w.mainSize(child.n.MeasuredSize))
hypotheticalMainSize := w.clampSize(mainSize, child.n)
if grow {
if growFactor(child.n) == 0 || float64(w.flexBaseSize(child.n)) > hypotheticalMainSize {
child.frozen = true
child.mainSize = hypotheticalMainSize
}
} else {
if shrinkFactor(child.n) == 0 || float64(w.flexBaseSize(child.n)) < hypotheticalMainSize {
child.frozen = true
child.mainSize = hypotheticalMainSize
}
}
}
// §9.7.3 calculate initial free space
initFreeSpace := float64(w.mainSize(w.Rect.Size()))
for _, child := range line.child {
if child.frozen {
initFreeSpace -= child.mainSize
} else {
initFreeSpace -= float64(w.flexBaseSize(child.n))
}
}
// §9.7.4 flex loop
for {
// Check for flexible items.
allFrozen := true
for _, child := range line.child {
if !child.frozen {
allFrozen = false
break
}
}
if allFrozen {
break
}
// Calculate remaining free space.
remFreeSpace := float64(w.mainSize(w.Rect.Size()))
unfrozenFlexFactor := 0.0
for _, child := range line.child {
if child.frozen {
remFreeSpace -= child.mainSize
} else {
remFreeSpace -= float64(w.flexBaseSize(child.n))
if grow {
unfrozenFlexFactor += growFactor(child.n)
} else {
unfrozenFlexFactor += shrinkFactor(child.n)
}
}
}
if unfrozenFlexFactor < 1 {
p := initFreeSpace * unfrozenFlexFactor
if math.Abs(p) < math.Abs(remFreeSpace) {
remFreeSpace = p
}
}
// Distribute free space proportional to flex factors.
if grow {
for _, child := range line.child {
if child.frozen {
continue
}
r := growFactor(child.n) / unfrozenFlexFactor
child.mainSize = float64(w.flexBaseSize(child.n)) + r*remFreeSpace
}
} else {
sumScaledShrinkFactor := 0.0
for _, child := range line.child {
if child.frozen {
continue
}
scaledShrinkFactor := float64(w.flexBaseSize(child.n)) * shrinkFactor(child.n)
sumScaledShrinkFactor += scaledShrinkFactor
}
for _, child := range line.child {
if child.frozen {
continue
}
scaledShrinkFactor := float64(w.flexBaseSize(child.n)) * shrinkFactor(child.n)
r := float64(scaledShrinkFactor) / sumScaledShrinkFactor
child.mainSize = float64(w.flexBaseSize(child.n)) - r*math.Abs(float64(remFreeSpace))
}
}
// Fix min/max violations.
sumClampDiff := 0.0
for _, child := range line.child {
// TODO: we work in whole pixels but flex calculations are done in
// fractional pixels. Take this oppertunity to clamp us to whole
// pixels and make sure we sum correctly.
if child.frozen {
continue
}
child.unclamped = child.mainSize
child.mainSize = w.clampSize(child.mainSize, child.n)
sumClampDiff += child.mainSize - child.unclamped
}
// Freeze over-flexed items.
switch {
case sumClampDiff == 0:
for _, child := range line.child {
child.frozen = true
}
case sumClampDiff > 0:
for _, child := range line.child {
if child.mainSize > child.unclamped {
child.frozen = true
}
}
case sumClampDiff < 0:
for _, child := range line.child {
if child.mainSize < child.unclamped {
child.frozen = true
}
}
}
}
// §9.7.5 set main size
// At this point, child.mainSize is right.
}
// §9.4 determine cross size
// §9.4.7 calculate hypothetical cross size of each element
for l := range lines {
for _, child := range lines[l].child {
child.crossSize = float64(w.crossSize(child.n.MeasuredSize))
if child.mainSize < float64(w.mainSize(child.n.MeasuredSize)) {
if r, ok := aspectRatio(child.n); ok {
child.crossSize = child.mainSize / r
}
}
if d, ok := child.n.LayoutData.(LayoutData); ok {
minSize := float64(w.crossSize(d.MinSize))
if minSize > child.crossSize {
child.crossSize = minSize
} else if d.MaxSize != nil {
maxSize := float64(w.crossSize(*d.MaxSize))
if child.crossSize > maxSize {
child.crossSize = maxSize
}
}
}
}
}
if len(lines) == 1 {
// §9.4.8 single line
switch w.Direction {
case Row, RowReverse:
lines[0].crossSize = float64(w.Rect.Size().Y)
case Column, ColumnReverse:
lines[0].crossSize = float64(w.Rect.Size().X)
}
} else {
// §9.4.8 multi-line
for l := range lines {
line := &lines[l]
// TODO §9.4.8.1, no concept of inline-axis yet
max := 0.0
for _, child := range line.child {
if child.crossSize > max {
max = child.crossSize
}
}
line.crossSize = max
}
}
off := 0.0
for l := range lines {
line := &lines[l]
line.crossOffset = off
off += line.crossSize
}
// §9.4.9 align-content: stretch
remCrossSize := containerCrossSize - off
if w.AlignContent == AlignContentStretch && remCrossSize > 0 {
add := remCrossSize / float64(len(lines))
for l := range lines {
line := &lines[l]
line.crossOffset += float64(l) * add
line.crossSize += add
}
}
// Note: no equiv. to §9.4.10 "visibility: collapse".
// §9.4.11 align-item: stretch
for l := range lines {
line := &lines[l]
for _, child := range line.child {
align := w.alignItem(child.n)
if align == AlignItemStretch && child.crossSize < line.crossSize {
child.crossSize = line.crossSize
}
}
}
// §9.5 main axis alignment
for l := range lines {
line := &lines[l]
total := 0.0
for _, child := range line.child {
total += child.mainSize
}
remFree := containerMainSize - total
off, spacing := 0.0, 0.0
switch w.Justify {
case JustifyStart:
case JustifyEnd:
off = remFree
case JustifyCenter:
off = remFree / 2
case JustifySpaceBetween:
spacing = remFree / float64(len(line.child)-1)
case JustifySpaceAround:
spacing = remFree / float64(len(line.child))
off = spacing / 2
}
for _, child := range line.child {
child.mainOffset = off
off += spacing + child.mainSize
}
}
// §9.6 cross axis alignment
// §9.6.13 no 'auto' margins
// §9.6.14 align items inside line, 'align-self'.
for l := range lines {
line := &lines[l]
for _, child := range line.child {
child.crossOffset = line.crossOffset
if child.crossSize == line.crossSize {
continue
}
diff := line.crossSize - child.crossSize
switch w.alignItem(child.n) {
case AlignItemStart:
// already laid out correctly
case AlignItemEnd:
child.crossOffset = line.crossOffset + diff
case AlignItemCenter:
child.crossOffset = line.crossOffset + diff/2
case AlignItemBaseline:
// TODO requires introducing inline-axis concept
case AlignItemStretch:
// handled earlier, so child.crossSize == line.crossSize
}
}
}
// §9.6.15 determine container cross size used
crossSize := lines[len(lines)-1].crossOffset + lines[len(lines)-1].crossSize
remFree := containerCrossSize - crossSize
// §9.6.16 align flex lines, 'align-content'.
if remFree > 0 {
spacing, off := 0.0, 0.0
switch w.AlignContent {
case AlignContentStart:
// already laid out correctly
case AlignContentEnd:
off = remFree
case AlignContentCenter:
off = remFree / 2
case AlignContentSpaceBetween:
spacing = remFree / float64(len(lines)-1)
case AlignContentSpaceAround:
spacing = remFree / float64(len(lines))
off = spacing / 2
case AlignContentStretch:
// handled earlier, why is remFree > 0?
}
if w.AlignContent != AlignContentStart && w.AlignContent != AlignContentStretch {
for l := range lines {
line := &lines[l]
line.crossOffset += off
for _, child := range line.child {
child.crossOffset += off
}
off += spacing
}
}
}
switch w.Direction {
case RowReverse, ColumnReverse:
// Invert main-start and main-end.
for l := range lines {
line := &lines[l]
for _, child := range line.child {
child.mainOffset = containerMainSize - child.mainOffset - child.mainSize
}
}
}
// Layout complete. Generate child Rect values.
for l := range lines {
line := &lines[l]
for _, child := range line.child {
switch w.Direction {
case Row, RowReverse:
child.n.Rect.Min.X = round(child.mainOffset)
child.n.Rect.Max.X = round(child.mainOffset + child.mainSize)
child.n.Rect.Min.Y = round(child.crossOffset)
child.n.Rect.Max.Y = round(child.crossOffset + child.crossSize)
case Column, ColumnReverse:
child.n.Rect.Min.Y = round(child.mainOffset)
child.n.Rect.Max.Y = round(child.mainOffset + child.mainSize)
child.n.Rect.Min.X = round(child.crossOffset)
child.n.Rect.Max.X = round(child.crossOffset + child.crossSize)
default:
panic(fmt.Sprint("flex: bad direction ", w.Direction))
}
child.n.Wrapper.Layout(t)
}
}
}
func round(f float64) int {
return int(math.Floor(f + .5))
}
type element struct {
n *node.Embed
flexBaseSize float64
frozen bool
unclamped float64
mainSize float64
mainOffset float64
crossSize float64
crossOffset float64
}
type flexLine struct {
mainSize float64
crossSize float64
crossOffset float64
child []*element
}
func (w *Flex) alignItem(n *node.Embed) AlignItem {
align := w.AlignItems
if d, ok := n.LayoutData.(LayoutData); ok {
align = d.Align
}
return align
}
// flexBaseSize calculates flex base size as per §9.2.3
func (w *Flex) flexBaseSize(n *node.Embed) int {
basis := Auto
if d, ok := n.LayoutData.(LayoutData); ok {
basis = d.Basis
}
// TODO Content §9.2.3.B, C, D
switch basis {
case Definite: // A
return n.LayoutData.(LayoutData).BasisPx
case Auto: // E
return w.mainSize(n.MeasuredSize)
default:
panic(fmt.Sprintf("flex: unknown flex-basis %v", basis))
}
}
func growFactor(n *node.Embed) float64 {
if d, ok := n.LayoutData.(LayoutData); ok {
return d.Grow
}
return 0
}
func shrinkFactor(n *node.Embed) float64 {
if d, ok := n.LayoutData.(LayoutData); ok && d.Shrink != nil {
return *d.Shrink
}
return 1
}
func aspectRatio(n *node.Embed) (ratio float64, ok bool) {
// TODO: source a formal description of "intrinsic aspect ratio"
d, ok := n.LayoutData.(LayoutData)
if ok && d.MinSize.X != 0 && d.MinSize.Y != 0 {
return float64(d.MinSize.X) / float64(d.MinSize.Y), true
}
return 0, false
}
func (w *Flex) clampSize(size float64, n *node.Embed) float64 {
if d, ok := n.LayoutData.(LayoutData); ok {
minSize := float64(w.mainSize(d.MinSize))
if minSize > size {
size = minSize
} else if d.MaxSize != nil {
maxSize := float64(w.mainSize(*d.MaxSize))
if size > maxSize {
size = maxSize
}
}
}
if size < 0 {
return 0
}
return size
}
func (w *Flex) mainSize(p image.Point) int {
switch w.Direction {
case Row, RowReverse:
return p.X
case Column, ColumnReverse:
return p.Y
default:
panic(fmt.Sprint("flex: bad direction ", w.Direction))
}
}
func (w *Flex) crossSize(p image.Point) int {
switch w.Direction {
case Row, RowReverse:
return p.Y
case Column, ColumnReverse:
return p.X
default:
panic(fmt.Sprint("flex: bad direction ", w.Direction))
}
}
// LayoutData is the node LayoutData type for a Flex's children.
type LayoutData struct {
MinSize image.Point // TODO use unit.Value
MaxSize *image.Point // TODO use unit.Value
// Grow determines how much a node will grow relative to its siblings.
Grow float64
// Shrink is the flex shrink factor which determines how much a node
// will shrink relative to its siblings. If nil, a default shrink
// factor of 1 is used.
Shrink *float64
// Basis determines the initial size of the node in the direction
// of the flex container (the main axis).
//
// If set to Definite, the value stored in BasisPx is used.
Basis Basis
BasisPx int // TODO use unit package?
Align AlignItem
// BreakAfter forces the next node onto the next flex line.
BreakAfter bool
}