From 55928f343d1687eed5f955ca17e73e517d286bed Mon Sep 17 00:00:00 2001 From: appleguy Date: Wed, 14 Jun 2017 19:36:13 -0700 Subject: [PATCH] [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with improved behavior and cleaner integration (#343) * [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with support for mixing with ASLayoutSpec. After experimentation with the ASYogaLayoutSpec (or non-contiguous) approach to integrating Yoga, test results and feedback from the authors of Yoga have shown that this approach can't be made completely correct, There are issues with some of the features required to represent Web-style flexbox; in particular: padding, margins, and border handling have varience. This diff is a first step towards a truly correct and elegant implementation of Yoga integration with Texture. In addition to reducing the footprint of the integration, which is an explicit goal of work at this stage, these changes already support improved behavior - including mixing between ASLayoutSpecs even as subnodes of Yoga layout-driven nodes, in addition to above them. Yoga may be used for any set of nodes. Because Yoga usage is limited at this time, it's safe to merge this diff and further improvements will be refinements in this direction. * [ASDKgram] Add Yoga layout implementation for PhotoCellNode. * [Yoga] Final fixes for the upgraded implementation of the Contiguous layout mode. * [Yoga] Add CHANGELOG.md entry and fix for Yoga rounding to screen scale. * [Yoga] Minor cleanup to remove old comments and generalize utility methods. --- CHANGELOG.md | 1 + Source/ASDisplayNode+Beta.h | 6 +- Source/ASDisplayNode+Yoga.mm | 168 +++++-------- Source/ASDisplayNode.mm | 36 +-- Source/Base/ASAvailability.h | 2 +- Source/Details/ASDataController.mm | 2 +- Source/Layout/ASDimension.h | 2 + Source/Layout/ASDimension.mm | 10 + Source/Layout/ASLayoutElement.mm | 225 ++++++++++++++++-- Source/Layout/ASYogaUtilities.h | 11 + Source/Layout/ASYogaUtilities.mm | 55 +++++ .../Private/ASDisplayNode+FrameworkPrivate.h | 12 - Source/Private/ASDisplayNodeInternal.h | 2 +- examples/ASDKgram/Podfile | 1 + examples/ASDKgram/Sample/PhotoCellNode.m | 72 ++++++ 15 files changed, 450 insertions(+), 155 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bc33956..e26c62f89 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## master * Add your own contributions to the next release on the line below this with your name. +- [Yoga] Rewrite YOGA_TREE_CONTIGUOUS mode with improved behavior and cleaner integration [Scott Goodson](https://github.com/appleguy) - [ASTraitCollection] Convert ASPrimitiveTraitCollection from lock to atomic. [Scott Goodson](https://github.com/appleguy) - Add a synchronous mode to ASCollectionNode, for colletion view data source debugging. [Hannah Troisi](https://github.com/hannahmbanana) - [ASDisplayNode+Layout] Add check for orphaned nodes after layout transition to clean up. #336. [Scott Goodson](https://github.com/appleguy) diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index 4078e936b..84f3df166 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -22,6 +22,7 @@ #if YOGA #import YOGA_HEADER_PATH + #import #endif NS_ASSUME_NONNULL_BEGIN @@ -178,6 +179,7 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute; #if YOGA_TREE_CONTIGUOUS +@property (nonatomic, assign) BOOL yogaLayoutInProgress; @property (nonatomic, strong, nullable) ASLayout *yogaCalculatedLayout; // These methods should not normally be called directly. - (void)invalidateCalculatedYogaLayout; @@ -188,9 +190,11 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable @interface ASLayoutElementStyle (Yoga) +- (YGNodeRef)yogaNodeCreateIfNeeded; +@property (nonatomic, assign, readonly) YGNodeRef yogaNode; + @property (nonatomic, assign, readwrite) ASStackLayoutDirection flexDirection; @property (nonatomic, assign, readwrite) YGDirection direction; -@property (nonatomic, assign, readwrite) CGFloat spacing; @property (nonatomic, assign, readwrite) ASStackLayoutJustifyContent justifyContent; @property (nonatomic, assign, readwrite) ASStackLayoutAlignItems alignItems; @property (nonatomic, assign, readwrite) YGPositionType positionType; diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 9bcdfcc99..ab31bfa1d 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -23,7 +23,7 @@ #import #import #import -#import +#import #import #import @@ -35,7 +35,11 @@ @interface ASDisplayNode (YogaInternal) @property (nonatomic, weak) ASDisplayNode *yogaParent; -@property (nonatomic, assign) YGNodeRef yogaNode; +- (ASSizeRange)_locked_constrainedSizeForLayoutPass; +@end + +@interface ASLayout (YogaInternal) +@property (nonatomic, getter=isFlattened) BOOL flattened; @end #endif /* YOGA_TREE_CONTIGUOUS */ @@ -71,12 +75,18 @@ - (void)addYogaChild:(ASDisplayNode *)child // Clean up state in case this child had another parent. [self removeYogaChild:child]; + + BOOL hadZeroChildren = (_yogaChildren.count == 0); + [_yogaChildren addObject:child]; #if YOGA_TREE_CONTIGUOUS + // Ensure any measure function is removed before inserting the YGNodeRef child. + if (hadZeroChildren) { + [self updateYogaMeasureFuncIfNeeded]; + } // YGNodeRef insertion is done in setParent: child.yogaParent = self; - self.hierarchyState |= ASHierarchyStateYogaLayoutEnabled; #else // When using non-contiguous Yoga layout, each level in the node hierarchy independently uses an ASYogaLayoutSpec __weak ASDisplayNode *weakSelf = self; @@ -94,13 +104,16 @@ - (void)removeYogaChild:(ASDisplayNode *)child if (child == nil) { return; } + + BOOL hadChildren = (_yogaChildren.count > 0); [_yogaChildren removeObjectIdenticalTo:child]; #if YOGA_TREE_CONTIGUOUS // YGNodeRef removal is done in setParent: child.yogaParent = nil; - if (_yogaChildren.count == 0 && self.yogaParent == nil) { - self.hierarchyState &= ~ASHierarchyStateYogaLayoutEnabled; + // Ensure any measure function is re-added after removing the YGNodeRef child. + if (hadChildren && _yogaChildren.count == 0) { + [self updateYogaMeasureFuncIfNeeded]; } #else if (_yogaChildren.count == 0) { @@ -121,26 +134,13 @@ - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute #if YOGA_TREE_CONTIGUOUS /* YOGA_TREE_CONTIGUOUS */ -- (void)setYogaNode:(YGNodeRef)yogaNode -{ - _yogaNode = yogaNode; -} - -- (YGNodeRef)yogaNode -{ - if (_yogaNode == NULL) { - _yogaNode = YGNodeNew(); - } - return _yogaNode; -} - - (void)setYogaParent:(ASDisplayNode *)yogaParent { if (_yogaParent == yogaParent) { return; } - YGNodeRef yogaNode = self.yogaNode; // Use property to assign Ref if needed. + YGNodeRef yogaNode = [self.style yogaNodeCreateIfNeeded]; YGNodeRef oldParentRef = YGNodeGetParent(yogaNode); if (oldParentRef != NULL) { YGNodeRemoveChild(oldParentRef, yogaNode); @@ -148,11 +148,8 @@ - (void)setYogaParent:(ASDisplayNode *)yogaParent _yogaParent = yogaParent; if (yogaParent) { - self.hierarchyState |= ASHierarchyStateYogaLayoutEnabled; - YGNodeRef newParentRef = yogaParent.yogaNode; + YGNodeRef newParentRef = [yogaParent.style yogaNodeCreateIfNeeded]; YGNodeInsertChild(newParentRef, yogaNode, YGNodeGetChildCount(newParentRef)); - } else { - self.hierarchyState &= ~ASHierarchyStateYogaLayoutEnabled; } } @@ -171,51 +168,60 @@ - (ASLayout *)yogaCalculatedLayout return _yogaCalculatedLayout; } +- (void)setYogaLayoutInProgress:(BOOL)yogaLayoutInProgress +{ + setFlag(YogaLayoutInProgress, yogaLayoutInProgress); +} + +- (BOOL)yogaLayoutInProgress +{ + return checkFlag(YogaLayoutInProgress); +} + - (ASLayout *)layoutForYogaNode { - YGNodeRef yogaNode = self.yogaNode; + YGNodeRef yogaNode = self.style.yogaNode; CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); CGPoint position = CGPointMake(YGNodeLayoutGetLeft(yogaNode), YGNodeLayoutGetTop(yogaNode)); - // TODO: If it were possible to set .flattened = YES, it would be valid to do so here. return [ASLayout layoutWithLayoutElement:self size:size position:position sublayouts:nil]; } - (void)setupYogaCalculatedLayout { - YGNodeRef yogaNode = self.yogaNode; // Use property to assign Ref if needed. + YGNodeRef yogaNode = self.style.yogaNode; uint32_t childCount = YGNodeGetChildCount(yogaNode); ASDisplayNodeAssert(childCount == self.yogaChildren.count, @"Yoga tree should always be in sync with .yogaNodes array! %@", self.yogaChildren); NSMutableArray *sublayouts = [NSMutableArray arrayWithCapacity:childCount]; for (ASDisplayNode *subnode in self.yogaChildren) { - [sublayouts addObject:[subnode layoutForYogaNode]]; + ASLayout *sublayout = [subnode layoutForYogaNode]; + sublayout.flattened = YES; + [sublayouts addObject:sublayout]; } // The layout for self should have position CGPointNull, but include the calculated size. CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); ASLayout *layout = [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts]; + self.yogaCalculatedLayout = layout; } -- (void)setYogaMeasureFuncIfNeeded +- (void)updateYogaMeasureFuncIfNeeded { // Size calculation via calculateSizeThatFits: or layoutSpecThatFits: // This will be used for ASTextNode, as well as any other node that has no Yoga children - if (self.yogaChildren.count == 0) { - YGNodeRef yogaNode = self.yogaNode; // Use property to assign Ref if needed. - YGNodeSetContext(yogaNode, (__bridge void *)self); - YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc); - } + id layoutElementToMeasure = (self.yogaChildren.count == 0 ? self : nil); + ASLayoutElementYogaUpdateMeasureFunc(self.style.yogaNode, layoutElementToMeasure); } - (void)invalidateCalculatedYogaLayout { // Yoga internally asserts that this method may only be called on nodes with a measurement function. - YGNodeRef yogaNode = self.yogaNode; - if (YGNodeGetMeasureFunc(yogaNode)) { + YGNodeRef yogaNode = self.style.yogaNode; + if (yogaNode && YGNodeGetMeasureFunc(yogaNode)) { YGNodeMarkDirty(yogaNode); } self.yogaCalculatedLayout = nil; @@ -223,88 +229,40 @@ - (void)invalidateCalculatedYogaLayout - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize { - if (self.yogaParent) { - if (self.yogaCalculatedLayout == nil) { - [self _setNeedsLayoutFromAbove]; - } - return; - } - if (ASHierarchyStateIncludesYogaLayoutMeasuring(self.hierarchyState)) { - ASDisplayNodeAssert(NO, @"A Yoga layout is being performed by a parent; children must not perform their own until it is done! %@", [self displayNodeRecursiveDescription]); + ASDisplayNode *yogaParent = self.yogaParent; + + if (yogaParent) { + ASYogaLog(@"ESCALATING to Yoga root: %@", self); + // TODO(appleguy): Consider how to get the constrainedSize for the yogaRoot when escalating manually. + [yogaParent calculateLayoutFromYogaRoot:ASSizeRangeUnconstrained]; return; } + ASDN::MutexLocker l(__instanceLock__); + + // Prepare all children for the layout pass with the current Yoga tree configuration. ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { - node.hierarchyState |= ASHierarchyStateYogaLayoutMeasuring; + node.yogaLayoutInProgress = YES; + [node updateYogaMeasureFuncIfNeeded]; }); - YGNodeRef rootYogaNode = self.yogaNode; + if (ASSizeRangeEqualToSizeRange(rootConstrainedSize, ASSizeRangeUnconstrained)) { + rootConstrainedSize = [self _locked_constrainedSizeForLayoutPass]; + } + + ASYogaLog(@"CALCULATING at Yoga root with constraint = {%@, %@}: %@", + NSStringFromCGSize(rootConstrainedSize.min), NSStringFromCGSize(rootConstrainedSize.max), self); + + YGNodeRef rootYogaNode = self.style.yogaNode; // Apply the constrainedSize as a base, known frame of reference. // If the root node also has style.*Size set, these will be overridden below. // YGNodeCalculateLayout currently doesn't offer the ability to pass a minimum size (max is passed there). + + // TODO(appleguy): Reconcile the self.style.*Size properties with rootConstrainedSize YGNodeStyleSetMinWidth (rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.width)); YGNodeStyleSetMinHeight(rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.height)); - ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { - ASLayoutElementStyle *style = node.style; - YGNodeRef yogaNode = node.yogaNode; - - YGNodeStyleSetDirection (yogaNode, style.direction); - - YGNodeStyleSetFlexWrap (yogaNode, style.flexWrap); - YGNodeStyleSetFlexGrow (yogaNode, style.flexGrow); - YGNodeStyleSetFlexShrink (yogaNode, style.flexShrink); - YGNODE_STYLE_SET_DIMENSION (yogaNode, FlexBasis, style.flexBasis); - - YGNodeStyleSetFlexDirection (yogaNode, yogaFlexDirection(style.flexDirection)); - YGNodeStyleSetJustifyContent(yogaNode, yogaJustifyContent(style.justifyContent)); - YGNodeStyleSetAlignSelf (yogaNode, yogaAlignSelf(style.alignSelf)); - ASStackLayoutAlignItems alignItems = style.alignItems; - if (alignItems != ASStackLayoutAlignItemsNotSet) { - YGNodeStyleSetAlignItems(yogaNode, yogaAlignItems(alignItems)); - } - - YGNodeStyleSetPositionType (yogaNode, style.positionType); - ASEdgeInsets position = style.position; - ASEdgeInsets margin = style.margin; - ASEdgeInsets padding = style.padding; - ASEdgeInsets border = style.border; - - YGEdge edge = YGEdgeLeft; - for (int i = 0; i < YGEdgeAll + 1; ++i) { - YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, Position, dimensionForEdgeWithEdgeInsets(edge, position), edge); - YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, Margin, dimensionForEdgeWithEdgeInsets(edge, margin), edge); - YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, Padding, dimensionForEdgeWithEdgeInsets(edge, padding), edge); - YGNODE_STYLE_SET_FLOAT_WITH_EDGE(yogaNode, Border, dimensionForEdgeWithEdgeInsets(edge, border), edge); - edge = (YGEdge)(edge + 1); - } - - CGFloat aspectRatio = style.aspectRatio; - if (aspectRatio > FLT_EPSILON && aspectRatio < CGFLOAT_MAX / 2.0) { - YGNodeStyleSetAspectRatio(yogaNode, aspectRatio); - } - - // For the root node, we use rootConstrainedSize above. For children, consult the style for their size. - if (node != self) { - YGNODE_STYLE_SET_DIMENSION(yogaNode, Width, style.width); - YGNODE_STYLE_SET_DIMENSION(yogaNode, Height, style.height); - - YGNODE_STYLE_SET_DIMENSION(yogaNode, MinWidth, style.minWidth); - YGNODE_STYLE_SET_DIMENSION(yogaNode, MinHeight, style.minHeight); - - YGNODE_STYLE_SET_DIMENSION(yogaNode, MaxWidth, style.maxWidth); - YGNODE_STYLE_SET_DIMENSION(yogaNode, MaxHeight, style.maxHeight); - } - - [node setYogaMeasureFuncIfNeeded]; - - /* TODO(appleguy): STYLE SETTER METHODS LEFT TO IMPLEMENT - void YGNodeStyleSetOverflow(YGNodeRef node, YGOverflow overflow); - void YGNodeStyleSetFlex(YGNodeRef node, float flex); - */ - }); - // It is crucial to use yogaFloat... to convert CGFLOAT_MAX into YGUndefined here. YGNodeCalculateLayout(rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.max.width), @@ -313,7 +271,7 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { [node setupYogaCalculatedLayout]; - node.hierarchyState &= ~ASHierarchyStateYogaLayoutMeasuring; + node.yogaLayoutInProgress = NO; }); #if YOGA_LAYOUT_LOGGING /* YOGA_LAYOUT_LOGGING */ diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index 2929227b2..1b548834a 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -424,12 +424,6 @@ - (void)dealloc [self _scheduleIvarsForMainDeallocation]; } -#if YOGA_TREE_CONTIGUOUS - if (_yogaNode != NULL) { - YGNodeFree(_yogaNode); - } -#endif - // TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway. [self _setSupernode:nil]; } @@ -966,22 +960,32 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize ASDN::MutexLocker l(__instanceLock__); #if YOGA_TREE_CONTIGUOUS /* YOGA */ - if (ASHierarchyStateIncludesYogaLayoutEnabled(_hierarchyState) == YES) { - if (ASHierarchyStateIncludesYogaLayoutMeasuring(_hierarchyState) == NO && self.yogaCalculatedLayout == nil) { - ASDN::MutexUnlocker ul(__instanceLock__); + // There are several cases where Yoga could arrive here: + // - This node is not in a Yoga tree: it has neither a yogaParent nor yogaChildren. + // - This node is a Yoga tree root: it has no yogaParent, but has yogaChildren. + // - This node is a Yoga tree node: it has both a yogaParent and yogaChildren. + // - This node is a Yoga tree leaf: it has a yogaParent, but no yogaChidlren. + // If we're a leaf node, we are probably being called by a measure function and proceed as normal. + // If we're a yoga root or tree node, initiate a new Yoga calculation pass from root. + YGNodeRef yogaNode = _style.yogaNode; + BOOL hasYogaParent = (_yogaParent != nil); + BOOL hasYogaChildren = (_yogaChildren.count > 0); + BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); + if (usesYoga && (_yogaParent == nil || _yogaChildren.count > 0)) { + // This node has some connection to a Yoga tree. + ASDN::MutexUnlocker ul(__instanceLock__); + + if (self.yogaLayoutInProgress == NO) { [self calculateLayoutFromYogaRoot:constrainedSize]; } - - // The call above may set yogaCalculatedLayout, even if it tested as nil to enter it. - if (self.yogaCalculatedLayout && self.yogaChildren.count > 0) { - return self.yogaCalculatedLayout; - } + ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self); + return _yogaCalculatedLayout; } + ASYogaLog(@"PROCEEDING past Yoga check to calculate ASLayout for: %@", self); #endif /* YOGA */ // Manual size calculation via calculateSizeThatFits: - if (((_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) || - (_layoutSpecBlock != NULL)) == NO) { + if (_layoutSpecBlock == NULL && (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == 0) { CGSize size = [self calculateSizeThatFits:constrainedSize.max]; ASDisplayNodeLogEvent(self, @"calculatedSize: %@", NSStringFromCGSize(size)); return [ASLayout layoutWithLayoutElement:self size:ASSizeRangeClamp(constrainedSize, size) sublayouts:nil]; diff --git a/Source/Base/ASAvailability.h b/Source/Base/ASAvailability.h index 393550600..0a9737512 100644 --- a/Source/Base/ASAvailability.h +++ b/Source/Base/ASAvailability.h @@ -45,7 +45,7 @@ // in the ASDisplayNode tree (based on .yogaChildren). When disabled, ASYogaLayoutSpec is used, with a // disjoint Yoga tree for each level in the hierarchy. Currently, both modes are experimental. #ifndef YOGA_TREE_CONTIGUOUS - #define YOGA_TREE_CONTIGUOUS 0 // To enable, set to YOGA, as the code depends on YOGA also being set. + #define YOGA_TREE_CONTIGUOUS YOGA // To enable, set to YOGA, as the code depends on YOGA also being set. #endif #define AS_PIN_REMOTE_IMAGE __has_include() diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 730975dc2..d2d5974ea 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -415,7 +415,7 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map nodeBlock = [dataSource dataController:self supplementaryNodeBlockOfKind:kind atIndexPath:indexPath]; } - ASSizeRange constrainedSize; + ASSizeRange constrainedSize = ASSizeRangeUnconstrained; if (shouldFetchSizeRanges) { constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; } diff --git a/Source/Layout/ASDimension.h b/Source/Layout/ASDimension.h index 74922a62c..92257efc3 100644 --- a/Source/Layout/ASDimension.h +++ b/Source/Layout/ASDimension.h @@ -316,6 +316,8 @@ typedef struct { extern ASEdgeInsets const ASEdgeInsetsZero; +extern ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets); + #endif NS_ASSUME_NONNULL_END diff --git a/Source/Layout/ASDimension.mm b/Source/Layout/ASDimension.mm index 0b71cb088..603885e44 100644 --- a/Source/Layout/ASDimension.mm +++ b/Source/Layout/ASDimension.mm @@ -115,4 +115,14 @@ ASSizeRange ASSizeRangeIntersect(ASSizeRange sizeRange, ASSizeRange otherSizeRan #if YOGA #pragma mark - Yoga - ASEdgeInsets ASEdgeInsets const ASEdgeInsetsZero = {}; + +extern ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets) +{ + ASEdgeInsets asEdgeInsets = ASEdgeInsetsZero; + asEdgeInsets.top = ASDimensionMake(edgeInsets.top); + asEdgeInsets.left = ASDimensionMake(edgeInsets.left); + asEdgeInsets.bottom = ASDimensionMake(edgeInsets.bottom); + asEdgeInsets.right = ASDimensionMake(edgeInsets.right); + return asEdgeInsets; +} #endif diff --git a/Source/Layout/ASLayoutElement.mm b/Source/Layout/ASLayoutElement.mm index 047c62a44..c8ad2d379 100644 --- a/Source/Layout/ASLayoutElement.mm +++ b/Source/Layout/ASLayoutElement.mm @@ -15,17 +15,19 @@ // http://www.apache.org/licenses/LICENSE-2.0 // -#import "ASDisplayNode+FrameworkPrivate.h" +#import #import #import #import #import #import +#import #import #if YOGA #import YOGA_HEADER_PATH + #import #endif #pragma mark - ASLayoutElementContext @@ -110,6 +112,21 @@ void ASLayoutElementPopContext() NSString * const ASLayoutElementStyleLayoutPositionProperty = @"ASLayoutElementStyleLayoutPositionProperty"; +#if YOGA +NSString * const ASYogaFlexWrapProperty = @"ASLayoutElementStyleLayoutFlexWrapProperty"; +NSString * const ASYogaFlexDirectionProperty = @"ASYogaFlexDirectionProperty"; +NSString * const ASYogaDirectionProperty = @"ASYogaDirectionProperty"; +NSString * const ASYogaSpacingProperty = @"ASYogaSpacingProperty"; +NSString * const ASYogaJustifyContentProperty = @"ASYogaJustifyContentProperty"; +NSString * const ASYogaAlignItemsProperty = @"ASYogaAlignItemsProperty"; +NSString * const ASYogaPositionTypeProperty = @"ASYogaPositionTypeProperty"; +NSString * const ASYogaPositionProperty = @"ASYogaPositionProperty"; +NSString * const ASYogaMarginProperty = @"ASYogaMarginProperty"; +NSString * const ASYogaPaddingProperty = @"ASYogaPaddingProperty"; +NSString * const ASYogaBorderProperty = @"ASYogaBorderProperty"; +NSString * const ASYogaAspectRatioProperty = @"ASYogaAspectRatioProperty"; +#endif + #define ASLayoutElementStyleSetSizeWithScope(x) \ __instanceLock__.lock(); \ ASLayoutElementSize newSize = _size.load(); \ @@ -119,6 +136,7 @@ void ASLayoutElementPopContext() #define ASLayoutElementStyleCallDelegate(propertyName)\ do {\ + [self propertyDidChange:propertyName];\ [_delegate style:self propertyDidChange:propertyName];\ } while(0) @@ -138,9 +156,10 @@ @implementation ASLayoutElementStyle { std::atomic _layoutPosition; #if YOGA + YGNodeRef _yogaNode; + std::atomic _flexWrap; std::atomic _flexDirection; std::atomic _direction; - std::atomic _spacing; std::atomic _justifyContent; std::atomic _alignItems; std::atomic _positionType; @@ -149,7 +168,6 @@ @implementation ASLayoutElementStyle { std::atomic _padding; std::atomic _border; std::atomic _aspectRatio; - std::atomic _flexWrap; #endif } @@ -590,13 +608,153 @@ - (NSString *)description return result; } +- (void)propertyDidChange:(NSString *)propertyName +{ +#if YOGA + /* TODO(appleguy): STYLE SETTER METHODS LEFT TO IMPLEMENT + void YGNodeStyleSetOverflow(YGNodeRef node, YGOverflow overflow); + void YGNodeStyleSetFlex(YGNodeRef node, float flex); + */ + + if (_yogaNode == NULL) { + return; + } + // Because the NSStrings used to identify each property are const, use efficient pointer comparison. + if (propertyName == ASLayoutElementStyleWidthProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, Width, self.width); + } + else if (propertyName == ASLayoutElementStyleMinWidthProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinWidth, self.minWidth); + } + else if (propertyName == ASLayoutElementStyleMaxWidthProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxWidth, self.maxWidth); + } + else if (propertyName == ASLayoutElementStyleHeightProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, Height, self.height); + } + else if (propertyName == ASLayoutElementStyleMinHeightProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinHeight, self.minHeight); + } + else if (propertyName == ASLayoutElementStyleMaxHeightProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxHeight, self.maxHeight); + } + else if (propertyName == ASLayoutElementStyleFlexGrowProperty) { + YGNodeStyleSetFlexGrow(_yogaNode, self.flexGrow); + } + else if (propertyName == ASLayoutElementStyleFlexShrinkProperty) { + YGNodeStyleSetFlexShrink(_yogaNode, self.flexShrink); + } + else if (propertyName == ASLayoutElementStyleFlexBasisProperty) { + YGNODE_STYLE_SET_DIMENSION(_yogaNode, FlexBasis, self.flexBasis); + } + else if (propertyName == ASLayoutElementStyleAlignSelfProperty) { + YGNodeStyleSetAlignSelf(_yogaNode, yogaAlignSelf(self.alignSelf)); + } + else if (propertyName == ASYogaFlexWrapProperty) { + YGNodeStyleSetFlexWrap(_yogaNode, self.flexWrap); + } + else if (propertyName == ASYogaFlexDirectionProperty) { + YGNodeStyleSetFlexDirection(_yogaNode, yogaFlexDirection(self.flexDirection)); + } + else if (propertyName == ASYogaDirectionProperty) { + YGNodeStyleSetDirection(_yogaNode, self.direction); + } + else if (propertyName == ASYogaJustifyContentProperty) { + YGNodeStyleSetJustifyContent(_yogaNode, yogaJustifyContent(self.justifyContent)); + } + else if (propertyName == ASYogaAlignItemsProperty) { + ASStackLayoutAlignItems alignItems = self.alignItems; + if (alignItems != ASStackLayoutAlignItemsNotSet) { + YGNodeStyleSetAlignItems(_yogaNode, yogaAlignItems(alignItems)); + } + } + else if (propertyName == ASYogaPositionTypeProperty) { + YGNodeStyleSetPositionType(_yogaNode, self.positionType); + } + else if (propertyName == ASYogaPositionProperty) { + ASEdgeInsets position = self.position; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Position, dimensionForEdgeWithEdgeInsets(edge, position), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaMarginProperty) { + ASEdgeInsets margin = self.margin; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Margin, dimensionForEdgeWithEdgeInsets(edge, margin), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaPaddingProperty) { + ASEdgeInsets padding = self.padding; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(_yogaNode, Padding, dimensionForEdgeWithEdgeInsets(edge, padding), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaBorderProperty) { + ASEdgeInsets border = self.border; + YGEdge edge = YGEdgeLeft; + for (int i = 0; i < YGEdgeAll + 1; ++i) { + YGNODE_STYLE_SET_FLOAT_WITH_EDGE(_yogaNode, Border, dimensionForEdgeWithEdgeInsets(edge, border), edge); + edge = (YGEdge)(edge + 1); + } + } + else if (propertyName == ASYogaAspectRatioProperty) { + CGFloat aspectRatio = self.aspectRatio; + if (aspectRatio > FLT_EPSILON && aspectRatio < CGFLOAT_MAX / 2.0) { + YGNodeStyleSetAspectRatio(_yogaNode, aspectRatio); + } + } +#endif +} + #pragma mark - Yoga Flexbox Properties #if YOGA ++ (void)initialize +{ + [super initialize]; + YGConfigSetPointScaleFactor(YGConfigGetDefault(), ASScreenScale()); + // Yoga recommends using Web Defaults for all new projects. This will be enabled for Texture very soon. + //YGConfigSetUseWebDefaults(YGConfigGetDefault(), true); +} + +- (YGNodeRef)yogaNode +{ + return _yogaNode; +} + +- (YGNodeRef)yogaNodeCreateIfNeeded +{ + if (_yogaNode == NULL) { + _yogaNode = YGNodeNew(); + } + return _yogaNode; +} + +- (void)destroyYogaNode +{ + if (_yogaNode != NULL) { + // Release the __bridge_retained Context object. + ASLayoutElementYogaUpdateMeasureFunc(_yogaNode, nil); + YGNodeFree(_yogaNode); + _yogaNode = NULL; + } +} + +- (void)dealloc +{ + [self destroyYogaNode]; +} + +- (YGWrap)flexWrap { return _flexWrap.load(); } - (ASStackLayoutDirection)flexDirection { return _flexDirection.load(); } - (YGDirection)direction { return _direction.load(); } -- (CGFloat)spacing { return _spacing.load(); } - (ASStackLayoutJustifyContent)justifyContent { return _justifyContent.load(); } - (ASStackLayoutAlignItems)alignItems { return _alignItems.load(); } - (YGPositionType)positionType { return _positionType.load(); } @@ -605,22 +763,53 @@ - (ASEdgeInsets)margin { return _margin.load(); } - (ASEdgeInsets)padding { return _padding.load(); } - (ASEdgeInsets)border { return _border.load(); } - (CGFloat)aspectRatio { return _aspectRatio.load(); } -- (YGWrap)flexWrap { return _flexWrap.load(); } -- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection { _flexDirection.store(flexDirection); } -- (void)setDirection:(YGDirection)direction { _direction.store(direction); } -- (void)setSpacing:(CGFloat)spacing { _spacing.store(spacing); } -- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify { _justifyContent.store(justify); } -- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { _alignItems.store(alignItems); } -- (void)setPositionType:(YGPositionType)positionType { _positionType.store(positionType); } -- (void)setPosition:(ASEdgeInsets)position { _position.store(position); } -- (void)setMargin:(ASEdgeInsets)margin { _margin.store(margin); } -- (void)setPadding:(ASEdgeInsets)padding { _padding.store(padding); } -- (void)setBorder:(ASEdgeInsets)border { _border.store(border); } -- (void)setAspectRatio:(CGFloat)aspectRatio { _aspectRatio.store(aspectRatio); } -- (void)setFlexWrap:(YGWrap)flexWrap { _flexWrap.store(flexWrap); } +- (void)setFlexWrap:(YGWrap)flexWrap { + _flexWrap.store(flexWrap); + ASLayoutElementStyleCallDelegate(ASYogaFlexWrapProperty); +} +- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection { + _flexDirection.store(flexDirection); + ASLayoutElementStyleCallDelegate(ASYogaFlexDirectionProperty); +} +- (void)setDirection:(YGDirection)direction { + _direction.store(direction); + ASLayoutElementStyleCallDelegate(ASYogaDirectionProperty); +} +- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify { + _justifyContent.store(justify); + ASLayoutElementStyleCallDelegate(ASYogaJustifyContentProperty); +} +- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { + _alignItems.store(alignItems); + ASLayoutElementStyleCallDelegate(ASYogaAlignItemsProperty); +} +- (void)setPositionType:(YGPositionType)positionType { + _positionType.store(positionType); + ASLayoutElementStyleCallDelegate(ASYogaPositionTypeProperty); +} +- (void)setPosition:(ASEdgeInsets)position { + _position.store(position); + ASLayoutElementStyleCallDelegate(ASYogaPositionProperty); +} +- (void)setMargin:(ASEdgeInsets)margin { + _margin.store(margin); + ASLayoutElementStyleCallDelegate(ASYogaMarginProperty); +} +- (void)setPadding:(ASEdgeInsets)padding { + _padding.store(padding); + ASLayoutElementStyleCallDelegate(ASYogaPaddingProperty); +} +- (void)setBorder:(ASEdgeInsets)border { + _border.store(border); + ASLayoutElementStyleCallDelegate(ASYogaBorderProperty); +} +- (void)setAspectRatio:(CGFloat)aspectRatio { + _aspectRatio.store(aspectRatio); + ASLayoutElementStyleCallDelegate(ASYogaAspectRatioProperty); +} -#endif +#endif /* YOGA */ #pragma mark Deprecated diff --git a/Source/Layout/ASYogaUtilities.h b/Source/Layout/ASYogaUtilities.h index 5327b5a33..7ad607af2 100644 --- a/Source/Layout/ASYogaUtilities.h +++ b/Source/Layout/ASYogaUtilities.h @@ -17,6 +17,16 @@ #import #import +#define ASYogaLog(...) //NSLog(__VA_ARGS__) + +@interface ASDisplayNode (YogaHelpers) + ++ (ASDisplayNode *)yogaNode; ++ (ASDisplayNode *)verticalYogaStack; ++ (ASDisplayNode *)horizontalYogaStack; + +@end + extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node)); ASDISPLAYNODE_EXTERN_C_BEGIN @@ -32,6 +42,7 @@ float yogaDimensionToPoints(ASDimension dimension); float yogaDimensionToPercent(ASDimension dimension); ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets); +void ASLayoutElementYogaUpdateMeasureFunc(YGNodeRef yogaNode, id layoutElement); YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode); diff --git a/Source/Layout/ASYogaUtilities.mm b/Source/Layout/ASYogaUtilities.mm index 9d355c92f..abfc2184a 100644 --- a/Source/Layout/ASYogaUtilities.mm +++ b/Source/Layout/ASYogaUtilities.mm @@ -14,6 +14,32 @@ #if YOGA /* YOGA */ +@implementation ASDisplayNode (YogaHelpers) + ++ (ASDisplayNode *)yogaNode +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.automaticallyManagesSubnodes = YES; + [node.style yogaNodeCreateIfNeeded]; + return node; +} + ++ (ASDisplayNode *)verticalYogaStack +{ + ASDisplayNode *stack = [self yogaNode]; + stack.style.flexDirection = ASStackLayoutDirectionVertical; + return stack; +} + ++ (ASDisplayNode *)horizontalYogaStack +{ + ASDisplayNode *stack = [self yogaNode]; + stack.style.flexDirection = ASStackLayoutDirectionHorizontal; + return stack; +} + +@end + extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node)) { if (node == nil) { @@ -109,6 +135,27 @@ ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets) } } +void ASLayoutElementYogaUpdateMeasureFunc(YGNodeRef yogaNode, id layoutElement) +{ + if (yogaNode == NULL) { + return; + } + BOOL hasMeasureFunc = (YGNodeGetMeasureFunc(yogaNode) != NULL); + if (layoutElement != nil && hasMeasureFunc == NO) { + // TODO(appleguy): Add override detection for calculateSizeThatFits: and calculateLayoutThatFits:, + // then we can set the MeasureFunc only for nodes that override one of the trio of measurement methods. + // if (_layoutSpecBlock == NULL && (_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits) == 0 && ...) { + // Retain the Context object. This must be explicitly released with a __bridge_transfer; YGNodeFree() is not sufficient. + YGNodeSetContext(yogaNode, (__bridge_retained void *)layoutElement); + YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc); + } else if (layoutElement == nil && hasMeasureFunc == YES){ + // Release the __bridge_retained Context object. + __unused id element = (__bridge_transfer id)YGNodeGetContext(yogaNode); + YGNodeSetContext(yogaNode, NULL); + YGNodeSetMeasureFunc(yogaNode, NULL); + } +} + YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) { @@ -133,6 +180,14 @@ YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasure sizeRange.min.height = (minHeight.unit == ASDimensionUnitPoints ? yogaDimensionToPoints(minHeight) : 0.0); } + ASDisplayNodeCAssert(isnan(sizeRange.min.width) == NO && isnan(sizeRange.min.height) == NO, @"Yoga size range for measurement should not have NaN in minimum"); + if (isnan(sizeRange.max.width)) { + sizeRange.max.width = CGFLOAT_MAX; + } + if (isnan(sizeRange.max.height)) { + sizeRange.max.height = CGFLOAT_MAX; + } + CGSize size = [[layoutElement layoutThatFits:sizeRange] size]; return (YGSize){ .width = (float)size.width, .height = (float)size.height }; } diff --git a/Source/Private/ASDisplayNode+FrameworkPrivate.h b/Source/Private/ASDisplayNode+FrameworkPrivate.h index 9bdfe0a46..ebcaf23c1 100644 --- a/Source/Private/ASDisplayNode+FrameworkPrivate.h +++ b/Source/Private/ASDisplayNode+FrameworkPrivate.h @@ -56,8 +56,6 @@ typedef NS_OPTIONS(NSUInteger, ASHierarchyState) /** One of the supernodes of this node is performing a transition. Any layout calculated during this state should not be applied immediately, but pending until later. */ ASHierarchyStateLayoutPending = 1 << 3, - ASHierarchyStateYogaLayoutEnabled = 1 << 4, - ASHierarchyStateYogaLayoutMeasuring = 1 << 5 }; ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState) @@ -70,16 +68,6 @@ ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRangeManaged(ASHierarchyState return ((hierarchyState & ASHierarchyStateRangeManaged) == ASHierarchyStateRangeManaged); } -ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesYogaLayoutMeasuring(ASHierarchyState hierarchyState) -{ - return ((hierarchyState & ASHierarchyStateYogaLayoutMeasuring) == ASHierarchyStateYogaLayoutMeasuring); -} - -ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesYogaLayoutEnabled(ASHierarchyState hierarchyState) -{ - return ((hierarchyState & ASHierarchyStateYogaLayoutEnabled) == ASHierarchyStateYogaLayoutEnabled); -} - ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRasterized(ASHierarchyState hierarchyState) { return ((hierarchyState & ASHierarchyStateRasterized) == ASHierarchyStateRasterized); diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 7d16fb8b1..797740c91 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -58,6 +58,7 @@ typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) typedef NS_OPTIONS(uint_least32_t, ASDisplayNodeAtomicFlags) { Synchronous = 1 << 0, + YogaLayoutInProgress = 1 << 1, }; #define checkFlag(flag) ((_atomicFlags.load() & flag) != 0) @@ -204,7 +205,6 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo NSMutableArray *_yogaChildren; #endif #if YOGA_TREE_CONTIGUOUS - YGNodeRef _yogaNode; __weak ASDisplayNode *_yogaParent; ASLayout *_yogaCalculatedLayout; #endif diff --git a/examples/ASDKgram/Podfile b/examples/ASDKgram/Podfile index 8c63ccf5b..3079a3f1d 100644 --- a/examples/ASDKgram/Podfile +++ b/examples/ASDKgram/Podfile @@ -3,4 +3,5 @@ platform :ios, '8.0' target 'Sample' do pod 'Texture/IGListKit', :path => '../..' pod 'Texture/PINRemoteImage', :path => '../..' + pod 'Texture/Yoga', :path => '../..' end diff --git a/examples/ASDKgram/Sample/PhotoCellNode.m b/examples/ASDKgram/Sample/PhotoCellNode.m index a4f79d6e0..29d70474b 100644 --- a/examples/ASDKgram/Sample/PhotoCellNode.m +++ b/examples/ASDKgram/Sample/PhotoCellNode.m @@ -29,6 +29,7 @@ // There are many ways to format ASLayoutSpec code. In this example, we offer two different formats: // A flatter, more ordinary Objective-C style; or a more structured, "visually" declarative style. +#define YOGA_LAYOUT 0 #define FLAT_LAYOUT 0 #define DEBUG_PHOTOCELL_LAYOUT 0 @@ -106,6 +107,8 @@ - (instancetype)initWithPhotoObject:(PhotoModel *)photo; // instead of adding everything addSubnode: self.automaticallyManagesSubnodes = YES; + + [self setupYogaLayoutIfNeeded]; #if DEBUG_PHOTOCELL_LAYOUT _userAvatarImageNode.backgroundColor = [UIColor greenColor]; @@ -121,6 +124,7 @@ - (instancetype)initWithPhotoObject:(PhotoModel *)photo; return self; } +#if !YOGA_LAYOUT - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize { // There are many ways to format ASLayoutSpec code. In this example, we offer two different formats: @@ -267,6 +271,7 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize ]]; } } +#endif #pragma mark - Instance Methods @@ -298,4 +303,71 @@ - (void)loadCommentsForPhoto:(PhotoModel *)photo } } +- (void)setupYogaLayoutIfNeeded +{ +#if YOGA_LAYOUT + [self.style yogaNodeCreateIfNeeded]; + [_userAvatarImageNode.style yogaNodeCreateIfNeeded]; + [_userNameLabel.style yogaNodeCreateIfNeeded]; + [_photoImageNode.style yogaNodeCreateIfNeeded]; + [_photoCommentsNode.style yogaNodeCreateIfNeeded]; + [_photoLikesLabel.style yogaNodeCreateIfNeeded]; + [_photoDescriptionLabel.style yogaNodeCreateIfNeeded]; + [_photoLocationLabel.style yogaNodeCreateIfNeeded]; + [_photoTimeIntervalSincePostLabel.style yogaNodeCreateIfNeeded]; + + ASDisplayNode *headerStack = [ASDisplayNode horizontalYogaStack]; + headerStack.style.margin = ASEdgeInsetsMake(InsetForHeader); + headerStack.style.alignItems = ASStackLayoutAlignItemsCenter; + headerStack.style.flexGrow = 1.0; + + // Avatar Image, with inset - first thing in the header stack. + _userAvatarImageNode.style.preferredSize = CGSizeMake(USER_IMAGE_HEIGHT, USER_IMAGE_HEIGHT); + _userAvatarImageNode.style.margin = ASEdgeInsetsMake(InsetForAvatar); + [headerStack addYogaChild:_userAvatarImageNode]; + + // User Name and Photo Location stack is next + ASDisplayNode *userPhotoLocationStack = [ASDisplayNode verticalYogaStack]; + userPhotoLocationStack.style.flexShrink = 1.0; + [headerStack addYogaChild:userPhotoLocationStack]; + + // Setup the inside of the User Name and Photo Location stack. + _userNameLabel.style.flexShrink = 1.0; + [userPhotoLocationStack addYogaChild:_userNameLabel]; + + if (_photoLocationLabel.attributedText) { + _photoLocationLabel.style.flexShrink = 1.0; + [userPhotoLocationStack addYogaChild:_photoLocationLabel]; + } + +/* TODO: These parameters aren't working as expected. For now the timestamp is next to the username. + // Add a spacer to allow a flexible space between the User Name / Location stack, and the Timestamp. + ASDisplayNode *spacer = [ASDisplayNode new]; + spacer.style.flexShrink = 1.0; + spacer.style.width = ASDimensionMakeWithFraction(1.0); + [headerStack addYogaChild:spacer]; +*/ + + // Photo Timestamp Label. + _photoTimeIntervalSincePostLabel.style.spacingBefore = HORIZONTAL_BUFFER; + [headerStack addYogaChild:_photoTimeIntervalSincePostLabel]; + + // Create the last stack before assembling everything: the Footer Stack contains the description and comments. + ASDisplayNode *footerStack = [ASDisplayNode verticalYogaStack]; + footerStack.style.margin = ASEdgeInsetsMake(InsetForFooter); + footerStack.style.padding = ASEdgeInsetsMake(UIEdgeInsetsMake(0.0, 0.0, VERTICAL_BUFFER, 0.0)); + footerStack.yogaChildren = @[_photoLikesLabel, _photoDescriptionLabel, _photoCommentsNode]; + + // Main Vertical Stack: contains header, large main photo with fixed aspect ratio, and footer. + _photoImageNode.style.aspectRatio = 1.0; + + ASDisplayNode *verticalStack = self; + self.style.flexDirection = ASStackLayoutDirectionVertical; + + [verticalStack addYogaChild:headerStack]; + [verticalStack addYogaChild:_photoImageNode]; + [verticalStack addYogaChild:footerStack]; +#endif +} + @end