Skip to content

Commit

Permalink
Merge pull request square#450 from square/watt/caffeinated-layout/8
Browse files Browse the repository at this point in the history
Infinite size elements
  • Loading branch information
watt authored Apr 14, 2023
2 parents 4df008b + 3a113ef commit e06aa03
Show file tree
Hide file tree
Showing 23 changed files with 244 additions and 38 deletions.
16 changes: 16 additions & 0 deletions BlueprintUI/Sources/Extensions/CGSize+Blueprint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import CoreGraphics

extension CGSize {
/// A size with `infinity` in both dimensions.
public static let infinity = CGSize(width: CGFloat.infinity, height: .infinity)

/// Returns a size with infinite dimensions replaced by the values from the given replacement.
public func replacingInfinity(with replacement: CGSize) -> CGSize {
assert(replacement.isFinite, "Infinity replacement value must be finite")

return CGSize(
width: width.replacingInfinity(with: replacement.width),
height: height.replacingInfinity(with: replacement.height)
)
}
}
10 changes: 10 additions & 0 deletions BlueprintUI/Sources/Extensions/FloatingPoint+Blueprint.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import Foundation

extension FloatingPoint {
/// Returns `replacement` if `self.isInfinite` is `true`, or `self` if `self` is finite.
public func replacingInfinity(with replacement: Self) -> Self {
assert(replacement.isFinite, "Infinity replacement value must be finite")

return isInfinite ? replacement : self
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import CoreGraphics
import UIKit

extension CGSize {
static func + (lhs: CGSize, rhs: CGSize) -> CGSize {
Expand Down
2 changes: 1 addition & 1 deletion BlueprintUI/Sources/Internal/LayoutModeKey.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ enum LayoutModeKey: EnvironmentKey {
extension Environment {
/// This mode will be inherited by descendant BlueprintViews that do not have an explicit
/// mode set.
var layoutMode: LayoutMode {
public internal(set) var layoutMode: LayoutMode {
get { self[LayoutModeKey.self] }
set { self[LayoutModeKey.self] = newValue }
}
Expand Down
20 changes: 20 additions & 0 deletions BlueprintUI/Sources/Layout/LayoutMode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,26 @@ public enum LayoutMode: Equatable {
public static let caffeinated = Self.caffeinated()
}

extension LayoutMode: CustomStringConvertible {
public var description: String {
switch self {
case .legacy:
return "Legacy"
case .caffeinated(let options):
switch (options.hintRangeBoundaries, options.searchUnconstrainedKeys) {
case (true, true):
return "Caffeinated (hint+search)"
case (true, false):
return "Caffeinated (hint)"
case (false, true):
return "Caffeinated (search)"
case (false, false):
return "Caffeinated"
}
}
}
}

extension Notification.Name {
static let defaultLayoutModeChanged: Self = .init(
"com.squareup.blueprint.defaultLayoutModeChanged"
Expand Down
18 changes: 14 additions & 4 deletions BlueprintUICommonControls/Sources/Image.swift
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ public struct Image: Element {

public var content: ElementContent {
let measurer = Measurer(contentMode: contentMode, imageSize: image?.size)
return ElementContent(measurable: measurer)
return ElementContent { constraint, environment in
measurer.measure(in: constraint, layoutMode: environment.layoutMode)
}
}

public func backingViewDescription(with context: ViewDescriptionContext) -> ViewDescription? {
Expand Down Expand Up @@ -106,18 +108,19 @@ extension CGSize {

extension Image {

fileprivate struct Measurer: Measurable {
fileprivate struct Measurer {

var contentMode: ContentMode
var imageSize: CGSize?

func measure(in constraint: SizeConstraint) -> CGSize {
func measure(in constraint: SizeConstraint, layoutMode: LayoutMode) -> CGSize {
guard let imageSize = imageSize else { return .zero }

enum Mode {
case fitWidth(CGFloat)
case fitHeight(CGFloat)
case useImageSize
case infinite
}

let mode: Mode
Expand All @@ -137,7 +140,12 @@ extension Image {
} else if case .atMost(let height) = constraint.height {
mode = .fitHeight(height)
} else {
mode = .useImageSize
switch layoutMode {
case .legacy:
mode = .useImageSize
case .caffeinated:
mode = .infinite
}
}
}

Expand All @@ -154,6 +162,8 @@ extension Image {
)
case .useImageSize:
return imageSize
case .infinite:
return .infinity
}


Expand Down
10 changes: 8 additions & 2 deletions BlueprintUICommonControls/Sources/ScrollView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -198,8 +198,12 @@ extension ScrollView {
result.width += contentInset.left + contentInset.right
result.height += contentInset.top + contentInset.bottom

result.width = min(result.width, proposal.width.maximum)
result.height = min(result.height, proposal.height.maximum)
if let maxWidth = proposal.width.constrainedValue {
result.width = min(result.width, maxWidth)
}
if let maxHeight = proposal.height.constrainedValue {
result.height = min(result.height, maxHeight)
}

return result
}
Expand All @@ -210,6 +214,8 @@ extension ScrollView {
insetSize.height -= contentInset.top + contentInset.bottom

var itemSize = fittedSize(in: .init(insetSize), subelement: subelement)
.replacingInfinity(with: insetSize)

if contentSize == .fittingHeight {
itemSize.width = insetSize.width
} else if contentSize == .fittingWidth {
Expand Down
18 changes: 13 additions & 5 deletions BlueprintUICommonControls/Sources/TextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,19 @@ public struct TextField: Element {
}

public var content: ElementContent {
ElementContent { constraint -> CGSize in
CGSize(
width: max(constraint.maximum.width, 44),
height: 44.0
)
ElementContent { constraint, environment -> CGSize in
switch environment.layoutMode {
case .legacy:
return CGSize(
width: max(constraint.maximum.width, 44),
height: 44.0
)
case .caffeinated:
return CGSize(
width: constraint.width.constrainedValue.map { max($0, 44) } ?? .infinity,
height: 44
)
}
}
}

Expand Down
78 changes: 63 additions & 15 deletions BlueprintUICommonControls/Tests/Sources/ImageTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,14 @@ class ImageTests: XCTestCase {
size: CGSize,
constraint: SizeConstraint,
expectedValue: CGSize,
layoutModes: [LayoutMode] = LayoutMode.testModes,
line: UInt = #line
) {
self.validate(
contentMode: .aspectFill,
imageSize: size,
constraint: constraint,
layoutModes: layoutModes,
expectedValue: expectedValue,
line: line
)
Expand All @@ -58,7 +60,15 @@ class ImageTests: XCTestCase {
validate(
size: .init(width: 20, height: 10),
constraint: .init(width: .unconstrained, height: .unconstrained),
expectedValue: .init(width: 20, height: 10)
expectedValue: .init(width: 20, height: 10),
layoutModes: [.legacy]
)

validate(
size: .init(width: 20, height: 10),
constraint: .init(width: .unconstrained, height: .unconstrained),
expectedValue: .infinity,
layoutModes: [.caffeinated]
)

validate(
Expand Down Expand Up @@ -108,7 +118,15 @@ class ImageTests: XCTestCase {
validate(
size: .init(width: 10, height: 20),
constraint: .init(width: .unconstrained, height: .unconstrained),
expectedValue: .init(width: 10, height: 20)
expectedValue: .init(width: 10, height: 20),
layoutModes: [.legacy]
)

validate(
size: .init(width: 10, height: 20),
constraint: .init(width: .unconstrained, height: .unconstrained),
expectedValue: .infinity,
layoutModes: [.caffeinated]
)

validate(
Expand Down Expand Up @@ -159,12 +177,14 @@ class ImageTests: XCTestCase {
size: CGSize,
constraint: SizeConstraint,
expectedValue: CGSize,
layoutModes: [LayoutMode] = LayoutMode.testModes,
line: UInt = #line
) {
self.validate(
contentMode: .aspectFit,
imageSize: size,
constraint: constraint,
layoutModes: layoutModes,
expectedValue: expectedValue,
line: line
)
Expand All @@ -175,7 +195,15 @@ class ImageTests: XCTestCase {
validate(
size: .init(width: 20, height: 10),
constraint: .init(width: .unconstrained, height: .unconstrained),
expectedValue: .init(width: 20, height: 10)
expectedValue: .init(width: 20, height: 10),
layoutModes: [.legacy]
)

validate(
size: .init(width: 20, height: 10),
constraint: .init(width: .unconstrained, height: .unconstrained),
expectedValue: .infinity,
layoutModes: [.caffeinated]
)

validate(
Expand Down Expand Up @@ -225,7 +253,15 @@ class ImageTests: XCTestCase {
validate(
size: .init(width: 10, height: 20),
constraint: .init(width: .unconstrained, height: .unconstrained),
expectedValue: .init(width: 10, height: 20)
expectedValue: .init(width: 10, height: 20),
layoutModes: [.legacy]
)

validate(
size: .init(width: 10, height: 20),
constraint: .init(width: .unconstrained, height: .unconstrained),
expectedValue: .infinity,
layoutModes: [.caffeinated]
)

validate(
Expand Down Expand Up @@ -276,12 +312,14 @@ class ImageTests: XCTestCase {
size: CGSize,
constraint: SizeConstraint,
expectedValue: CGSize,
layoutModes: [LayoutMode] = LayoutMode.testModes,
line: UInt = #line
) {
self.validate(
contentMode: .center,
imageSize: size,
constraint: constraint,
layoutModes: layoutModes,
expectedValue: expectedValue,
line: line
)
Expand Down Expand Up @@ -393,12 +431,14 @@ class ImageTests: XCTestCase {
size: CGSize,
constraint: SizeConstraint,
expectedValue: CGSize,
layoutModes: [LayoutMode] = LayoutMode.testModes,
line: UInt = #line
) {
self.validate(
contentMode: .stretch,
imageSize: size,
constraint: constraint,
layoutModes: layoutModes,
expectedValue: expectedValue,
line: line
)
Expand Down Expand Up @@ -509,6 +549,7 @@ class ImageTests: XCTestCase {
contentMode: Image.ContentMode,
imageSize: CGSize,
constraint: SizeConstraint,
layoutModes: [LayoutMode],
expectedValue: CGSize,
line: UInt = #line
) {
Expand All @@ -518,17 +559,24 @@ class ImageTests: XCTestCase {
contentMode: contentMode
)

XCTAssertEqual(
element.content.measure(in: constraint),
expectedValue,
"""
Image in content mode: \(contentMode),
of size (\(Int(imageSize.width))x\(Int(imageSize.height)))
expected to be measured as (\(Int(expectedValue.width))x\(Int(expectedValue.height)))
in constraint: (\(constraint))
""",
line: line
)
for layoutMode in layoutModes {
let actualSize = layoutMode.performAsDefault {
element.content.measure(in: constraint)
}

XCTAssertEqual(
actualSize,
expectedValue,
"""
Image in content mode: \(contentMode),
of size \(imageSize)
expected to be measured as \(expectedValue)
in constraint: (\(constraint))
and layout mode: \(layoutMode)
""",
line: line
)
}
}
}

Expand Down
16 changes: 16 additions & 0 deletions BlueprintUICommonControls/Tests/Sources/LayoutMode+Testing.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import BlueprintUI

extension LayoutMode {
static let testModes: [LayoutMode] = [.legacy, .caffeinated]

/// Run the given block with `self` as the default layout mode, restoring the previous default
/// afterwards, and returning the result of the block.
func performAsDefault<Result>(block: () throws -> Result) rethrows -> Result {
let oldLayoutMode = LayoutMode.default
defer { LayoutMode.default = oldLayoutMode }

LayoutMode.default = self

return try block()
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit e06aa03

Please sign in to comment.