Skip to content

Commit

Permalink
Properly constrain EqualStack measurement
Browse files Browse the repository at this point in the history
We know the constrained size when measuring an EqualStack, since it is divided
evenly. Use that knowledge when measuring. This fixes measurement issues when
adding multiline text elements to an EqualStack.
  • Loading branch information
bencochran committed Sep 21, 2020
1 parent 85c24cc commit f195020
Show file tree
Hide file tree
Showing 3 changed files with 185 additions and 4 deletions.
21 changes: 18 additions & 3 deletions BlueprintUI/Sources/Layout/EqualStack.swift
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,21 @@ extension EqualStack {
var spacing: CGFloat

func measure(in constraint: SizeConstraint, items: [(traits: Void, content: Measurable)]) -> CGSize {
let itemSizes = items.map { $1.measure(in: constraint) }
let totalSpacing = (spacing * CGFloat(items.count - 1))
let itemConstraint: SizeConstraint
switch direction {
case .horizontal:
itemConstraint = SizeConstraint(
width: (constraint.width - totalSpacing) / CGFloat(items.count),
height: constraint.height
)
case .vertical:
itemConstraint = SizeConstraint(
width: constraint.width,
height: (constraint.height - totalSpacing) / CGFloat(items.count)
)
}
let itemSizes = items.map { $1.measure(in: itemConstraint) }

let maximumItemWidth = itemSizes.map { $0.width }.max() ?? 0
let maximumItemHeight = itemSizes.map { $0.height }.max() ?? 0
Expand All @@ -88,16 +102,17 @@ extension EqualStack {
func layout(size: CGSize, items: [(traits: (), content: Measurable)]) -> [LayoutAttributes] {
guard items.count > 0 else { return [] }

let totalSpacing = (spacing * CGFloat(items.count - 1))
let itemSize: CGSize
switch direction {
case .horizontal:
itemSize = CGSize(
width: (size.width - (spacing * CGFloat(items.count - 1))) / CGFloat(items.count),
width: (size.width - totalSpacing) / CGFloat(items.count),
height: size.height)
case .vertical:
itemSize = CGSize(
width: size.width,
height: (size.height - (spacing * CGFloat(items.count - 1))) / CGFloat(items.count))
height: (size.height - totalSpacing) / CGFloat(items.count))
}

var result: [LayoutAttributes] = []
Expand Down
59 changes: 59 additions & 0 deletions BlueprintUI/Sources/Measuring/SizeConstraint.swift
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,19 @@ extension SizeConstraint {

private static var maxValue : CGFloat = .greatestFiniteMagnitude

/// Adds a scalar value to an Axis. If the Axis is unconstrained the
/// result will remain unconstrained.
public static func +(lhs: SizeConstraint.Axis, rhs: CGFloat) -> SizeConstraint.Axis {
switch lhs {
case .atMost(let limit):
return .atMost(limit + rhs)
case .unconstrained:
return .unconstrained
}
}

/// Subtracts a scalar value from an Axis. If the Axis is unconstrained
/// the result will remain unconstrained.
public static func -(lhs: SizeConstraint.Axis, rhs: CGFloat) -> SizeConstraint.Axis {
switch lhs {
case .atMost(let limit):
Expand All @@ -114,5 +127,51 @@ extension SizeConstraint {
}
}

/// Divides an Axis by a scalar value. If the Axis is unconstrained the
/// result will remain unconstrained.
public static func /(lhs: SizeConstraint.Axis, rhs: CGFloat) -> SizeConstraint.Axis {
switch lhs {
case .atMost(let limit):
return .atMost(limit / rhs)
case .unconstrained:
return .unconstrained
}
}

/// Multiplies an Axis by a scalar value. If the Axis is unconstrained
/// the result will remain unconstrained.
public static func *(lhs: SizeConstraint.Axis, rhs: CGFloat) -> SizeConstraint.Axis {
switch lhs {
case .atMost(let limit):
return .atMost(limit * rhs)
case .unconstrained:
return .unconstrained
}
}

/// Adds a scalar value to an Axis. If the Axis is unconstrained the
/// result will remain unconstrained.
public static func +=(lhs: inout SizeConstraint.Axis, rhs: CGFloat) {
lhs = lhs + rhs
}

/// Subtracts a scalar value from an Axis. If the Axis is unconstrained
/// the result will remain unconstrained.
public static func -=(lhs: inout SizeConstraint.Axis, rhs: CGFloat) {
lhs = lhs - rhs
}

/// Divides an Axis by a scalar value. If the Axis is unconstrained the
/// result will remain unconstrained.
public static func /=(lhs: inout SizeConstraint.Axis, rhs: CGFloat) {
lhs = lhs / rhs
}

/// Multiplies an Axis by a scalar value. If the Axis is unconstrained
/// the result will remain unconstrained.
public static func *=(lhs: inout SizeConstraint.Axis, rhs: CGFloat) {
lhs = lhs * rhs
}

}
}
109 changes: 108 additions & 1 deletion BlueprintUI/Tests/EqualStackTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class EqualStackTests: XCTestCase {
XCTAssertTrue(stack.children.isEmpty)
}

func test_measuring() {
func test_measuring_unconstrained() {

let children = [
TestElement(size: CGSize(width: 50, height: 50)),
Expand Down Expand Up @@ -61,6 +61,77 @@ class EqualStackTests: XCTestCase {

}

func test_measuring_constrained() {

let children = [
AreaElement(area: 100),
AreaElement(area: 1200), // The only one affecting the cross-axis size
AreaElement(area: 100),
]

// direction = .horizontal
do {
let constraint = SizeConstraint(width: .atMost(300), height: .unconstrained)

// spacing = 0
do {
let stack = EqualStack(direction: .horizontal) { stack in
stack.spacing = 0
stack.children = children
}

// 300 / 3 = 100
// 1200 / 100 = 12
XCTAssertEqual(stack.content.measure(in: constraint), CGSize(width: 300, height: 12))
}

// spacing = 30
do {
let stack = EqualStack(direction: .horizontal) { stack in
stack.spacing = 30
stack.children = children
}

// 300 - 30 - 30 = 240
// 240 / 3 = 80
// 1200 / 80 = 15
XCTAssertEqual(stack.content.measure(in: constraint), CGSize(width: 300, height: 15))
}
}

// direction = .vertical
do {
let constraint = SizeConstraint(width: .unconstrained, height: .atMost(400))

// spacing = 0
do {
let stack = EqualStack(direction: .vertical) { stack in
stack.spacing = 0
stack.children = children
}

// 400 / 3 = 133.333…
// 1200 / 133.333… = 9
XCTAssertEqual(stack.content.measure(in: constraint), CGSize(width: 9, height: 400))
}

// spacing = 50
do {
let stack = EqualStack(direction: .vertical) { stack in
stack.spacing = 50
stack.children = children
}

// 400 - 50 - 50 = 300
// 300 / 3 = 100
// 1200 / 100 = 12
XCTAssertEqual(stack.content.measure(in: constraint), CGSize(width: 12, height: 400))

}
}

}

func test_layout() {

let children = [
Expand Down Expand Up @@ -168,3 +239,39 @@ fileprivate struct TestElement: Element {
}

}

/// Test element that will measure itself to take up the given area in points
fileprivate struct AreaElement: Element {

var area: CGFloat

init(area: CGFloat = 25) {
self.area = area
}

var content: ElementContent {
return ElementContent { [area] constraint in
if case .atMost(let maxWidth) = constraint.width {
return CGSize(
width: maxWidth,
height: area / maxWidth
)
} else if case .atMost(let maxHeight) = constraint.height {
return CGSize(
width: area / maxHeight,
height: maxHeight
)
} else {
return CGSize(
width: area.squareRoot(),
height: area.squareRoot()
)
}
}
}

func backingViewDescription(bounds: CGRect, subtreeExtent: CGRect?) -> ViewDescription? {
return nil
}

}

0 comments on commit f195020

Please sign in to comment.