diff --git a/AsyncDisplayKit/Layout/ASBackgroundLayoutNode.h b/AsyncDisplayKit/Layout/ASBackgroundLayoutNode.h new file mode 100644 index 0000000000..6a4f2374bb --- /dev/null +++ b/AsyncDisplayKit/Layout/ASBackgroundLayoutNode.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +/** + Lays out a single child node, then lays out a background node behind it stretched to its size. + */ +@interface ASBackgroundLayoutNode : ASLayoutNode + +/** + @param node A child that is laid out to determine the size of this node. If this is nil, then this method + returns nil. + @param background A child that is laid out behind it. May be nil, in which case the background is omitted. + */ ++ (instancetype)newWithNode:(ASLayoutNode *)node + background:(ASLayoutNode *)background; + +@end diff --git a/AsyncDisplayKit/Layout/ASBackgroundLayoutNode.mm b/AsyncDisplayKit/Layout/ASBackgroundLayoutNode.mm new file mode 100644 index 0000000000..bb090d8006 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASBackgroundLayoutNode.mm @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASBackgroundLayoutNode.h" + +#import "ASAssert.h" +#import "ASBaseDefines.h" + +#import "ASLayoutNodeSubclass.h" + +@interface ASBackgroundLayoutNode () +{ + ASLayoutNode *_node; + ASLayoutNode *_background; +} +@end + +@implementation ASBackgroundLayoutNode + ++ (instancetype)newWithNode:(ASLayoutNode *)node + background:(ASLayoutNode *)background +{ + if (node == nil) { + return nil; + } + ASBackgroundLayoutNode *n = [super newWithSize:{}]; + n->_node = node; + n->_background = background; + return n; +} + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); +} + +/** + First layout the contents, then fit the background image. + */ +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutNodeSize)size + relativeToParentSize:(CGSize)parentSize +{ + ASDisplayNodeAssert(ASLayoutNodeSizeEqualToNodeSize(size, ASLayoutNodeSizeZero), + @"ASBackgroundLayoutNode only passes size {} to the super class initializer, but received size %@ " + "(node=%@, background=%@)", NSStringFromASLayoutNodeSize(size), _node, _background); + + ASLayout *contentsLayout = [_node layoutThatFits:constrainedSize parentSize:parentSize]; + + NSMutableArray *children = [NSMutableArray arrayWithCapacity:2]; + if (_background) { + // Size background to exactly the same size. + ASLayout *backgroundLayout = [_background layoutThatFits:{contentsLayout.size, contentsLayout.size} + parentSize:contentsLayout.size]; + [children addObject:[ASLayoutChild newWithPosition:{0,0} layout:backgroundLayout]]; + } + [children addObject:[ASLayoutChild newWithPosition:{0,0} layout:contentsLayout]]; + + return [ASLayout newWithNode:self size:contentsLayout.size children:children]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASCenterLayoutNode.h b/AsyncDisplayKit/Layout/ASCenterLayoutNode.h new file mode 100644 index 0000000000..0f9180582f --- /dev/null +++ b/AsyncDisplayKit/Layout/ASCenterLayoutNode.h @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +typedef NS_OPTIONS(NSUInteger, ASCenterLayoutNodeCenteringOptions) { + /** The child is positioned in {0,0} relatively to the layout bounds */ + ASCenterLayoutNodeCenteringNone = 0, + /** The child is centered along the X axis */ + ASCenterLayoutNodeCenteringX = 1 << 0, + /** The child is centered along the Y axis */ + ASCenterLayoutNodeCenteringY = 1 << 1, + /** Convenience option to center both along the X and Y axis */ + ASCenterLayoutNodeCenteringXY = ASCenterLayoutNodeCenteringX | ASCenterLayoutNodeCenteringY +}; + +typedef NS_OPTIONS(NSUInteger, ASCenterLayoutNodeSizingOptions) { + /** The node will take up the maximum size possible */ + ASCenterLayoutNodeSizingOptionDefault, + /** The node will take up the minimum size possible along the X axis */ + ASCenterLayoutNodeSizingOptionMinimumX = 1 << 0, + /** The node will take up the minimum size possible along the Y axis */ + ASCenterLayoutNodeSizingOptionMinimumY = 1 << 1, + /** Convenience option to take up the minimum size along both the X and Y axis */ + ASCenterLayoutNodeSizingOptionMinimumXY = ASCenterLayoutNodeSizingOptionMinimumX | ASCenterLayoutNodeSizingOptionMinimumY, +}; + +/** Lays out a single child layout node and position it so that it is centered into the layout bounds. */ +@interface ASCenterLayoutNode : ASLayoutNode + +/** + @param centeringOptions, see ASCenterLayoutNodeCenteringOptions. + @param child The child to center. + @param size The node size or {} for the default which is for the layout to take the maximum space available. + */ ++ (instancetype)newWithCenteringOptions:(ASCenterLayoutNodeCenteringOptions)centeringOptions + sizingOptions:(ASCenterLayoutNodeSizingOptions)sizingOptions + child:(ASLayoutNode *)child + size:(ASLayoutNodeSize)size; + +@end diff --git a/AsyncDisplayKit/Layout/ASCenterLayoutNode.mm b/AsyncDisplayKit/Layout/ASCenterLayoutNode.mm new file mode 100644 index 0000000000..60932b63cf --- /dev/null +++ b/AsyncDisplayKit/Layout/ASCenterLayoutNode.mm @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASCenterLayoutNode.h" + +#import "ASInternalHelpers.h" +#import "ASLayoutNodeSubclass.h" + +@implementation ASCenterLayoutNode +{ + ASCenterLayoutNodeCenteringOptions _centeringOptions; + ASCenterLayoutNodeSizingOptions _sizingOptions; + ASLayoutNode *_child; +} + ++ (instancetype)newWithCenteringOptions:(ASCenterLayoutNodeCenteringOptions)centeringOptions + sizingOptions:(ASCenterLayoutNodeSizingOptions)sizingOptions + child:(ASLayoutNode *)child + size:(ASLayoutNodeSize)size +{ + ASCenterLayoutNode *n = [super newWithSize:size]; + if (n) { + n->_centeringOptions = centeringOptions; + n->_sizingOptions = sizingOptions; + n->_child = child; + } + return n; +} + +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize +{ + // If we have a finite size in any direction, pass this so that the child can + // resolve percentages agains it. Otherwise pass kASLayoutNodeParentDimensionUndefined + // as the size will depend on the content + CGSize size = { + isinf(constrainedSize.max.width) ? kASLayoutNodeParentDimensionUndefined : constrainedSize.max.width, + isinf(constrainedSize.max.height) ? kASLayoutNodeParentDimensionUndefined : constrainedSize.max.height + }; + + // Layout the child + const CGSize minChildSize = { + (_centeringOptions & ASCenterLayoutNodeCenteringX) != 0 ? 0 : constrainedSize.min.width, + (_centeringOptions & ASCenterLayoutNodeCenteringY) != 0 ? 0 : constrainedSize.min.height, + }; + ASLayout *childLayout = [_child layoutThatFits:ASSizeRangeMake(minChildSize, constrainedSize.max) parentSize:size]; + + // If we have an undetermined height or width, use the child size to define the layout + // size + size = ASSizeRangeClamp(constrainedSize, { + isnan(size.width) ? childLayout.size.width : size.width, + isnan(size.height) ? childLayout.size.height : size.height + }); + + // If minimum size options are set, attempt to shrink the size to the size of the child + size = ASSizeRangeClamp(constrainedSize, { + MIN(size.width, (_sizingOptions & ASCenterLayoutNodeSizingOptionMinimumX) != 0 ? childLayout.size.width : size.width), + MIN(size.height, (_sizingOptions & ASCenterLayoutNodeSizingOptionMinimumY) != 0 ? childLayout.size.height : size.height) + }); + + // Compute the centered postion for the child + BOOL shouldCenterAlongX = (_centeringOptions & ASCenterLayoutNodeCenteringX); + BOOL shouldCenterAlongY = (_centeringOptions & ASCenterLayoutNodeCenteringY); + const CGPoint childPosition = { + ASRoundPixelValue(shouldCenterAlongX ? (size.width - childLayout.size.width) * 0.5f : 0), + ASRoundPixelValue(shouldCenterAlongY ? (size.height - childLayout.size.height) * 0.5f : 0) + }; + + return [ASLayout newWithNode:self + size:size + children:@[[ASLayoutChild newWithPosition:childPosition layout:childLayout]]]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASCompositeNode.h b/AsyncDisplayKit/Layout/ASCompositeNode.h new file mode 100644 index 0000000000..b749f4d60d --- /dev/null +++ b/AsyncDisplayKit/Layout/ASCompositeNode.h @@ -0,0 +1,22 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +@class ASDisplayNode; + +@interface ASCompositeNode : ASLayoutNode + +@property (nonatomic, readonly) ASDisplayNode *displayNode; + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size displayNode:(ASDisplayNode *)displayNode; ++ (instancetype)newWithDisplayNode:(ASDisplayNode *)displayNode; + +@end \ No newline at end of file diff --git a/AsyncDisplayKit/Layout/ASCompositeNode.mm b/AsyncDisplayKit/Layout/ASCompositeNode.mm new file mode 100644 index 0000000000..f95912522d --- /dev/null +++ b/AsyncDisplayKit/Layout/ASCompositeNode.mm @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASCompositeNode.h" + +#import "ASBaseDefines.h" + +#import "ASDisplayNode.h" +#import "ASLayoutNodeSubclass.h" + +@implementation ASCompositeNode + ++ (instancetype)newWithDisplayNode:(ASDisplayNode *)displayNode +{ + return [self newWithSize:ASLayoutNodeSizeZero displayNode:displayNode]; +} + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size displayNode:(ASDisplayNode *)displayNode +{ + if (displayNode == nil) { + return nil; + } + ASCompositeNode *n = [super newWithSize:size]; + if (n) { + n->_displayNode = displayNode; + } + return n; +} + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); +} + +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize +{ + CGSize measuredSize = ASSizeRangeClamp(constrainedSize, [_displayNode measure:constrainedSize.max]); + return [ASLayout newWithNode:self size:measuredSize]; +} + +@end \ No newline at end of file diff --git a/AsyncDisplayKit/Layout/ASDimension.h b/AsyncDisplayKit/Layout/ASDimension.h new file mode 100644 index 0000000000..f1f9d96720 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASDimension.h @@ -0,0 +1,152 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import +#import + +/** + A dimension relative to constraints to be provided in the future. + A RelativeDimension can be one of three types: + + "Auto" - This indicated "I have no opinion" and may be resolved in whatever way makes most sense given + the circumstances. This is the default type. + + "Points" - Just a number. It will always resolve to exactly this amount. + + "Percent" - Multiplied to a provided parent amount to resolve a final amount. + */ +typedef NS_ENUM(NSInteger, ASRelativeDimensionType) { + ASRelativeDimensionTypeAuto, + ASRelativeDimensionTypePoints, + ASRelativeDimensionTypePercent, +}; +typedef struct { + ASRelativeDimensionType type; + CGFloat value; +} ASRelativeDimension; + +/** Expresses an inclusive range of sizes. Used to provide a simple constraint to layout. */ +typedef struct { + CGSize min; + CGSize max; +} ASSizeRange; + +/** Expresses a size with relative dimensions. */ +typedef struct { + ASRelativeDimension width; + ASRelativeDimension height; +} ASRelativeSize; + +/** + Expresses an inclusive range of relative sizes. Used to provide additional constraint to layout. + */ +typedef struct { + ASRelativeSize min; + ASRelativeSize max; +} ASRelativeSizeRange; + +/** type = Auto; value = 0 */ +extern ASRelativeDimension const ASRelativeDimensionAuto; + +/** min = {0,0}; max = {INFINITY, INFINITY} */ +extern ASSizeRange const ASSizeRangeUnconstrained; + +/** width = Auto; height = Auto */ +extern ASRelativeSize const ASRelativeSizeAuto; + +/** min = {Auto, Auto}; max = {Auto, Auto} */ +extern ASRelativeSizeRange const ASRelativeSizeRangeAuto; + +ASDISPLAYNODE_EXTERN_C_BEGIN + +#pragma mark ASRelativeDimension + +extern ASRelativeDimension ASRelativeDimensionMake(ASRelativeDimensionType type, CGFloat value); + +extern ASRelativeDimension ASRelativeDimensionMakeWithPoints(CGFloat points); + +extern ASRelativeDimension ASRelativeDimensionMakeWithPercent(CGFloat percent); + +extern ASRelativeDimension ASRelativeDimensionCopy(ASRelativeDimension aDimension); + +extern BOOL ASRelativeDimensionEqualToDimension(ASRelativeDimension lhs, ASRelativeDimension rhs); + +extern NSString *NSStringFromASRelativeDimension(ASRelativeDimension dimension); + +extern CGFloat ASRelativeDimensionResolve(ASRelativeDimension dimension, CGFloat autoSize, CGFloat parent); + +#pragma mark - +#pragma mark ASSizeRange + +extern ASSizeRange ASSizeRangeMake(CGSize min, CGSize max); + +/** Clamps the provided CGSize between the [min, max] bounds of this ASSizeRange. */ +extern CGSize ASSizeRangeClamp(ASSizeRange sizeRange, CGSize size); + +/** + Intersects another size range. If the other size range does not overlap in either dimension, this size range + "wins" by returning a single point within its own range that is closest to the non-overlapping range. + */ +extern ASSizeRange ASSizeRangeIntersect(ASSizeRange sizeRange, ASSizeRange otherSizeRange); + +extern BOOL ASSizeRangeEqualToRange(ASSizeRange lhs, ASSizeRange rhs); + +extern NSString * NSStringFromASSizeRange(ASSizeRange sizeRange); + +#pragma mark - +#pragma mark ASRelativeSize + +extern ASRelativeSize ASRelativeSizeMake(ASRelativeDimension width, ASRelativeDimension height); + +/** Convenience constructor to provide size in Points. */ +extern ASRelativeSize ASRelativeSizeMakeWithCGSize(CGSize size); + +/** Resolve this relative size relative to a parent size and an auto size. */ +extern CGSize ASRelativeSizeResolveSize(ASRelativeSize relativeSize, CGSize parentSize, CGSize autoSize); + +extern BOOL ASRelativeSizeEqualToSize(ASRelativeSize lhs, ASRelativeSize rhs); + +extern NSString *NSStringFromASRelativeSize(ASRelativeSize size); + +#pragma mark - +#pragma mark ASRelativeSizeRange + +extern ASRelativeSizeRange ASRelativeSizeRangeMake(ASRelativeSize min, ASRelativeSize max); + +#pragma mark Convenience constructors to provide an exact size (min == max). +extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSize exact); + +extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactCGSize(CGSize exact); + +extern ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeDimensions(ASRelativeDimension exactWidth, + ASRelativeDimension exactHeight); + +/** + Provided a parent size and values to use in place of Auto, compute final dimensions for this RelativeSizeRange + to arrive at a SizeRange. + */ +extern ASSizeRange ASRelativeSizeRangeResolveSizeRange(ASRelativeSizeRange relativeSizeRange, + CGSize parentSize, + ASSizeRange autoSizeRange); + +/** + Provided a parent size and a default autoSizeRange, compute final dimensions for this RelativeSizeRange + to arrive at a SizeRange. As an example: + + CGSize parent = {200, 120}; + RelativeSizeRange rel = {Percent(0.5), Percent(2/3)} + ASRelativeSizeRangeResolve(rel, parent); // {{100, 60}, {100, 60}} + + The default autoSizeRange is *everything*, meaning ASSizeRangeUnconstrained. + */ +extern ASSizeRange ASRelativeSizeRangeResolveSizeRangeWithDefaultAutoSizeRange(ASRelativeSizeRange relativeSizeRange, + CGSize parentSize); + +ASDISPLAYNODE_EXTERN_C_END diff --git a/AsyncDisplayKit/Layout/ASDimension.mm b/AsyncDisplayKit/Layout/ASDimension.mm new file mode 100644 index 0000000000..3438b1d06e --- /dev/null +++ b/AsyncDisplayKit/Layout/ASDimension.mm @@ -0,0 +1,218 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASDimension.h" + +#import "ASAssert.h" + +ASRelativeDimension const ASRelativeDimensionAuto = {ASRelativeDimensionTypeAuto, 0}; +ASSizeRange const ASSizeRangeUnconstrained = {{0,0}, {INFINITY, INFINITY}}; +ASRelativeSize const ASRelativeSizeAuto = {ASRelativeDimensionAuto, ASRelativeDimensionAuto}; +ASRelativeSizeRange const ASRelativeSizeRangeAuto = {ASRelativeSizeAuto, ASRelativeSizeAuto}; + +#pragma mark ASRelativeDimension + +ASRelativeDimension ASRelativeDimensionMake(ASRelativeDimensionType type, CGFloat value) +{ + if (type == ASRelativeDimensionTypePoints) { ASDisplayNodeCAssertPositiveReal(@"Points", value); } + if (type == ASRelativeDimensionTypeAuto) { ASDisplayNodeCAssertTrue(value == 0); } + ASRelativeDimension dimension; dimension.type = type; dimension.value = value; return dimension; +} + +ASRelativeDimension ASRelativeDimensionMakeWithPoints(CGFloat points) +{ + return ASRelativeDimensionMake(ASRelativeDimensionTypePoints, points); +} + +ASRelativeDimension ASRelativeDimensionMakeWithPercent(CGFloat percent) +{ + return ASRelativeDimensionMake(ASRelativeDimensionTypePercent, percent); +} + +ASRelativeDimension ASRelativeDimensionCopy(ASRelativeDimension aDimension) +{ + return ASRelativeDimensionMake(aDimension.type, aDimension.value); +} + +BOOL ASRelativeDimensionEqualToDimension(ASRelativeDimension lhs, ASRelativeDimension rhs) +{ + // Implementation assumes that "auto" assigns '0' to value. + if (lhs.type != rhs.type) { + return false; + } + switch (lhs.type) { + case ASRelativeDimensionTypeAuto: + return true; + case ASRelativeDimensionTypePoints: + case ASRelativeDimensionTypePercent: + return lhs.value == rhs.value; + } +} + +NSString *NSStringFromASRelativeDimension(ASRelativeDimension dimension) +{ + switch (dimension.type) { + case ASRelativeDimensionTypeAuto: + return @"Auto"; + case ASRelativeDimensionTypePoints: + return [NSString stringWithFormat:@"%.0fpt", dimension.value]; + case ASRelativeDimensionTypePercent: + return [NSString stringWithFormat:@"%.0f%%", dimension.value * 100.0]; + } +} + +CGFloat ASRelativeDimensionResolve(ASRelativeDimension dimension, CGFloat autoSize, CGFloat parent) +{ + switch (dimension.type) { + case ASRelativeDimensionTypeAuto: + return autoSize; + case ASRelativeDimensionTypePoints: + return dimension.value; + case ASRelativeDimensionTypePercent: + return round(dimension.value * parent); + } +} + +#pragma mark - +#pragma mark ASSizeRange + +ASSizeRange ASSizeRangeMake(CGSize min, CGSize max) +{ + ASDisplayNodeCAssertPositiveReal(@"Range min width", min.width); + ASDisplayNodeCAssertPositiveReal(@"Range min height", min.height); + ASDisplayNodeCAssertInfOrPositiveReal(@"Range max width", max.width); + ASDisplayNodeCAssertInfOrPositiveReal(@"Range max height", max.height); + ASDisplayNodeCAssert(min.width <= max.width, + @"Range min width (%f) must not be larger than max width (%f).", min.width, max.width); + ASDisplayNodeCAssert(min.height <= max.height, + @"Range min height (%f) must not be larger than max height (%f).", min.height, max.height); + ASSizeRange sizeRange; sizeRange.min = min; sizeRange.max = max; return sizeRange; +} + +CGSize ASSizeRangeClamp(ASSizeRange sizeRange, CGSize size) +{ + return CGSizeMake(MAX(sizeRange.min.width, MIN(sizeRange.max.width, size.width)), + MAX(sizeRange.min.height, MIN(sizeRange.max.height, size.height))); +} + +struct _Range { + CGFloat min; + CGFloat max; + + /** + Intersects another dimension range. If the other range does not overlap, this size range "wins" by returning a + single point within its own range that is closest to the non-overlapping range. + */ + _Range intersect(const _Range &other) const + { + CGFloat newMin = MAX(min, other.min); + CGFloat newMax = MIN(max, other.max); + if (!(newMin > newMax)) { + return {newMin, newMax}; + } else { + // No intersection. If we're before the other range, return our max; otherwise our min. + if (min < other.min) { + return {max, max}; + } else { + return {min, min}; + } + } + } +}; + +ASSizeRange ASSizeRangeIntersect(ASSizeRange sizeRange, ASSizeRange otherSizeRange) +{ + auto w = _Range({sizeRange.min.width, sizeRange.max.width}).intersect({otherSizeRange.min.width, otherSizeRange.max.width}); + auto h = _Range({sizeRange.min.height, sizeRange.max.height}).intersect({otherSizeRange.min.height, otherSizeRange.max.height}); + return {{w.min, h.min}, {w.max, h.max}}; +} + +BOOL ASSizeRangeEqualToRange(ASSizeRange lhs, ASSizeRange rhs) +{ + return CGSizeEqualToSize(lhs.min, rhs.min) && CGSizeEqualToSize(lhs.max, rhs.max); +} + +NSString * NSStringFromASSizeRange(ASSizeRange sizeRange) +{ + return [NSString stringWithFormat:@"", + NSStringFromCGSize(sizeRange.min), + NSStringFromCGSize(sizeRange.max)]; +} + +#pragma mark - +#pragma mark ASRelativeSize + +ASRelativeSize ASRelativeSizeMake(ASRelativeDimension width, ASRelativeDimension height) +{ + ASRelativeSize size; size.width = width; size.height = height; return size; +} + +ASRelativeSize ASRelativeSizeMakeWithCGSize(CGSize size) +{ + return ASRelativeSizeMake(ASRelativeDimensionMakeWithPoints(size.width), + ASRelativeDimensionMakeWithPoints(size.height)); +} + +CGSize ASRelativeSizeResolveSize(ASRelativeSize relativeSize, CGSize parentSize, CGSize autoSize) +{ + return CGSizeMake(ASRelativeDimensionResolve(relativeSize.width, autoSize.width, parentSize.width), + ASRelativeDimensionResolve(relativeSize.height, autoSize.height, parentSize.height)); +} + +BOOL ASRelativeSizeEqualToSize(ASRelativeSize lhs, ASRelativeSize rhs) +{ + return ASRelativeDimensionEqualToDimension(lhs.width, rhs.width) + && ASRelativeDimensionEqualToDimension(lhs.height, rhs.height); +} + +NSString *NSStringFromASRelativeSize(ASRelativeSize size) +{ + return [NSString stringWithFormat:@"{%@, %@}", + NSStringFromASRelativeDimension(size.width), + NSStringFromASRelativeDimension(size.height)]; +} + +#pragma mark - +#pragma mark ASRelativeSizeRange + +ASRelativeSizeRange ASRelativeSizeRangeMake(ASRelativeSize min, ASRelativeSize max) +{ + ASRelativeSizeRange sizeRange; sizeRange.min = min; sizeRange.max = max; return sizeRange; +} + +ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSize exact) +{ + return ASRelativeSizeRangeMake(exact, exact); +} + +ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactCGSize(CGSize exact) +{ + return ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSizeMakeWithCGSize(exact)); +} + +ASRelativeSizeRange ASRelativeSizeRangeMakeWithExactRelativeDimensions(ASRelativeDimension exactWidth, + ASRelativeDimension exactHeight) +{ + return ASRelativeSizeRangeMakeWithExactRelativeSize(ASRelativeSizeMake(exactWidth, exactHeight)); +} + +ASSizeRange ASRelativeSizeRangeResolveSizeRange(ASRelativeSizeRange relativeSizeRange, + CGSize parentSize, + ASSizeRange autoSizeRange) +{ + return ASSizeRangeMake(ASRelativeSizeResolveSize(relativeSizeRange.min, parentSize, autoSizeRange.min), + ASRelativeSizeResolveSize(relativeSizeRange.max, parentSize, autoSizeRange.max)); +} + +ASSizeRange ASRelativeSizeRangeResolveSizeRangeWithDefaultAutoSizeRange(ASRelativeSizeRange relativeSizeRange, + CGSize parentSize) +{ + return ASRelativeSizeRangeResolveSizeRange(relativeSizeRange, parentSize, ASSizeRangeUnconstrained); +} diff --git a/AsyncDisplayKit/Layout/ASInsetLayoutNode.h b/AsyncDisplayKit/Layout/ASInsetLayoutNode.h new file mode 100644 index 0000000000..269b67c58b --- /dev/null +++ b/AsyncDisplayKit/Layout/ASInsetLayoutNode.h @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +/** + A layout node that wraps another node, applying insets around it. + + If the child node has a size specified as a percentage, the percentage is resolved against this node's parent + size **after** applying insets. + + @example ASOuterLayoutNode contains an ASInsetLayoutNode with an ASInnerLayoutNode. Suppose that: + - ASOuterLayoutNode is 200pt wide. + - ASInnerLayoutNode specifies its width as 100%. + - The ASInsetLayoutNode has insets of 10pt on every side. + ASInnerLayoutNode will have size 180pt, not 200pt, because it receives a parent size that has been adjusted for insets. + + If you're familiar with CSS: ASInsetLayoutNode's child behaves similarly to "box-sizing: border-box". + + An infinite inset is resolved as an inset equal to all remaining space after applying the other insets and child size. + @example An ASInsetLayoutNode with an infinite left inset and 10px for all other edges will position it's child 10px from the right edge. + */ +@interface ASInsetLayoutNode : ASLayoutNode + +/** + @param insets The amount of space to inset on each side. + @param node The wrapped child layout node to inset. If nil, this method returns nil. + */ ++ (instancetype)newWithInsets:(UIEdgeInsets)insets + node:(ASLayoutNode *)node; + +@end diff --git a/AsyncDisplayKit/Layout/ASInsetLayoutNode.mm b/AsyncDisplayKit/Layout/ASInsetLayoutNode.mm new file mode 100644 index 0000000000..657ada8c7e --- /dev/null +++ b/AsyncDisplayKit/Layout/ASInsetLayoutNode.mm @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASInsetLayoutNode.h" + +#import "ASAssert.h" +#import "ASBaseDefines.h" + +#import "ASInternalHelpers.h" +#import "ASLayoutNodeSubclass.h" + +@interface ASInsetLayoutNode () +{ + UIEdgeInsets _insets; + ASLayoutNode *_node; +} +@end + +/* Returns f if f is finite, substitute otherwise */ +static CGFloat finite(CGFloat f, CGFloat substitute) +{ + return isinf(f) ? substitute : f; +} + +/* Returns f if f is finite, 0 otherwise */ +static CGFloat finiteOrZero(CGFloat f) +{ + return finite(f, 0); +} + +/* Returns the inset required to center 'inner' in 'outer' */ +static CGFloat centerInset(CGFloat outer, CGFloat inner) +{ + return ASRoundPixelValue((outer - inner) / 2); +} + +@implementation ASInsetLayoutNode + ++ (instancetype)newWithInsets:(UIEdgeInsets)insets + node:(ASLayoutNode *)node +{ + if (node == nil) { + return nil; + } + ASInsetLayoutNode *n = [super newWithSize:{}]; + if (n) { + n->_insets = insets; + n->_node = node; + } + return n; +} + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); +} + +/** + Inset will compute a new constrained size for it's child after applying insets and re-positioning + the child to respect the inset. + */ +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutNodeSize)size + relativeToParentSize:(CGSize)parentSize +{ + ASDisplayNodeAssert(ASLayoutNodeSizeEqualToNodeSize(size, ASLayoutNodeSizeZero), + @"ASInsetLayoutNode only passes size {} to the super class initializer, but received size %@ " + "(node=%@)", NSStringFromASLayoutNodeSize(size), _node); + + const CGFloat insetsX = (finiteOrZero(_insets.left) + finiteOrZero(_insets.right)); + const CGFloat insetsY = (finiteOrZero(_insets.top) + finiteOrZero(_insets.bottom)); + + // if either x-axis inset is infinite, let child be intrinsic width + const CGFloat minWidth = (isinf(_insets.left) || isinf(_insets.right)) ? 0 : constrainedSize.min.width; + // if either y-axis inset is infinite, let child be intrinsic height + const CGFloat minHeight = (isinf(_insets.top) || isinf(_insets.bottom)) ? 0 : constrainedSize.min.height; + + const ASSizeRange insetConstrainedSize = { + { + MAX(0, minWidth - insetsX), + MAX(0, minHeight - insetsY), + }, + { + MAX(0, constrainedSize.max.width - insetsX), + MAX(0, constrainedSize.max.height - insetsY), + } + }; + const CGSize insetParentSize = { + MAX(0, parentSize.width - insetsX), + MAX(0, parentSize.height - insetsY) + }; + ASLayout *childLayout = [_node layoutThatFits:insetConstrainedSize parentSize:insetParentSize]; + + const CGSize computedSize = ASSizeRangeClamp(constrainedSize, { + finite(childLayout.size.width + _insets.left + _insets.right, parentSize.width), + finite(childLayout.size.height + _insets.top + _insets.bottom, parentSize.height), + }); + + ASDisplayNodeAssert(!isnan(computedSize.width) && !isnan(computedSize.height), + @"Inset node computed size is NaN; you may not specify infinite insets against a NaN parent size\n" + "parentSize = %@, insets = %@", NSStringFromCGSize(parentSize), NSStringFromUIEdgeInsets(_insets)); + + const CGFloat x = finite(_insets.left, constrainedSize.max.width - + (finite(_insets.right, + centerInset(constrainedSize.max.width, childLayout.size.width)) + childLayout.size.width)); + + const CGFloat y = finite(_insets.top, + constrainedSize.max.height - + (finite(_insets.bottom, + centerInset(constrainedSize.max.height, childLayout.size.height)) + childLayout.size.height)); + return [ASLayout newWithNode:self + size:computedSize + children:@[[ASLayoutChild newWithPosition:{x,y} layout:childLayout]]]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASLayout.h b/AsyncDisplayKit/Layout/ASLayout.h new file mode 100644 index 0000000000..2d20911ada --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayout.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import +#import + +@class ASLayoutNode; + +/** Represents the computed size of a layout node, as well as the computed sizes and positions of its children. */ +@interface ASLayout : NSObject + +@property (nonatomic, readonly) ASLayoutNode *node; +@property (nonatomic, readonly) CGSize size; +/** + * Each item is of type ASLayoutChild. + */ +@property (nonatomic, readonly) NSArray *children; + ++ (instancetype)newWithNode:(ASLayoutNode *)node size:(CGSize)size children:(NSArray *)children; + +/** + * Convenience that does not have any children. + */ ++ (instancetype)newWithNode:(ASLayoutNode *)node size:(CGSize)size; + +@end + +@interface ASLayoutChild : NSObject + +@property (nonatomic, readonly) CGPoint position; +@property (nonatomic, readonly) ASLayout *layout; + +/** + * Designated initializer + */ ++ (instancetype)newWithPosition:(CGPoint)position layout:(ASLayout *)layout; + +@end diff --git a/AsyncDisplayKit/Layout/ASLayout.mm b/AsyncDisplayKit/Layout/ASLayout.mm new file mode 100644 index 0000000000..f3a0ea2f4a --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayout.mm @@ -0,0 +1,45 @@ + /* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASLayout.h" + +@implementation ASLayout + ++ (instancetype)newWithNode:(ASLayoutNode *)node size:(CGSize)size children:(NSArray *)children +{ + ASLayout *l = [super new]; + if (l) { + l->_node = node; + l->_size = size; + l->_children = [children copy]; + } + return l; +} + ++ (instancetype)newWithNode:(ASLayoutNode *)node size:(CGSize)size +{ + return [self newWithNode:node size:size children:nil]; +} + +@end + +@implementation ASLayoutChild + ++ (instancetype)newWithPosition:(CGPoint)position layout:(ASLayout *)layout +{ + ASLayoutChild *c = [super new]; + if (c) { + c->_position = position; + c->_layout = layout; + } + return c; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASLayoutNode.h b/AsyncDisplayKit/Layout/ASLayoutNode.h new file mode 100644 index 0000000000..8d46989fee --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayoutNode.h @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +/** A layout node is an immutable object that describes a layout, loosely inspired by React. */ +@interface ASLayoutNode : NSObject + +/** + @param size A size constraint that should apply to this layout node. Pass {} to specify no size constraint. + + @example A layout node of a square: + [ASLayoutNode newWithSize:{100, 100}] + */ ++ (instancetype)newWithSize:(ASLayoutNodeSize)size; + +@end diff --git a/AsyncDisplayKit/Layout/ASLayoutNode.mm b/AsyncDisplayKit/Layout/ASLayoutNode.mm new file mode 100644 index 0000000000..7b80790c08 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayoutNode.mm @@ -0,0 +1,94 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASLayoutNode.h" +#import "ASLayoutNodeSubclass.h" + +#import "ASAssert.h" +#import "ASBaseDefines.h" + +#import "ASInternalHelpers.h" +#import "ASLayout.h" + +CGFloat const kASLayoutNodeParentDimensionUndefined = NAN; +CGSize const kASLayoutNodeParentSizeUndefined = {kASLayoutNodeParentDimensionUndefined, kASLayoutNodeParentDimensionUndefined}; + +@implementation ASLayoutNode +{ + ASLayoutNodeSize _size; +} + +#if DEBUG ++ (void)initialize +{ + ASDisplayNodeConditionalAssert(self != [ASLayoutNode class], + !ASSubclassOverridesSelector([ASLayoutNode class], self, @selector(layoutThatFits:parentSize:)), + @"%@ overrides -layoutThatFits:parentSize: which is not allowed. Override -computeLayoutThatFits: " + "or -computeLayoutThatFits:restrictedToSize:relativeToParentSize: instead.", + NSStringFromClass(self)); +} +#endif + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size +{ + return [[self alloc] initWithLayoutNodeSize:size]; +} + ++ (instancetype)new +{ + return [self newWithSize:{}]; +} + +- (instancetype)init +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); +} + +- (instancetype)initWithLayoutNodeSize:(ASLayoutNodeSize)size +{ + if (self = [super init]) { + _size = size; + } + return self; +} + +#pragma mark - Layout + +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize +{ + ASLayout *layout = [self computeLayoutThatFits:constrainedSize + restrictedToSize:_size + relativeToParentSize:parentSize]; + ASDisplayNodeAssert(layout.node == self, @"Layout computed by %@ should return self as node, but returned %@", + [self class], [layout.node class]); + ASSizeRange resolvedRange = ASSizeRangeIntersect(constrainedSize, ASLayoutNodeSizeResolve(_size, parentSize)); + ASDisplayNodeAssert(layout.size.width <= resolvedRange.max.width + && layout.size.width >= resolvedRange.min.width + && layout.size.height <= resolvedRange.max.height + && layout.size.height >= resolvedRange.min.height, + @"Computed size %@ for %@ does not fall within constrained size %@", + NSStringFromCGSize(layout.size), [self class], NSStringFromASSizeRange(resolvedRange)); + return layout; +} + +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutNodeSize)size + relativeToParentSize:(CGSize)parentSize +{ + ASSizeRange resolvedRange = ASSizeRangeIntersect(constrainedSize, ASLayoutNodeSizeResolve(_size, parentSize)); + return [self computeLayoutThatFits:resolvedRange]; +} + +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize +{ + return [ASLayout newWithNode:self size:constrainedSize.min]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASLayoutNodeSize.h b/AsyncDisplayKit/Layout/ASLayoutNodeSize.h new file mode 100644 index 0000000000..a3010583f9 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayoutNodeSize.h @@ -0,0 +1,52 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import +#import + +/** + A struct specifying a layout node's size. Example: + + ASLayoutNodeSize size = { + .width = Percent(0.5), + .maxWidth = 200, + .minHeight = Percent(0.75) + }; + + // + size.description(); + + */ +typedef struct { + ASRelativeDimension width; + ASRelativeDimension height; + + ASRelativeDimension minWidth; + ASRelativeDimension minHeight; + + ASRelativeDimension maxWidth; + ASRelativeDimension maxHeight; +} ASLayoutNodeSize; + +extern ASLayoutNodeSize const ASLayoutNodeSizeZero; + +ASDISPLAYNODE_EXTERN_C_BEGIN + +extern ASLayoutNodeSize ASLayoutNodeSizeMakeWithCGSize(CGSize size); + +extern ASLayoutNodeSize ASLayoutNodeSizeMake(CGFloat width, CGFloat height); + +extern ASSizeRange ASLayoutNodeSizeResolve(ASLayoutNodeSize nodeSize, CGSize parentSize); + +extern BOOL ASLayoutNodeSizeEqualToNodeSize(ASLayoutNodeSize lhs, ASLayoutNodeSize rhs); + +extern NSString *NSStringFromASLayoutNodeSize(ASLayoutNodeSize nodeSize); + +ASDISPLAYNODE_EXTERN_C_END diff --git a/AsyncDisplayKit/Layout/ASLayoutNodeSize.mm b/AsyncDisplayKit/Layout/ASLayoutNodeSize.mm new file mode 100644 index 0000000000..ce633d8f73 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayoutNodeSize.mm @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASLayoutNodeSize.h" +#import "ASAssert.h" + +ASLayoutNodeSize const ASLayoutNodeSizeZero = {}; + +ASLayoutNodeSize ASLayoutNodeSizeMakeWithCGSize(CGSize size) +{ + return ASLayoutNodeSizeMake(size.width, size.height); +} + +ASLayoutNodeSize ASLayoutNodeSizeMake(CGFloat width, CGFloat height) +{ + return {ASRelativeDimensionMakeWithPoints(width), ASRelativeDimensionMakeWithPoints(height)}; +} + +ASDISPLAYNODE_INLINE void ASLNSConstrain(CGFloat minVal, CGFloat exactVal, CGFloat maxVal, CGFloat *outMin, CGFloat *outMax) +{ + ASDisplayNodeCAssert(!isnan(minVal), @"minVal must not be NaN"); + ASDisplayNodeCAssert(!isnan(maxVal), @"maxVal must not be NaN"); + // Avoid use of min/max primitives since they're harder to reason + // about in the presence of NaN (in exactVal) + // Follow CSS: min overrides max overrides exact. + + // Begin with the min/max range + *outMin = minVal; + *outMax = maxVal; + if (maxVal <= minVal) { + // min overrides max and exactVal is irrelevant + *outMax = minVal; + return; + } + if (isnan(exactVal)) { + // no exact value, so leave as a min/max range + return; + } + if (exactVal > maxVal) { + // clip to max value + *outMin = maxVal; + } else if (exactVal < minVal) { + // clip to min value + *outMax = minVal; + } else { + // use exact value + *outMin = *outMax = exactVal; + } +} + +ASSizeRange ASLayoutNodeSizeResolve(ASLayoutNodeSize nodeSize, CGSize parentSize) +{ + CGSize resolvedExact = ASRelativeSizeResolveSize(ASRelativeSizeMake(nodeSize.width, nodeSize.height), parentSize, {NAN, NAN}); + CGSize resolvedMin = ASRelativeSizeResolveSize(ASRelativeSizeMake(nodeSize.minWidth, nodeSize.minHeight), parentSize, {0, 0}); + CGSize resolvedMax = ASRelativeSizeResolveSize(ASRelativeSizeMake(nodeSize.maxWidth, nodeSize.maxHeight), parentSize, {INFINITY, INFINITY}); + + CGSize rangeMin, rangeMax; + ASLNSConstrain(resolvedMin.width, resolvedExact.width, resolvedMax.width, &rangeMin.width, &rangeMax.width); + ASLNSConstrain(resolvedMin.height, resolvedExact.height, resolvedMax.height, &rangeMin.height, &rangeMax.height); + return {rangeMin, rangeMax}; +} + +BOOL ASLayoutNodeSizeEqualToNodeSize(ASLayoutNodeSize lhs, ASLayoutNodeSize rhs) +{ + return ASRelativeDimensionEqualToDimension(lhs.width, rhs.width) + && ASRelativeDimensionEqualToDimension(lhs.height, rhs.height) + && ASRelativeDimensionEqualToDimension(lhs.minWidth, rhs.minWidth) + && ASRelativeDimensionEqualToDimension(lhs.minHeight, rhs.minHeight) + && ASRelativeDimensionEqualToDimension(lhs.maxWidth, rhs.maxWidth) + && ASRelativeDimensionEqualToDimension(lhs.maxHeight, rhs.maxHeight); +} + +NSString *NSStringFromASLayoutNodeSize(ASLayoutNodeSize nodeSize) +{ + return [NSString stringWithFormat:@"", + NSStringFromASRelativeSize(ASRelativeSizeMake(nodeSize.width, nodeSize.height)), + NSStringFromASRelativeSize(ASRelativeSizeMake(nodeSize.minWidth, nodeSize.minHeight)), + NSStringFromASRelativeSize(ASRelativeSizeMake(nodeSize.maxWidth, nodeSize.maxHeight))]; +} diff --git a/AsyncDisplayKit/Layout/ASLayoutNodeSubclass.h b/AsyncDisplayKit/Layout/ASLayoutNodeSubclass.h new file mode 100644 index 0000000000..e23aa0ffad --- /dev/null +++ b/AsyncDisplayKit/Layout/ASLayoutNodeSubclass.h @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import +#import + +@interface ASLayoutNode () + +/** A constant that indicates that the parent's size is not yet determined in a given dimension. */ +extern CGFloat const kASLayoutNodeParentDimensionUndefined; + +/** A constant that indicates that the parent's size is not yet determined in either dimension. */ +extern CGSize const kASLayoutNodeParentSizeUndefined; + +/** + Call this on children layout nodes to compute their layouts within your implementation of -computeLayoutThatFits:. + + @warning You may not override this method. Override -computeLayoutThatFits: instead. + + @param constrainedSize Specifies a minimum and maximum size. The receiver must choose a size that is in this range. + @param parentSize The parent layout node's size. If the parent layout node does not have a final size in a given dimension, + then it should be passed as kASLayoutNodeParentDimensionUndefined (for example, if the parent's width + depends on the child's size). + + @return An ASLayout instance defining the layout of the receiver and its children. + */ +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize + parentSize:(CGSize)parentSize; + +/** + Override this method to compute your node's layout. + + @discussion Why do you need to override -computeLayoutThatFits: instead of -layoutThatFits:parentSize:? + The base implementation of -layoutThatFits:parentSize: does the following for you: + 1. First, it uses the parentSize parameter to resolve the node's size (the one passed into -initWithSize:). + 2. Then, it intersects the resolved size with the constrainedSize parameter. If the two don't intersect, + constrainedSize wins. This allows a node to always override its childrens' sizes when computing its layout. + (The analogy for UIView: you might return a certain size from -sizeThatFits:, but a parent view can always override + that size and set your frame to any size.) + + @param constrainedSize A min and max size. This is computed as described in the description. The ASLayout you + return MUST have a size between these two sizes. This is enforced by assertion. + */ +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize; + +/** + ASLayoutNode's implementation of -layoutThatFits:parentSize: calls this method to resolve the node's size + against parentSize, intersect it with constrainedSize, and call -computeLayoutThatFits: with the result. + + In certain advanced cases, you may want to customize this logic. Overriding this method allows you to receive all + three parameters and do the computation yourself. + + @warning Overriding this method should be done VERY rarely. + */ +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutNodeSize)size + relativeToParentSize:(CGSize)parentSize; + +@end diff --git a/AsyncDisplayKit/Layout/ASOverlayLayoutNode.h b/AsyncDisplayKit/Layout/ASOverlayLayoutNode.h new file mode 100644 index 0000000000..1232922b97 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASOverlayLayoutNode.h @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +/** + This node lays out a single node and then overlays a node on top of it streched to its size + */ +@interface ASOverlayLayoutNode : ASLayoutNode + ++ (instancetype)newWithNode:(ASLayoutNode *)node overlay:(ASLayoutNode *)overlay; + +@end diff --git a/AsyncDisplayKit/Layout/ASOverlayLayoutNode.mm b/AsyncDisplayKit/Layout/ASOverlayLayoutNode.mm new file mode 100644 index 0000000000..22cc7ff8ef --- /dev/null +++ b/AsyncDisplayKit/Layout/ASOverlayLayoutNode.mm @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASOverlayLayoutNode.h" + +#import "ASAssert.h" +#import "ASBaseDefines.h" + +#import "ASLayoutNodeSubclass.h" + +@implementation ASOverlayLayoutNode +{ + ASLayoutNode *_overlay; + ASLayoutNode *_node; +} + ++ (instancetype)newWithNode:(ASLayoutNode *)node + overlay:(ASLayoutNode *)overlay +{ + ASOverlayLayoutNode *n = [super newWithSize:{}]; + if (n) { + ASDisplayNodeAssertNotNil(node, @"Node that will be overlayed on shouldn't be nil"); + n->_overlay = overlay; + n->_node = node; + } + return n; +} + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); +} + +/** + First layout the contents, then fit the overlay on top of it. + */ +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutNodeSize)size + relativeToParentSize:(CGSize)parentSize +{ + ASDisplayNodeAssert(ASLayoutNodeSizeEqualToNodeSize(size, ASLayoutNodeSizeZero), + @"ASOverlayLayoutNode only passes size {} to the super class initializer, but received size %@ " + "(node=%@, overlay=%@)", NSStringFromASLayoutNodeSize(size), _node, _overlay); + + ASLayout *contentsLayout = [_node layoutThatFits:constrainedSize parentSize:parentSize]; + NSMutableArray *layoutChildren = [NSMutableArray arrayWithObject:[ASLayoutChild newWithPosition:{0, 0} layout:contentsLayout]]; + if (_overlay) { + ASLayout *overlayLayout = [_overlay layoutThatFits:{contentsLayout.size, contentsLayout.size} parentSize:contentsLayout.size]; + [layoutChildren addObject:[ASLayoutChild newWithPosition:{0, 0} layout:overlayLayout]]; + } + + return [ASLayout newWithNode:self size:contentsLayout.size children:layoutChildren]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASRatioLayoutNode.h b/AsyncDisplayKit/Layout/ASRatioLayoutNode.h new file mode 100644 index 0000000000..a5ebee69b5 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASRatioLayoutNode.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +/** + Ratio layout node + For when the content should respect a certain inherent ratio but can be scaled (think photos or videos) + The ratio passed is the ratio of height / width you expect + + For a ratio 0.5, the node will have a flat rectangle shape + _ _ _ _ + | | + |_ _ _ _| + + For a ratio 2.0, the node will be twice as tall as it is wide + _ _ + | | + | | + | | + |_ _| + + **/ +@interface ASRatioLayoutNode : ASLayoutNode + ++ (instancetype)newWithRatio:(CGFloat)ratio + size:(ASLayoutNodeSize)size + node:(ASLayoutNode *)node; + +@end diff --git a/AsyncDisplayKit/Layout/ASRatioLayoutNode.mm b/AsyncDisplayKit/Layout/ASRatioLayoutNode.mm new file mode 100644 index 0000000000..814b12a42e --- /dev/null +++ b/AsyncDisplayKit/Layout/ASRatioLayoutNode.mm @@ -0,0 +1,81 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASRatioLayoutNode.h" + +#import +#import + +#import "ASAssert.h" +#import "ASBaseDefines.h" + +#import "ASLayoutNodeSubclass.h" + +#import "ASInternalHelpers.h" + +@implementation ASRatioLayoutNode +{ + CGFloat _ratio; + ASLayoutNode *_node; +} + ++ (instancetype)newWithRatio:(CGFloat)ratio + size:(ASLayoutNodeSize)size + node:(ASLayoutNode *)node +{ + ASDisplayNodeAssert(ratio > 0, @"Ratio should be strictly positive, but received %f", ratio); + if (ratio <= 0) { + return nil; + } + + ASRatioLayoutNode *n = [super newWithSize:size]; + if (n) { + n->_ratio = ratio; + n->_node = node; + } + return n; +} + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); +} + +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize +{ + std::vector sizeOptions; + if (!isinf(constrainedSize.max.width)) { + sizeOptions.push_back(ASSizeRangeClamp(constrainedSize, { + constrainedSize.max.width, + ASFloorPixelValue(_ratio * constrainedSize.max.width) + })); + } + if (!isinf(constrainedSize.max.height)) { + sizeOptions.push_back(ASSizeRangeClamp(constrainedSize, { + ASFloorPixelValue(constrainedSize.max.height / _ratio), + constrainedSize.max.height + })); + } + + // Choose the size closest to the desired ratio. + const auto &bestSize = std::max_element(sizeOptions.begin(), sizeOptions.end(), [&](const CGSize &a, const CGSize &b){ + return fabs((a.height / a.width) - _ratio) > fabs((b.height / b.width) - _ratio); + }); + + // If there is no max size in *either* dimension, we can't apply the ratio, so just pass our size range through. + const ASSizeRange childRange = (bestSize == sizeOptions.end()) ? constrainedSize : ASSizeRangeMake(*bestSize, *bestSize); + const CGSize parentSize = (bestSize == sizeOptions.end()) ? kASLayoutNodeParentSizeUndefined : *bestSize; + ASLayout *childLayout = [_node layoutThatFits:childRange parentSize:parentSize]; + return [ASLayout newWithNode:self + size:childLayout.size + children:@[[ASLayoutChild newWithPosition:{0, 0} layout:childLayout]]]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASStackLayoutNode.h b/AsyncDisplayKit/Layout/ASStackLayoutNode.h new file mode 100644 index 0000000000..fb6e958436 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASStackLayoutNode.h @@ -0,0 +1,147 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +typedef NS_ENUM(NSUInteger, ASStackLayoutDirection) { + ASStackLayoutDirectionVertical, + ASStackLayoutDirectionHorizontal, +}; + +/** If no children are flexible, how should this node justify its children in the available space? */ +typedef NS_ENUM(NSUInteger, ASStackLayoutJustifyContent) { + /** + On overflow, children overflow out of this node's bounds on the right/bottom side. + On underflow, children are left/top-aligned within this node's bounds. + */ + ASStackLayoutJustifyContentStart, + /** + On overflow, children are centered and overflow on both sides. + On underflow, children are centered within this node's bounds in the stacking direction. + */ + ASStackLayoutJustifyContentCenter, + /** + On overflow, children overflow out of this node's bounds on the left/top side. + On underflow, children are right/bottom-aligned within this node's bounds. + */ + ASStackLayoutJustifyContentEnd, +}; + +typedef NS_ENUM(NSUInteger, ASStackLayoutAlignItems) { + /** Align children to start of cross axis */ + ASStackLayoutAlignItemsStart, + /** Align children with end of cross axis */ + ASStackLayoutAlignItemsEnd, + /** Center children on cross axis */ + ASStackLayoutAlignItemsCenter, + /** Expand children to fill cross axis */ + ASStackLayoutAlignItemsStretch, +}; + +/** + Each child may override their parent stack's cross axis alignment. + @see ASStackLayoutNodeAlignItems + */ +typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) { + /** Inherit alignment value from containing stack. */ + ASStackLayoutAlignSelfAuto, + ASStackLayoutAlignSelfStart, + ASStackLayoutAlignSelfEnd, + ASStackLayoutAlignSelfCenter, + ASStackLayoutAlignSelfStretch, +}; + +typedef struct { + /** Specifies the direction children are stacked in. */ + ASStackLayoutDirection direction; + /** The amount of space between each child. */ + CGFloat spacing; + /** How children are aligned if there are no flexible children. */ + ASStackLayoutJustifyContent justifyContent; + /** Orientation of children along cross axis */ + ASStackLayoutAlignItems alignItems; +} ASStackLayoutNodeStyle; + +@class ASMutableStackLayoutNodeChild; + +@interface ASStackLayoutNodeChild : NSObject + +@property (nonatomic, readonly) ASLayoutNode *node; +/** Additional space to place before the node in the stacking direction. */ +@property (nonatomic, readonly) CGFloat spacingBefore; +/** Additional space to place after the node in the stacking direction. */ +@property (nonatomic, readonly) CGFloat spacingAfter; +/** If the sum of childrens' stack dimensions is less than the minimum size, should this node grow? */ +@property (nonatomic, readonly) BOOL flexGrow; +/** If the sum of childrens' stack dimensions is greater than the maximum size, should this node shrink? */ +@property (nonatomic, readonly) BOOL flexShrink; +/** Specifies the initial size in the stack dimension for the child. */ +@property (nonatomic, readonly) ASRelativeDimension flexBasis; +/** Orientation of the child along cross axis, overriding alignItems */ +@property (nonatomic, readonly) ASStackLayoutAlignSelf alignSelf; + ++(instancetype)newWithInitializer:(void(^)(ASMutableStackLayoutNodeChild *mutableChild))initializer; + +@end + + +/** A mutable stack layout node child intended for configuration. */ +@interface ASMutableStackLayoutNodeChild : ASStackLayoutNodeChild + +/** A read-write version of ASStackLayoutNodeChild node property */ +@property (nonatomic, readwrite) ASLayoutNode *node; +/** A read-write version of ASStackLayoutNodeChild spacingBefore property */ +@property (nonatomic, readwrite) CGFloat spacingBefore; +/** A read-write version of ASStackLayoutNodeChild spacingAfter property */ +@property (nonatomic, readwrite) CGFloat spacingAfter; +/** A read-write version of ASStackLayoutNodeChild flexGrow property */ +@property (nonatomic, readwrite) BOOL flexGrow; +/** A read-write version of ASStackLayoutNodeChild flexShrink property */ +@property (nonatomic, readwrite) BOOL flexShrink; +/** A read-write version of ASStackLayoutNodeChild flexBasis property */ +@property (nonatomic, readwrite) ASRelativeDimension flexBasis; +/** A read-write version of ASStackLayoutNodeChild alignSelf property */ +@property (nonatomic, readwrite) ASStackLayoutAlignSelf alignSelf; + +@end + + +/** + A simple layout node that stacks a list of children vertically or horizontally. + + - All children are initially laid out with the an infinite available size in the stacking direction. + - In the other direction, this node's constraint is passed. + - The children's sizes are summed in the stacking direction. + - If this sum is less than this node's minimum size in stacking direction, children with flexGrow are flexed. + - If it is greater than this node's maximum size in the stacking direction, children with flexShrink are flexed. + - If, even after flexing, the sum is still greater than this node's maximum size in the stacking direction, + justifyContent determines how children are laid out. + + For example: + - Suppose stacking direction is Vertical, min-width=100, max-width=300, min-height=200, max-height=500. + - All children are laid out with min-width=100, max-width=300, min-height=0, max-height=INFINITY. + - If the sum of the childrens' heights is less than 200, nodes with flexGrow are flexed larger. + - If the sum of the childrens' heights is greater than 500, nodes with flexShrink are flexed smaller. + Each node is shrunk by `((sum of heights) - 500)/(number of nodes)`. + - If the sum of the childrens' heights is greater than 500 even after flexShrink-able nodes are flexed, + justifyContent determines how children are laid out. + */ +@interface ASStackLayoutNode : ASLayoutNode + +/** + @param size A size, or {} for the default size. + @param style Specifies how children are laid out. + @param children Children to be positioned, each is of type ASStackLayoutNodeChild. + */ ++ (instancetype)newWithSize:(ASLayoutNodeSize)size + style:(ASStackLayoutNodeStyle)style + children:(NSArray *)children; + +@end diff --git a/AsyncDisplayKit/Layout/ASStackLayoutNode.mm b/AsyncDisplayKit/Layout/ASStackLayoutNode.mm new file mode 100644 index 0000000000..4be9667be8 --- /dev/null +++ b/AsyncDisplayKit/Layout/ASStackLayoutNode.mm @@ -0,0 +1,133 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASStackLayoutNode.h" + +#import +#import + +#import "ASBaseDefines.h" +#import "ASInternalHelpers.h" + +#import "ASLayoutNodeUtilities.h" +#import "ASLayoutNodeSubclass.h" +#import "ASStackLayoutNodeUtilities.h" +#import "ASStackPositionedLayout.h" +#import "ASStackUnpositionedLayout.h" + +@implementation ASMutableStackLayoutNodeChild +@synthesize node, spacingBefore, spacingAfter, flexGrow, flexShrink, flexBasis, alignSelf; +@end + +@implementation ASStackLayoutNodeChild + +- (instancetype)initWithNode:(ASLayoutNode *)node + spacingBefore:(CGFloat)spacingBefore + spacingAfter:(CGFloat)spacingAfter + flexGrow:(BOOL)flexGrow + flexShrink:(BOOL)flexShrink + flexBasis:(ASRelativeDimension)flexBasis + alignSelf:(ASStackLayoutAlignSelf)alignSelf +{ + if (node == nil) + return nil; + + if (self = [super init]) { + _node = node; + _spacingBefore = spacingBefore; + _spacingAfter = spacingAfter; + _flexGrow = flexGrow; + _flexShrink = flexShrink; + _flexBasis = flexBasis; + _alignSelf = alignSelf; + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + if ([self isKindOfClass:[ASMutableStackLayoutNodeChild class]]) { + return [[ASStackLayoutNodeChild alloc] initWithNode:self.node + spacingBefore:self.spacingBefore + spacingAfter:self.spacingAfter + flexGrow:self.flexGrow + flexShrink:self.flexShrink + flexBasis:self.flexBasis + alignSelf:self.alignSelf]; + } else { + return self; + } +} + +- (id)mutableCopyWithZone:(NSZone *)zone +{ + ASMutableStackLayoutNodeChild *mutableChild = [[ASMutableStackLayoutNodeChild alloc] init]; + mutableChild.node = self.node; + mutableChild.spacingBefore = self.spacingBefore; + mutableChild.spacingAfter = self.spacingAfter; + mutableChild.flexGrow = self.flexGrow; + mutableChild.flexShrink = self.flexShrink; + mutableChild.flexBasis = self.flexBasis; + mutableChild.alignSelf = self.alignSelf; + return mutableChild; +} + ++ (instancetype)newWithInitializer:(void (^)(ASMutableStackLayoutNodeChild *))initializer +{ + ASStackLayoutNodeChild *c = [super new]; + if (c && initializer) { + ASMutableStackLayoutNodeChild *mutableChild = [[ASMutableStackLayoutNodeChild alloc] init]; + initializer(mutableChild); + c = [mutableChild copy]; + } + return c; +} + +@end + + +@implementation ASStackLayoutNode +{ + ASStackLayoutNodeStyle _style; + std::vector _children; +} + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size style:(ASStackLayoutNodeStyle)style children:(NSArray *)children +{ + ASStackLayoutNode *n = [super newWithSize:size]; + if (n) { + n->_style = style; + n->_children = std::vector(); + for (ASStackLayoutNodeChild *child in children) { + if (child.node != nil) { + n->_children.push_back(child); + } + } + } + return n; +} + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size +{ + ASDISPLAYNODE_NOT_DESIGNATED_INITIALIZER(); +} + +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize +{ + const auto unpositionedLayout = ASStackUnpositionedLayout::compute(_children, _style, constrainedSize); + const auto positionedLayout = ASStackPositionedLayout::compute(unpositionedLayout, _style, constrainedSize); + const CGSize finalSize = directionSize(_style.direction, unpositionedLayout.stackDimensionSum, positionedLayout.crossSize); + NSArray *children = [NSArray arrayWithObjects:&positionedLayout.children[0] count:positionedLayout.children.size()]; + return [ASLayout newWithNode:self + size:ASSizeRangeClamp(constrainedSize, finalSize) + children:children]; +} + +@end diff --git a/AsyncDisplayKit/Layout/ASStaticLayoutNode.h b/AsyncDisplayKit/Layout/ASStaticLayoutNode.h new file mode 100644 index 0000000000..e851a98a7f --- /dev/null +++ b/AsyncDisplayKit/Layout/ASStaticLayoutNode.h @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +@interface ASStaticLayoutNodeChild : NSObject + +@property (nonatomic, readonly) CGPoint position; +@property (nonatomic, readonly) ASLayoutNode *node; + +/** + If specified, the node's size is restricted according to this size. Percentages are resolved relative to the + static layout node. + */ +@property (nonatomic, readonly) ASRelativeSizeRange size; + ++ (instancetype)newWithPosition:(CGPoint)position node:(ASLayoutNode *)node size:(ASRelativeSizeRange)size; + +/** + Convenience with default size is Auto in both dimensions, which sets the child's min size to zero + and max size to the maximum available space it can consume without overflowing the node's bounds. + */ ++ (instancetype)newWithPosition:(CGPoint)position node:(ASLayoutNode *)node; + +@end + +/* + A layout node that positions children at fixed positions. + + Computes a size that is the union of all childrens' frames. + */ +@interface ASStaticLayoutNode : ASLayoutNode + +/** + @param children Children to be positioned at fixed positions, each is of type ASStaticLayoutNodeChild. + */ ++ (instancetype)newWithSize:(ASLayoutNodeSize)size + children:(NSArray *)children; + +/** + Convenience that does not have a size. + + @param children Children to be positioned at fixed positions, each is of type ASStaticLayoutNodeChild. + */ ++ (instancetype)newWithChildren:(NSArray *)children; + +@end diff --git a/AsyncDisplayKit/Layout/ASStaticLayoutNode.mm b/AsyncDisplayKit/Layout/ASStaticLayoutNode.mm new file mode 100644 index 0000000000..e761e9631d --- /dev/null +++ b/AsyncDisplayKit/Layout/ASStaticLayoutNode.mm @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASStaticLayoutNode.h" + +#import "ASLayoutNodeUtilities.h" +#import "ASLayoutNodeSubclass.h" +#import "ASInternalHelpers.h" + +@implementation ASStaticLayoutNodeChild + ++ (instancetype)newWithPosition:(CGPoint)position node:(ASLayoutNode *)node size:(ASRelativeSizeRange)size +{ + ASStaticLayoutNodeChild *c = [super new]; + if (c) { + c->_position = position; + c->_node = node; + c->_size = size; + } + return c; +} + ++ (instancetype)newWithPosition:(CGPoint)position node:(ASLayoutNode *)node +{ + return [self newWithPosition:position node:node size:{}]; +} + +@end + +@implementation ASStaticLayoutNode +{ + NSArray *_children; +} + ++ (instancetype)newWithSize:(ASLayoutNodeSize)size + children:(NSArray *)children +{ + ASStaticLayoutNode *n = [super newWithSize:size]; + if (n) { + n->_children = children; + } + return n; +} + ++ (instancetype)newWithChildren:(NSArray *)children +{ + return [self newWithSize:{} children:children]; +} + +- (ASLayout *)computeLayoutThatFits:(ASSizeRange)constrainedSize +{ + CGSize size = { + isinf(constrainedSize.max.width) ? kASLayoutNodeParentDimensionUndefined : constrainedSize.max.width, + isinf(constrainedSize.max.height) ? kASLayoutNodeParentDimensionUndefined : constrainedSize.max.height + }; + + NSMutableArray *layoutChildren = [NSMutableArray arrayWithCapacity:_children.count]; + for (ASStaticLayoutNodeChild *child in _children) { + CGSize autoMaxSize = { + constrainedSize.max.width - child.position.x, + constrainedSize.max.height - child.position.y + }; + ASSizeRange childConstraint = ASRelativeSizeRangeResolveSizeRange(child.size, size, {{0,0}, autoMaxSize}); + ASLayoutChild *layoutChild = [ASLayoutChild newWithPosition:child.position + layout:[child.node layoutThatFits:childConstraint parentSize: size]]; + [layoutChildren addObject:layoutChild]; + } + + if (isnan(size.width)) { + size.width = constrainedSize.min.width; + for (ASLayoutChild *child in layoutChildren) { + size.width = MAX(size.width, child.position.x + child.layout.size.width); + } + } + + if (isnan(size.height)) { + size.height = constrainedSize.min.height; + for (ASLayoutChild *child in layoutChildren) { + size.height = MAX(size.height, child.position.y + child.layout.size.height); + } + } + + return [ASLayout newWithNode:self size:ASSizeRangeClamp(constrainedSize, size) children:layoutChildren]; +} + +@end diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.h b/AsyncDisplayKit/Private/ASInternalHelpers.h new file mode 100644 index 0000000000..9502d2e94c --- /dev/null +++ b/AsyncDisplayKit/Private/ASInternalHelpers.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import +#import "ASBaseDefines.h" + +@class ASLayoutChild; + +ASDISPLAYNODE_EXTERN_C_BEGIN + +BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector); + +CGFloat ASScreenScale(); + +CGFloat ASFloorPixelValue(CGFloat f); + +CGFloat ASCeilPixelValue(CGFloat f); + +CGFloat ASRoundPixelValue(CGFloat f); + +ASDISPLAYNODE_EXTERN_C_END \ No newline at end of file diff --git a/AsyncDisplayKit/Private/ASInternalHelpers.mm b/AsyncDisplayKit/Private/ASInternalHelpers.mm new file mode 100644 index 0000000000..d161523f2b --- /dev/null +++ b/AsyncDisplayKit/Private/ASInternalHelpers.mm @@ -0,0 +1,63 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASInternalHelpers.h" + +#import +#import + +#import "ASLayout.h" + +BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector) +{ + Method superclassMethod = class_getInstanceMethod(superclass, selector); + Method subclassMethod = class_getInstanceMethod(subclass, selector); + IMP superclassIMP = superclassMethod ? method_getImplementation(superclassMethod) : NULL; + IMP subclassIMP = subclassMethod ? method_getImplementation(subclassMethod) : NULL; + return (superclassIMP != subclassIMP); +} + +static void ASDispatchOnceOnMainThread(dispatch_once_t *predicate, dispatch_block_t block) +{ + if ([NSThread isMainThread]) { + dispatch_once(predicate, block); + } else { + if (DISPATCH_EXPECT(*predicate == 0L, NO)) { + dispatch_sync(dispatch_get_main_queue(), ^{ + dispatch_once(predicate, block); + }); + } + } +} + +CGFloat ASScreenScale() +{ + static CGFloat _scale; + static dispatch_once_t onceToken; + ASDispatchOnceOnMainThread(&onceToken, ^{ + _scale = [UIScreen mainScreen].scale; + }); + return _scale; +} + +CGFloat ASFloorPixelValue(CGFloat f) +{ + return floorf(f * ASScreenScale()) / ASScreenScale(); +} + +CGFloat ASCeilPixelValue(CGFloat f) +{ + return ceilf(f * ASScreenScale()) / ASScreenScale(); +} + +CGFloat ASRoundPixelValue(CGFloat f) +{ + return roundf(f * ASScreenScale()) / ASScreenScale(); +} diff --git a/AsyncDisplayKit/Private/ASLayoutNodeUtilities.h b/AsyncDisplayKit/Private/ASLayoutNodeUtilities.h new file mode 100644 index 0000000000..178b9655c0 --- /dev/null +++ b/AsyncDisplayKit/Private/ASLayoutNodeUtilities.h @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import +#import +#import +#import + +#import + +namespace AS { + // adopted from http://stackoverflow.com/questions/14945223/map-function-with-c11-constructs + // Takes an iterable, applies a function to every element, + // and returns a vector of the results + // + template + auto map(const T &iterable, Func &&func) -> std::vector()))> + { + // Some convenience type definitions + typedef decltype(func(std::declval())) value_type; + typedef std::vector result_type; + + // Prepares an output vector of the appropriate size + result_type res(iterable.size()); + + // Let std::transform apply `func` to all elements + // (use perfect forwarding for the function object) + std::transform( + begin(iterable), end(iterable), res.begin(), + std::forward(func) + ); + + return res; + } + + template + auto map(id collection, Func &&func) -> std::vector()))> + { + std::vector()))> to; + for (id obj in collection) { + to.push_back(func(obj)); + } + return to; + } + + template + auto filter(const T &iterable, Func &&func) -> std::vector + { + std::vector to; + for (auto obj : iterable) { + if (func(obj)) { + to.push_back(obj); + } + } + return to; + } +}; + +inline CGPoint operator+(const CGPoint &p1, const CGPoint &p2) +{ + return { p1.x + p2.x, p1.y + p2.y }; +} + +inline CGPoint operator-(const CGPoint &p1, const CGPoint &p2) +{ + return { p1.x - p2.x, p1.y - p2.y }; +} + +inline CGSize operator+(const CGSize &s1, const CGSize &s2) +{ + return { s1.width + s2.width, s1.height + s2.height }; +} + +inline CGSize operator-(const CGSize &s1, const CGSize &s2) +{ + return { s1.width - s2.width, s1.height - s2.height }; +} + +inline UIEdgeInsets operator+(const UIEdgeInsets &e1, const UIEdgeInsets &e2) +{ + return { e1.top + e2.top, e1.left + e2.left, e1.bottom + e2.bottom, e1.right + e2.right }; +} + +inline UIEdgeInsets operator-(const UIEdgeInsets &e1, const UIEdgeInsets &e2) +{ + return { e1.top - e2.top, e1.left - e2.left, e1.bottom - e2.bottom, e1.right - e2.right }; +} + +inline UIEdgeInsets operator*(const UIEdgeInsets &e1, const UIEdgeInsets &e2) +{ + return { e1.top * e2.top, e1.left * e2.left, e1.bottom * e2.bottom, e1.right * e2.right }; +} + +inline UIEdgeInsets operator-(const UIEdgeInsets &e) +{ + return { -e.top, -e.left, -e.bottom, -e.right }; +} + diff --git a/AsyncDisplayKit/Private/ASStackLayoutNodeUtilities.h b/AsyncDisplayKit/Private/ASStackLayoutNodeUtilities.h new file mode 100644 index 0000000000..32ce92f3ed --- /dev/null +++ b/AsyncDisplayKit/Private/ASStackLayoutNodeUtilities.h @@ -0,0 +1,62 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASStackLayoutNode.h" + +inline CGFloat stackDimension(const ASStackLayoutDirection direction, const CGSize size) +{ + return (direction == ASStackLayoutDirectionVertical) ? size.height : size.width; +} + +inline CGFloat crossDimension(const ASStackLayoutDirection direction, const CGSize size) +{ + return (direction == ASStackLayoutDirectionVertical) ? size.width : size.height; +} + +inline BOOL compareCrossDimension(const ASStackLayoutDirection direction, const CGSize a, const CGSize b) +{ + return crossDimension(direction, a) < crossDimension(direction, b); +} + +inline CGPoint directionPoint(const ASStackLayoutDirection direction, const CGFloat stack, const CGFloat cross) +{ + return (direction == ASStackLayoutDirectionVertical) ? CGPointMake(cross, stack) : CGPointMake(stack, cross); +} + +inline CGSize directionSize(const ASStackLayoutDirection direction, const CGFloat stack, const CGFloat cross) +{ + return (direction == ASStackLayoutDirectionVertical) ? CGSizeMake(cross, stack) : CGSizeMake(stack, cross); +} + +inline ASSizeRange directionSizeRange(const ASStackLayoutDirection direction, + const CGFloat stackMin, + const CGFloat stackMax, + const CGFloat crossMin, + const CGFloat crossMax) +{ + return {directionSize(direction, stackMin, crossMin), directionSize(direction, stackMax, crossMax)}; +} + +inline ASStackLayoutAlignItems alignment(ASStackLayoutAlignSelf childAlignment, ASStackLayoutAlignItems stackAlignment) +{ + switch (childAlignment) { + case ASStackLayoutAlignSelfCenter: + return ASStackLayoutAlignItemsCenter; + case ASStackLayoutAlignSelfEnd: + return ASStackLayoutAlignItemsEnd; + case ASStackLayoutAlignSelfStart: + return ASStackLayoutAlignItemsStart; + case ASStackLayoutAlignSelfStretch: + return ASStackLayoutAlignItemsStretch; + case ASStackLayoutAlignSelfAuto: + default: + return stackAlignment; + } +} diff --git a/AsyncDisplayKit/Private/ASStackPositionedLayout.h b/AsyncDisplayKit/Private/ASStackPositionedLayout.h new file mode 100644 index 0000000000..d04a175c84 --- /dev/null +++ b/AsyncDisplayKit/Private/ASStackPositionedLayout.h @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASLayout.h" +#import "ASDimension.h" +#import "ASStackLayoutNode.h" +#import "ASStackUnpositionedLayout.h" + +/** Represents a set of laid out and positioned stack layout children. */ +struct ASStackPositionedLayout { + const std::vector children; + const CGFloat crossSize; + + /** Given an unpositioned layout, computes the positions each child should be placed at. */ + static ASStackPositionedLayout compute(const ASStackUnpositionedLayout &unpositionedLayout, + const ASStackLayoutNodeStyle &style, + const ASSizeRange &constrainedSize); +}; diff --git a/AsyncDisplayKit/Private/ASStackPositionedLayout.mm b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm new file mode 100644 index 0000000000..2e26f1d69a --- /dev/null +++ b/AsyncDisplayKit/Private/ASStackPositionedLayout.mm @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASStackPositionedLayout.h" + +#import "ASInternalHelpers.h" +#import "ASLayoutNodeUtilities.h" +#import "ASStackLayoutNodeUtilities.h" + +static CGFloat crossOffset(const ASStackLayoutNodeStyle &style, + const ASStackUnpositionedItem &l, + const CGFloat crossSize) +{ + switch (alignment(l.child.alignSelf, style.alignItems)) { + case ASStackLayoutAlignItemsEnd: + return crossSize - crossDimension(style.direction, l.layout.size); + case ASStackLayoutAlignItemsCenter: + return ASFloorPixelValue((crossSize - crossDimension(style.direction, l.layout.size)) / 2); + case ASStackLayoutAlignItemsStart: + case ASStackLayoutAlignItemsStretch: + return 0; + } +} + +static ASStackPositionedLayout stackedLayout(const ASStackLayoutNodeStyle &style, + const CGFloat offset, + const ASStackUnpositionedLayout &unpositionedLayout, + const ASSizeRange &constrainedSize) +{ + // The cross dimension is the max of the childrens' cross dimensions (clamped to our constraint below). + const auto it = std::max_element(unpositionedLayout.items.begin(), unpositionedLayout.items.end(), + [&](const ASStackUnpositionedItem &a, const ASStackUnpositionedItem &b){ + return compareCrossDimension(style.direction, a.layout.size, b.layout.size); + }); + const auto largestChildCrossSize = it == unpositionedLayout.items.end() ? 0 : crossDimension(style.direction, it->layout.size); + const auto minCrossSize = crossDimension(style.direction, constrainedSize.min); + const auto maxCrossSize = crossDimension(style.direction, constrainedSize.max); + const CGFloat crossSize = MIN(MAX(minCrossSize, largestChildCrossSize), maxCrossSize); + + CGPoint p = directionPoint(style.direction, offset, 0); + BOOL first = YES; + auto stackedChildren = AS::map(unpositionedLayout.items, [&](const ASStackUnpositionedItem &l) -> ASLayoutChild *{ + p = p + directionPoint(style.direction, l.child.spacingBefore, 0); + if (!first) { + p = p + directionPoint(style.direction, style.spacing, 0); + } + first = NO; + ASLayoutChild *c = [ASLayoutChild newWithPosition:p + directionPoint(style.direction, 0, crossOffset(style, l, crossSize)) + layout:l.layout]; + p = p + directionPoint(style.direction, stackDimension(style.direction, l.layout.size) + l.child.spacingAfter, 0); + return c; + }); + return {stackedChildren, crossSize}; +} + +ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnpositionedLayout &unpositionedLayout, + const ASStackLayoutNodeStyle &style, + const ASSizeRange &constrainedSize) +{ + switch (style.justifyContent) { + case ASStackLayoutJustifyContentStart: + return stackedLayout(style, 0, unpositionedLayout, constrainedSize); + case ASStackLayoutJustifyContentCenter: + return stackedLayout(style, floorf(unpositionedLayout.violation / 2), unpositionedLayout, constrainedSize); + case ASStackLayoutJustifyContentEnd: + return stackedLayout(style, unpositionedLayout.violation, unpositionedLayout, constrainedSize); + } +} diff --git a/AsyncDisplayKit/Private/ASStackUnpositionedLayout.h b/AsyncDisplayKit/Private/ASStackUnpositionedLayout.h new file mode 100644 index 0000000000..7a308d8073 --- /dev/null +++ b/AsyncDisplayKit/Private/ASStackUnpositionedLayout.h @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +#import "ASLayout.h" +#import "ASStackLayoutNode.h" + +struct ASStackUnpositionedItem { + /** The original source child. */ + ASStackLayoutNodeChild *child; + /** The proposed layout. */ + ASLayout *layout; +}; + +/** Represents a set of stack layout children that have their final layout computed, but are not yet positioned. */ +struct ASStackUnpositionedLayout { + /** A set of proposed child layouts, not yet positioned. */ + const std::vector items; + /** The total size of the children in the stack dimension, including all spacing. */ + const CGFloat stackDimensionSum; + /** The amount by which stackDimensionSum violates constraints. If positive, less than min; negative, greater than max. */ + const CGFloat violation; + + /** Given a set of children, computes the unpositioned layouts for those children. */ + static ASStackUnpositionedLayout compute(const std::vector &children, + const ASStackLayoutNodeStyle &style, + const ASSizeRange &sizeRange); +}; diff --git a/AsyncDisplayKit/Private/ASStackUnpositionedLayout.mm b/AsyncDisplayKit/Private/ASStackUnpositionedLayout.mm new file mode 100644 index 0000000000..e05e103459 --- /dev/null +++ b/AsyncDisplayKit/Private/ASStackUnpositionedLayout.mm @@ -0,0 +1,351 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASStackUnpositionedLayout.h" + +#import + +#import "ASLayoutNodeUtilities.h" +#import "ASLayoutNodeSubclass.h" +#import "ASStackLayoutNodeUtilities.h" + +/** + Sizes the child given the parameters specified, and returns the computed layout. + + @param size the size of the stack layout node. May be undefined in either or both directions. + */ +static ASLayout *crossChildLayout(const ASStackLayoutNodeChild *child, + const ASStackLayoutNodeStyle style, + const CGFloat stackMin, + const CGFloat stackMax, + const CGFloat crossMin, + const CGFloat crossMax, + const CGSize size) +{ + const ASStackLayoutAlignItems alignItems = alignment(child.alignSelf, style.alignItems); + // stretched children will have a cross dimension of at least crossMin + const CGFloat childCrossMin = alignItems == ASStackLayoutAlignItemsStretch ? crossMin : 0; + const ASSizeRange childSizeRange = directionSizeRange(style.direction, stackMin, stackMax, childCrossMin, crossMax); + return [child.node layoutThatFits:childSizeRange parentSize:size]; +} + +/** + Stretches children to lay out along the cross axis according to the alignment stretch settings of the children + (child.alignSelf), and the stack layout's alignment settings (style.alignItems). This does not do the actual alignment + of the items once stretched though; ASStackPositionedLayout will do centering etc. + + Finds the maximum cross dimension among child layouts. If that dimension exceeds the minimum cross layout size then + we must stretch any children whose alignItems specify ASStackLayoutAlignItemsStretch. + + The diagram below shows 3 children in a horizontal stack. The second child is larger than the minCrossDimension, so + its height is used as the childCrossMax. Any children that are stretchable (which may be all children if + style.alignItems specifies stretch) like the first child must be stretched to match that maximum. All children must be + at least minCrossDimension in cross dimension size, which is shown by the sizing of the third child. + + Stack Dimension + +---------------------> + + +-+-------------+-+-------------+--+---------------+ + + + + | | child. | | | | | | | | + | | alignSelf | | | | | | | | + Cross | | = stretch | | | +-------+-------+ | | | + Dimension | +-----+-------+ | | | | | | | | + | | | | | | | | | | + | | | | | v | | | | + v +-+- - - - - - -+-+ - - - - - - +--+- - - - - - - -+ | | + minCrossDimension + | | | | | + | v | | | | | + +- - - - - - -+ +-------------+ | + childCrossMax + | + +--------------------------------------------------+ + crossMax + + @param layouts pre-computed child layouts; modified in-place as needed + @param style the layout style of the overall stack layout + @param size the size of the stack layout node. May be undefined in either or both directions. + */ +static void stretchChildrenAlongCrossDimension(std::vector &layouts, + const ASStackLayoutNodeStyle &style, + const CGSize size) +{ + // Find the maximum cross dimension size among child layouts + const auto it = std::max_element(layouts.begin(), layouts.end(), + [&](const ASStackUnpositionedItem &a, const ASStackUnpositionedItem &b) { + return compareCrossDimension(style.direction, a.layout.size, b.layout.size); + }); + + const CGFloat childCrossMax = it == layouts.end() ? 0 : crossDimension(style.direction, it->layout.size); + for (auto &l : layouts) { + const ASStackLayoutAlignItems alignItems = alignment(l.child.alignSelf, style.alignItems); + + const CGFloat cross = crossDimension(style.direction, l.layout.size); + const CGFloat stack = stackDimension(style.direction, l.layout.size); + + // restretch all stretchable children along the cross axis using the new min. set their max size to childCrossMax, + // not crossMax, so that if any of them would choose a larger size just because the min size increased (weird!) + // they are forced to choose the same width as all the other nodes. + if (alignItems == ASStackLayoutAlignItemsStretch && fabs(cross - childCrossMax) > 0.01) { + l.layout = crossChildLayout(l.child, style, stack, stack, childCrossMax, childCrossMax, size); + } + } +} + +/** + Computes the consumed stack dimension length for the given vector of children and stacking style. + + stackDimensionSum + <-----------------------> + +-----+ +-------+ +---+ + | | | | | | + | | | | | | + +-----+ | | +---+ + +-------+ + + @param children unpositioned layouts for the child nodes of the stack node + @param style the layout style of the overall stack layout + */ +static CGFloat computeStackDimensionSum(const std::vector &children, + const ASStackLayoutNodeStyle &style) +{ + // Sum up the childrens' spacing + const CGFloat childSpacingSum = std::accumulate(children.begin(), children.end(), + // Start from default spacing between each child: + children.empty() ? 0 : style.spacing * (children.size() - 1), + [&](CGFloat x, const ASStackUnpositionedItem &l) { + return x + l.child.spacingBefore + l.child.spacingAfter; + }); + + // Sum up the childrens' dimensions (including spacing) in the stack direction. + const CGFloat childStackDimensionSum = std::accumulate(children.begin(), children.end(), childSpacingSum, + [&](CGFloat x, const ASStackUnpositionedItem &l) { + return x + stackDimension(style.direction, l.layout.size); + }); + return childStackDimensionSum; +} + +/** + Computes the violation by comparing a stack dimension sum with the overall allowable size range for the stack. + + Violation is the distance you would have to add to the unbounded stack-direction length of the stack node's + children in order to bring the stack within its allowed sizeRange. The diagram below shows 3 horizontal stacks with + the different types of violation. + + sizeRange + |------------| + +------+ +-------+ +-------+ +---------+ + | | | | | | | | | | + | | | | | | | | (zero violation) + | | | | | | | | | | + +------+ +-------+ +-------+ +---------+ + | | + +------+ +-------+ +-------+ + | | | | | | | | + | | | | | |<--> (positive violation) + | | | | | | | | + +------+ +-------+ +-------+ + | |<------> (negative violation) + +------+ +-------+ +-------+ +---------+ +-----------+ + | | | | | | | | | | | | + | | | | | | | | | | + | | | | | | | | | | | | + +------+ +-------+ +-------+ +---------+ +-----------+ + + @param stackDimensionSum the consumed length of the children in the stack along the stack dimension + @param style layout style to be applied to all children + @param sizeRange the range of allowable sizes for the stack layout node + */ +static CGFloat computeViolation(const CGFloat stackDimensionSum, + const ASStackLayoutNodeStyle &style, + const ASSizeRange &sizeRange) +{ + const CGFloat minStackDimension = stackDimension(style.direction, sizeRange.min); + const CGFloat maxStackDimension = stackDimension(style.direction, sizeRange.max); + if (stackDimensionSum < minStackDimension) { + return minStackDimension - stackDimensionSum; + } else if (stackDimensionSum > maxStackDimension) { + return maxStackDimension - stackDimensionSum; + } + return 0; +} + +/** The threshold that determines if a violation has actually occurred. */ +static const CGFloat kViolationEpsilon = 0.01; + +/** + Returns a lambda that determines if the given unpositioned item's child is flexible in the direction of the violation. + + @param violation the amount that the stack layout violates its size range. See header for sign interpretation. + */ +static std::function isFlexibleInViolationDirection(const CGFloat violation) +{ + if (fabs(violation) < kViolationEpsilon) { + return [](const ASStackUnpositionedItem &l) { return NO; }; + } else if (violation > 0) { + return [](const ASStackUnpositionedItem &l) { return l.child.flexGrow; }; + } else { + return [](const ASStackUnpositionedItem &l) { return l.child.flexShrink; }; + } +} + +ASDISPLAYNODE_INLINE BOOL isFlexibleInBothDirections(ASStackLayoutNodeChild *child) +{ + return child.flexGrow && child.flexShrink; +} + +/** + If we have a single flexible (both shrinkable and growable) child, and our allowed size range is set to a specific + number then we may avoid the first "intrinsic" size calculation. + */ +ASDISPLAYNODE_INLINE BOOL useOptimizedFlexing(const std::vector &children, + const ASStackLayoutNodeStyle &style, + const ASSizeRange &sizeRange) +{ + const NSUInteger flexibleChildren = std::count_if(children.begin(), children.end(), isFlexibleInBothDirections); + return ((flexibleChildren == 1) + && (stackDimension(style.direction, sizeRange.min) == + stackDimension(style.direction, sizeRange.max))); +} + +/** + The flexible children may have been left not laid out in the initial layout pass, so we may have to go through and size + these children at zero size so that the children layouts are at least present. + */ +static void layoutFlexibleChildrenAtZeroSize(std::vector &items, + const ASStackLayoutNodeStyle &style, + const ASSizeRange &sizeRange, + const CGSize size) +{ + for (ASStackUnpositionedItem &item : items) { + if (isFlexibleInBothDirections(item.child)) { + item.layout = crossChildLayout(item.child, + style, + 0, + 0, + crossDimension(style.direction, sizeRange.min), + crossDimension(style.direction, sizeRange.max), + size); + } + } +} + +/** + Flexes children in the stack axis to resolve a min or max stack size violation. First, determines which children are + flexible (see computeViolation and isFlexibleInViolationDirection). Then computes how much to flex each flexible child + and performs re-layout. Note that there may still be a non-zero violation even after flexing. + + The actual CSS flexbox spec describes an iterative looping algorithm here, which may be adopted in t5837937: + http://www.w3.org/TR/css3-flexbox/#resolve-flexible-lengths + + @param items Reference to unpositioned items from the original, unconstrained layout pass; modified in-place + @param style layout style to be applied to all children + @param sizeRange the range of allowable sizes for the stack layout node + @param size Size of the stack layout node. May be undefined in either or both directions. + */ +static void flexChildrenAlongStackDimension(std::vector &items, + const ASStackLayoutNodeStyle &style, + const ASSizeRange &sizeRange, + const CGSize size, + const BOOL useOptimizedFlexing) +{ + const CGFloat stackDimensionSum = computeStackDimensionSum(items, style); + const CGFloat violation = computeViolation(stackDimensionSum, style, sizeRange); + + // We count the number of children which are flexible in the direction of the violation + std::function isFlex = isFlexibleInViolationDirection(violation); + const NSUInteger flexibleChildren = std::count_if(items.begin(), items.end(), isFlex); + if (flexibleChildren == 0) { + // If optimized flexing was used then we have to clean up the unsized children, and lay them out at zero size + if (useOptimizedFlexing) { + layoutFlexibleChildrenAtZeroSize(items, style, sizeRange, size); + } + return; + } + + // Each flexible child along the direction of the violation is expanded or contracted equally + const CGFloat violationPerFlexChild = floorf(violation / flexibleChildren); + // If the floor operation above left a remainder we may have a remainder after deducting the adjustments from all the + // contributions of the flexible children. + const CGFloat violationRemainder = violation - (violationPerFlexChild * flexibleChildren); + + BOOL isFirstFlex = YES; + for (ASStackUnpositionedItem &item : items) { + if (isFlex(item)) { + const CGFloat originalStackSize = stackDimension(style.direction, item.layout.size); + // The first flexible child is given the additional violation remainder + const CGFloat flexedStackSize = originalStackSize + violationPerFlexChild + (isFirstFlex ? violationRemainder : 0); + item.layout = crossChildLayout(item.child, + style, + MAX(flexedStackSize, 0), + MAX(flexedStackSize, 0), + crossDimension(style.direction, sizeRange.min), + crossDimension(style.direction, sizeRange.max), + size); + isFirstFlex = NO; + } + } +} + +/** + Performs the first unconstrained layout of the children, generating the unpositioned items that are then flexed and + stretched. + */ +static std::vector layoutChildrenAlongUnconstrainedStackDimension(const std::vector &children, + const ASStackLayoutNodeStyle &style, + const ASSizeRange &sizeRange, + const CGSize size, + const BOOL useOptimizedFlexing) +{ + const CGFloat minCrossDimension = crossDimension(style.direction, sizeRange.min); + const CGFloat maxCrossDimension = crossDimension(style.direction, sizeRange.max); + return AS::map(children, [&](ASStackLayoutNodeChild *child) -> ASStackUnpositionedItem { + if (useOptimizedFlexing && isFlexibleInBothDirections(child)) { + return { child, [ASLayout newWithNode:child.node size:{0, 0}] }; + } else { + return { + child, + crossChildLayout(child, + style, + ASRelativeDimensionResolve(child.flexBasis, 0, stackDimension(style.direction, size)), + ASRelativeDimensionResolve(child.flexBasis, INFINITY, stackDimension(style.direction, size)), + minCrossDimension, + maxCrossDimension, + size) + }; + } + }); +} + +ASStackUnpositionedLayout ASStackUnpositionedLayout::compute(const std::vector &children, + const ASStackLayoutNodeStyle &style, + const ASSizeRange &sizeRange) +{ + // If we have a fixed size in either dimension, pass it to children so they can resolve percentages against it. + // Otherwise, we pass kASLayoutNodeParentDimensionUndefined since it will depend on the content. + const CGSize size = { + (sizeRange.min.width == sizeRange.max.width) ? sizeRange.min.width : kASLayoutNodeParentDimensionUndefined, + (sizeRange.min.height == sizeRange.max.height) ? sizeRange.min.height : kASLayoutNodeParentDimensionUndefined, + }; + + // We may be able to avoid some redundant layout passes + const BOOL optimizedFlexing = useOptimizedFlexing(children, style, sizeRange); + + // We do a first pass of all the children, generating an unpositioned layout for each with an unbounded range along + // the stack dimension. This allows us to compute the "intrinsic" size of each child and find the available violation + // which determines whether we must grow or shrink the flexible children. + std::vector items = layoutChildrenAlongUnconstrainedStackDimension(children, + style, + sizeRange, + size, + optimizedFlexing); + + flexChildrenAlongStackDimension(items, style, sizeRange, size, optimizedFlexing); + stretchChildrenAlongCrossDimension(items, style, size); + + const CGFloat stackDimensionSum = computeStackDimensionSum(items, style); + return {items, stackDimensionSum, computeViolation(stackDimensionSum, style, sizeRange)}; +}