shiny/widget/flex: basics of flex algorithm
Covers sections 9.3, 9.4, and implements a decent chunk of the sizing
described in section 9.7.
Change-Id: Ia3b24c2b568ef94a3ae779525971f29d61c39fe9
Reviewed-on: https://go-review.googlesource.com/23016
Reviewed-by: Nigel Tao <nigeltao@golang.org>
diff --git a/shiny/widget/flex/flex.go b/shiny/widget/flex/flex.go
index 4e49523..94e4dd6 100644
--- a/shiny/widget/flex/flex.go
+++ b/shiny/widget/flex/flex.go
@@ -13,9 +13,12 @@
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.
@@ -124,6 +127,421 @@
return w
}
+func (w *Flex) Measure(t *theme.Theme) {
+ // 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 {
+ c.Wrapper.Measure(t)
+ 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]
+ if line.mainSize > 0 && line.mainSize+child.flexBaseSize > containerMainSize {
+ lines = append(lines, line)
+ line = flexLine{}
+ }
+ line.child = append(line.child, child)
+ line.mainSize += child.flexBaseSize
+
+ if d, ok := child.n.LayoutData.(LayoutData); ok && d.BreakAfter {
+ lines = append(lines, line)
+ line = flexLine{}
+ }
+ }
+ if len(line.child) > 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 := w.mainSize(child.n.MeasuredSize)
+ if grow {
+ if growFactor(child.n) == 0 || w.flexBaseSize(child.n) > mainSize {
+ child.frozen = true
+ child.mainSize = float64(mainSize)
+ }
+ } else {
+ if shrinkFactor(child.n) == 0 || w.flexBaseSize(child.n) < mainSize {
+ child.frozen = true
+ child.mainSize = float64(mainSize)
+ }
+ }
+ }
+
+ // §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
+ if d, ok := child.n.LayoutData.(LayoutData); ok {
+ minSize := float64(w.mainSize(d.MinSize))
+ if minSize > child.mainSize {
+ child.mainSize = minSize
+ } else if d.MaxSize != nil {
+ maxSize := float64(w.mainSize(*d.MaxSize))
+ if child.mainSize > maxSize {
+ child.mainSize = maxSize
+ }
+ }
+ }
+ if child.mainSize < 0 {
+ child.mainSize = 0
+ }
+ 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
+ }
+
+ // TODO: alternate values of w.Justify
+ off := 0.0
+ for _, child := range line.child {
+ child.mainOffset = off
+ off += child.mainSize
+ }
+ }
+
+ // TODO
+ // §9.6 cross axis alignment
+
+ // TODO: RowReverse, ColumnReverse
+
+ // 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 = int(math.Ceil(child.mainOffset))
+ child.n.Rect.Max.X = int(math.Ceil(child.mainOffset + child.mainSize))
+ child.n.Rect.Min.Y = int(math.Ceil(child.crossOffset))
+ child.n.Rect.Max.Y = int(math.Ceil(child.crossOffset + child.crossSize))
+ case Column, ColumnReverse:
+ child.n.Rect.Min.Y = int(math.Ceil(child.mainOffset))
+ child.n.Rect.Max.Y = int(math.Ceil(child.mainOffset + child.mainSize))
+ child.n.Rect.Min.X = int(math.Ceil(child.crossOffset))
+ child.n.Rect.Max.X = int(math.Ceil(child.crossOffset + child.crossSize))
+ default:
+ panic(fmt.Sprint("flex: bad direction ", w.Direction))
+ }
+ child.n.Wrapper.Layout(t)
+ }
+ }
+}
+
+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) 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
diff --git a/shiny/widget/flex/flex_test.go b/shiny/widget/flex/flex_test.go
index f7750e4..47f940f 100644
--- a/shiny/widget/flex/flex_test.go
+++ b/shiny/widget/flex/flex_test.go
@@ -136,6 +136,44 @@
want: []image.Rectangle{
image.Rect(0, 0, 100, 100),
},
+}, {
+ size: image.Point{350, 100},
+ measured: [][2]float64{{100, 100}, {100, 100}, {100, 100}},
+ want: []image.Rectangle{
+ image.Rect(0, 0, 100, 100),
+ image.Rect(100, 0, 200, 100),
+ image.Rect(200, 0, 300, 100),
+ },
+}, {
+ size: image.Point{300, 100},
+ measured: [][2]float64{{100, 100}, {100, 100}},
+ want: []image.Rectangle{
+ image.Rect(0, 0, 100, 100),
+ image.Rect(100, 0, 300, 100),
+ },
+ layoutData: []LayoutData{{}, {Grow: 1}},
+}, {
+ size: image.Point{300, 100},
+ measured: [][2]float64{{50, 50}, {100, 100}, {100, 100}},
+ want: []image.Rectangle{
+ image.Rect(0, 0, 50, 50),
+ image.Rect(50, 0, 175, 100),
+ image.Rect(175, 0, 300, 100),
+ },
+ layoutData: []LayoutData{{}, {Grow: 1}, {Grow: 1}},
+}, {
+ size: image.Point{300, 100},
+ measured: [][2]float64{{20, 100}, {20, 100}, {20, 100}},
+ want: []image.Rectangle{
+ image.Rect(0, 0, 30, 100),
+ image.Rect(30, 0, 130, 100),
+ image.Rect(130, 0, 300, 100),
+ },
+ layoutData: []LayoutData{
+ {MaxSize: &image.Point{30, 100}, Grow: 1},
+ {MinSize: image.Point{100, 0}, Grow: 1},
+ {Grow: 4},
+ },
}}
func TestLayout(t *testing.T) {