// // ASLayout.mm // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. // Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // #import #import #import #import #import #import "ASLayoutSpecUtilities.h" #import "ASLayoutSpec+Subclasses.h" #import #import #import NSString *const ASThreadDictMaxConstraintSizeKey = @"kASThreadDictMaxConstraintSizeKey"; CGPoint const ASPointNull = {NAN, NAN}; BOOL ASPointIsNull(CGPoint point) { return isnan(point.x) && isnan(point.y); } /** * Creates an defined number of " |" indent blocks for the recursive description. */ ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString * descriptionIndents(NSUInteger indents) { NSMutableString *description = [NSMutableString string]; for (NSUInteger i = 0; i < indents; i++) { [description appendString:@" |"]; } if (indents > 0) { [description appendString:@" "]; } return description; } ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsDisplayNodeType(ASLayout *layout) { return layout.type == ASLayoutElementTypeDisplayNode; } @interface ASLayout () { ASLayoutElementType _layoutElementType; std::atomic_bool _retainSublayoutElements; } @end @implementation ASLayout @dynamic frame, type; static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(NO); + (void)setShouldRetainSublayoutLayoutElements:(BOOL)shouldRetain { static_retainsSublayoutLayoutElements.store(shouldRetain); } + (BOOL)shouldRetainSublayoutLayoutElements { return static_retainsSublayoutLayoutElements.load(); } - (instancetype)initWithLayoutElement:(id)layoutElement size:(CGSize)size position:(CGPoint)position sublayouts:(nullable NSArray *)sublayouts { NSParameterAssert(layoutElement); self = [super init]; if (self) { #if ASDISPLAYNODE_ASSERTIONS_ENABLED for (ASLayout *sublayout in sublayouts) { ASDisplayNodeAssert(ASPointIsNull(sublayout.position) == NO, @"Invalid position is not allowed in sublayout."); } #endif _layoutElement = layoutElement; // Read this now to avoid @c weak overhead later. _layoutElementType = layoutElement.layoutElementType; if (!ASIsCGSizeValidForSize(size)) { //ASDisplayNodeFailAssert(@"layoutSize is invalid and unsafe to provide to Core Animation! Release configurations will force to 0, 0. Size = %@, node = %@", NSStringFromCGSize(size), layoutElement); size = CGSizeZero; } else { size = CGSizeMake(ASCeilPixelValue(size.width), ASCeilPixelValue(size.height)); } _size = size; if (ASPointIsNull(position) == NO) { _position = ASCeilPointValues(position); } else { _position = position; } _sublayouts = [sublayouts copy] ?: @[]; if ([ASLayout shouldRetainSublayoutLayoutElements]) { [self retainSublayoutElements]; } } return self; } #pragma mark - Class Constructors + (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size position:(CGPoint)position sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED { return [[self alloc] initWithLayoutElement:layoutElement size:size position:position sublayouts:sublayouts]; } + (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED { return [self layoutWithLayoutElement:layoutElement size:size position:ASPointNull sublayouts:sublayouts]; } + (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size NS_RETURNS_RETAINED { return [self layoutWithLayoutElement:layoutElement size:size position:ASPointNull sublayouts:nil]; } - (void)dealloc { if (_retainSublayoutElements.load()) { for (ASLayout *sublayout in _sublayouts) { // We retained this, so there's no risk of it deallocating on us. if (CFTypeRef cfElement = (__bridge CFTypeRef)sublayout->_layoutElement) { CFRelease(cfElement); } } } } #pragma mark - Sublayout Elements Caching - (void)retainSublayoutElements { if (_retainSublayoutElements.exchange(true)) { return; } for (ASLayout *sublayout in _sublayouts) { // CFBridgingRetain atomically casts and retains. We need the atomicity. CFBridgingRetain(sublayout->_layoutElement); } } #pragma mark - Layout Flattening - (BOOL)isFlattened { // A layout is flattened if its position is null, and all of its sublayouts are of type displaynode with no sublayouts. if (!ASPointIsNull(_position)) { return NO; } for (ASLayout *sublayout in _sublayouts) { if (ASLayoutIsDisplayNodeType(sublayout) == NO || sublayout->_sublayouts.count > 0) { return NO; } } return YES; } - (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED { if ([self isFlattened]) { // All flattened layouts must retain sublayout elements until they are applied. [self retainSublayoutElements]; return self; } struct Context { unowned ASLayout *layout; CGPoint absolutePosition; }; // Queue used to keep track of sublayouts while traversing this layout in a DFS fashion. std::deque queue; for (ASLayout *sublayout in _sublayouts) { queue.push_back({sublayout, sublayout.position}); } std::vector flattenedSublayouts; while (!queue.empty()) { const Context context = std::move(queue.front()); queue.pop_front(); unowned ASLayout *layout = context.layout; // Direct ivar access to avoid retain/release, use existing +1. const NSUInteger sublayoutsCount = layout->_sublayouts.count; const CGPoint absolutePosition = context.absolutePosition; if (ASLayoutIsDisplayNodeType(layout)) { if (sublayoutsCount > 0 || CGPointEqualToPoint(ASCeilPointValues(absolutePosition), layout.position) == NO) { // Only create a new layout if the existing one can't be reused, which means it has either some sublayouts or an invalid absolute position. const auto newLayout = [ASLayout layoutWithLayoutElement:layout->_layoutElement size:layout.size position:absolutePosition sublayouts:@[]]; flattenedSublayouts.push_back(newLayout); } else { flattenedSublayouts.push_back(layout); } } else if (sublayoutsCount > 0) { // Fast-reverse-enumerate the sublayouts array by copying it into a C-array and push_front'ing each into the queue. unowned ASLayout *rawSublayouts[sublayoutsCount]; [layout->_sublayouts getObjects:rawSublayouts range:NSMakeRange(0, sublayoutsCount)]; for (NSInteger i = sublayoutsCount - 1; i >= 0; i--) { queue.push_front({rawSublayouts[i], absolutePosition + rawSublayouts[i].position}); } } } NSArray *array = [NSArray arrayByTransferring:flattenedSublayouts.data() count:flattenedSublayouts.size()]; // flattenedSublayouts is now all nils. ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:array]; // All flattened layouts must retain sublayout elements until they are applied. [layout retainSublayoutElements]; return layout; } #pragma mark - Equality Checking - (BOOL)isEqual:(id)object { if (self == object) return YES; ASLayout *layout = ASDynamicCast(object, ASLayout); if (layout == nil) { return NO; } if (!CGSizeEqualToSize(_size, layout.size)) return NO; if (!((ASPointIsNull(self.position) && ASPointIsNull(layout.position)) || CGPointEqualToPoint(self.position, layout.position))) return NO; if (_layoutElement != layout.layoutElement) return NO; if (!ASObjectIsEqual(_sublayouts, layout.sublayouts)) { return NO; } return YES; } #pragma mark - Accessors - (ASLayoutElementType)type { return _layoutElementType; } - (CGRect)frameForElement:(id)layoutElement { for (ASLayout *l in _sublayouts) { if (l->_layoutElement == layoutElement) { return l.frame; } } return CGRectNull; } - (CGRect)frame { CGRect subnodeFrame = CGRectZero; CGPoint adjustedOrigin = _position; if (isfinite(adjustedOrigin.x) == NO) { ASDisplayNodeAssert(0, @"Layout has an invalid position"); adjustedOrigin.x = 0; } if (isfinite(adjustedOrigin.y) == NO) { ASDisplayNodeAssert(0, @"Layout has an invalid position"); adjustedOrigin.y = 0; } subnodeFrame.origin = adjustedOrigin; CGSize adjustedSize = _size; if (isfinite(adjustedSize.width) == NO) { ASDisplayNodeAssert(0, @"Layout has an invalid size"); adjustedSize.width = 0; } if (isfinite(adjustedSize.height) == NO) { ASDisplayNodeAssert(0, @"Layout has an invalid position"); adjustedSize.height = 0; } subnodeFrame.size = adjustedSize; return subnodeFrame; } #pragma mark - Description - (NSMutableArray *)propertiesForDescription { NSMutableArray *result = [NSMutableArray array]; [result addObject:@{ @"size" : [NSValue valueWithCGSize:self.size] }]; if (id layoutElement = self.layoutElement) { [result addObject:@{ @"layoutElement" : layoutElement }]; } const auto pos = self.position; if (!ASPointIsNull(pos)) { [result addObject:@{ @"position" : [NSValue valueWithCGPoint:pos] }]; } return result; } - (NSString *)description { return ASObjectDescriptionMake(self, [self propertiesForDescription]); } - (NSString *)recursiveDescription { return [self _recursiveDescriptionForLayout:self level:0]; } - (NSString *)_recursiveDescriptionForLayout:(ASLayout *)layout level:(NSUInteger)level { NSMutableString *description = [NSMutableString string]; [description appendString:descriptionIndents(level)]; [description appendString:[layout description]]; for (ASLayout *sublayout in layout.sublayouts) { [description appendString:@"\n"]; [description appendString:[self _recursiveDescriptionForLayout:sublayout level:level + 1]]; } return description; } @end ASLayout *ASCalculateLayout(id layoutElement, const ASSizeRange sizeRange, const CGSize parentSize) { NSCParameterAssert(layoutElement != nil); return [layoutElement layoutThatFits:sizeRange parentSize:parentSize]; } ASLayout *ASCalculateRootLayout(id rootLayoutElement, const ASSizeRange sizeRange) { ASLayout *layout = ASCalculateLayout(rootLayoutElement, sizeRange, sizeRange.max); // Here could specific verfication happen return layout; }