Skip to content

Commit

Permalink
Merge pull request #41 from yohamta/flexalgo
Browse files Browse the repository at this point in the history
Implement flex container size calculation
  • Loading branch information
yohamta authored Apr 9, 2023
2 parents 95d92e3 + f12d2c5 commit 9b731d3
Show file tree
Hide file tree
Showing 4 changed files with 205 additions and 18 deletions.
3 changes: 3 additions & 0 deletions container.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ type containerEmbed struct {
isDirty bool
frame image.Rectangle
touchIDs []ebiten.TouchID

calculatedWidth int
calculatedHeight int
}

func (ct *containerEmbed) processEvent() {
Expand Down
149 changes: 132 additions & 17 deletions flex.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ func (f *flexEmbed) layout(width, height int, container *containerEmbed) {

// §9.7.2 freeze inflexible children.
for _, child := range line.child {
mainSize := float64(f.mainSize(child.node.item.Width, child.node.item.Height))
mainSize := float64(f.mainSize(child.node.item.width(), child.node.item.height()))
if grow {
if child.node.item.Grow == 0 {
child.frozen = true
Expand Down Expand Up @@ -199,7 +199,7 @@ func (f *flexEmbed) layout(width, height int, container *containerEmbed) {
if child.frozen {
remFreeSpace -= (child.mainSize + mainMargin)
} else {
remFreeSpace -= (float64(f.mainSize(child.node.item.Width, child.node.item.Height)) + mainMargin)
remFreeSpace -= (float64(f.mainSize(child.node.item.width(), child.node.item.height())) + mainMargin)
if grow {
unfrozenFlexFactor += child.node.item.Grow
} else {
Expand All @@ -223,7 +223,7 @@ func (f *flexEmbed) layout(width, height int, container *containerEmbed) {
}
r := child.node.item.Grow / unfrozenFlexFactor
child.mainSize = float64(f.mainSize(
child.node.item.Width, child.node.item.Height,
child.node.item.width(), child.node.item.height(),
)) + r*remFreeSpace
}
} else {
Expand All @@ -233,7 +233,7 @@ func (f *flexEmbed) layout(width, height int, container *containerEmbed) {
continue
}
scaledShrinkFactor := float64(f.mainSize(
child.node.item.Width, child.node.item.Height,
child.node.item.width(), child.node.item.height(),
)) * child.node.item.Shrink
sumScaledShrinkFactor += scaledShrinkFactor
}
Expand All @@ -242,11 +242,11 @@ func (f *flexEmbed) layout(width, height int, container *containerEmbed) {
continue
}
scaledShrinkFactor := float64(f.mainSize(
child.node.item.Width, child.node.item.Height,
child.node.item.width(), child.node.item.height(),
)) * child.node.item.Shrink
r := float64(scaledShrinkFactor) / sumScaledShrinkFactor
child.mainSize = float64(f.mainSize(
child.node.item.Width, child.node.item.Height,
child.node.item.width(), child.node.item.height(),
)) - r*math.Abs(float64(remFreeSpace))
}
}
Expand All @@ -264,7 +264,7 @@ func (f *flexEmbed) layout(width, height int, container *containerEmbed) {
for _, c := range lines[l].child {
c.crossMargin = f.crossMargin(c.node)
c.crossSize = float64(
f.crossSize(c.node.item.Width, c.node.item.Height),
f.crossSize(c.node.item.width(), c.node.item.height()),
)
}
}
Expand Down Expand Up @@ -403,6 +403,90 @@ func (f *flexEmbed) layout(width, height int, container *containerEmbed) {
}
}

// §9.9.1. Flex Container Intrinsic Main Sizes
intrinsicMainSize := 0.0
for _, line := range lines {
for _, child := range line.child {
// 1. For each flex item, subtract its outer flex base size from its max-content
// contribution size. If that result is positive, divide by its flex grow factor
// floored at 1; if negative, divide by its scaled flex shrink factor having floored
// the flex shrink factor at 1. This is the item’s max-content flex fraction.
maxContentFlexFraction := 0.0
maxContentDiff := child.mainSize - child.flexBaseSize

if maxContentDiff > 0 {
maxContentFlexFraction = maxContentDiff / math.Max(1, child.node.item.Grow)
} else {
maxContentFlexFraction = maxContentDiff / math.Max(1, child.node.item.Shrink*child.mainSize)
}

child.maxContentFlexFraction = maxContentFlexFraction
}

// 2. Place all flex items into lines of infinite length.
maxContentFlexFraction := 0.0
for _, child := range line.child {
if child.maxContentFlexFraction > maxContentFlexFraction {
maxContentFlexFraction = child.maxContentFlexFraction
}
}

// 3. Within each line, find the largest max-content flex fraction among
// all the flex items. Add each item’s flex base size to the product of
// its flex grow factor (or scaled flex shrink factor,
// if the chosen max-content flex fraction was negative) and the
// chosen max-content flex fraction, then clamp that result by
// the max main size floored by the min main size.
maxMainSize := 0.0
for _, child := range line.child {
mainSize := 0.0
if maxContentFlexFraction > 0 {
mainSize = child.flexBaseSize + child.node.item.Grow*maxContentFlexFraction
} else {
mainSize = child.flexBaseSize - child.node.item.Shrink*child.mainSize*maxContentFlexFraction
}
if mainSize > maxMainSize {
maxMainSize = mainSize
}
}
intrinsicMainSize += maxMainSize
}
f.setMainSize(int(intrinsicMainSize))

// §9.9.2. Flex Container Intrinsic Cross Sizes
// The min-content/max-content cross size of a single-line flex container
// is the largest min-content contribution/max-content contribution (respectively)
// of its flex items.
intrinsicCrossSize := 0.0
for _, line := range lines {
if intrinsicCrossSize < line.crossOffset+line.crossSize {
intrinsicCrossSize = line.crossOffset + line.crossSize
}

min := math.Inf(1)
max := -1.
for _, child := range line.child {
if min == -1 || child.crossOffset < min {
min = child.crossOffset
}
if max == -1 || child.crossOffset+child.crossSize > max {
max = child.crossOffset + child.crossSize
}
}
if intrinsicCrossSize < max-min {
intrinsicCrossSize = max - min
}
}
f.setCrossSize(int(intrinsicCrossSize))

// For a multi-line flex container, the min-content/max-content cross size is
// the sum of the flex line cross sizes resulting from sizing the flex container
// under a cross-axis min-content constraint/max-content constraint (respectively).
// However, if the flex container is flex-flow: column wrap;, then it’s sized
// by first finding the largest min-content/max-content cross-size contribution
// among the flex items (respectively), then using that size as the available
// space in the cross axis for each of the flex items during layout.

// Layout complete. Update children position
for l := range lines {
line := &lines[l]
Expand Down Expand Up @@ -430,15 +514,16 @@ func (f *flexEmbed) layout(width, height int, container *containerEmbed) {
}

type element struct {
node *child
flexBaseSize float64
mainSize float64
mainOffset float64
mainMargin []float64
crossSize float64
crossOffset float64
crossMargin []float64
frozen bool
node *child
flexBaseSize float64
mainSize float64
mainOffset float64
mainMargin []float64
crossSize float64
crossOffset float64
crossMargin []float64
frozen bool
maxContentFlexFraction float64
}

type flexLine struct {
Expand All @@ -459,6 +544,28 @@ func (f *flexEmbed) mainSize(x, y int) int {
}
}

func (f *flexEmbed) setCrossSize(v int) {
switch f.Direction {
case Row:
f.calculatedHeight = v
case Column:
f.calculatedWidth = v
default:
panic(fmt.Sprint("flex: bad direction ", f.Direction))
}
}

func (f *flexEmbed) setMainSize(v int) {
switch f.Direction {
case Row:
f.calculatedWidth = v
case Column:
f.calculatedHeight = v
default:
panic(fmt.Sprint("flex: bad direction ", f.Direction))
}
}

func (f *flexEmbed) crossSize(x, y int) int {
switch f.Direction {
case Row:
Expand Down Expand Up @@ -501,7 +608,15 @@ func (f *flexEmbed) crossMargin(c *child) []float64 {
}

func (f *flexEmbed) flexBaseSize(c *child) int {
return f.mainSize(c.item.Width, c.item.Height)
w := c.item.Width
if w == 0 {
w = c.item.calculatedWidth
}
h := c.item.Height
if h == 0 {
h = c.item.calculatedHeight
}
return f.mainSize(w, h)
}

func (f *flexEmbed) clampSize(size, width, height int) int {
Expand Down
50 changes: 49 additions & 1 deletion flex_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ func TestMerginWithChild(t *testing.T) {
flex.Update()
flex.Draw(nil)

assert.Equal(t, image.Rect(950, 900, 950, 900), mock1.Frame)
assert.Equal(t, image.Rect(850, 800, 950, 900), mock1.Frame)
assert.Equal(t, image.Rect(850, 800, 950, 900), mock2.Frame)
}

Expand Down Expand Up @@ -824,6 +824,54 @@ func TestAbsoluteViewChildren(t *testing.T) {
assert.Equal(t, image.Rect(100, 100, 110, 110), mock3.Frame)
}

func TestAutoHeightCalculation(t *testing.T) {
flex := &View{
Width: 1000,
Height: 1000,
Grow: 0,
Shrink: 0,
AlignItems: AlignItemStart,
Justify: JustifyStart,
Direction: Column,
}

mock1 := mockHandler{}
mock2 := mockHandler{}

firstRow := &View{
Direction: Column,
Grow: 0,
Shrink: 0,
Width: 100,
Handler: &mock1,
}

firstRow.AddChild(&View{
Width: 100,
Height: 100,
})

secondRow := &View{
Direction: Row,
Width: 200,
Height: 200,
Grow: 0,
Shrink: 0,
Handler: &mock2,
}

flex.AddChild(
firstRow,
secondRow,
)

flex.Update()
flex.Draw(nil)

assert.Equal(t, image.Rect(0, 0, 100, 100), mock1.Frame)
assert.Equal(t, image.Rect(0, 100, 200, 300), mock2.Frame)
}

func flexItemBounds(parent *View, child *View) image.Rectangle {
mock := &mockHandler{}
child.Handler = mock
Expand Down
21 changes: 21 additions & 0 deletions view.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,13 @@ func (v *View) startLayout() {
v.frame = image.Rect(v.Left, v.Top, v.Left+v.Width, v.Top+v.Height)
}
v.flexEmbed.View = v

for _, child := range v.children {
if child.item.Position == PositionStatic {
child.item.startLayout()
}
}

v.layout(v.frame.Dx(), v.frame.Dy(), &v.containerEmbed)
v.isDirty = false
}
Expand Down Expand Up @@ -141,3 +148,17 @@ func (v *View) addChild(cv *View) *View {
cv.hasParent = true
return v
}

func (v *View) width() int {
if v.Width == 0 {
return v.calculatedWidth
}
return v.Width
}

func (v *View) height() int {
if v.Height == 0 {
return v.calculatedHeight
}
return v.Height
}

0 comments on commit 9b731d3

Please sign in to comment.