diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index a807a817d6..85b121acea 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -180,6 +180,10 @@ 8BBBAB8D1CEBAF1E00107FC6 /* ASDefaultPlaybackButton.m in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.m */; }; 8BDA5FC71CDBDF91007D13B2 /* ASVideoPlayerNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */; }; + 9019FBBD1ED8061D00C45F72 /* ASYogaLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 9019FBB91ED8061D00C45F72 /* ASYogaLayoutSpec.h */; }; + 9019FBBE1ED8061D00C45F72 /* ASYogaLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9019FBBA1ED8061D00C45F72 /* ASYogaLayoutSpec.mm */; }; + 9019FBBF1ED8061D00C45F72 /* ASYogaUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 9019FBBB1ED8061D00C45F72 /* ASYogaUtilities.h */; }; + 9019FBC01ED8061D00C45F72 /* ASYogaUtilities.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9019FBBC1ED8061D00C45F72 /* ASYogaUtilities.mm */; }; 90FC784F1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm in Sources */ = {isa = PBXBuildFile; fileRef = 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */; }; 92DD2FE61BF4D05E0074C9DD /* MapKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; 92DD2FE71BF4D0850074C9DD /* ASMapNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */; }; @@ -676,6 +680,10 @@ 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASDefaultPlaybackButton.m; sourceTree = ""; }; 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASVideoPlayerNode.h; sourceTree = ""; }; 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASVideoPlayerNode.mm; sourceTree = ""; }; + 9019FBB91ED8061D00C45F72 /* ASYogaLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASYogaLayoutSpec.h; sourceTree = ""; }; + 9019FBBA1ED8061D00C45F72 /* ASYogaLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASYogaLayoutSpec.mm; sourceTree = ""; }; + 9019FBBB1ED8061D00C45F72 /* ASYogaUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASYogaUtilities.h; sourceTree = ""; }; + 9019FBBC1ED8061D00C45F72 /* ASYogaUtilities.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASYogaUtilities.mm; sourceTree = ""; }; 90FC784E1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+Yoga.mm"; sourceTree = ""; }; 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMapNode.h; sourceTree = ""; }; 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMapNode.mm; sourceTree = ""; }; @@ -1481,6 +1489,10 @@ 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */, ACF6ED161B17843500DA7C62 /* ASStackLayoutSpec.h */, ACF6ED171B17843500DA7C62 /* ASStackLayoutSpec.mm */, + 9019FBB91ED8061D00C45F72 /* ASYogaLayoutSpec.h */, + 9019FBBA1ED8061D00C45F72 /* ASYogaLayoutSpec.mm */, + 9019FBBB1ED8061D00C45F72 /* ASYogaUtilities.h */, + 9019FBBC1ED8061D00C45F72 /* ASYogaUtilities.mm */, ); path = Layout; sourceTree = ""; @@ -1643,6 +1655,7 @@ B35062571B010F070018CF92 /* ASAssert.h in Headers */, CCBBBF5D1EB161760069AA91 /* ASRangeManagingNode.h in Headers */, B35062581B010F070018CF92 /* ASAvailability.h in Headers */, + 9019FBBF1ED8061D00C45F72 /* ASYogaUtilities.h in Headers */, DE84918D1C8FFF2B003D89E9 /* ASRunLoopQueue.h in Headers */, CC0F88621E4281E200576FED /* ASSectionController.h in Headers */, A2763D7A1CBDD57D00A9ADBD /* ASPINRemoteImageDownloader.h in Headers */, @@ -1751,6 +1764,7 @@ CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */, 83A7D95C1D44548100BF333E /* ASWeakMap.h in Headers */, E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */, + 9019FBBD1ED8061D00C45F72 /* ASYogaLayoutSpec.h in Headers */, 6947B0BE1E36B4E30007C478 /* ASStackUnpositionedLayout.h in Headers */, CC4C2A771D88E3BF0039ACAB /* ASTraceEvent.h in Headers */, 254C6B7B1BF94DF4003EC431 /* ASTextKitRenderer+Positioning.h in Headers */, @@ -2086,6 +2100,7 @@ AC026B721BD57DBF00BBC17E /* _ASHierarchyChangeSet.mm in Sources */, B35062421B010EFD0018CF92 /* _ASAsyncTransactionGroup.m in Sources */, CCA282BD1E9EABDD0037E8B7 /* ASTipProvider.m in Sources */, + 9019FBC01ED8061D00C45F72 /* ASYogaUtilities.mm in Sources */, B350624A1B010EFD0018CF92 /* _ASCoreAnimationExtras.mm in Sources */, 68EE0DC01C1B4ED300BA1B99 /* ASMainSerialQueue.mm in Sources */, B35062101B010EFD0018CF92 /* _ASDisplayLayer.mm in Sources */, @@ -2167,6 +2182,7 @@ 6907C25A1DC4ECFE00374C66 /* ASObjectDescriptionHelpers.m in Sources */, B35062051B010EFD0018CF92 /* ASMultiplexImageNode.mm in Sources */, B35062251B010EFD0018CF92 /* ASMutableAttributedStringBuilder.m in Sources */, + 9019FBBE1ED8061D00C45F72 /* ASYogaLayoutSpec.mm in Sources */, B35062071B010EFD0018CF92 /* ASNetworkImageNode.mm in Sources */, 34EFC76D1B701CF100AD841F /* ASOverlayLayoutSpec.mm in Sources */, 044285101BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.m in Sources */, diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d86b3204b..ef7a088d1e 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] Implement ASYogaLayoutSpec, a simplified integration strategy for Yoga-powered layout calculation. [Scott Goodson](https://github.com/appleguy) - Fixed an issue where calls to setNeedsDisplay and setNeedsLayout would stop working on loaded nodes. [Garrett Moon](https://github.com/garrettmoon) - [ASTextKitFontSizeAdjuster] [Ricky Cancro] Replace use of NSAttributedString's boundingRectWithSize:options:context: with NSLayoutManager's boundingRectForGlyphRange:inTextContainer: - Add support for IGListKit post-removal-of-IGListSectionType, in preparation for IGListKit 3.0.0 release. [Adlai Holler](https://github.com/Adlai-Holler) [#49](https://github.com/TextureGroup/Texture/pull/49) diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index 65d02790cc..4078e936b2 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -171,15 +171,18 @@ extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable @interface ASDisplayNode (Yoga) @property (nonatomic, strong, nullable) NSArray *yogaChildren; -@property (nonatomic, strong, nullable) ASLayout *yogaCalculatedLayout; - (void)addYogaChild:(ASDisplayNode *)child; - (void)removeYogaChild:(ASDisplayNode *)child; +- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute; + +#if YOGA_TREE_CONTIGUOUS +@property (nonatomic, strong, nullable) ASLayout *yogaCalculatedLayout; // These methods should not normally be called directly. - (void)invalidateCalculatedYogaLayout; - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize; -- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute; +#endif @end diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 4454e3561d..9bcdfcc99a 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -19,174 +19,108 @@ #if YOGA /* YOGA */ -#import -#import +#import +#import #import +#import #import +#import #import #define YOGA_LAYOUT_LOGGING 0 -extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable node, void(^block)(ASDisplayNode *node)) -{ - if (node == nil) { - return; - } - block(node); - for (ASDisplayNode *child in [node yogaChildren]) { - ASDisplayNodePerformBlockOnEveryYogaChild(child, block); - } -} - -#pragma mark - Yoga Type Conversion Helpers - -YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems); -YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent); -YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf); -YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction); -float yogaFloatForCGFloat(CGFloat value); -float yogaDimensionToPoints(ASDimension dimension); -float yogaDimensionToPercent(ASDimension dimension); -ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets); -YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, - float width, YGMeasureMode widthMode, - float height, YGMeasureMode heightMode); - -#define YGNODE_STYLE_SET_DIMENSION(yogaNode, property, dimension) \ - if (dimension.unit == ASDimensionUnitPoints) { \ - YGNodeStyleSet##property(yogaNode, yogaDimensionToPoints(dimension)); \ - } else if (dimension.unit == ASDimensionUnitFraction) { \ - YGNodeStyleSet##property##Percent(yogaNode, yogaDimensionToPercent(dimension)); \ - } else { \ - YGNodeStyleSet##property(yogaNode, YGUndefined); \ - }\ - -#define YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, property, dimension, edge) \ - if (dimension.unit == ASDimensionUnitPoints) { \ - YGNodeStyleSet##property(yogaNode, edge, yogaDimensionToPoints(dimension)); \ - } else if (dimension.unit == ASDimensionUnitFraction) { \ - YGNodeStyleSet##property##Percent(yogaNode, edge, yogaDimensionToPercent(dimension)); \ - } else { \ - YGNodeStyleSet##property(yogaNode, edge, YGUndefined); \ - } \ - -#define YGNODE_STYLE_SET_FLOAT_WITH_EDGE(yogaNode, property, dimension, edge) \ - if (dimension.unit == ASDimensionUnitPoints) { \ - YGNodeStyleSet##property(yogaNode, edge, yogaDimensionToPoints(dimension)); \ - } else if (dimension.unit == ASDimensionUnitFraction) { \ - ASDisplayNodeAssert(NO, @"Unexpected Fraction value in applying ##property## values to YGNode"); \ - } else { \ - YGNodeStyleSet##property(yogaNode, edge, YGUndefined); \ - } \ - -YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems) -{ - switch (alignItems) { - case ASStackLayoutAlignItemsNotSet: return YGAlignAuto; - case ASStackLayoutAlignItemsStart: return YGAlignFlexStart; - case ASStackLayoutAlignItemsEnd: return YGAlignFlexEnd; - case ASStackLayoutAlignItemsCenter: return YGAlignCenter; - case ASStackLayoutAlignItemsStretch: return YGAlignStretch; - case ASStackLayoutAlignItemsBaselineFirst: return YGAlignBaseline; - // FIXME: WARNING, Yoga does not currently support last-baseline item alignment. - case ASStackLayoutAlignItemsBaselineLast: return YGAlignBaseline; - } -} - -YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent) -{ - switch (justifyContent) { - case ASStackLayoutJustifyContentStart: return YGJustifyFlexStart; - case ASStackLayoutJustifyContentCenter: return YGJustifyCenter; - case ASStackLayoutJustifyContentEnd: return YGJustifyFlexEnd; - case ASStackLayoutJustifyContentSpaceBetween: return YGJustifySpaceBetween; - case ASStackLayoutJustifyContentSpaceAround: return YGJustifySpaceAround; - } -} - -YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf) -{ - switch (alignSelf) { - case ASStackLayoutAlignSelfStart: return YGAlignFlexStart; - case ASStackLayoutAlignSelfCenter: return YGAlignCenter; - case ASStackLayoutAlignSelfEnd: return YGAlignFlexEnd; - case ASStackLayoutAlignSelfStretch: return YGAlignStretch; - case ASStackLayoutAlignSelfAuto: return YGAlignAuto; - } -} - -YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction) -{ - return direction == ASStackLayoutDirectionVertical ? YGFlexDirectionColumn : YGFlexDirectionRow; -} - -float yogaFloatForCGFloat(CGFloat value) -{ - if (value < CGFLOAT_MAX / 2) { - return value; - } else { - return YGUndefined; - } -} - -float yogaDimensionToPoints(ASDimension dimension) -{ - ASDisplayNodeCAssert(dimension.unit == ASDimensionUnitPoints, - @"Dimensions should not be type Fraction for this method: %f", dimension.value); - return yogaFloatForCGFloat(dimension.value); -} - -float yogaDimensionToPercent(ASDimension dimension) -{ - ASDisplayNodeCAssert(dimension.unit == ASDimensionUnitFraction, - @"Dimensions should not be type Points for this method: %f", dimension.value); - return 100.0 * yogaFloatForCGFloat(dimension.value); - -} - -ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets) -{ - switch (edge) { - case YGEdgeLeft: return insets.left; - case YGEdgeTop: return insets.top; - case YGEdgeRight: return insets.right; - case YGEdgeBottom: return insets.bottom; - case YGEdgeStart: return insets.start; - case YGEdgeEnd: return insets.end; - case YGEdgeHorizontal: return insets.horizontal; - case YGEdgeVertical: return insets.vertical; - case YGEdgeAll: return insets.all; - default: ASDisplayNodeCAssert(NO, @"YGEdge other than ASEdgeInsets is not supported."); - return ASDimensionAuto; - } -} - -YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasureMode widthMode, - float height, YGMeasureMode heightMode) -{ - id layoutElement = (__bridge id )YGNodeGetContext(yogaNode); - ASSizeRange sizeRange; - sizeRange.max = CGSizeMake(width, height); - sizeRange.min = sizeRange.max; - if (widthMode == YGMeasureModeAtMost) { - sizeRange.min.width = 0.0; - } - if (heightMode == YGMeasureModeAtMost) { - sizeRange.min.height = 0.0; - } - CGSize size = [[layoutElement layoutThatFits:sizeRange] size]; - return (YGSize){ .width = (float)size.width, .height = (float)size.height }; -} - #pragma mark - ASDisplayNode+Yoga +#if YOGA_TREE_CONTIGUOUS + @interface ASDisplayNode (YogaInternal) @property (nonatomic, weak) ASDisplayNode *yogaParent; @property (nonatomic, assign) YGNodeRef yogaNode; @end +#endif /* YOGA_TREE_CONTIGUOUS */ + @implementation ASDisplayNode (Yoga) +- (void)setYogaChildren:(NSArray *)yogaChildren +{ + for (ASDisplayNode *child in _yogaChildren) { + // Make sure to un-associate the YGNodeRef tree before replacing _yogaChildren + // If this becomes a performance bottleneck, it can be optimized by not doing the NSArray removals here. + [self removeYogaChild:child]; + } + _yogaChildren = nil; + for (ASDisplayNode *child in yogaChildren) { + [self addYogaChild:child]; + } +} + +- (NSArray *)yogaChildren +{ + return _yogaChildren; +} + +- (void)addYogaChild:(ASDisplayNode *)child +{ + if (child == nil) { + return; + } + if (_yogaChildren == nil) { + _yogaChildren = [NSMutableArray array]; + } + + // Clean up state in case this child had another parent. + [self removeYogaChild:child]; + [_yogaChildren addObject:child]; + +#if YOGA_TREE_CONTIGUOUS + // 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; + self.layoutSpecBlock = ^ASLayoutSpec * _Nonnull(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) { + ASYogaLayoutSpec *spec = [[ASYogaLayoutSpec alloc] init]; + spec.rootNode = weakSelf; + spec.children = weakSelf.yogaChildren; + return spec; + }; +#endif +} + +- (void)removeYogaChild:(ASDisplayNode *)child +{ + if (child == nil) { + return; + } + [_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; + } +#else + if (_yogaChildren.count == 0) { + self.layoutSpecBlock = nil; + } +#endif +} + +- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute +{ + if (AS_AT_LEAST_IOS9) { + UIUserInterfaceLayoutDirection layoutDirection = + [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:attribute]; + self.style.direction = (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight + ? YGDirectionLTR : YGDirectionRTL); + } +} + +#if YOGA_TREE_CONTIGUOUS /* YOGA_TREE_CONTIGUOUS */ + - (void)setYogaNode:(YGNodeRef)yogaNode { _yogaNode = yogaNode; @@ -227,57 +161,6 @@ YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasure return _yogaParent; } -- (void)setYogaChildren:(NSArray *)yogaChildren -{ - for (ASDisplayNode *child in _yogaChildren) { - // Make sure to un-associate the YGNodeRef tree before replacing _yogaChildren - // If this becomes a performance bottleneck, it can be optimized by not doing the NSArray removals here. - [self removeYogaChild:child]; - } - _yogaChildren = nil; - for (ASDisplayNode *child in yogaChildren) { - [self addYogaChild:child]; - } -} - -- (NSArray *)yogaChildren -{ - return _yogaChildren; -} - -- (void)addYogaChild:(ASDisplayNode *)child -{ - if (child == nil) { - return; - } - if (_yogaChildren == nil) { - _yogaChildren = [NSMutableArray array]; - } - - // Clean up state in case this child had another parent. - [self removeYogaChild:child]; - - // YGNodeRef insertion is done in setParent: - child.yogaParent = self; - [_yogaChildren addObject:child]; - - self.hierarchyState |= ASHierarchyStateYogaLayoutEnabled; -} - -- (void)removeYogaChild:(ASDisplayNode *)child -{ - if (child == nil) { - return; - } - // YGNodeRef removal is done in setParent: - child.yogaParent = nil; - [_yogaChildren removeObjectIdenticalTo:child]; - - if (_yogaChildren.count == 0 && self.yogaParent == nil) { - self.hierarchyState &= ~ASHierarchyStateYogaLayoutEnabled; - } -} - - (void)setYogaCalculatedLayout:(ASLayout *)yogaCalculatedLayout { _yogaCalculatedLayout = yogaCalculatedLayout; @@ -338,16 +221,6 @@ YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasure self.yogaCalculatedLayout = nil; } -- (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute -{ - if (AS_AT_LEAST_IOS9) { - UIUserInterfaceLayoutDirection layoutDirection = - [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:attribute]; - self.style.direction = (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight - ? YGDirectionLTR : YGDirectionRTL); - } -} - - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize { if (self.yogaParent) { @@ -443,7 +316,7 @@ YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasure node.hierarchyState &= ~ASHierarchyStateYogaLayoutMeasuring; }); -#if YOGA_LAYOUT_LOGGING +#if YOGA_LAYOUT_LOGGING /* YOGA_LAYOUT_LOGGING */ // Concurrent layouts will interleave the NSLog messages unless we serialize. // Use @synchornize rather than trampolining to the main thread so the tree state isn't changed. @synchronized ([ASDisplayNode class]) { @@ -458,9 +331,11 @@ YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasure YGNodePrint(node.yogaNode, (YGPrintOptions)(YGPrintOptionsStyle | YGPrintOptionsLayout)); }); } -#endif +#endif /* YOGA_LAYOUT_LOGGING */ } +#endif /* YOGA_TREE_CONTIGUOUS */ + @end #endif /* YOGA */ diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index b6042722ef..6e1752b96f 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -429,7 +429,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) [self _scheduleIvarsForMainDeallocation]; } -#if YOGA +#if YOGA_TREE_CONTIGUOUS if (_yogaNode != NULL) { YGNodeFree(_yogaNode); } @@ -898,7 +898,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) _pendingDisplayNodeLayout->invalidate(); } -#if YOGA +#if YOGA_TREE_CONTIGUOUS [self invalidateCalculatedYogaLayout]; #endif } @@ -972,7 +972,7 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) ASDN::MutexLocker l(__instanceLock__); -#if YOGA /* YOGA */ +#if YOGA_TREE_CONTIGUOUS /* YOGA */ if (ASHierarchyStateIncludesYogaLayoutEnabled(_hierarchyState) == YES) { if (ASHierarchyStateIncludesYogaLayoutMeasuring(_hierarchyState) == NO && self.yogaCalculatedLayout == nil) { ASDN::MutexUnlocker ul(__instanceLock__); diff --git a/Source/Base/ASAvailability.h b/Source/Base/ASAvailability.h index 52f2045114..e2dedbb3c5 100644 --- a/Source/Base/ASAvailability.h +++ b/Source/Base/ASAvailability.h @@ -32,6 +32,7 @@ // If Yoga is available, make it available anywhere we use ASAvailability. // This reduces Yoga-specific code in other files. +// NOTE: Yoga integration is experimental and not fully tested. Use with caution and test layouts carefully. #ifndef YOGA_HEADER_PATH #define YOGA_HEADER_PATH #endif @@ -40,6 +41,13 @@ #define YOGA __has_include(YOGA_HEADER_PATH) #endif +// Contiguous Yoga layout attempts to build a connected tree of YGNodeRef objects, across multiple levels +// 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. +#endif + #define AS_PIN_REMOTE_IMAGE __has_include() #define AS_IG_LIST_KIT __has_include() diff --git a/Source/Layout/ASYogaLayoutSpec.h b/Source/Layout/ASYogaLayoutSpec.h new file mode 100644 index 0000000000..4323b484c0 --- /dev/null +++ b/Source/Layout/ASYogaLayoutSpec.h @@ -0,0 +1,26 @@ +// +// ASYogaLayoutSpec.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if YOGA /* YOGA */ +#if !YOGA_TREE_CONTIGUOUS /* !YOGA_TREE_CONTIGUOUS */ + +#import +#import + +@interface ASYogaLayoutSpec : ASLayoutSpec +@property (nonatomic, strong, nonnull) ASDisplayNode *rootNode; +@end + +#endif /* !YOGA_TREE_CONTIGUOUS */ +#endif /* YOGA */ diff --git a/Source/Layout/ASYogaLayoutSpec.mm b/Source/Layout/ASYogaLayoutSpec.mm new file mode 100644 index 0000000000..a5c34294b8 --- /dev/null +++ b/Source/Layout/ASYogaLayoutSpec.mm @@ -0,0 +1,180 @@ +// +// ASYogaLayoutSpec.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if YOGA /* YOGA */ +#if !YOGA_TREE_CONTIGUOUS /* !YOGA_TREE_CONTIGUOUS */ + +#import +#import +#import +#import +#import + +#define YOGA_LAYOUT_LOGGING 0 + +@implementation ASYogaLayoutSpec + +- (ASLayout *)layoutForYogaNode:(YGNodeRef)yogaNode +{ + BOOL isRootNode = (YGNodeGetParent(yogaNode) == NULL); + uint32_t childCount = YGNodeGetChildCount(yogaNode); + + NSMutableArray *sublayouts = [NSMutableArray arrayWithCapacity:childCount]; + for (uint32_t i = 0; i < childCount; i++) { + [sublayouts addObject:[self layoutForYogaNode:YGNodeGetChild(yogaNode, i)]]; + } + + id layoutElement = (__bridge id )YGNodeGetContext(yogaNode); + CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); + + if (isRootNode) { + // The layout for root should have position CGPointNull, but include the calculated size. + return [ASLayout layoutWithLayoutElement:layoutElement size:size sublayouts:sublayouts]; + } else { + 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:layoutElement size:size position:position sublayouts:nil]; + } +} + +- (void)destroyYogaNode:(YGNodeRef)yogaNode +{ + // Release the __bridge_retained Context object. + __unused id element = (__bridge_transfer id)YGNodeGetContext(yogaNode); + YGNodeFree(yogaNode); +} + +- (void)setupYogaNode:(YGNodeRef)yogaNode forElement:(id )element withParentYogaNode:(YGNodeRef)parentYogaNode +{ + ASLayoutElementStyle *style = element.style; + + // Retain the Context object. This must be explicitly released with a __bridge_transfer; YGNodeFree() is not sufficient. + YGNodeSetContext(yogaNode, (__bridge_retained void *)element); + + 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 (parentYogaNode != NULL) { + YGNodeInsertChild(parentYogaNode, yogaNode, YGNodeGetChildCount(parentYogaNode)); + + 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); + + YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc); + } + + // TODO(appleguy): STYLE SETTER METHODS LEFT TO IMPLEMENT: YGNodeStyleSetOverflow, YGNodeStyleSetFlex +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)layoutElementSize + relativeToParentSize:(CGSize)parentSize +{ + ASSizeRange styleAndParentSize = ASLayoutElementSizeResolve(layoutElementSize, parentSize); + const ASSizeRange rootConstrainedSize = ASSizeRangeIntersect(constrainedSize, styleAndParentSize); + + YGNodeRef rootYogaNode = YGNodeNew(); + + // YGNodeCalculateLayout currently doesn't offer the ability to pass a minimum size (max is passed there). + // Apply the constrainedSize.min directly to the root node so that layout accounts for it. + YGNodeStyleSetMinWidth (rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.width)); + YGNodeStyleSetMinHeight(rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.height)); + + // It's crucial to set these values. YGNodeCalculateLayout has unusual behavior for its width and height parameters: + // 1. If no maximum size set, infer this means YGMeasureModeExactly. Even if a small minWidth & minHeight are set, + // these will never be used because the output size of the root will always exactly match this value. + // 2. If a maximum size is set, infer that this means YGMeasureModeAtMost, and allow down to the min* values in output. + YGNodeStyleSetMaxWidthPercent(rootYogaNode, 100.0); + YGNodeStyleSetMaxHeightPercent(rootYogaNode, 100.0); + + [self setupYogaNode:rootYogaNode forElement:self.rootNode withParentYogaNode:NULL]; + for (id child in self.children) { + YGNodeRef yogaNode = YGNodeNew(); + [self setupYogaNode:yogaNode forElement:child withParentYogaNode:rootYogaNode]; + } + + // It is crucial to use yogaFloat... to convert CGFLOAT_MAX into YGUndefined here. + YGNodeCalculateLayout(rootYogaNode, + yogaFloatForCGFloat(rootConstrainedSize.max.width), + yogaFloatForCGFloat(rootConstrainedSize.max.height), + YGDirectionInherit); + + ASLayout *layout = [self layoutForYogaNode:rootYogaNode]; + +#if YOGA_LAYOUT_LOGGING + // Concurrent layouts will interleave the NSLog messages unless we serialize. + // Use @synchornize rather than trampolining to the main thread so the tree state isn't changed. + @synchronized ([ASDisplayNode class]) { + NSLog(@"****************************************************************************"); + NSLog(@"******************** STARTING YOGA -> ASLAYOUT CREATION ********************"); + NSLog(@"****************************************************************************"); + NSLog(@"node = %@", self.rootNode); + NSLog(@"style = %@", self.rootNode.style); + YGNodePrint(rootYogaNode, (YGPrintOptions)(YGPrintOptionsStyle | YGPrintOptionsLayout)); + } + NSLog(@"rootConstraint = (%@, %@), layout = %@, sublayouts = %@", NSStringFromCGSize(rootConstrainedSize.min), NSStringFromCGSize(rootConstrainedSize.max), layout, layout.sublayouts); +#endif + + while(YGNodeGetChildCount(rootYogaNode) > 0) { + YGNodeRef yogaNode = YGNodeGetChild(rootYogaNode, 0); + [self destroyYogaNode:yogaNode]; + } + [self destroyYogaNode:rootYogaNode]; + + return layout; +} + +@end + +#endif /* !YOGA_TREE_CONTIGUOUS */ +#endif /* YOGA */ diff --git a/Source/Layout/ASYogaUtilities.h b/Source/Layout/ASYogaUtilities.h new file mode 100644 index 0000000000..5327b5a335 --- /dev/null +++ b/Source/Layout/ASYogaUtilities.h @@ -0,0 +1,70 @@ +// +// ASYogaUtilities.h +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if YOGA /* YOGA */ + +#import +#import + +extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node)); + +ASDISPLAYNODE_EXTERN_C_BEGIN + +#pragma mark - Yoga Type Conversion Helpers + +YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems); +YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent); +YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf); +YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction); +float yogaFloatForCGFloat(CGFloat value); +float yogaDimensionToPoints(ASDimension dimension); +float yogaDimensionToPercent(ASDimension dimension); +ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets); + +YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, + float width, YGMeasureMode widthMode, + float height, YGMeasureMode heightMode); + +#pragma mark - Yoga Style Setter Helpers + +#define YGNODE_STYLE_SET_DIMENSION(yogaNode, property, dimension) \ + if (dimension.unit == ASDimensionUnitPoints) { \ + YGNodeStyleSet##property(yogaNode, yogaDimensionToPoints(dimension)); \ + } else if (dimension.unit == ASDimensionUnitFraction) { \ + YGNodeStyleSet##property##Percent(yogaNode, yogaDimensionToPercent(dimension)); \ + } else { \ + YGNodeStyleSet##property(yogaNode, YGUndefined); \ + }\ + +#define YGNODE_STYLE_SET_DIMENSION_WITH_EDGE(yogaNode, property, dimension, edge) \ + if (dimension.unit == ASDimensionUnitPoints) { \ + YGNodeStyleSet##property(yogaNode, edge, yogaDimensionToPoints(dimension)); \ + } else if (dimension.unit == ASDimensionUnitFraction) { \ + YGNodeStyleSet##property##Percent(yogaNode, edge, yogaDimensionToPercent(dimension)); \ + } else { \ + YGNodeStyleSet##property(yogaNode, edge, YGUndefined); \ + } \ + +#define YGNODE_STYLE_SET_FLOAT_WITH_EDGE(yogaNode, property, dimension, edge) \ + if (dimension.unit == ASDimensionUnitPoints) { \ + YGNodeStyleSet##property(yogaNode, edge, yogaDimensionToPoints(dimension)); \ + } else if (dimension.unit == ASDimensionUnitFraction) { \ + ASDisplayNodeAssert(NO, @"Unexpected Fraction value in applying ##property## values to YGNode"); \ + } else { \ + YGNodeStyleSet##property(yogaNode, edge, YGUndefined); \ + } \ + +ASDISPLAYNODE_EXTERN_C_END + +#endif /* YOGA */ diff --git a/Source/Layout/ASYogaUtilities.mm b/Source/Layout/ASYogaUtilities.mm new file mode 100644 index 0000000000..9d355c92f4 --- /dev/null +++ b/Source/Layout/ASYogaUtilities.mm @@ -0,0 +1,140 @@ +// +// ASYogaUtilities.mm +// Texture +// +// Copyright (c) 2017-present, Pinterest, Inc. All rights reserved. +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#if YOGA /* YOGA */ + +extern void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node)) +{ + if (node == nil) { + return; + } + block(node); + for (ASDisplayNode *child in [node yogaChildren]) { + ASDisplayNodePerformBlockOnEveryYogaChild(child, block); + } +} + +#pragma mark - Yoga Type Conversion Helpers + +YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems) +{ + switch (alignItems) { + case ASStackLayoutAlignItemsNotSet: return YGAlignAuto; + case ASStackLayoutAlignItemsStart: return YGAlignFlexStart; + case ASStackLayoutAlignItemsEnd: return YGAlignFlexEnd; + case ASStackLayoutAlignItemsCenter: return YGAlignCenter; + case ASStackLayoutAlignItemsStretch: return YGAlignStretch; + case ASStackLayoutAlignItemsBaselineFirst: return YGAlignBaseline; + // FIXME: WARNING, Yoga does not currently support last-baseline item alignment. + case ASStackLayoutAlignItemsBaselineLast: return YGAlignBaseline; + } +} + +YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent) +{ + switch (justifyContent) { + case ASStackLayoutJustifyContentStart: return YGJustifyFlexStart; + case ASStackLayoutJustifyContentCenter: return YGJustifyCenter; + case ASStackLayoutJustifyContentEnd: return YGJustifyFlexEnd; + case ASStackLayoutJustifyContentSpaceBetween: return YGJustifySpaceBetween; + case ASStackLayoutJustifyContentSpaceAround: return YGJustifySpaceAround; + } +} + +YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf) +{ + switch (alignSelf) { + case ASStackLayoutAlignSelfStart: return YGAlignFlexStart; + case ASStackLayoutAlignSelfCenter: return YGAlignCenter; + case ASStackLayoutAlignSelfEnd: return YGAlignFlexEnd; + case ASStackLayoutAlignSelfStretch: return YGAlignStretch; + case ASStackLayoutAlignSelfAuto: return YGAlignAuto; + } +} + +YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction) +{ + return direction == ASStackLayoutDirectionVertical ? YGFlexDirectionColumn : YGFlexDirectionRow; +} + +float yogaFloatForCGFloat(CGFloat value) +{ + if (value < CGFLOAT_MAX / 2) { + return value; + } else { + return YGUndefined; + } +} + +float yogaDimensionToPoints(ASDimension dimension) +{ + ASDisplayNodeCAssert(dimension.unit == ASDimensionUnitPoints, + @"Dimensions should not be type Fraction for this method: %f", dimension.value); + return yogaFloatForCGFloat(dimension.value); +} + +float yogaDimensionToPercent(ASDimension dimension) +{ + ASDisplayNodeCAssert(dimension.unit == ASDimensionUnitFraction, + @"Dimensions should not be type Points for this method: %f", dimension.value); + return 100.0 * yogaFloatForCGFloat(dimension.value); + +} + +ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets) +{ + switch (edge) { + case YGEdgeLeft: return insets.left; + case YGEdgeTop: return insets.top; + case YGEdgeRight: return insets.right; + case YGEdgeBottom: return insets.bottom; + case YGEdgeStart: return insets.start; + case YGEdgeEnd: return insets.end; + case YGEdgeHorizontal: return insets.horizontal; + case YGEdgeVertical: return insets.vertical; + case YGEdgeAll: return insets.all; + default: ASDisplayNodeCAssert(NO, @"YGEdge other than ASEdgeInsets is not supported."); + return ASDimensionAuto; + } +} + +YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasureMode widthMode, + float height, YGMeasureMode heightMode) +{ + id layoutElement = (__bridge id )YGNodeGetContext(yogaNode); + ASDisplayNodeCAssert([layoutElement conformsToProtocol:@protocol(ASLayoutElement)], @"Yoga context must be "); + + ASSizeRange sizeRange; + sizeRange.min = CGSizeZero; + sizeRange.max = CGSizeMake(width, height); + if (widthMode == YGMeasureModeExactly) { + sizeRange.min.width = sizeRange.max.width; + } else { + // Mode is (YGMeasureModeAtMost | YGMeasureModeUndefined) + ASDimension minWidth = layoutElement.style.minWidth; + sizeRange.min.width = (minWidth.unit == ASDimensionUnitPoints ? yogaDimensionToPoints(minWidth) : 0.0); + } + if (heightMode == YGMeasureModeExactly) { + sizeRange.min.height = sizeRange.max.height; + } else { + // Mode is (YGMeasureModeAtMost | YGMeasureModeUndefined) + ASDimension minHeight = layoutElement.style.minHeight; + sizeRange.min.height = (minHeight.unit == ASDimensionUnitPoints ? yogaDimensionToPoints(minHeight) : 0.0); + } + + CGSize size = [[layoutElement layoutThatFits:sizeRange] size]; + return (YGSize){ .width = (float)size.width, .height = (float)size.height }; +} + +#endif /* YOGA */ diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index 6175bd4a30..b276728d17 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -198,9 +198,14 @@ FOUNDATION_EXPORT NSString * const ASRenderingEngineDidDisplayNodesScheduledBefo NSInteger _layoutComputationNumberOfPasses; #if YOGA - YGNodeRef _yogaNode; - ASDisplayNode *_yogaParent; + // Only ASDisplayNodes are supported in _yogaChildren currently. This means that it is necessary to + // create ASDisplayNodes to make a stack layout when using Yoga. + // However, the implementation is mostly ready for id , with a few areas requiring updates. NSMutableArray *_yogaChildren; +#endif +#if YOGA_TREE_CONTIGUOUS + YGNodeRef _yogaNode; + __weak ASDisplayNode *_yogaParent; ASLayout *_yogaCalculatedLayout; #endif