diff --git a/CHANGELOG.md b/CHANGELOG.md index 42bc339561..e26c62f899 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 4078e936b2..84f3df166d 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 9bcdfcc99a..ab31bfa1d8 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 @@ // 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 @@ 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 @@ #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 @@ _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 @@ 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)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 @@ 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 2929227b20..1b548834a6 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -424,12 +424,6 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [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 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) 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 3935506002..0a9737512b 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 730975dc20..d2d5974ea3 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -415,7 +415,7 @@ typedef void (^ASDataControllerCompletionBlock)(NSArray * 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 74922a62c1..92257efc35 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 0b71cb0885..603885e441 100644 --- a/Source/Layout/ASDimension.mm +++ b/Source/Layout/ASDimension.mm @@ -115,4 +115,14 @@ NSString *NSStringFromASSizeRange(ASSizeRange sizeRange) #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 047c62a441..c8ad2d379f 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 @@ NSString * const ASLayoutElementStyleDescenderProperty = @"ASLayoutElementStyleD 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 @@ NSString * const ASLayoutElementStyleLayoutPositionProperty = @"ASLayoutElementS #define ASLayoutElementStyleCallDelegate(propertyName)\ do {\ + [self propertyDidChange:propertyName];\ [_delegate style:self propertyDidChange:propertyName];\ } while(0) @@ -138,9 +156,10 @@ do {\ 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 @@ do {\ std::atomic _padding; std::atomic _border; std::atomic _aspectRatio; - std::atomic _flexWrap; #endif } @@ -590,13 +608,153 @@ do {\ 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 @@ do {\ - (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 5327b5a335..7ad607af24 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 9d355c92f4..abfc2184ae 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 9bdfe0a46c..ebcaf23c1c 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 7d16fb8b1b..797740c91a 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 8c63ccf5b5..3079a3f1d2 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 a4f79d6e0b..29d70474bb 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 @@ // instead of adding everything addSubnode: self.automaticallyManagesSubnodes = YES; + + [self setupYogaLayoutIfNeeded]; #if DEBUG_PHOTOCELL_LAYOUT _userAvatarImageNode.backgroundColor = [UIColor greenColor]; @@ -121,6 +124,7 @@ 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 @@ ]]; } } +#endif #pragma mark - Instance Methods @@ -298,4 +303,71 @@ } } +- (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