diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index fcf1c500b0..bea602effe 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -1996,6 +1996,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + GCC_PREPROCESSOR_DEFINITIONS = ""; INFOPLIST_FILE = AsyncDisplayKitTestHost/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; @@ -2106,7 +2107,6 @@ "DEBUG=1", "$(inherited)", "COCOAPODS=1", - "FB_REFERENCE_IMAGE_DIR=\"\\\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\\\"\"", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; @@ -2132,7 +2132,6 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", - "FB_REFERENCE_IMAGE_DIR=\"\\\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\\\"\"", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; @@ -2193,6 +2192,7 @@ GCC_NO_COMMON_BLOCKS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "AsyncDisplayKit/AsyncDisplayKit-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ""; INFOPLIST_FILE = "$(SRCROOT)/AsyncDisplayKit/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; @@ -2260,7 +2260,6 @@ GCC_PREPROCESSOR_DEFINITIONS = ( "$(inherited)", "COCOAPODS=1", - "FB_REFERENCE_IMAGE_DIR=\"\\\"$(SOURCE_ROOT)/$(PROJECT_NAME)Tests/ReferenceImages\\\"\"", ); GCC_TREAT_WARNINGS_AS_ERRORS = YES; GCC_WARN_INITIALIZER_NOT_FULLY_BRACKETED = YES; @@ -2281,6 +2280,7 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_INSTRUMENT_PROGRAM_FLOW_ARCS = NO; + GCC_PREPROCESSOR_DEFINITIONS = ""; INFOPLIST_FILE = AsyncDisplayKitTestHost/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; MTL_ENABLE_DEBUG_INFO = NO; @@ -2307,6 +2307,7 @@ GCC_NO_COMMON_BLOCKS = YES; GCC_PRECOMPILE_PREFIX_HEADER = YES; GCC_PREFIX_HEADER = "AsyncDisplayKit/AsyncDisplayKit-Prefix.pch"; + GCC_PREPROCESSOR_DEFINITIONS = ""; INFOPLIST_FILE = "$(SRCROOT)/AsyncDisplayKit/Info.plist"; INSTALL_PATH = "$(LOCAL_LIBRARY_DIR)/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; diff --git a/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme b/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme index 5421b65a77..f37a5b1a83 100644 --- a/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme +++ b/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme @@ -75,6 +75,13 @@ ReferencedContainer = "container:AsyncDisplayKit.xcodeproj"> + + + + diff --git a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h index 772c1022e4..79281d73dc 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutDefines.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutDefines.h @@ -86,6 +86,22 @@ typedef NS_ENUM(NSUInteger, ASStackLayoutAlignSelf) { ASStackLayoutAlignSelfStretch, }; +// TODO documentation +typedef NS_ENUM(NSUInteger, ASStackLayoutFlexWrap) { + ASStackLayoutFlexWrapNoWrap, + ASStackLayoutFlexWrapWrap, +}; + +// TODO documentation +typedef NS_ENUM(NSUInteger, ASStackLayoutAlignContent) { + ASStackLayoutAlignContentStart, + ASStackLayoutAlignContentCenter, + ASStackLayoutAlignContentEnd, + ASStackLayoutAlignContentSpaceBetween, + ASStackLayoutAlignContentSpaceAround, + ASStackLayoutAlignContentStretch, +}; + /** Orientation of children along horizontal axis */ typedef NS_ENUM(NSUInteger, ASHorizontalAlignment) { /** No alignment specified. Default value */ diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h index d805f1e9dc..d218db7b42 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.h +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.h @@ -59,6 +59,10 @@ NS_ASSUME_NONNULL_BEGIN @property (nonatomic, assign) ASStackLayoutJustifyContent justifyContent; /** Orientation of children along cross axis. Defaults to ASStackLayoutAlignItemsStretch */ @property (nonatomic, assign) ASStackLayoutAlignItems alignItems; +//TODO documentation. Defaults to ASStackLayoutFlexWrapNoWrap +@property (nonatomic, assign) ASStackLayoutFlexWrap flexWrap; +//TODO documentation. Defaults to ASStackLayoutAlignContentStart +@property (nonatomic, assign) ASStackLayoutAlignContent alignContent; - (instancetype)init; @@ -69,7 +73,27 @@ NS_ASSUME_NONNULL_BEGIN @param alignItems Orientation of the children along the cross axis @param children ASLayoutElement children to be positioned. */ -+ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray> *)children AS_WARN_UNUSED_RESULT; ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + spacing:(CGFloat)spacing + justifyContent:(ASStackLayoutJustifyContent)justifyContent + alignItems:(ASStackLayoutAlignItems)alignItems + children:(NSArray> *)children AS_WARN_UNUSED_RESULT; + +/** + @param direction The direction of the stack view (horizontal or vertical) + @param spacing The spacing between the children + @param justifyContent If no children are flexible, this describes how to fill any extra space + @param alignItems Orientation of the children along the cross axis + @param children ASLayoutElement children to be positioned. + TODO documentation flex wrap and align content + */ ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction + spacing:(CGFloat)spacing + justifyContent:(ASStackLayoutJustifyContent)justifyContent + alignItems:(ASStackLayoutAlignItems)alignItems + flexWrap:(ASStackLayoutFlexWrap)flexWrap + alignContent:(ASStackLayoutAlignContent)alignContent + children:(NSArray> *)children AS_WARN_UNUSED_RESULT; /** * @return A stack layout spec with direction of ASStackLayoutDirectionVertical diff --git a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm index 1089592f91..1a78a5428c 100644 --- a/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm +++ b/AsyncDisplayKit/Layout/ASStackLayoutSpec.mm @@ -25,12 +25,17 @@ - (instancetype)init { - return [self initWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch children:nil]; + return [self initWithDirection:ASStackLayoutDirectionHorizontal spacing:0.0 justifyContent:ASStackLayoutJustifyContentStart alignItems:ASStackLayoutAlignItemsStretch flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart children:nil]; } + (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children { - return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems children:children]; + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:ASStackLayoutFlexWrapNoWrap alignContent:ASStackLayoutAlignContentStart children:children]; +} + ++ (instancetype)stackLayoutSpecWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray> *)children +{ + return [[self alloc] initWithDirection:direction spacing:spacing justifyContent:justifyContent alignItems:alignItems flexWrap:flexWrap alignContent:alignContent children:children]; } + (instancetype)verticalStackLayoutSpec @@ -47,7 +52,7 @@ return stackLayoutSpec; } -- (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems children:(NSArray *)children +- (instancetype)initWithDirection:(ASStackLayoutDirection)direction spacing:(CGFloat)spacing justifyContent:(ASStackLayoutJustifyContent)justifyContent alignItems:(ASStackLayoutAlignItems)alignItems flexWrap:(ASStackLayoutFlexWrap)flexWrap alignContent:(ASStackLayoutAlignContent)alignContent children:(NSArray *)children { if (!(self = [super init])) { return nil; @@ -58,6 +63,8 @@ _verticalAlignment = ASVerticalAlignmentNone; _alignItems = alignItems; _justifyContent = justifyContent; + _flexWrap = flexWrap; + _alignContent = alignContent; [self setChildren:children]; return self; @@ -127,7 +134,7 @@ return {child, style, style.size}; }); - const ASStackLayoutSpecStyle style = {.direction = _direction, .spacing = _spacing, .justifyContent = _justifyContent, .alignItems = _alignItems}; + const ASStackLayoutSpecStyle style = {.direction = _direction, .spacing = _spacing, .justifyContent = _justifyContent, .alignItems = _alignItems, .flexWrap = _flexWrap, .alignContent = _alignContent}; const auto unpositionedLayout = ASStackUnpositionedLayout::compute(stackChildren, style, constrainedSize); const auto positionedLayout = ASStackPositionedLayout::compute(unpositionedLayout, style, constrainedSize); @@ -137,14 +144,12 @@ self.style.descender = stackChildren.back().style.descender; } - const CGSize finalSize = directionSize(style.direction, unpositionedLayout.stackDimensionSum, unpositionedLayout.crossSize); - NSMutableArray *sublayouts = [NSMutableArray array]; - for (const auto &l : positionedLayout.items) { - [sublayouts addObject:l.layout]; + for (const auto &item : positionedLayout.items) { + [sublayouts addObject:item.layout]; } - - return [ASLayout layoutWithLayoutElement:self size:ASSizeRangeClamp(constrainedSize, finalSize) sublayouts:sublayouts]; + + return [ASLayout layoutWithLayoutElement:self size:positionedLayout.size sublayouts:sublayouts]; } - (void)resolveHorizontalAlignment diff --git a/AsyncDisplayKit/Private/Layout/ASStackLayoutSpecUtilities.h b/AsyncDisplayKit/Private/Layout/ASStackLayoutSpecUtilities.h index 6bee0a6866..fbcafdff41 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackLayoutSpecUtilities.h +++ b/AsyncDisplayKit/Private/Layout/ASStackLayoutSpecUtilities.h @@ -15,6 +15,8 @@ typedef struct { CGFloat spacing; ASStackLayoutJustifyContent justifyContent; ASStackLayoutAlignItems alignItems; + ASStackLayoutFlexWrap flexWrap; + ASStackLayoutAlignContent alignContent; } ASStackLayoutSpecStyle; inline CGFloat stackDimension(const ASStackLayoutDirection direction, const CGSize size) @@ -42,6 +44,10 @@ inline CGSize directionSize(const ASStackLayoutDirection direction, const CGFloa return (direction == ASStackLayoutDirectionVertical) ? CGSizeMake(cross, stack) : CGSizeMake(stack, cross); } +inline void setStackValueToPoint(const ASStackLayoutDirection direction, const CGFloat stack, CGPoint &point) { + (direction == ASStackLayoutDirectionVertical) ? (point.y = stack) : (point.x = stack); +} + inline ASSizeRange directionSizeRange(const ASStackLayoutDirection direction, const CGFloat stackMin, const CGFloat stackMax, diff --git a/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.h b/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.h index d4c7b3628a..dd0b697555 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.h +++ b/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.h @@ -15,7 +15,9 @@ /** Represents a set of laid out and positioned stack layout children. */ struct ASStackPositionedLayout { const std::vector items; - + /** Final size of the stack */ + const CGSize size; + /** Given an unpositioned layout, computes the positions each child should be placed at. */ static ASStackPositionedLayout compute(const ASStackUnpositionedLayout &unpositionedLayout, const ASStackLayoutSpecStyle &style, diff --git a/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.mm b/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.mm index 870bfec09b..c39bdcdab6 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.mm +++ b/AsyncDisplayKit/Private/Layout/ASStackPositionedLayout.mm @@ -11,24 +11,26 @@ #import #import +#import +#import #import #import #import -static CGFloat crossOffset(const ASStackLayoutSpecStyle &style, - const ASStackLayoutSpecItem &l, - const CGFloat crossSize, - const CGFloat baseline) +static CGFloat crossOffsetForItem(const ASStackLayoutSpecItem &item, + const ASStackLayoutSpecStyle &style, + const CGFloat crossSize, + const CGFloat baseline) { - switch (alignment(l.child.style.alignSelf, style.alignItems)) { + switch (alignment(item.child.style.alignSelf, style.alignItems)) { case ASStackLayoutAlignItemsEnd: - return crossSize - crossDimension(style.direction, l.layout.size); + return crossSize - crossDimension(style.direction, item.layout.size); case ASStackLayoutAlignItemsCenter: - return ASFloorPixelValue((crossSize - crossDimension(style.direction, l.layout.size)) / 2); + return ASFloorPixelValue((crossSize - crossDimension(style.direction, item.layout.size)) / 2); case ASStackLayoutAlignItemsBaselineFirst: case ASStackLayoutAlignItemsBaselineLast: - return baseline - ASStackUnpositionedLayout::baselineForItem(style, l); + return baseline - ASStackUnpositionedLayout::baselineForItem(style, item); case ASStackLayoutAlignItemsStart: case ASStackLayoutAlignItemsStretch: case ASStackLayoutAlignItemsNotSet: @@ -36,77 +38,149 @@ static CGFloat crossOffset(const ASStackLayoutSpecStyle &style, } } -/** - * Positions children according to the stack style and positioning properties. - * - * @param style The layout style of the overall stack layout - * @param firstChildOffset Offset of the first child - * @param extraSpacing Spacing between children, in addition to spacing set to the stack's layout style - * @param unpositionedLayout Unpositioned children of the stack - * @param constrainedSize Constrained size of the stack - */ -static ASStackPositionedLayout stackedLayout(const ASStackLayoutSpecStyle &style, - const CGFloat firstChildOffset, - const CGFloat extraSpacing, - const ASStackUnpositionedLayout &unpositionedLayout, - const ASSizeRange &constrainedSize) +static void crossOffsetAndSpacingForEachLine(const std::size_t numOfLines, + const CGFloat crossViolation, + ASStackLayoutAlignContent alignContent, + CGFloat &offset, + CGFloat &spacing) { - CGFloat crossSize = unpositionedLayout.crossSize; - CGFloat baseline = unpositionedLayout.baseline; + ASDisplayNodeCAssertTrue(numOfLines > 0); - // Adjust the position of the unpositioned layouts to be positioned - CGPoint p = directionPoint(style.direction, firstChildOffset, 0); - BOOL first = YES; - const auto stackedChildren = unpositionedLayout.items; - for (const auto &l : stackedChildren) { - p = p + directionPoint(style.direction, l.child.style.spacingBefore, 0); - if (!first) { - p = p + directionPoint(style.direction, style.spacing + extraSpacing, 0); - } - first = NO; - l.layout.position = p + directionPoint(style.direction, 0, crossOffset(style, l, crossSize, baseline)); - - p = p + directionPoint(style.direction, stackDimension(style.direction, l.layout.size) + l.child.style.spacingAfter, 0); + // Handle edge cases + if (alignContent == ASStackLayoutAlignContentSpaceBetween && (crossViolation < kViolationEpsilon || numOfLines == 1)) { + alignContent = ASStackLayoutAlignContentStart; + } else if (alignContent == ASStackLayoutAlignContentSpaceAround && (crossViolation < kViolationEpsilon || numOfLines == 1)) { + alignContent = ASStackLayoutAlignContentCenter; + } + + offset = 0; + spacing = 0; + + switch (alignContent) { + case ASStackLayoutAlignContentCenter: + offset = crossViolation / 2; + break; + case ASStackLayoutAlignContentEnd: + offset = crossViolation; + break; + case ASStackLayoutAlignContentSpaceBetween: + // Spacing between the items, no spaces at the edges, evenly distributed + spacing = crossViolation / (numOfLines - 1); + break; + case ASStackLayoutAlignContentSpaceAround: { + // Spacing between items are twice the spacing on the edges + CGFloat spacingUnit = crossViolation / (numOfLines * 2); + offset = spacingUnit; + spacing = spacingUnit * 2; + break; + } + case ASStackLayoutAlignContentStart: + case ASStackLayoutAlignContentStretch: + break; } - - return {std::move(stackedChildren)}; } -ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnpositionedLayout &unpositionedLayout, - const ASStackLayoutSpecStyle &style, - const ASSizeRange &constrainedSize) +static void stackOffsetAndSpacingForEachItem(const std::size_t numOfItems, + const CGFloat stackViolation, + ASStackLayoutJustifyContent justifyContent, + CGFloat &offset, + CGFloat &spacing) { - const auto numOfItems = unpositionedLayout.items.size(); ASDisplayNodeCAssertTrue(numOfItems > 0); - const CGFloat violation = unpositionedLayout.violation; - ASStackLayoutJustifyContent justifyContent = style.justifyContent; - // Handle edge cases of "space between" and "space around" - if (justifyContent == ASStackLayoutJustifyContentSpaceBetween && (violation < 0 || numOfItems == 1)) { + // Handle edge cases + if (justifyContent == ASStackLayoutJustifyContentSpaceBetween && (stackViolation < kViolationEpsilon || numOfItems == 1)) { justifyContent = ASStackLayoutJustifyContentStart; - } else if (justifyContent == ASStackLayoutJustifyContentSpaceAround && (violation < 0 || numOfItems == 1)) { + } else if (justifyContent == ASStackLayoutJustifyContentSpaceAround && (stackViolation < kViolationEpsilon || numOfItems == 1)) { justifyContent = ASStackLayoutJustifyContentCenter; } + offset = 0; + spacing = 0; + switch (justifyContent) { - case ASStackLayoutJustifyContentStart: { - return stackedLayout(style, 0, 0, unpositionedLayout, constrainedSize); - } - case ASStackLayoutJustifyContentCenter: { - return stackedLayout(style, std::floor(violation / 2), 0, unpositionedLayout, constrainedSize); - } - case ASStackLayoutJustifyContentEnd: { - return stackedLayout(style, violation, 0, unpositionedLayout, constrainedSize); - } - case ASStackLayoutJustifyContentSpaceBetween: { + case ASStackLayoutJustifyContentCenter: + offset = stackViolation / 2; + break; + case ASStackLayoutJustifyContentEnd: + offset = stackViolation; + break; + case ASStackLayoutJustifyContentSpaceBetween: // Spacing between the items, no spaces at the edges, evenly distributed - const auto numOfSpacings = numOfItems - 1; - return stackedLayout(style, 0, violation / numOfSpacings, unpositionedLayout, constrainedSize); - } + spacing = stackViolation / (numOfItems - 1); + break; case ASStackLayoutJustifyContentSpaceAround: { // Spacing between items are twice the spacing on the edges - CGFloat spacingUnit = violation / (numOfItems * 2); - return stackedLayout(style, spacingUnit, spacingUnit * 2, unpositionedLayout, constrainedSize); + CGFloat spacingUnit = stackViolation / (numOfItems * 2); + offset = spacingUnit; + spacing = spacingUnit * 2; + break; } + case ASStackLayoutJustifyContentStart: + break; } } + +static void positionItemsInLine(const ASStackUnpositionedLine &line, + const ASStackLayoutSpecStyle &style, + const CGPoint &startingPoint, + const CGFloat stackSpacing) +{ + CGPoint p = startingPoint; + BOOL first = YES; + + for (const auto &item : line.items) { + p = p + directionPoint(style.direction, item.child.style.spacingBefore, 0); + if (!first) { + p = p + directionPoint(style.direction, style.spacing + stackSpacing, 0); + } + first = NO; + item.layout.position = p + directionPoint(style.direction, 0, crossOffsetForItem(item, style, line.crossSize, line.baseline)); + + p = p + directionPoint(style.direction, stackDimension(style.direction, item.layout.size) + item.child.style.spacingAfter, 0); + } +} + +ASStackPositionedLayout ASStackPositionedLayout::compute(const ASStackUnpositionedLayout &layout, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + const auto &lines = layout.lines; + if (lines.empty()) { + return {}; + } + + const auto numOfLines = lines.size(); + const auto direction = style.direction; + const auto alignContent = style.alignContent; + const auto justifyContent = style.justifyContent; + const auto crossViolation = ASStackUnpositionedLayout::computeCrossViolation(layout.crossDimensionSum, style, sizeRange); + CGFloat crossOffset; + CGFloat crossSpacing; + crossOffsetAndSpacingForEachLine(numOfLines, crossViolation, alignContent, crossOffset, crossSpacing); + + std::vector positionedItems; + CGPoint p = directionPoint(direction, 0, crossOffset); + BOOL first = YES; + for (const auto &line : lines) { + if (!first) { + p = p + directionPoint(direction, 0, crossSpacing); + } + first = NO; + + const auto &items = line.items; + const auto stackViolation = ASStackUnpositionedLayout::computeStackViolation(line.stackDimensionSum, style, sizeRange); + CGFloat stackOffset; + CGFloat stackSpacing; + stackOffsetAndSpacingForEachItem(items.size(), stackViolation, justifyContent, stackOffset, stackSpacing); + + setStackValueToPoint(direction, stackOffset, p); + positionItemsInLine(line, style, p, stackSpacing); + std::move(items.begin(), items.end(), std::back_inserter(positionedItems)); + + p = p + directionPoint(direction, -stackOffset, line.crossSize); + } + + const CGSize finalSize = directionSize(direction, layout.stackDimensionSum, layout.crossDimensionSum); + return {std::move(positionedItems), ASSizeRangeClamp(sizeRange, finalSize)}; +} diff --git a/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.h b/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.h index af7bb8d0cd..e98852b948 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.h +++ b/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.h @@ -14,6 +14,9 @@ #import #import +/** The threshold that determines if a violation has actually occurred. */ +extern CGFloat const kViolationEpsilon; + struct ASStackLayoutSpecChild { /** The original source child. */ id element; @@ -30,19 +33,27 @@ struct ASStackLayoutSpecItem { ASLayout *layout; }; +struct ASStackUnpositionedLine { + /** The set of proposed children in this line, each contains child layout, not yet positioned. */ + std::vector items; + /** The total size of the children in the stack dimension, including all spacing. */ + CGFloat stackDimensionSum; + /** The size in the cross dimension */ + CGFloat crossSize; + /** The baseline of the stack which baseline aligned children should align to */ + CGFloat baseline; +}; /** 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. */ + /** The set of proposed lines, each contains child layouts, not yet positioned. */ + const std::vector lines; + /** + * In a single line stack (e.g no wrao), this is the total size of the children in the stack dimension, including all spacing. + * In a multi-line stack, this is the largest stack dimension among lines. + */ const CGFloat stackDimensionSum; - /** The amount by which stackDimensionSum violates constraints. If positive, less than min; negative, greater than max. */ - const CGFloat violation; - /** The size in the cross dimension */ - const CGFloat crossSize; - /** The baseline of the stack which baseline aligned children should align to */ - const CGFloat baseline; + const CGFloat crossDimensionSum; /** Given a set of children, computes the unpositioned layouts for those children. */ static ASStackUnpositionedLayout compute(const std::vector &children, @@ -51,4 +62,12 @@ struct ASStackUnpositionedLayout { static CGFloat baselineForItem(const ASStackLayoutSpecStyle &style, const ASStackLayoutSpecItem &l); + + static CGFloat computeStackViolation(const CGFloat stackDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange); + + static CGFloat computeCrossViolation(const CGFloat crossDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange); }; diff --git a/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.mm b/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.mm index 8b7e06d4df..4ec2568591 100644 --- a/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.mm +++ b/AsyncDisplayKit/Private/Layout/ASStackUnpositionedLayout.mm @@ -16,6 +16,8 @@ #import #import +CGFloat const kViolationEpsilon = 0.01; + static CGFloat resolveCrossDimensionMaxForStretchChild(const ASStackLayoutSpecStyle &style, const ASStackLayoutSpecChild &child, const CGFloat stackMax, @@ -65,8 +67,76 @@ static ASLayout *crossChildLayout(const ASStackLayoutSpecChild &child, return layout ? : [ASLayout layoutWithLayoutElement:child.element size:{0, 0}]; } -/** The threshold that determines if a violation has actually occurred. */ -static const CGFloat kViolationEpsilon = 0.01; +/** + Computes the consumed cross dimension length for the given vector of lines and stacking style. + + Cross Dimension + +---------------------> + +--------+ +--------+ +--------+ +---------+ + Vertical |Vertical| |Vertical| |Vertical| |Vertical | + Stack | Line 1 | | Line 2 | | Line 3 | | Line 4 | + | | | | | | | | + +--------+ +--------+ +--------+ +---------+ + crossDimensionSum + |------------------------------------------| + + @param lines unpositioned lines + */ +static CGFloat computeLinesCrossDimensionSum(const std::vector &lines) +{ + return std::accumulate(lines.begin(), lines.end(), 0.0, + [&](CGFloat x, const ASStackUnpositionedLine &l) { + return x + l.crossSize; + }); +} + + +/** + Computes the violation by comparing a cross dimension sum with the overall allowable size range for the stack. + + Violation is the distance you would have to add to the unbounded cross-direction length of the stack spec's + lines in order to bring the stack within its allowed sizeRange. The diagram below shows 3 vertical stacks, each contains 3-5 vertical lines, + with the different types of violation. + + Cross Dimension + +---------------------> + cross size range + |------------| + +--------+ +--------+ +--------+ +---------+ - - - - - - - - + Vertical |Vertical| |Vertical| |Vertical| |Vertical | | ^ + Stack 1 | Line 1 | | Line 2 | | Line 3 | | Line 4 | (zero violation) | stack size range + | | | | | | | | | | v + +--------+ +--------+ +--------+ +---------+ - - - - - - - - + | | + +--------+ +--------+ +--------+ - - - - - - - - - - - - + Vertical | | | | | | | | ^ + Stack 2 | | | | | |<--> (positive violation) | stack size range + | | | | | | | | v + +--------+ +--------+ +--------+ - - - - - - - - - - - - + | |<------> (negative violation) + +--------+ +--------+ +--------+ +---------+ +-----------+ - - - + Vertical | | | | | | | | | | | | ^ + Stack 3 | | | | | | | | | | | stack size range + | | | | | | | | | | | | v + +--------+ +--------+ +--------+ +---------+ +-----------+ - - - + + @param crossDimensionSum the consumed length of the lines in the stack along the cross dimension + @param style layout style to be applied to all children + @param sizeRange the range of allowable sizes for the stack layout spec + */ +CGFloat ASStackUnpositionedLayout::computeCrossViolation(const CGFloat crossDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + const CGFloat minCrossDimension = crossDimension(style.direction, sizeRange.min); + const CGFloat maxCrossDimension = crossDimension(style.direction, sizeRange.max); + if (crossDimensionSum < minCrossDimension) { + return minCrossDimension - crossDimensionSum; + } else if (crossDimensionSum > maxCrossDimension) { + return maxCrossDimension - crossDimensionSum; + } + return 0; +} /** Stretches children to lay out along the cross axis according to the alignment stretch settings of the children @@ -97,13 +167,13 @@ static const CGFloat kViolationEpsilon = 0.01; | +--------------------------------------------------+ + crossMax - @param items pre-computed child layouts; modified in-place as needed + @param items pre-computed items; modified in-place as needed @param style the layout style of the overall stack layout */ -static void stretchChildrenAlongCrossDimension(std::vector &items, - const ASStackLayoutSpecStyle &style, - const CGSize parentSize, - const CGFloat crossSize) +static void stretchItemsAlongCrossDimension(std::vector &items, + const ASStackLayoutSpecStyle &style, + const CGSize parentSize, + const CGFloat crossSize) { for (auto &item : items) { const ASStackLayoutAlignItems alignItems = alignment(item.child.style.alignSelf, style.alignItems); @@ -122,6 +192,33 @@ static void stretchChildrenAlongCrossDimension(std::vector &lines, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange, + const CGSize parentSize) +{ + ASDisplayNodeCAssertFalse(lines.empty()); + const std::size_t numOfLines = lines.size(); + const CGFloat violation = ASStackUnpositionedLayout::computeCrossViolation(computeLinesCrossDimensionSum(lines), style, sizeRange); + // Don't stretch if the stack is single line, because the line's cross size was clamped against the stack's constrained size. + const BOOL shouldStretchLines = (numOfLines > 1 + && style.alignContent == ASStackLayoutAlignContentStretch + && violation > kViolationEpsilon); + + CGFloat extraCrossSizePerLine = violation / numOfLines; + for (auto &line : lines) { + if (shouldStretchLines) { + line.crossSize += extraCrossSizePerLine; + } + + stretchItemsAlongCrossDimension(line.items, style, parentSize, line.crossSize); + } +} static BOOL itemIsBaselineAligned(const ASStackLayoutSpecStyle &style, const ASStackLayoutSpecItem &l) @@ -131,20 +228,20 @@ static BOOL itemIsBaselineAligned(const ASStackLayoutSpecStyle &style, } CGFloat ASStackUnpositionedLayout::baselineForItem(const ASStackLayoutSpecStyle &style, - const ASStackLayoutSpecItem &l) + const ASStackLayoutSpecItem &item) { - switch (alignment(l.child.style.alignSelf, style.alignItems)) { + switch (alignment(item.child.style.alignSelf, style.alignItems)) { case ASStackLayoutAlignItemsBaselineFirst: - return l.child.style.ascender; + return item.child.style.ascender; case ASStackLayoutAlignItemsBaselineLast: - return crossDimension(style.direction, l.layout.size) + l.child.style.descender; + return crossDimension(style.direction, item.layout.size) + item.child.style.descender; default: return 0; } } /** - * Finds cross dimension size and baseline of the stack. + * Computes cross size and baseline of each line. * https://www.w3.org/TR/css-flexbox-1/#algo-cross-line * * @param items All items to lay out @@ -153,41 +250,64 @@ CGFloat ASStackUnpositionedLayout::baselineForItem(const ASStackLayoutSpecStyle * @param crossSize result of the cross size * @param baseline result of the stack baseline */ -static void computeCrossSizeAndBaseline(const std::vector &items, - const ASStackLayoutSpecStyle &style, - const ASSizeRange &sizeRange, - CGFloat &crossSize, - CGFloat &baseline) +static void computeLinesCrossSizeAndBaseline(std::vector &lines, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) { + ASDisplayNodeCAssertFalse(lines.empty()); + const BOOL isSingleLine = (lines.size() == 1); + const auto minCrossSize = crossDimension(style.direction, sizeRange.min); const auto maxCrossSize = crossDimension(style.direction, sizeRange.max); + const BOOL definiteCrossSize = (minCrossSize == maxCrossSize); - // Step 1. Collect all the flex items whose align-self is baseline. Find the largest of the distances - // between each item’s baseline and its hypothetical outer cross-start edge (aka. its ascender value), - // and the largest of the distances between each item’s baseline and its hypothetical outer cross-end edge - // (aka. the opposite of its descender value, because a negative descender means the item extends below its baseline), - // and sum these two values. - // - // Step 2. Find the maximum cross dimension size among child layouts. - CGFloat maxStartToBaselineDistance = 0; - CGFloat maxBaselineToEndDistance = 0; - CGFloat maxItemCrossSize = 0; - for (const auto &item : items) { - if (itemIsBaselineAligned(style, item)) { - CGFloat baseline = ASStackUnpositionedLayout::baselineForItem(style, item); - maxStartToBaselineDistance = MAX(maxStartToBaselineDistance, baseline); - maxBaselineToEndDistance = MAX(maxBaselineToEndDistance, crossDimension(style.direction, item.layout.size) - baseline); - } else { - maxItemCrossSize = MAX(maxItemCrossSize, crossDimension(style.direction, item.layout.size)); + // If the stack is single-line and has a definite cross size, the cross size of the line is the stack's definite cross size. + if (isSingleLine && definiteCrossSize) { + auto &line = lines[0]; + line.crossSize = minCrossSize; + + // We still need to determine the line's baseline + //TODO unit test + for (const auto &item : line.items) { + if (itemIsBaselineAligned(style, item)) { + CGFloat baseline = ASStackUnpositionedLayout::baselineForItem(style, item); + line.baseline = MAX(line.baseline, baseline); + } } + + return; } - // Step 3. The used cross-size of the flex line is the largest of the numbers found in the previous two steps and zero. - crossSize = MAX(maxStartToBaselineDistance + maxBaselineToEndDistance, maxItemCrossSize); - // Clamp the cross-size to be within the stack's min and max cross-size properties. - crossSize = MIN(MAX(minCrossSize, crossSize), maxCrossSize); - - baseline = maxStartToBaselineDistance; + for (auto &line : lines) { + const auto &items = line.items; + CGFloat maxStartToBaselineDistance = 0; + CGFloat maxBaselineToEndDistance = 0; + CGFloat maxItemCrossSize = 0; + + for (const auto &item : items) { + if (itemIsBaselineAligned(style, item)) { + // Step 1. Collect all the items whose align-self is baseline. Find the largest of the distances + // between each item’s baseline and its hypothetical outer cross-start edge (aka. its baseline value), + // and the largest of the distances between each item’s baseline and its hypothetical outer cross-end edge, + // and sum these two values. + CGFloat baseline = ASStackUnpositionedLayout::baselineForItem(style, item); + maxStartToBaselineDistance = MAX(maxStartToBaselineDistance, baseline); + maxBaselineToEndDistance = MAX(maxBaselineToEndDistance, crossDimension(style.direction, item.layout.size) - baseline); + } else { + // Step 2. Among all the items not collected by the previous step, find the largest outer hypothetical cross size. + maxItemCrossSize = MAX(maxItemCrossSize, crossDimension(style.direction, item.layout.size)); + } + } + + // Step 3. The used cross-size of the flex line is the largest of the numbers found in the previous two steps and zero. + line.crossSize = MAX(maxStartToBaselineDistance + maxBaselineToEndDistance, maxItemCrossSize); + if (isSingleLine) { + // If the stack is single-line, then clamp the line’s cross-size to be within the stack's min and max cross-size properties. + line.crossSize = MIN(MAX(minCrossSize, line.crossSize), maxCrossSize); + } + + line.baseline = maxStartToBaselineDistance; + } } /** @@ -314,8 +434,8 @@ static void layoutFlexibleChildrenAtZeroSize(std::vector @param items unpositioned layouts for items @param style the layout style of the overall stack layout */ -static CGFloat computeStackDimensionSum(const std::vector &items, - const ASStackLayoutSpecStyle &style) +static CGFloat computeItemsStackDimensionSum(const std::vector &items, + const ASStackLayoutSpecStyle &style) { // Sum up the childrens' spacing const CGFloat childSpacingSum = std::accumulate(items.begin(), items.end(), @@ -333,6 +453,7 @@ static CGFloat computeStackDimensionSum(const std::vector return childStackDimensionSum; } +//TODO move this up near computeCrossViolation and make both methods share the same code path, to make sure they share the same concept of "negative" and "positive" violations. /** Computes the violation by comparing a stack dimension sum with the overall allowable size range for the stack. @@ -364,9 +485,9 @@ static CGFloat computeStackDimensionSum(const std::vector @param style layout style to be applied to all children @param sizeRange the range of allowable sizes for the stack layout spec */ -static CGFloat computeViolation(const CGFloat stackDimensionSum, - const ASStackLayoutSpecStyle &style, - const ASSizeRange &sizeRange) +CGFloat ASStackUnpositionedLayout::computeStackViolation(const CGFloat stackDimensionSum, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) { const CGFloat minStackDimension = stackDimension(style.direction, sizeRange.min); const CGFloat maxStackDimension = stackDimension(style.direction, sizeRange.max); @@ -394,65 +515,106 @@ ASDISPLAYNODE_INLINE BOOL useOptimizedFlexing(const std::vector &items, - const ASStackLayoutSpecStyle &style, - const ASSizeRange &sizeRange, - const CGSize parentSize, - const BOOL useOptimizedFlexing) +static void flexLinesAlongStackDimension(std::vector &lines, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange, + const CGSize parentSize, + const BOOL useOptimizedFlexing) { - const CGFloat violation = computeViolation(computeStackDimensionSum(items, style), style, sizeRange); - std::function flexFactor = flexFactorInViolationDirection(violation); - // The flex factor sum is needed to determine if flexing is necessary. - // This value is also needed if the violation is positive and flexible children need to grow, so keep it around. - const CGFloat flexFactorSum = std::accumulate(items.begin(), items.end(), 0.0, [&](CGFloat x, const ASStackLayoutSpecItem &item) { - return x + flexFactor(item); - }); - // If no children are able to flex then there is nothing left to do. Bail. - if (flexFactorSum == 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, parentSize); + for (auto &line : lines) { + auto &items = line.items; + const CGFloat violation = ASStackUnpositionedLayout::computeStackViolation(computeItemsStackDimensionSum(items, style), style, sizeRange); + std::function flexFactor = flexFactorInViolationDirection(violation); + // The flex factor sum is needed to determine if flexing is necessary. + // This value is also needed if the violation is positive and flexible children need to grow, so keep it around. + const CGFloat flexFactorSum = std::accumulate(items.begin(), items.end(), 0.0, [&](CGFloat x, const ASStackLayoutSpecItem &item) { + return x + flexFactor(item); + }); + // If no children are able to flex then there is nothing left to do. Bail. + if (flexFactorSum == 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, parentSize); + } + return; + } + std::function flexAdjustment = flexAdjustmentInViolationDirection(items, + style, + violation, + flexFactorSum); + + // Compute any remaining violation to the first flexible child. + const CGFloat remainingViolation = std::accumulate(items.begin(), items.end(), violation, [&](CGFloat x, const ASStackLayoutSpecItem &item) { + return x - flexAdjustment(item); + }); + BOOL isFirstFlex = YES; + for (ASStackLayoutSpecItem &item : items) { + const CGFloat currentFlexAdjustment = flexAdjustment(item); + // Children are consider inflexible if they do not need to make a flex adjustment. + if (currentFlexAdjustment != 0) { + const CGFloat originalStackSize = stackDimension(style.direction, item.layout.size); + // Only apply the remaining violation for the first flexible child that has a flex grow factor. + const CGFloat flexedStackSize = originalStackSize + currentFlexAdjustment + (isFirstFlex && item.child.style.flexGrow > 0 ? remainingViolation : 0); + item.layout = crossChildLayout(item.child, + style, + MAX(flexedStackSize, 0), + MAX(flexedStackSize, 0), + crossDimension(style.direction, sizeRange.min), + crossDimension(style.direction, sizeRange.max), + parentSize); + isFirstFlex = NO; + } } - return; } - std::function flexAdjustment = flexAdjustmentInViolationDirection(items, - style, - violation, - flexFactorSum); +} - // Compute any remaining violation to the first flexible child. - const CGFloat remainingViolation = std::accumulate(items.begin(), items.end(), violation, [&](CGFloat x, const ASStackLayoutSpecItem &item) { - return x - flexAdjustment(item); - }); - BOOL isFirstFlex = YES; - for (ASStackLayoutSpecItem &item : items) { - const CGFloat currentFlexAdjustment = flexAdjustment(item); - // Children are consider inflexible if they do not need to make a flex adjustment. - if (currentFlexAdjustment != 0) { - const CGFloat originalStackSize = stackDimension(style.direction, item.layout.size); - // Only apply the remaining violation for the first flexible child that has a flex grow factor. - const CGFloat flexedStackSize = originalStackSize + currentFlexAdjustment + (isFirstFlex && item.child.style.flexGrow > 0 ? remainingViolation : 0); - item.layout = crossChildLayout(item.child, - style, - MAX(flexedStackSize, 0), - MAX(flexedStackSize, 0), - crossDimension(style.direction, sizeRange.min), - crossDimension(style.direction, sizeRange.max), - parentSize); - isFirstFlex = NO; - } +/** + https://www.w3.org/TR/css-flexbox-1/#algo-line-break + */ +static std::vector collectChildrenIntoLines(const std::vector &items, + const ASStackLayoutSpecStyle &style, + const ASSizeRange &sizeRange) +{ + //TODO if infinite max stack size, fast path + if (style.flexWrap == ASStackLayoutFlexWrapNoWrap) { + return std::vector (1, {.items = std::move(items)}); } + + std::vector lines; + std::vector lineItems; + CGFloat lineStackDimensionSum = 0; + + for(auto it = items.begin(); it != items.end(); ++it) { + const auto &item = *it; + const CGFloat itemStackDimension = stackDimension(style.direction, item.layout.size); + const BOOL negativeViolationIfAddItem = (ASStackUnpositionedLayout::computeStackViolation(lineStackDimensionSum + itemStackDimension, style, sizeRange) < 0); + const BOOL breakCurrentLine = negativeViolationIfAddItem && !lineItems.empty(); + + if (breakCurrentLine) { + lines.push_back({.items = std::vector (lineItems)}); + lineItems.clear(); + lineStackDimensionSum = 0; + } + + lineItems.push_back(std::move(item)); + lineStackDimensionSum += itemStackDimension; + } + + // Handle last line + lines.push_back({.items = std::vector (lineItems)}); + + return lines; } /** @@ -507,25 +669,34 @@ ASStackUnpositionedLayout ASStackUnpositionedLayout::compute(const std::vector items = layoutChildrenAlongUnconstrainedStackDimension(children, - style, - sizeRange, - parentSize, - optimizedFlexing); + style, + sizeRange, + parentSize, + optimizedFlexing); - // Resolve the flexible lengths (https://www.w3.org/TR/css-flexbox-1/#algo-flex) - // Determine the hypothetical cross size of each item (https://www.w3.org/TR/css-flexbox-1/#algo-cross-item) - flexChildrenAlongStackDimension(items, style, sizeRange, parentSize, optimizedFlexing); + // Collect items into lines (https://www.w3.org/TR/css-flexbox-1/#algo-line-break) + std::vector lines = collectChildrenIntoLines(items, style, sizeRange); - // Step 4. Cross Size Determination (https://www.w3.org/TR/css-flexbox-1/#cross-sizing) - // - // Calculate the cross size of the stack (https://www.w3.org/TR/css-flexbox-1/#algo-cross-line) - CGFloat crossSize; - CGFloat baseline; - computeCrossSizeAndBaseline(items, style, sizeRange, crossSize, baseline); + // Resolve the flexible lengths (https://www.w3.org/TR/css-flexbox-1/#resolve-flexible-lengths) + flexLinesAlongStackDimension(lines, style, sizeRange, parentSize, optimizedFlexing); + + // Calculate the cross size of each flex line (https://www.w3.org/TR/css-flexbox-1/#algo-cross-line) + computeLinesCrossSizeAndBaseline(lines, style, sizeRange); + + // Handle 'align-content: stretch' (https://www.w3.org/TR/css-flexbox-1/#algo-line-stretch) // Determine the used cross size of each item (https://www.w3.org/TR/css-flexbox-1/#algo-stretch) - // If the flex item has stretch alignment, redo layout - stretchChildrenAlongCrossDimension(items, style, parentSize, crossSize); + stretchLinesAlongCrossDimension(lines, style, sizeRange, parentSize); - const CGFloat stackDimensionSum = computeStackDimensionSum(items, style); - return {std::move(items), stackDimensionSum, computeViolation(stackDimensionSum, style, sizeRange), crossSize, baseline}; + // Compute stack dimension sum of each line and the whole stack + CGFloat layoutStackDimensionSum = 0; + for (auto &line : lines) { + line.stackDimensionSum = computeItemsStackDimensionSum(line.items, style); + // layoutStackDimensionSum is the max stackDimensionSum among all lines + layoutStackDimensionSum = MAX(line.stackDimensionSum, layoutStackDimensionSum); + } + // Compute cross dimension sum of the stack. + // This should be done before `lines` are moved to a new ASStackUnpositionedLayout struct (i.e `std::move(lines)`) + CGFloat layoutCrossDimensionSum = computeLinesCrossDimensionSum(lines); + + return {.lines = std::move(lines), .stackDimensionSum = layoutStackDimensionSum, .crossDimensionSum = layoutCrossDimensionSum}; } diff --git a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm index f0438e6496..a21a37a83b 100644 --- a/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm +++ b/AsyncDisplayKitTests/ASStackLayoutSpecSnapshotTests.mm @@ -22,15 +22,6 @@ @implementation ASStackLayoutSpecSnapshotTests -#pragma mark - XCTestCase - -- (void)setUp -{ - [super setUp]; - - self.recordMode = NO; -} - #pragma mark - Utility methods static NSArray *defaultSubnodes() @@ -110,6 +101,8 @@ static NSArray *defaultTextNodes() spacing:style.spacing justifyContent:style.justifyContent alignItems:style.alignItems + flexWrap:style.flexWrap + alignContent:style.alignContent children:children]; [self testStackLayoutSpec:stackLayoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; @@ -162,6 +155,28 @@ static NSArray *defaultTextNodes() [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:newSubnodes identifier:identifier]; } +- (void)testStackLayoutSpecWithAlignContent:(ASStackLayoutAlignContent)alignContent + sizeRange:(ASSizeRange)sizeRange + identifier:(NSString *)identifier +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = alignContent, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor magentaColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor cyanColor], subnodeSize), + ]; + + [self testStackLayoutSpecWithStyle:style sizeRange:sizeRange subnodes:subnodes identifier:identifier]; +} #pragma mark - @@ -1167,4 +1182,77 @@ static NSArray *defaultTextNodes() [self testStackLayoutSpec:stackLayoutSpec sizeRange:kSize subnodes:children identifier:nil]; } +#pragma mark - Content alignment tests + +- (void)testAlignContentUnderflow +{ + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{110, 300}, {110, 300}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround sizeRange:kSize identifier:@"alignContentSpaceAround"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStretch sizeRange:kSize identifier:@"alignContentStretch"]; +} + +- (void)testAlignContentOverflow +{ + // 6 lines, each line has 1 item, each item has a size of {50, 50} + // width is 40px. It's 10px smaller than the width of each item (40px vs 50px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{40, 260}, {40, 260}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart sizeRange:kSize identifier:@"alignContentStart"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter sizeRange:kSize identifier:@"alignContentCenter"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd sizeRange:kSize identifier:@"alignContentEnd"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween sizeRange:kSize identifier:@"alignContentSpaceBetween"]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround sizeRange:kSize identifier:@"alignContentSpaceAround"]; +} + +- (void)testAlignContentWithUnconstrainedCrossSize +{ + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + // height is unconstrained. It causes no cross size violation and the end results are all similar to ASStackLayoutAlignContentStart. + static ASSizeRange kSize = {{110, 0}, {110, CGFLOAT_MAX}}; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStart sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentCenter sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentEnd sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceBetween sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentSpaceAround sizeRange:kSize identifier:nil]; + [self testStackLayoutSpecWithAlignContent:ASStackLayoutAlignContentStretch sizeRange:kSize identifier:nil]; +} + +- (void)testAlignContentStretchAndOtherAlignments +{ + ASStackLayoutSpecStyle style = { + .direction = ASStackLayoutDirectionHorizontal, + .flexWrap = ASStackLayoutFlexWrapWrap, + .alignContent = ASStackLayoutAlignContentStretch, + .alignItems = ASStackLayoutAlignItemsStart, + }; + + CGSize subnodeSize = {50, 50}; + NSArray *subnodes = @[ + // 1st line + ASDisplayNodeWithBackgroundColor([UIColor redColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor yellowColor], subnodeSize), + // 2nd line + ASDisplayNodeWithBackgroundColor([UIColor blueColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor magentaColor], subnodeSize), + // 3rd line + ASDisplayNodeWithBackgroundColor([UIColor greenColor], subnodeSize), + ASDisplayNodeWithBackgroundColor([UIColor cyanColor], subnodeSize), + ]; + + subnodes[1].style.alignSelf = ASStackLayoutAlignSelfStart; + subnodes[3].style.alignSelf = ASStackLayoutAlignSelfCenter; + subnodes[5].style.alignSelf = ASStackLayoutAlignSelfEnd; + + // 3 lines, each line has 2 items, each item has a size of {50, 50} + // width is 110px. It's 10px bigger than the required width of each line (110px vs 100px) to test that items are still correctly collected into lines + static ASSizeRange kSize = {{110, 300}, {110, 300}}; + [self testStackLayoutSpecWithStyle:style sizeRange:kSize subnodes:subnodes identifier:nil]; +} + @end diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png new file mode 100644 index 0000000000..cee1ed8cf7 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png new file mode 100644 index 0000000000..d6736f9153 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000..cee1ed8cf7 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000..1e32492bac Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png new file mode 100644 index 0000000000..1e32492bac Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png new file mode 100644 index 0000000000..c091f22768 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png new file mode 100644 index 0000000000..888442daec Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000..ecc727a501 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000..b8ef3d2aee Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png new file mode 100644 index 0000000000..d7907975ae Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_64/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png new file mode 100644 index 0000000000..032e9fac3a Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentCenter@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png new file mode 100644 index 0000000000..6d08d18e4a Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentEnd@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000..032e9fac3a Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceAround@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000..4df2a81184 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentSpaceBetween@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png new file mode 100644 index 0000000000..4df2a81184 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentOverflow_alignContentStart@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentStretchAndOtherAlignments@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentStretchAndOtherAlignments@2x.png new file mode 100644 index 0000000000..51d060062c Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentStretchAndOtherAlignments@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png new file mode 100644 index 0000000000..fea7203a26 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentCenter@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png new file mode 100644 index 0000000000..9e0bdad429 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentEnd@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png new file mode 100644 index 0000000000..bb608622fd Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceAround@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png new file mode 100644 index 0000000000..17b8dbfbc4 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentSpaceBetween@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png new file mode 100644 index 0000000000..ab969dd638 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStart@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStretch@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStretch@2x.png new file mode 100644 index 0000000000..584aca77db Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentUnderflow_alignContentStretch@2x.png differ diff --git a/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentWithUnconstrainedCrossSize@2x.png b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentWithUnconstrainedCrossSize@2x.png new file mode 100644 index 0000000000..12936781c0 Binary files /dev/null and b/AsyncDisplayKitTests/ReferenceImages_iOS_10/ASStackLayoutSpecSnapshotTests/testAlignContentWithUnconstrainedCrossSize@2x.png differ