From 42f860600e91c9dd4abfd295ff30affd3c725584 Mon Sep 17 00:00:00 2001 From: Peter <> Date: Wed, 10 Oct 2018 20:10:04 +0300 Subject: [PATCH] no message --- .../xcschemes/AsyncDisplayKit.xcscheme.orig | 115 + Source/ASDisplayNode.mm.orig | 3927 +++++++++++++++++ Source/ASEditableTextNode.h.orig | 221 + Source/ASImageNode.mm | 2 +- Source/ASImageNode.mm.orig | 791 ++++ Source/ASMapNode.h.orig | 99 + Source/ASMapNode.mm.orig | 456 ++ Source/ASMultiplexImageNode.mm | 1 + Source/Base/ASAssert.m.orig | 72 + Source/Base/ASAvailability.h | 6 +- .../ASPhotosFrameworkImageRequest.h.orig | 81 + .../ASPhotosFrameworkImageRequest.m.orig | 163 + Source/Layout/ASLayoutElement.mm | 1 - Source/Layout/ASLayoutElement.mm.orig | 869 ++++ Source/Private/_ASPendingState.mm | 72 +- 15 files changed, 6833 insertions(+), 43 deletions(-) create mode 100644 AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme.orig create mode 100644 Source/ASDisplayNode.mm.orig create mode 100644 Source/ASEditableTextNode.h.orig create mode 100644 Source/ASImageNode.mm.orig create mode 100644 Source/ASMapNode.h.orig create mode 100644 Source/ASMapNode.mm.orig create mode 100644 Source/Base/ASAssert.m.orig create mode 100644 Source/Details/ASPhotosFrameworkImageRequest.h.orig create mode 100644 Source/Details/ASPhotosFrameworkImageRequest.m.orig create mode 100644 Source/Layout/ASLayoutElement.mm.orig diff --git a/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme.orig b/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme.orig new file mode 100644 index 0000000000..6129951d1c --- /dev/null +++ b/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme.orig @@ -0,0 +1,115 @@ + +>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + version = "1.3"> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Source/ASDisplayNode.mm.orig b/Source/ASDisplayNode.mm.orig new file mode 100644 index 0000000000..50510817ab --- /dev/null +++ b/Source/ASDisplayNode.mm.orig @@ -0,0 +1,3927 @@ +// +// ASDisplayNode.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 + +#import + +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +// Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) +#if TIME_DISPLAYNODE_OPS + #define TIME_SCOPED(outVar) ASDN::ScopeTimer t(outVar) +#else + #define TIME_SCOPED(outVar) +#endif +// This is trying to merge non-rangeManaged with rangeManaged, so both range-managed and standalone nodes wait before firing their exit-visibility handlers, as UIViewController transitions now do rehosting at both start & end of animation. +// Enable this will mitigate interface updating state when coalescing disabled. +// TODO(wsdwsd0829): Rework enabling code to ensure that interface state behavior is not altered when ASCATransactionQueue is disabled. +#define ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR 0 + +static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil; +NSInteger const ASDefaultDrawingPriority = ASDefaultTransactionPriority; + +// Forward declare CALayerDelegate protocol as the iOS 10 SDK moves CALayerDelegate from an informal delegate to a protocol. +// We have to forward declare the protocol as this place otherwise it will not compile compiling with an Base SDK < iOS 10 +@protocol CALayerDelegate; + +@interface ASDisplayNode () +/** + * See ASDisplayNodeInternal.h for ivars + */ + +@end + +@implementation ASDisplayNode + +@dynamic layoutElementType; + +@synthesize threadSafeBounds = _threadSafeBounds; + +static std::atomic_bool storesUnflattenedLayouts = ATOMIC_VAR_INIT(NO); + +BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector) +{ + return ASSubclassOverridesSelector([ASDisplayNode class], subclass, selector); +} + +// For classes like ASTableNode, ASCollectionNode, ASScrollNode and similar - we have to be sure to set certain properties +// like setFrame: and setBackgroundColor: directly to the UIView and not apply it to the layer only. +BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLayerBacked) +{ + return isSynchronous && !isLayerBacked; +} + +_ASPendingState *ASDisplayNodeGetPendingState(ASDisplayNode *node) +{ + ASLockScope(node); + _ASPendingState *result = node->_pendingViewState; + if (result == nil) { + result = [[_ASPendingState alloc] init]; + node->_pendingViewState = result; + } + return result; +} + +/** + * Returns ASDisplayNodeFlags for the given class/instance. instance MAY BE NIL. + * + * @param c the class, required + * @param instance the instance, which may be nil. (If so, the class is inspected instead) + * @remarks The instance value is used only if we suspect the class may be dynamic (because it overloads + * +respondsToSelector: or -respondsToSelector.) In that case we use our "slow path", calling this + * method on each -init and passing the instance value. While this may seem like an unlikely scenario, + * it turns our our own internal tests use a dynamic class, so it's worth capturing this edge case. + * + * @return ASDisplayNode flags. + */ +static struct ASDisplayNodeFlags GetASDisplayNodeFlags(Class c, ASDisplayNode *instance) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + struct ASDisplayNodeFlags flags = {0}; + + flags.isInHierarchy = NO; + flags.displaysAsynchronously = YES; + flags.shouldAnimateSizeChanges = YES; + flags.implementsDrawRect = ([c respondsToSelector:@selector(drawRect:withParameters:isCancelled:isRasterizing:)] ? 1 : 0); + flags.implementsImageDisplay = ([c respondsToSelector:@selector(displayWithParameters:isCancelled:)] ? 1 : 0); + if (instance) { + flags.implementsDrawParameters = ([instance respondsToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); + } else { + flags.implementsDrawParameters = ([c instancesRespondToSelector:@selector(drawParametersForAsyncLayer:)] ? 1 : 0); + } + + + return flags; +} + +/** + * Returns ASDisplayNodeMethodOverrides for the given class + * + * @param c the class, required. + * + * @return ASDisplayNodeMethodOverrides. + */ +static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + ASDisplayNodeMethodOverrides overrides = ASDisplayNodeMethodOverrideNone; + + // Handling touches + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesBegan:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesBegan; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesMoved:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesMoved; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesCancelled:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesCancelled; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(touchesEnded:withEvent:))) { + overrides |= ASDisplayNodeMethodOverrideTouchesEnded; + } + + // Responder chain + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(canBecomeFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideCanBecomeFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(becomeFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideBecomeFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(canResignFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideCanResignFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(resignFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideResignFirstResponder; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(isFirstResponder))) { + overrides |= ASDisplayNodeMethodOverrideIsFirstResponder; + } + + // Layout related methods + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(layoutSpecThatFits:))) { + overrides |= ASDisplayNodeMethodOverrideLayoutSpecThatFits; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateLayoutThatFits:)) || + ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateLayoutThatFits: + restrictedToSize: + relativeToParentSize:))) { + overrides |= ASDisplayNodeMethodOverrideCalcLayoutThatFits; + } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateSizeThatFits:))) { + overrides |= ASDisplayNodeMethodOverrideCalcSizeThatFits; + } + + return overrides; +} + ++ (void)initialize +{ +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + if (self != [ASDisplayNode class]) { + + // Subclasses should never override these. Use unused to prevent warnings + __unused NSString *classString = NSStringFromClass(self); + + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedSize)), @"Subclass %@ must not override calculatedSize method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(calculatedLayout)), @"Subclass %@ must not override calculatedLayout method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:)), @"Subclass %@ must not override layoutThatFits: method. Instead override calculateLayoutThatFits:.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(layoutThatFits:parentSize:)), @"Subclass %@ must not override layoutThatFits:parentSize method. Instead override calculateLayoutThatFits:.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearContents)), @"Subclass %@ must not override recursivelyClearContents method.", classString); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self, @selector(recursivelyClearPreloadedData)), @"Subclass %@ must not override recursivelyClearFetchedData method.", classString); + } else { + // Check if subnodes where modified during the creation of the layout + __block IMP originalLayoutSpecThatFitsIMP = ASReplaceMethodWithBlock(self, @selector(_locked_layoutElementThatFits:), ^(ASDisplayNode *_self, ASSizeRange sizeRange) { + NSArray *oldSubnodes = _self.subnodes; + ASLayoutSpec *layoutElement = ((ASLayoutSpec *( *)(id, SEL, ASSizeRange))originalLayoutSpecThatFitsIMP)(_self, @selector(_locked_layoutElementThatFits:), sizeRange); + NSArray *subnodes = _self.subnodes; + ASDisplayNodeAssert(oldSubnodes.count == subnodes.count, @"Adding or removing nodes in layoutSpecBlock or layoutSpecThatFits: is not allowed and can cause unexpected behavior."); + for (NSInteger i = 0; i < oldSubnodes.count; i++) { + ASDisplayNodeAssert(oldSubnodes[i] == subnodes[i], @"Adding or removing nodes in layoutSpecBlock or layoutSpecThatFits: is not allowed and can cause unexpected behavior."); + } + return layoutElement; + }); + } +#endif + + // Below we are pre-calculating values per-class and dynamically adding a method (_staticInitialize) to populate these values + // when each instance is constructed. These values don't change for each class, so there is significant performance benefit + // in doing it here. +initialize is guaranteed to be called before any instance method so it is safe to add this method here. + // Note that we take care to detect if the class overrides +respondsToSelector: or -respondsToSelector and take the slow path + // (recalculating for each instance) to make sure we are always correct. + + BOOL classOverridesRespondsToSelector = ASSubclassOverridesClassSelector([NSObject class], self, @selector(respondsToSelector:)); + BOOL instancesOverrideRespondsToSelector = ASSubclassOverridesSelector([NSObject class], self, @selector(respondsToSelector:)); + struct ASDisplayNodeFlags flags = GetASDisplayNodeFlags(self, nil); + ASDisplayNodeMethodOverrides methodOverrides = GetASDisplayNodeMethodOverrides(self); + + __unused Class initializeSelf = self; + + IMP staticInitialize = imp_implementationWithBlock(^(ASDisplayNode *node) { + ASDisplayNodeAssert(node.class == initializeSelf, @"Node class %@ does not have a matching _staticInitialize method; check to ensure [super initialize] is called within any custom +initialize implementations! Overridden methods will not be called unless they are also implemented by superclass %@", node.class, initializeSelf); + node->_flags = (classOverridesRespondsToSelector || instancesOverrideRespondsToSelector) ? GetASDisplayNodeFlags(node.class, node) : flags; + node->_methodOverrides = (classOverridesRespondsToSelector) ? GetASDisplayNodeMethodOverrides(node.class) : methodOverrides; + }); + + class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@"); +} + +#if !AS_INITIALIZE_FRAMEWORK_MANUALLY ++ (void)load +{ + ASInitializeFrameworkMainThread(); +} +#endif + ++ (Class)viewClass +{ + return [_ASDisplayView class]; +} + ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +#pragma mark - Lifecycle + +- (void)_staticInitialize +{ + ASDisplayNodeAssert(NO, @"_staticInitialize must be overridden"); +} + +- (void)_initializeInstance +{ + [self _staticInitialize]; + +#if ASEVENTLOG_ENABLE + _eventLog = [[ASEventLog alloc] initWithObject:self]; +#endif + + _viewClass = [self.class viewClass]; + _layerClass = [self.class layerClass]; + BOOL isSynchronous = ![_viewClass isSubclassOfClass:[_ASDisplayView class]] + || ![_layerClass isSubclassOfClass:[_ASDisplayLayer class]]; + setFlag(Synchronous, isSynchronous); + + + _contentsScaleForDisplay = ASScreenScale(); + _drawingPriority = ASDefaultDrawingPriority; + + _primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); + + _layoutVersion = 1; + + _defaultLayoutTransitionDuration = 0.2; + _defaultLayoutTransitionDelay = 0.0; + _defaultLayoutTransitionOptions = UIViewAnimationOptionCurveEaseInOut | UIViewAnimationOptionTransitionNone; + + _flags.canClearContentsOfLayer = YES; + _flags.canCallSetNeedsDisplayOfLayer = YES; + + _fallbackSafeAreaInsets = UIEdgeInsetsZero; + _fallbackInsetsLayoutMarginsFromSafeArea = YES; + _isViewControllerRoot = NO; + + _automaticallyRelayoutOnSafeAreaChanges = NO; + _automaticallyRelayoutOnLayoutMarginsChanges = NO; + + ASDisplayNodeLogEvent(self, @"init"); +} + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + [self _initializeInstance]; + + return self; +} + +- (instancetype)initWithViewClass:(Class)viewClass +{ + if (!(self = [self init])) + return nil; + + ASDisplayNodeAssert([viewClass isSubclassOfClass:[UIView class]], @"should initialize with a subclass of UIView"); + + _viewClass = viewClass; + setFlag(Synchronous, ![viewClass isSubclassOfClass:[_ASDisplayView class]]); + + return self; +} + +- (instancetype)initWithLayerClass:(Class)layerClass +{ + if (!(self = [self init])) { + return nil; + } + + ASDisplayNodeAssert([layerClass isSubclassOfClass:[CALayer class]], @"should initialize with a subclass of CALayer"); + + _layerClass = layerClass; + _flags.layerBacked = YES; + setFlag(Synchronous, ![layerClass isSubclassOfClass:[_ASDisplayLayer class]]); + + return self; +} + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock +{ + return [self initWithViewBlock:viewBlock didLoadBlock:nil]; +} + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock +{ + if (!(self = [self init])) { + return nil; + } + + [self setViewBlock:viewBlock]; + if (didLoadBlock != nil) { + [self onDidLoad:didLoadBlock]; + } + + return self; +} + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock +{ + return [self initWithLayerBlock:layerBlock didLoadBlock:nil]; +} + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)layerBlock didLoadBlock:(ASDisplayNodeDidLoadBlock)didLoadBlock +{ + if (!(self = [self init])) { + return nil; + } + + [self setLayerBlock:layerBlock]; + if (didLoadBlock != nil) { + [self onDidLoad:didLoadBlock]; + } + + return self; +} + +ASSynthesizeLockingMethodsWithMutex(__instanceLock__); + +- (void)setViewBlock:(ASDisplayNodeViewBlock)viewBlock +{ + ASDisplayNodeAssertFalse(self.nodeLoaded); + ASDisplayNodeAssertNotNil(viewBlock, @"should initialize with a valid block that returns a UIView"); + + _viewBlock = viewBlock; + setFlag(Synchronous, YES); +} + +- (void)setLayerBlock:(ASDisplayNodeLayerBlock)layerBlock +{ + ASDisplayNodeAssertFalse(self.nodeLoaded); + ASDisplayNodeAssertNotNil(layerBlock, @"should initialize with a valid block that returns a CALayer"); + + _layerBlock = layerBlock; + _flags.layerBacked = YES; + setFlag(Synchronous, YES); +} + +- (ASDisplayNodeMethodOverrides)methodOverrides +{ + return _methodOverrides; +} + +- (void)onDidLoad:(ASDisplayNodeDidLoadBlock)body +{ + ASDN::MutexLocker l(__instanceLock__); + + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeAssertThreadAffinity(self); + ASDN::MutexUnlocker l(__instanceLock__); + body(self); + } else if (_onDidLoadBlocks == nil) { + _onDidLoadBlocks = [NSMutableArray arrayWithObject:body]; + } else { + [_onDidLoadBlocks addObject:body]; + } +} + +- (void)dealloc +{ + _flags.isDeallocating = YES; + + // Synchronous nodes may not be able to call the hierarchy notifications, so only enforce for regular nodes. + ASDisplayNodeAssert(checkFlag(Synchronous) || !ASInterfaceStateIncludesVisible(_interfaceState), @"Node should always be marked invisible before deallocating. Node: %@", self); + + self.asyncLayer.asyncDelegate = nil; + _view.asyncdisplaykit_node = nil; + _layer.asyncdisplaykit_node = nil; + + // Remove any subnodes so they lose their connection to the now deallocated parent. This can happen + // because subnodes do not retain their supernode, but subnodes can legitimately remain alive if another + // thing outside the view hierarchy system (e.g. async display, controller code, etc). keeps a retained + // reference to subnodes. + + for (ASDisplayNode *subnode in _subnodes) + [subnode _setSupernode:nil]; + + [self scheduleIvarsForMainThreadDeallocation]; + + // TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway. + [self _setSupernode:nil]; +} + +#pragma mark - Loading + +- (BOOL)_locked_shouldLoadViewOrLayer +{ + ASAssertLocked(__instanceLock__); + return !_flags.isDeallocating && !(_hierarchyState & ASHierarchyStateRasterized); +} + +- (UIView *)_locked_viewToLoad +{ + ASAssertLocked(__instanceLock__); + + UIView *view = nil; + if (_viewBlock) { + view = _viewBlock(); + ASDisplayNodeAssertNotNil(view, @"View block returned nil"); + ASDisplayNodeAssert(![view isKindOfClass:[_ASDisplayView class]], @"View block should return a synchronously displayed view"); + _viewBlock = nil; + _viewClass = [view class]; + } else { + view = [[_viewClass alloc] init]; + } + + // Special handling of wrapping UIKit components + if (checkFlag(Synchronous)) { + [self checkResponderCompatibility]; + + // UIImageView layers. More details on the flags + if ([_viewClass isSubclassOfClass:[UIImageView class]]) { + _flags.canClearContentsOfLayer = NO; + _flags.canCallSetNeedsDisplayOfLayer = NO; + } + + // UIActivityIndicator + if ([_viewClass isSubclassOfClass:[UIActivityIndicatorView class]] + || [_viewClass isSubclassOfClass:[UIVisualEffectView class]]) { + self.opaque = NO; + } + + // CAEAGLLayer + if([[view.layer class] isSubclassOfClass:[CAEAGLLayer class]]){ + _flags.canClearContentsOfLayer = NO; + } + } + + return view; +} + +- (CALayer *)_locked_layerToLoad +{ + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssert(_flags.layerBacked, @"_layerToLoad is only for layer-backed nodes"); + + CALayer *layer = nil; + if (_layerBlock) { + layer = _layerBlock(); + ASDisplayNodeAssertNotNil(layer, @"Layer block returned nil"); + ASDisplayNodeAssert(![layer isKindOfClass:[_ASDisplayLayer class]], @"Layer block should return a synchronously displayed layer"); + _layerBlock = nil; + _layerClass = [layer class]; + } else { + layer = [[_layerClass alloc] init]; + } + + return layer; +} + +- (void)_locked_loadViewOrLayer +{ + ASAssertLocked(__instanceLock__); + + if (_flags.layerBacked) { + TIME_SCOPED(_debugTimeToCreateView); + _layer = [self _locked_layerToLoad]; + static int ASLayerDelegateAssociationKey; + + /** + * CALayer's .delegate property is documented to be weak, but the implementation is actually assign. + * Because our layer may survive longer than the node (e.g. if someone else retains it, or if the node + * begins deallocation on a background thread and it waiting for the -dealloc call to reach main), the only + * way to avoid a dangling pointer is to use a weak proxy. + */ + ASWeakProxy *instance = [ASWeakProxy weakProxyWithTarget:self]; + _layer.delegate = (id)instance; + objc_setAssociatedObject(_layer, &ASLayerDelegateAssociationKey, instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + } else { + TIME_SCOPED(_debugTimeToCreateView); + _view = [self _locked_viewToLoad]; + _view.asyncdisplaykit_node = self; + _layer = _view.layer; + } + _layer.asyncdisplaykit_node = self; + + self._locked_asyncLayer.asyncDelegate = self; +} + +- (void)_didLoad +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeLogEvent(self, @"didLoad"); + as_log_verbose(ASNodeLog(), "didLoad %@", self); + TIME_SCOPED(_debugTimeForDidLoad); + + [self didLoad]; + + __instanceLock__.lock(); + let onDidLoadBlocks = ASTransferStrong(_onDidLoadBlocks); + __instanceLock__.unlock(); + + for (ASDisplayNodeDidLoadBlock block in onDidLoadBlocks) { + block(self); + } + [self enumerateInterfaceStateDelegates:^(id del) { + [del nodeDidLoad]; + }]; +} + +- (void)didLoad +{ + ASDisplayNodeAssertMainThread(); + + // Subclass hook +} + +- (BOOL)isNodeLoaded +{ + if (ASDisplayNodeThreadIsMain()) { + // Because the view and layer can only be created and destroyed on Main, that is also the only thread + // where the state of this property can change. As an optimization, we can avoid locking. + return _loaded(self); + } else { + ASDN::MutexLocker l(__instanceLock__); + return [self _locked_isNodeLoaded]; + } +} + +- (BOOL)_locked_isNodeLoaded +{ + ASAssertLocked(__instanceLock__); + return _loaded(self); +} + +#pragma mark - Misc Setter / Getter + +- (UIView *)view +{ + ASDN::MutexLocker l(__instanceLock__); + + ASDisplayNodeAssert(!_flags.layerBacked, @"Call to -view undefined on layer-backed nodes"); + BOOL isLayerBacked = _flags.layerBacked; + if (isLayerBacked) { + return nil; + } + + if (_view != nil) { + return _view; + } + + if (![self _locked_shouldLoadViewOrLayer]) { + return nil; + } + + // Loading a view needs to happen on the main thread + ASDisplayNodeAssertMainThread(); + [self _locked_loadViewOrLayer]; + + // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout + // but automatic subnode management would require us to modify the node tree + // in the background on a loaded node, which isn't currently supported. + if (_pendingViewState.hasSetNeedsLayout) { + // Need to unlock before calling setNeedsLayout to avoid deadlocks. + // MutexUnlocker will re-lock at the end of scope. + ASDN::MutexUnlocker u(__instanceLock__); + [self __setNeedsLayout]; + } + + [self _locked_applyPendingStateToViewOrLayer]; + + { + // The following methods should not be called with a lock + ASDN::MutexUnlocker u(__instanceLock__); + + // No need for the lock as accessing the subviews or layers are always happening on main + [self _addSubnodeViewsAndLayers]; + + // A subclass hook should never be called with a lock + [self _didLoad]; + } + + return _view; +} + +- (CALayer *)layer +{ + ASDN::MutexLocker l(__instanceLock__); + if (_layer != nil) { + return _layer; + } + + if (![self _locked_shouldLoadViewOrLayer]) { + return nil; + } + + // Loading a layer needs to happen on the main thread + ASDisplayNodeAssertMainThread(); + [self _locked_loadViewOrLayer]; + + // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout + // but automatic subnode management would require us to modify the node tree + // in the background on a loaded node, which isn't currently supported. + if (_pendingViewState.hasSetNeedsLayout) { + // Need to unlock before calling setNeedsLayout to avoid deadlocks. + // MutexUnlocker will re-lock at the end of scope. + ASDN::MutexUnlocker u(__instanceLock__); + [self __setNeedsLayout]; + } + + [self _locked_applyPendingStateToViewOrLayer]; + + { + // The following methods should not be called with a lock + ASDN::MutexUnlocker u(__instanceLock__); + + // No need for the lock as accessing the subviews or layers are always happening on main + [self _addSubnodeViewsAndLayers]; + + // A subclass hook should never be called with a lock + [self _didLoad]; + } + + return _layer; +} + +// Returns nil if the layer is not an _ASDisplayLayer; will not create the layer if nil. +- (_ASDisplayLayer *)asyncLayer +{ + ASDN::MutexLocker l(__instanceLock__); + return [self _locked_asyncLayer]; +} + +- (_ASDisplayLayer *)_locked_asyncLayer +{ + ASAssertLocked(__instanceLock__); + return [_layer isKindOfClass:[_ASDisplayLayer class]] ? (_ASDisplayLayer *)_layer : nil; +} + +- (BOOL)isSynchronous +{ + return checkFlag(Synchronous); +} + +- (void)setLayerBacked:(BOOL)isLayerBacked +{ + // Only call this if assertions are enabled – it could be expensive. + ASDisplayNodeAssert(!isLayerBacked || self.supportsLayerBacking, @"Node %@ does not support layer backing.", self); + + ASDN::MutexLocker l(__instanceLock__); + if (_flags.layerBacked == isLayerBacked) { + return; + } + + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeFailAssert(@"Cannot change layerBacked after view/layer has loaded."); + return; + } + + _flags.layerBacked = isLayerBacked; +} + +- (BOOL)isLayerBacked +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.layerBacked; +} + +- (BOOL)supportsLayerBacking +{ + ASDN::MutexLocker l(__instanceLock__); + return !checkFlag(Synchronous) && !_flags.viewEverHadAGestureRecognizerAttached && _viewClass == [_ASDisplayView class] && _layerClass == [_ASDisplayLayer class]; +} + +- (BOOL)shouldAnimateSizeChanges +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.shouldAnimateSizeChanges; +} + +- (void)setShouldAnimateSizeChanges:(BOOL)shouldAnimateSizeChanges +{ + ASDN::MutexLocker l(__instanceLock__); + _flags.shouldAnimateSizeChanges = shouldAnimateSizeChanges; +} + +- (CGRect)threadSafeBounds +{ + ASDN::MutexLocker l(__instanceLock__); + return [self _locked_threadSafeBounds]; +} + +- (CGRect)_locked_threadSafeBounds +{ + ASAssertLocked(__instanceLock__); + return _threadSafeBounds; +} + +- (void)setThreadSafeBounds:(CGRect)newBounds +{ + ASDN::MutexLocker l(__instanceLock__); + _threadSafeBounds = newBounds; +} + +- (void)nodeViewDidAddGestureRecognizer +{ + ASDN::MutexLocker l(__instanceLock__); + _flags.viewEverHadAGestureRecognizerAttached = YES; +} + +- (UIEdgeInsets)fallbackSafeAreaInsets +{ + ASDN::MutexLocker l(__instanceLock__); + return _fallbackSafeAreaInsets; +} + +- (void)setFallbackSafeAreaInsets:(UIEdgeInsets)insets +{ + BOOL needsManualUpdate; + BOOL updatesLayoutMargins; + + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssertThreadAffinity(self); + + if (UIEdgeInsetsEqualToEdgeInsets(insets, _fallbackSafeAreaInsets)) { + return; + } + + _fallbackSafeAreaInsets = insets; + needsManualUpdate = !AS_AT_LEAST_IOS11 || _flags.layerBacked; + updatesLayoutMargins = needsManualUpdate && [self _locked_insetsLayoutMarginsFromSafeArea]; + } + + if (needsManualUpdate) { + [self safeAreaInsetsDidChange]; + } + + if (updatesLayoutMargins) { + [self layoutMarginsDidChange]; + } +} + +- (void)_fallbackUpdateSafeAreaOnChildren +{ + ASDisplayNodeAssertThreadAffinity(self); + + UIEdgeInsets insets = self.safeAreaInsets; + CGRect bounds = self.bounds; + + for (ASDisplayNode *child in self.subnodes) { + if (AS_AT_LEAST_IOS11 && !child.layerBacked) { + // In iOS 11 view-backed nodes already know what their safe area is. + continue; + } + + if (child.viewControllerRoot) { + // Its safe area is controlled by a view controller. Don't override it. + continue; + } + + CGRect childFrame = child.frame; + UIEdgeInsets childInsets = UIEdgeInsetsMake(MAX(insets.top - (CGRectGetMinY(childFrame) - CGRectGetMinY(bounds)), 0), + MAX(insets.left - (CGRectGetMinX(childFrame) - CGRectGetMinX(bounds)), 0), + MAX(insets.bottom - (CGRectGetMaxY(bounds) - CGRectGetMaxY(childFrame)), 0), + MAX(insets.right - (CGRectGetMaxX(bounds) - CGRectGetMaxX(childFrame)), 0)); + + child.fallbackSafeAreaInsets = childInsets; + } +} + +- (BOOL)isViewControllerRoot +{ + ASDN::MutexLocker l(__instanceLock__); + return _isViewControllerRoot; +} + +- (void)setViewControllerRoot:(BOOL)flag +{ + ASDN::MutexLocker l(__instanceLock__); + _isViewControllerRoot = flag; +} + +- (BOOL)automaticallyRelayoutOnSafeAreaChanges +{ + ASDN::MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnSafeAreaChanges; +} + +- (void)setAutomaticallyRelayoutOnSafeAreaChanges:(BOOL)flag +{ + ASDN::MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnSafeAreaChanges = flag; +} + +- (BOOL)automaticallyRelayoutOnLayoutMarginsChanges +{ + ASDN::MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnLayoutMarginsChanges; +} + +- (void)setAutomaticallyRelayoutOnLayoutMarginsChanges:(BOOL)flag +{ + ASDN::MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnLayoutMarginsChanges = flag; +} + +- (void)__setNodeController:(ASNodeController *)controller +{ + // See docs for why we don't lock. + if (controller.shouldInvertStrongReference) { + _strongNodeController = controller; + _weakNodeController = nil; + } else { + _weakNodeController = controller; + _strongNodeController = nil; + } +} + +#pragma mark - UIResponder + +#define HANDLE_NODE_RESPONDER_METHOD(__sel) \ + /* All responder methods should be called on the main thread */ \ + ASDisplayNodeAssertMainThread(); \ + if (checkFlag(Synchronous)) { \ + /* If the view is not a _ASDisplayView subclass (Synchronous) just call through to the view as we + expect it's a non _ASDisplayView subclass that will respond */ \ + return [_view __sel]; \ + } else { \ + if (ASSubclassOverridesSelector([_ASDisplayView class], _viewClass, @selector(__sel))) { \ + /* If the subclass overwrites canBecomeFirstResponder just call through + to it as we expect it will handle it */ \ + return [_view __sel]; \ + } else { \ + /* Call through to _ASDisplayView's superclass to get it handled */ \ + return [(_ASDisplayView *)_view __##__sel]; \ + } \ + } \ + +- (void)checkResponderCompatibility +{ +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // There are certain cases we cannot handle and are not supported: + // 1. If the _view class is not a subclass of _ASDisplayView + if (checkFlag(Synchronous)) { + // 2. At least one UIResponder methods are overwritten in the node subclass + NSString *message = @"Overwritting %@ and having a backing view that is not an _ASDisplayView is not supported."; + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canBecomeFirstResponder)), ([NSString stringWithFormat:message, @"canBecomeFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(becomeFirstResponder)), ([NSString stringWithFormat:message, @"becomeFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(canResignFirstResponder)), ([NSString stringWithFormat:message, @"canResignFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(resignFirstResponder)), ([NSString stringWithFormat:message, @"resignFirstResponder"])); + ASDisplayNodeAssert(!ASDisplayNodeSubclassOverridesSelector(self.class, @selector(isFirstResponder)), ([NSString stringWithFormat:message, @"isFirstResponder"])); + } +#endif +} + +- (BOOL)__canBecomeFirstResponder +{ + if (_view == nil) { + // By default we return NO if not view is created yet + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(canBecomeFirstResponder); +} + +- (BOOL)__becomeFirstResponder +{ + // Note: This implicitly loads the view if it hasn't been loaded yet. + [self view]; + + if (![self canBecomeFirstResponder]) { + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(becomeFirstResponder); +} + +- (BOOL)__canResignFirstResponder +{ + if (_view == nil) { + // By default we return YES if no view is created yet + return YES; + } + + HANDLE_NODE_RESPONDER_METHOD(canResignFirstResponder); +} + +- (BOOL)__resignFirstResponder +{ + // Note: This implicitly loads the view if it hasn't been loaded yet. + [self view]; + + if (![self canResignFirstResponder]) { + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(resignFirstResponder); +} + +- (BOOL)__isFirstResponder +{ + if (_view == nil) { + // If no view is created yet we can just return NO as it's unlikely it's the first responder + return NO; + } + + HANDLE_NODE_RESPONDER_METHOD(isFirstResponder); +} + +#pragma mark + +- (NSString *)debugName +{ + ASDN::MutexLocker l(__instanceLock__); + return _debugName; +} + +- (void)setDebugName:(NSString *)debugName +{ + ASDN::MutexLocker l(__instanceLock__); + if (!ASObjectIsEqual(_debugName, debugName)) { + _debugName = [debugName copy]; + } +} + +#pragma mark - Layout + +// At most a layoutSpecBlock or one of the three layout methods is overridden +#define __ASDisplayNodeCheckForLayoutMethodOverrides \ + ASDisplayNodeAssert(_layoutSpecBlock != NULL || \ + ((ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateSizeThatFits:)) ? 1 : 0) \ + + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(layoutSpecThatFits:)) ? 1 : 0) \ + + (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateLayoutThatFits:)) ? 1 : 0)) <= 1, \ + @"Subclass %@ must at least provide a layoutSpecBlock or override at most one of the three layout methods: calculateLayoutThatFits:, layoutSpecThatFits:, or calculateSizeThatFits:", NSStringFromClass(self.class)) + + +#pragma mark + +- (BOOL)canLayoutAsynchronous +{ + return !self.isNodeLoaded; +} + +#pragma mark Layout Pass + +- (void)__setNeedsLayout +{ + [self invalidateCalculatedLayout]; +} + +- (void)invalidateCalculatedLayout +{ + ASDN::MutexLocker l(__instanceLock__); + + _layoutVersion++; + + _unflattenedLayout = nil; + +#if YOGA + [self invalidateCalculatedYogaLayout]; +#endif +} + +- (void)__layout +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + BOOL loaded = NO; + { + ASDN::MutexLocker l(__instanceLock__); + loaded = [self _locked_isNodeLoaded]; + CGRect bounds = _threadSafeBounds; + + if (CGRectEqualToRect(bounds, CGRectZero)) { + // Performing layout on a zero-bounds view often results in frame calculations + // with negative sizes after applying margins, which will cause + // layoutThatFits: on subnodes to assert. + as_log_debug(OS_LOG_DISABLED, "Warning: No size given for node before node was trying to layout itself: %@. Please provide a frame for the node.", self); + return; + } + + // If a current layout transition is in progress there is no need to do a measurement and layout pass in here as + // this is supposed to happen within the layout transition process + if (_transitionID != ASLayoutElementContextInvalidTransitionID) { + return; + } + + as_activity_create_for_scope("-[ASDisplayNode __layout]"); + + // This method will confirm that the layout is up to date (and update if needed). + // Importantly, it will also APPLY the layout to all of our subnodes if (unless parent is transitioning). + { + ASDN::MutexUnlocker u(__instanceLock__); + [self _u_measureNodeWithBoundsIfNecessary:bounds]; + } + + [self _locked_layoutPlaceholderIfNecessary]; + } + + [self _layoutSublayouts]; + + // Per API contract, `-layout` and `-layoutDidFinish` are called only if the node is loaded. + if (loaded) { + ASPerformBlockOnMainThread(^{ + [self layout]; + [self _layoutClipCornersIfNeeded]; + [self layoutDidFinish]; + }); + } + + [self _fallbackUpdateSafeAreaOnChildren]; +} + +- (void)layoutDidFinish +{ + // Hook for subclasses + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertTrue(self.isNodeLoaded); +} + +#pragma mark Calculation + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize +{ +<<<<<<< HEAD + // We only want one calculateLayout signpost interval per thread. +#ifndef MINIMAL_ASDK + static _Thread_local NSInteger tls_callDepth; +======= +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + as_activity_scope_verbose(as_activity_create("Calculate node layout", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); + as_log_verbose(ASLayoutLog(), "Calculating layout for %@ sizeRange %@", self, NSStringFromASSizeRange(constrainedSize)); + +#if AS_KDEBUG_ENABLE + // We only want one calculateLayout signpost interval per thread. + // Currently there is no fallback for profiling i386, since it's not useful. + static _Thread_local NSInteger tls_callDepth; + if (tls_callDepth++ == 0) { + ASSignpostStart(ASSignpostCalculateLayout); + } +#endif + + ASSizeRange styleAndParentSize = ASLayoutElementSizeResolve(self.style.size, parentSize); + const ASSizeRange resolvedRange = ASSizeRangeIntersect(constrainedSize, styleAndParentSize); + ASLayout *result = [self calculateLayoutThatFits:resolvedRange]; +#ifndef MINIMAL_ASDK + as_log_verbose(ASLayoutLog(), "Calculated layout %@", result); + +#if AS_KDEBUG_ENABLE + if (--tls_callDepth == 0) { + ASSignpostEnd(ASSignpostCalculateLayout); + } +#endif +<<<<<<< HEAD +======= + +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + return result; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + ASDN::MutexLocker l(__instanceLock__); + +#if YOGA + // 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. + YGNodeRef yogaNode = _style.yogaNode; + BOOL hasYogaParent = (_yogaParent != nil); + BOOL hasYogaChildren = (_yogaChildren.count > 0); + BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); + if (usesYoga) { + // This node has some connection to a Yoga tree. + if ([self shouldHaveYogaMeasureFunc] == NO) { + // If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then + // initiate a new Yoga calculation pass from root. + ASDN::MutexUnlocker ul(__instanceLock__); + as_activity_create_for_scope("Yoga layout calculation"); + if (self.yogaLayoutInProgress == NO) { + ASYogaLog("Calculating yoga layout from root %@, %@", self, NSStringFromASSizeRange(constrainedSize)); + [self calculateLayoutFromYogaRoot:constrainedSize]; + } else { + ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout); + } + ASDisplayNodeAssert(_yogaCalculatedLayout, @"Yoga node should have a non-nil layout at this stage: %@", self); + return _yogaCalculatedLayout; + } else { + // If we're a yoga leaf node with custom measurement function, proceed with normal layout so layoutSpecs can run (e.g. ASButtonNode). + ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self); + } + } +#endif /* YOGA */ + + // Manual size calculation via calculateSizeThatFits: + 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]; + } + + // Size calcualtion with layout elements + BOOL measureLayoutSpec = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec; + if (measureLayoutSpec) { + _layoutSpecNumberOfPasses++; + } + + // Get layout element from the node + id layoutElement = [self _locked_layoutElementThatFits:constrainedSize]; +#if ASEnableVerboseLogging + for (NSString *asciiLine in [[layoutElement asciiArtString] componentsSeparatedByString:@"\n"]) { + as_log_verbose(ASLayoutLog(), "%@", asciiLine); + } +#endif + + + // Certain properties are necessary to set on an element of type ASLayoutSpec + if (layoutElement.layoutElementType == ASLayoutElementTypeLayoutSpec) { + ASLayoutSpec *layoutSpec = (ASLayoutSpec *)layoutElement; + +#if AS_DEDUPE_LAYOUT_SPEC_TREE + NSHashTable *duplicateElements = [layoutSpec findDuplicatedElementsInSubtree]; + if (duplicateElements.count > 0) { + ASDisplayNodeFailAssert(@"Node %@ returned a layout spec that contains the same elements in multiple positions. Elements: %@", self, duplicateElements); + // Use an empty layout spec to avoid crashes + layoutSpec = [[ASLayoutSpec alloc] init]; + } +#endif + + ASDisplayNodeAssert(layoutSpec.isMutable, @"Node %@ returned layout spec %@ that has already been used. Layout specs should always be regenerated.", self, layoutSpec); + + layoutSpec.isMutable = NO; + } + + // Manually propagate the trait collection here so that any layoutSpec children of layoutSpec will get a traitCollection + { + ASDN::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + ASTraitCollectionPropagateDown(layoutElement, self.primitiveTraitCollection); + } + + BOOL measureLayoutComputation = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutComputation; + if (measureLayoutComputation) { + _layoutComputationNumberOfPasses++; + } + + // Layout element layout creation + ASLayout *layout = ({ + ASDN::SumScopeTimer t(_layoutComputationTotalTime, measureLayoutComputation); + [layoutElement layoutThatFits:constrainedSize]; + }); + ASDisplayNodeAssertNotNil(layout, @"[ASLayoutElement layoutThatFits:] should never return nil! %@, %@", self, layout); + + // Make sure layoutElementObject of the root layout is `self`, so that the flattened layout will be structurally correct. + BOOL isFinalLayoutElement = (layout.layoutElement != self); + if (isFinalLayoutElement) { + layout.position = CGPointZero; + layout = [ASLayout layoutWithLayoutElement:self size:layout.size sublayouts:@[layout]]; + } + ASDisplayNodeLogEvent(self, @"computedLayout: %@", layout); + + // PR #1157: Reduces accuracy of _unflattenedLayout for debugging/Weaver + if ([ASDisplayNode shouldStoreUnflattenedLayouts]) { + _unflattenedLayout = layout; + } + layout = [layout filteredNodeLayoutTree]; + + return layout; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + ASDisplayNodeLogEvent(self, @"calculateSizeThatFits: with constrainedSize: %@", NSStringFromCGSize(constrainedSize)); + + return ASIsCGSizeValidForSize(constrainedSize) ? constrainedSize : CGSizeZero; +} + +- (id)_locked_layoutElementThatFits:(ASSizeRange)constrainedSize +{ + ASAssertLocked(__instanceLock__); + __ASDisplayNodeCheckForLayoutMethodOverrides; + + BOOL measureLayoutSpec = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec; + + if (_layoutSpecBlock != NULL) { + return ({ + ASDN::MutexLocker l(__instanceLock__); + ASDN::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + _layoutSpecBlock(self, constrainedSize); + }); + } else { + return ({ + ASDN::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + [self layoutSpecThatFits:constrainedSize]; + }); + } +} + +- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + ASDisplayNodeAssert(NO, @"-[ASDisplayNode layoutSpecThatFits:] should never return an empty value. One way this is caused is by calling -[super layoutSpecThatFits:] which is not currently supported."); + return [[ASLayoutSpec alloc] init]; +} + +- (void)layout +{ + // Hook for subclasses + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertTrue(self.isNodeLoaded); + [self enumerateInterfaceStateDelegates:^(id del) { + [del nodeDidLayout]; + }]; +} + +#pragma mark Layout Transition + +- (void)_layoutTransitionMeasurementDidFinish +{ + // Hook for subclasses - No-Op in ASDisplayNode +} + +#pragma mark <_ASTransitionContextCompletionDelegate> + +/** + * After completeTransition: is called on the ASContextTransitioning object in animateLayoutTransition: this + * delegate method will be called that start the completion process of the transition + */ +- (void)transitionContext:(_ASTransitionContext *)context didComplete:(BOOL)didComplete +{ + ASDisplayNodeAssertMainThread(); + + [self didCompleteLayoutTransition:context]; + + _pendingLayoutTransitionContext = nil; + + [self _pendingLayoutTransitionDidComplete]; +} + +- (void)calculatedLayoutDidChange +{ + // Subclass override +} + +#pragma mark - Display + +NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification = @"ASRenderingEngineDidDisplayScheduledNodes"; +NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp"; + +- (BOOL)displaysAsynchronously +{ + ASDN::MutexLocker l(__instanceLock__); + return [self _locked_displaysAsynchronously]; +} + +/** + * Core implementation of -displaysAsynchronously. + */ +- (BOOL)_locked_displaysAsynchronously +{ + ASAssertLocked(__instanceLock__); + return checkFlag(Synchronous) == NO && _flags.displaysAsynchronously; +} + +- (void)setDisplaysAsynchronously:(BOOL)displaysAsynchronously +{ + ASDisplayNodeAssertThreadAffinity(self); + + ASDN::MutexLocker l(__instanceLock__); + + // Can't do this for synchronous nodes (using layers that are not _ASDisplayLayer and so we can't control display prevention/cancel) + if (checkFlag(Synchronous)) { + return; + } + + if (_flags.displaysAsynchronously == displaysAsynchronously) { + return; + } + + _flags.displaysAsynchronously = displaysAsynchronously; + + self._locked_asyncLayer.displaysAsynchronously = displaysAsynchronously; +} + +- (BOOL)rasterizesSubtree +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.rasterizesSubtree; +} + +- (void)enableSubtreeRasterization +{ + ASDN::MutexLocker l(__instanceLock__); + // Already rasterized from self. + if (_flags.rasterizesSubtree) { + return; + } + + // If rasterized from above, bail. + if (ASHierarchyStateIncludesRasterized(_hierarchyState)) { + ASDisplayNodeFailAssert(@"Subnode of a rasterized node should not have redundant -enableSubtreeRasterization."); + return; + } + + // Ensure not loaded. + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeFailAssert(@"Cannot call %@ on loaded node: %@", NSStringFromSelector(_cmd), self); + return; + } + + // Ensure no loaded subnodes + ASDisplayNode *loadedSubnode = ASDisplayNodeFindFirstSubnode(self, ^BOOL(ASDisplayNode * _Nonnull node) { + return node.nodeLoaded; + }); + if (loadedSubnode != nil) { + ASDisplayNodeFailAssert(@"Cannot call %@ on node %@ with loaded subnode %@", NSStringFromSelector(_cmd), self, loadedSubnode); + return; + } + + _flags.rasterizesSubtree = YES; + + // Tell subnodes that now they're in a rasterized hierarchy (while holding lock!) + for (ASDisplayNode *subnode in _subnodes) { + [subnode enterHierarchyState:ASHierarchyStateRasterized]; + } +} + +- (CGFloat)contentsScaleForDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + + return _contentsScaleForDisplay; +} + +- (void)setContentsScaleForDisplay:(CGFloat)contentsScaleForDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + + if (_contentsScaleForDisplay == contentsScaleForDisplay) { + return; + } + + _contentsScaleForDisplay = contentsScaleForDisplay; +} + +- (void)displayImmediately +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!checkFlag(Synchronous), @"this method is designed for asynchronous mode only"); + + [self.asyncLayer displayImmediately]; +} + +- (void)recursivelyDisplayImmediately +{ + for (ASDisplayNode *child in self.subnodes) { + [child recursivelyDisplayImmediately]; + } + [self displayImmediately]; +} + +- (void)__setNeedsDisplay +{ + BOOL shouldScheduleForDisplay = NO; + { + ASDN::MutexLocker l(__instanceLock__); + BOOL nowDisplay = ASInterfaceStateIncludesDisplay(_interfaceState); + // FIXME: This should not need to recursively display, so create a non-recursive variant. + // The semantics of setNeedsDisplay (as defined by CALayer behavior) are not recursive. + if (_layer != nil && !checkFlag(Synchronous) && nowDisplay && [self _implementsDisplay]) { + shouldScheduleForDisplay = YES; + } + } + + if (shouldScheduleForDisplay) { + [ASDisplayNode scheduleNodeForRecursiveDisplay:self]; + } +} + ++ (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node +{ + static dispatch_once_t onceToken; + static ASRunLoopQueue *renderQueue; + dispatch_once(&onceToken, ^{ + renderQueue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() + retainObjects:NO + handler:^(ASDisplayNode * _Nonnull dequeuedItem, BOOL isQueueDrained) { + [dequeuedItem _recursivelyTriggerDisplayAndBlock:NO]; + if (isQueueDrained) { + CFTimeInterval timestamp = CACurrentMediaTime(); + [[NSNotificationCenter defaultCenter] postNotificationName:ASRenderingEngineDidDisplayScheduledNodesNotification + object:nil + userInfo:@{ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp: @(timestamp)}]; + } + }]; + }); + + as_log_verbose(ASDisplayLog(), "%s %@", sel_getName(_cmd), node); + [renderQueue enqueue:node]; +} + +/// Helper method to summarize whether or not the node run through the display process +- (BOOL)_implementsDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + + return _flags.implementsDrawRect || _flags.implementsImageDisplay || _flags.rasterizesSubtree; +} + +// Track that a node will be displayed as part of the current node hierarchy. +// The node sending the message should usually be passed as the parameter, similar to the delegation pattern. +- (void)_pendingNodeWillDisplay:(ASDisplayNode *)node +{ + ASDisplayNodeAssertMainThread(); + + // No lock needed as _pendingDisplayNodes is main thread only + if (!_pendingDisplayNodes) { + _pendingDisplayNodes = [[ASWeakSet alloc] init]; + } + + [_pendingDisplayNodes addObject:node]; +} + +// Notify that a node that was pending display finished +// The node sending the message should usually be passed as the parameter, similar to the delegation pattern. +- (void)_pendingNodeDidDisplay:(ASDisplayNode *)node +{ + ASDisplayNodeAssertMainThread(); + + // No lock for _pendingDisplayNodes needed as it's main thread only + [_pendingDisplayNodes removeObject:node]; + + if (_pendingDisplayNodes.isEmpty) { + + [self hierarchyDisplayDidFinish]; + [self enumerateInterfaceStateDelegates:^(id delegate) { + [delegate hierarchyDisplayDidFinish]; + }]; + + BOOL placeholderShouldPersist = [self placeholderShouldPersist]; + + __instanceLock__.lock(); + if (_placeholderLayer.superlayer && !placeholderShouldPersist) { + void (^cleanupBlock)() = ^{ + [_placeholderLayer removeFromSuperlayer]; + }; + + if (_placeholderFadeDuration > 0.0 && ASInterfaceStateIncludesVisible(self.interfaceState)) { + [CATransaction begin]; + [CATransaction setCompletionBlock:cleanupBlock]; + [CATransaction setAnimationDuration:_placeholderFadeDuration]; + _placeholderLayer.opacity = 0.0; + [CATransaction commit]; + } else { + cleanupBlock(); + } + } + __instanceLock__.unlock(); + } +} + +- (void)hierarchyDisplayDidFinish +{ + // Subclass hook +} + +// Helper method to determine if it's safe to call setNeedsDisplay on a layer without throwing away the content. +// For details look at the comment on the canCallSetNeedsDisplayOfLayer flag +- (BOOL)_canCallSetNeedsDisplayOfLayer +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.canCallSetNeedsDisplayOfLayer; +} + +void recursivelyTriggerDisplayForLayer(CALayer *layer, BOOL shouldBlock) +{ + // This recursion must handle layers in various states: + // 1. Just added to hierarchy, CA hasn't yet called -display + // 2. Previously in a hierarchy (such as a working window owned by an Intelligent Preloading class, like ASTableView / ASCollectionView / ASViewController) + // 3. Has no content to display at all + // Specifically for case 1), we need to explicitly trigger a -display call now. + // Otherwise, there is no opportunity to block the main thread after CoreAnimation's transaction commit + // (even a runloop observer at a late call order will not stop the next frame from compositing, showing placeholders). + + ASDisplayNode *node = [layer asyncdisplaykit_node]; + + if (node.isSynchronous && [node _canCallSetNeedsDisplayOfLayer]) { + // Layers for UIKit components that are wrapped within a node needs to be set to be displayed as the contents of + // the layer get's cleared and would not be recreated otherwise. + // We do not call this for _ASDisplayLayer as an optimization. + [layer setNeedsDisplay]; + } + + if ([node _implementsDisplay]) { + // For layers that do get displayed here, this immediately kicks off the work on the concurrent -[_ASDisplayLayer displayQueue]. + // At the same time, it creates an associated _ASAsyncTransaction, which we can use to block on display completion. See ASDisplayNode+AsyncDisplay.mm. + [layer displayIfNeeded]; + } + + // Kick off the recursion first, so that all necessary display calls are sent and the displayQueue is full of parallelizable work. + // NOTE: The docs report that `sublayers` returns a copy but it actually doesn't. + for (CALayer *sublayer in [layer.sublayers copy]) { + recursivelyTriggerDisplayForLayer(sublayer, shouldBlock); + } + + if (shouldBlock) { + // As the recursion unwinds, verify each transaction is complete and block if it is not. + // While blocking on one transaction, others may be completing concurrently, so it doesn't matter which blocks first. + BOOL waitUntilComplete = (!node.shouldBypassEnsureDisplay); + if (waitUntilComplete) { + for (_ASAsyncTransaction *transaction in [layer.asyncdisplaykit_asyncLayerTransactions copy]) { + // Even if none of the layers have had a chance to start display earlier, they will still be allowed to saturate a multicore CPU while blocking main. + // This significantly reduces time on the main thread relative to UIKit. + [transaction waitUntilComplete]; + } + } + } +} + +- (void)_recursivelyTriggerDisplayAndBlock:(BOOL)shouldBlock +{ + ASDisplayNodeAssertMainThread(); + + CALayer *layer = self.layer; + // -layoutIfNeeded is recursive, and even walks up to superlayers to check if they need layout, + // so we should call it outside of starting the recursion below. If our own layer is not marked + // as dirty, we can assume layout has run on this subtree before. + if ([layer needsLayout]) { + [layer layoutIfNeeded]; + } + recursivelyTriggerDisplayForLayer(layer, shouldBlock); +} + +- (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously +{ + [self _recursivelyTriggerDisplayAndBlock:synchronously]; +} + +- (void)setShouldBypassEnsureDisplay:(BOOL)shouldBypassEnsureDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + _flags.shouldBypassEnsureDisplay = shouldBypassEnsureDisplay; +} + +- (BOOL)shouldBypassEnsureDisplay +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.shouldBypassEnsureDisplay; +} + +- (void)setNeedsDisplayAtScale:(CGFloat)contentsScale +{ + { + ASDN::MutexLocker l(__instanceLock__); + if (contentsScale == _contentsScaleForDisplay) { + return; + } + + _contentsScaleForDisplay = contentsScale; + } + + [self setNeedsDisplay]; +} + +- (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale +{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + [node setNeedsDisplayAtScale:contentsScale]; + }); +} + +- (void)_layoutClipCornersIfNeeded +{ + ASDisplayNodeAssertMainThread(); + if (_clipCornerLayers[0] == nil) { + return; + } + + CGSize boundsSize = self.bounds.size; + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + if (_clipCornerLayers[idx]) { + // Note the Core Animation coordinates are reversed for y; 0 is at the bottom. + _clipCornerLayers[idx].position = CGPointMake(isRight ? boundsSize.width : 0.0, isTop ? boundsSize.height : 0.0); + [_layer addSublayer:_clipCornerLayers[idx]]; + } + } +} + +- (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor:(UIColor *)backgroundColor +{ + ASPerformBlockOnMainThread(^{ + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + // Layers are, in order: Top Left, Top Right, Bottom Right, Bottom Left. + // anchorPoint is Bottom Left at 0,0 and Top Right at 1,1. + BOOL isTop = (idx == 0 || idx == 1); + BOOL isRight = (idx == 1 || idx == 2); + + CGSize size = CGSizeMake(radius + 1, radius + 1); + ASGraphicsBeginImageContextWithOptions(size, NO, self.contentsScaleForDisplay); + + CGContextRef ctx = UIGraphicsGetCurrentContext(); + if (isRight == YES) { + CGContextTranslateCTM(ctx, -radius + 1, 0); + } + if (isTop == YES) { + CGContextTranslateCTM(ctx, 0, -radius + 1); + } + UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius]; + [roundedRect setUsesEvenOddFillRule:YES]; + [roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]]; + [backgroundColor setFill]; + [roundedRect fill]; + + // No lock needed, as _clipCornerLayers is only modified on the main thread. + CALayer *clipCornerLayer = _clipCornerLayers[idx]; + clipCornerLayer.contents = (id)(ASGraphicsGetImageAndEndCurrentContext().CGImage); + clipCornerLayer.bounds = CGRectMake(0.0, 0.0, size.width, size.height); + clipCornerLayer.anchorPoint = CGPointMake(isRight ? 1.0 : 0.0, isTop ? 1.0 : 0.0); + } + [self _layoutClipCornersIfNeeded]; + }); +} + +- (void)_setClipCornerLayersVisible:(BOOL)visible +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + if (visible) { + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + if (_clipCornerLayers[idx] == nil) { + static ASDisplayNodeCornerLayerDelegate *clipCornerLayers; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + clipCornerLayers = [[ASDisplayNodeCornerLayerDelegate alloc] init]; + }); + _clipCornerLayers[idx] = [[CALayer alloc] init]; + _clipCornerLayers[idx].zPosition = 99999; + _clipCornerLayers[idx].delegate = clipCornerLayers; + } + } + [self _updateClipCornerLayerContentsWithRadius:_cornerRadius backgroundColor:self.backgroundColor]; + } else { + for (int idx = 0; idx < NUM_CLIP_CORNER_LAYERS; idx++) { + [_clipCornerLayers[idx] removeFromSuperlayer]; + _clipCornerLayers[idx] = nil; + } + } + }); +} + +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius +{ + __instanceLock__.lock(); + CGFloat oldCornerRadius = _cornerRadius; + ASCornerRoundingType oldRoundingType = _cornerRoundingType; + + _cornerRadius = newCornerRadius; + _cornerRoundingType = newRoundingType; + __instanceLock__.unlock(); + + ASPerformBlockOnMainThread(^{ + ASDisplayNodeAssertMainThread(); + + if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius) { + if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + if (newRoundingType == ASCornerRoundingTypePrecomposited) { + self.layerCornerRadius = 0.0; + if (oldCornerRadius > 0.0) { + [self displayImmediately]; + } else { + [self setNeedsDisplay]; // Async display is OK if we aren't replacing an existing .cornerRadius. + } + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + self.layerCornerRadius = 0.0; + [self _setClipCornerLayersVisible:YES]; + } else if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + } + } + else if (oldRoundingType == ASCornerRoundingTypePrecomposited) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + // Corners are already precomposited, but the radius has changed. + // Default to async re-display. The user may force a synchronous display if desired. + [self setNeedsDisplay]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + [self _setClipCornerLayersVisible:YES]; + [self setNeedsDisplay]; + } + } + else if (oldRoundingType == ASCornerRoundingTypeClipping) { + if (newRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { + self.layerCornerRadius = newCornerRadius; + [self _setClipCornerLayersVisible:NO]; + } + else if (newRoundingType == ASCornerRoundingTypePrecomposited) { + [self _setClipCornerLayersVisible:NO]; + [self displayImmediately]; + } + else if (newRoundingType == ASCornerRoundingTypeClipping) { + // Clip corners already exist, but the radius has changed. + [self _updateClipCornerLayerContentsWithRadius:newCornerRadius backgroundColor:self.backgroundColor]; + } + } + } + }); +} + +- (void)recursivelySetDisplaySuspended:(BOOL)flag +{ + _recursivelySetDisplaySuspended(self, nil, flag); +} + +// TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or a variant with a condition / test block. +static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, BOOL flag) +{ + // If there is no layer, but node whose its view is loaded, then we can traverse down its layer hierarchy. Otherwise we must stick to the node hierarchy to avoid loading views prematurely. Note that for nodes that haven't loaded their views, they can't possibly have subviews/sublayers, so we don't need to traverse the layer hierarchy for them. + if (!layer && node && node.nodeLoaded) { + layer = node.layer; + } + + // If we don't know the node, but the layer is an async layer, get the node from the layer. + if (!node && layer && [layer isKindOfClass:[_ASDisplayLayer class]]) { + node = layer.asyncdisplaykit_node; + } + + // Set the flag on the node. If this is a pure layer (no node) then this has no effect (plain layers don't support preventing/cancelling display). + node.displaySuspended = flag; + + if (layer && !node.rasterizesSubtree) { + // If there is a layer, recurse down the layer hierarchy to set the flag on descendants. This will cover both layer-based and node-based children. + for (CALayer *sublayer in layer.sublayers) { + _recursivelySetDisplaySuspended(nil, sublayer, flag); + } + } else { + // If there is no layer (view not loaded yet) or this node rasterizes descendants (there won't be a layer tree to traverse), recurse down the subnode hierarchy to set the flag on descendants. This covers only node-based children, but for a node whose view is not loaded it can't possibly have nodeless children. + for (ASDisplayNode *subnode in node.subnodes) { + _recursivelySetDisplaySuspended(subnode, nil, flag); + } + } +} + +- (BOOL)displaySuspended +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.displaySuspended; +} + +- (void)setDisplaySuspended:(BOOL)flag +{ + ASDisplayNodeAssertThreadAffinity(self); + __instanceLock__.lock(); + + // Can't do this for synchronous nodes (using layers that are not _ASDisplayLayer and so we can't control display prevention/cancel) + if (checkFlag(Synchronous) || _flags.displaySuspended == flag) { + __instanceLock__.unlock(); + return; + } + + _flags.displaySuspended = flag; + + self._locked_asyncLayer.displaySuspended = flag; + + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + if ([self _implementsDisplay]) { + // Display start and finish methods needs to happen on the main thread + ASPerformBlockOnMainThread(^{ + if (flag) { + [supernode subnodeDisplayDidFinish:self]; + } else { + [supernode subnodeDisplayWillStart:self]; + } + }); + } +} + +#pragma mark <_ASDisplayLayerDelegate> + +- (void)willDisplayAsyncLayer:(_ASDisplayLayer *)layer asynchronously:(BOOL)asynchronously +{ + // Subclass hook. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-declarations" + [self displayWillStart]; +#pragma clang diagnostic pop + + [self displayWillStartAsynchronously:asynchronously]; +} + +- (void)didDisplayAsyncLayer:(_ASDisplayLayer *)layer +{ + // Subclass hook. + [self displayDidFinish]; +} + +- (void)displayWillStart {} +- (void)displayWillStartAsynchronously:(BOOL)asynchronously +{ + ASDisplayNodeAssertMainThread(); + + ASDisplayNodeLogEvent(self, @"displayWillStart"); + // in case current node takes longer to display than it's subnodes, treat it as a dependent node + [self _pendingNodeWillDisplay:self]; + + __instanceLock__.lock(); + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + [supernode subnodeDisplayWillStart:self]; +} + +- (void)displayDidFinish +{ + ASDisplayNodeAssertMainThread(); + + ASDisplayNodeLogEvent(self, @"displayDidFinish"); + [self _pendingNodeDidDisplay:self]; + + __instanceLock__.lock(); + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + [supernode subnodeDisplayDidFinish:self]; +} + +- (void)subnodeDisplayWillStart:(ASDisplayNode *)subnode +{ + // Subclass hook + [self _pendingNodeWillDisplay:subnode]; +} + +- (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode +{ + // Subclass hook + [self _pendingNodeDidDisplay:subnode]; +} + +#pragma mark + +// We are only the delegate for the layer when we are layer-backed, as UIView performs this function normally +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event +{ + if (event == kCAOnOrderIn) { + [self __enterHierarchy]; + } else if (event == kCAOnOrderOut) { + [self __exitHierarchy]; + } + + ASDisplayNodeAssert(_flags.layerBacked, @"We shouldn't get called back here unless we are layer-backed."); + return (id)kCFNull; +} + +#pragma mark - Error Handling + ++ (void)setNonFatalErrorBlock:(ASDisplayNodeNonFatalErrorBlock)nonFatalErrorBlock +{ + if (_nonFatalErrorBlock != nonFatalErrorBlock) { + _nonFatalErrorBlock = [nonFatalErrorBlock copy]; + } +} + ++ (ASDisplayNodeNonFatalErrorBlock)nonFatalErrorBlock +{ + return _nonFatalErrorBlock; +} + +#pragma mark - Converting to and from the Node's Coordinate System + +- (CATransform3D)_transformToAncestor:(ASDisplayNode *)ancestor +{ + CATransform3D transform = CATransform3DIdentity; + ASDisplayNode *currentNode = self; + while (currentNode.supernode) { + if (currentNode == ancestor) { + return transform; + } + + CGPoint anchorPoint = currentNode.anchorPoint; + CGRect bounds = currentNode.bounds; + CGPoint position = currentNode.position; + CGPoint origin = CGPointMake(position.x - bounds.size.width * anchorPoint.x, + position.y - bounds.size.height * anchorPoint.y); + + transform = CATransform3DTranslate(transform, origin.x, origin.y, 0); + transform = CATransform3DTranslate(transform, -bounds.origin.x, -bounds.origin.y, 0); + currentNode = currentNode.supernode; + } + return transform; +} + +static inline CATransform3D _calculateTransformFromReferenceToTarget(ASDisplayNode *referenceNode, ASDisplayNode *targetNode) +{ + ASDisplayNode *ancestor = ASDisplayNodeFindClosestCommonAncestor(referenceNode, targetNode); + + // Transform into global (away from reference coordinate space) + CATransform3D transformToGlobal = [referenceNode _transformToAncestor:ancestor]; + + // Transform into local (via inverse transform from target to ancestor) + CATransform3D transformToLocal = CATransform3DInvert([targetNode _transformToAncestor:ancestor]); + + return CATransform3DConcat(transformToGlobal, transformToLocal); +} + +- (CGPoint)convertPoint:(CGPoint)point fromNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + /** + * When passed node=nil, all methods in this family use the UIView-style + * behavior – that is, convert from/to window coordinates if there's a window, + * otherwise return the point untransformed. + */ + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertPoint:point fromLayer:window.layer]; + } else { + return point; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to point + return CGPointApplyAffineTransform(point, flattenedTransform); +} + +- (CGPoint)convertPoint:(CGPoint)point toNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertPoint:point toLayer:window.layer]; + } else { + return point; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to point + return CGPointApplyAffineTransform(point, flattenedTransform); +} + +- (CGRect)convertRect:(CGRect)rect fromNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertRect:rect fromLayer:window.layer]; + } else { + return rect; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to rect + return CGRectApplyAffineTransform(rect, flattenedTransform); +} + +- (CGRect)convertRect:(CGRect)rect toNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (node == nil && self.nodeLoaded) { + CALayer *layer = self.layer; + if (UIWindow *window = ASFindWindowOfLayer(layer)) { + return [layer convertRect:rect toLayer:window.layer]; + } else { + return rect; + } + } + + // Get root node of the accessible node hierarchy, if node not specified + node = node ? : ASDisplayNodeUltimateParentOfNode(self); + + // Calculate transform to map points between coordinate spaces + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); + ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); + + // Apply to rect + return CGRectApplyAffineTransform(rect, flattenedTransform); +} + +#pragma mark - Managing the Node Hierarchy + +ASDISPLAYNODE_INLINE bool shouldDisableNotificationsForMovingBetweenParents(ASDisplayNode *from, ASDisplayNode *to) { + if (!from || !to) return NO; + if (from.isSynchronous) return NO; + if (to.isSynchronous) return NO; + if (from.isInHierarchy != to.isInHierarchy) return NO; + return YES; +} + +/// Returns incremented value of i if i is not NSNotFound +ASDISPLAYNODE_INLINE NSInteger incrementIfFound(NSInteger i) { + return i == NSNotFound ? NSNotFound : i + 1; +} + +/// Returns if a node is a member of a rasterized tree +ASDISPLAYNODE_INLINE BOOL canUseViewAPI(ASDisplayNode *node, ASDisplayNode *subnode) { + return (subnode.isLayerBacked == NO && node.isLayerBacked == NO); +} + +/// Returns if node is a member of a rasterized tree +ASDISPLAYNODE_INLINE BOOL subtreeIsRasterized(ASDisplayNode *node) { + return (node.rasterizesSubtree || (node.hierarchyState & ASHierarchyStateRasterized)); +} + +// NOTE: This method must be dealloc-safe (should not retain self). +- (ASDisplayNode *)supernode +{ +#if CHECK_LOCKING_SAFETY + if (__instanceLock__.locked()) { + NSLog(@"WARNING: Accessing supernode while holding recursive instance lock of this node is worrisome. It's likely that you will soon try to acquire the supernode's lock, and this can easily cause deadlocks."); + } +#endif + + ASDN::MutexLocker l(__instanceLock__); + return _supernode; +} + +- (void)_setSupernode:(ASDisplayNode *)newSupernode +{ + BOOL supernodeDidChange = NO; + ASDisplayNode *oldSupernode = nil; + { + ASDN::MutexLocker l(__instanceLock__); + if (_supernode != newSupernode) { + oldSupernode = _supernode; // Access supernode properties outside of lock to avoid remote chance of deadlock, + // in case supernode implementation must access one of our properties. + _supernode = newSupernode; + supernodeDidChange = YES; + } + } + + if (supernodeDidChange) { + ASDisplayNodeLogEvent(self, @"supernodeDidChange: %@, oldValue = %@", ASObjectDescriptionMakeTiny(newSupernode), ASObjectDescriptionMakeTiny(oldSupernode)); + // Hierarchy state + ASHierarchyState stateToEnterOrExit = (newSupernode ? newSupernode.hierarchyState + : oldSupernode.hierarchyState); + + // Rasterized state + BOOL parentWasOrIsRasterized = (newSupernode ? newSupernode.rasterizesSubtree + : oldSupernode.rasterizesSubtree); + if (parentWasOrIsRasterized) { + stateToEnterOrExit |= ASHierarchyStateRasterized; + } + if (newSupernode) { + [self enterHierarchyState:stateToEnterOrExit]; + + // If a node was added to a supernode, the supernode could be in a layout pending state. All of the hierarchy state + // properties related to the transition need to be copied over as well as propagated down the subtree. + // This is especially important as with automatic subnode management, adding subnodes can happen while a transition + // is in fly + if (ASHierarchyStateIncludesLayoutPending(stateToEnterOrExit)) { + int32_t pendingTransitionId = newSupernode->_pendingTransitionID; + if (pendingTransitionId != ASLayoutElementContextInvalidTransitionID) { + { + _pendingTransitionID = pendingTransitionId; + + // Propagate down the new pending transition id + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + node->_pendingTransitionID = pendingTransitionId; + }); + } + } + } + + // Now that we have a supernode, propagate its traits to self. + ASTraitCollectionPropagateDown(self, newSupernode.primitiveTraitCollection); + + } else { + // If a node will be removed from the supernode it should go out from the layout pending state to remove all + // layout pending state related properties on the node + stateToEnterOrExit |= ASHierarchyStateLayoutPending; + + [self exitHierarchyState:stateToEnterOrExit]; + + // We only need to explicitly exit hierarchy here if we were rasterized. + // Otherwise we will exit the hierarchy when our view/layer does so + // which has some nice carry-over machinery to handle cases where we are removed from a hierarchy + // and then added into it again shortly after. + __instanceLock__.lock(); + BOOL isInHierarchy = _flags.isInHierarchy; + __instanceLock__.unlock(); + + if (parentWasOrIsRasterized && isInHierarchy) { + [self __exitHierarchy]; + } + } + } +} + +- (NSArray *)subnodes +{ + ASDN::MutexLocker l(__instanceLock__); + if (_cachedSubnodes == nil) { + _cachedSubnodes = [_subnodes copy]; + } else { + ASDisplayNodeAssert(ASObjectIsEqual(_cachedSubnodes, _subnodes), @"Expected _subnodes and _cachedSubnodes to have the same contents."); + } + return _cachedSubnodes ?: @[]; +} + +/* + * Central private helper method that should eventually be called if submethods add, insert or replace subnodes + * This method is called with thread affinity and without lock held. + * + * @param subnode The subnode to insert + * @param subnodeIndex The index in _subnodes to insert it + * @param viewSublayerIndex The index in layer.sublayers (not view.subviews) at which to insert the view (use if we can use the view API) otherwise pass NSNotFound + * @param sublayerIndex The index in layer.sublayers at which to insert the layer (use if either parent or subnode is layer-backed) otherwise pass NSNotFound + * @param oldSubnode Remove this subnode before inserting; ok to be nil if no removal is desired + */ +- (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnodeIndex sublayerIndex:(NSInteger)sublayerIndex andRemoveSubnode:(ASDisplayNode *)oldSubnode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + as_log_verbose(ASNodeLog(), "Insert subnode %@ at index %zd of %@ and remove subnode %@", subnode, subnodeIndex, self, oldSubnode); + + if (subnode == nil || subnode == self) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode or self as subnode"); + return; + } + + if (subnodeIndex == NSNotFound) { + ASDisplayNodeFailAssert(@"Try to insert node on an index that was not found"); + return; + } + + if (self.layerBacked && !subnode.layerBacked) { + ASDisplayNodeFailAssert(@"Cannot add a view-backed node as a subnode of a layer-backed node. Supernode: %@, subnode: %@", self, subnode); + return; + } + + BOOL isRasterized = subtreeIsRasterized(self); + if (isRasterized && subnode.nodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot add loaded node %@ to rasterized subtree of node %@", ASObjectDescriptionMakeTiny(subnode), ASObjectDescriptionMakeTiny(self)); + return; + } + + __instanceLock__.lock(); + NSUInteger subnodesCount = _subnodes.count; + __instanceLock__.unlock(); + if (subnodeIndex > subnodesCount || subnodeIndex < 0) { + ASDisplayNodeFailAssert(@"Cannot insert a subnode at index %ld. Count is %ld", (long)subnodeIndex, (long)subnodesCount); + return; + } + + // Disable appearance methods during move between supernodes, but make sure we restore their state after we do our thing + ASDisplayNode *oldParent = subnode.supernode; + BOOL disableNotifications = shouldDisableNotificationsForMovingBetweenParents(oldParent, self); + if (disableNotifications) { + [subnode __incrementVisibilityNotificationsDisabled]; + } + + [subnode _removeFromSupernode]; + [oldSubnode _removeFromSupernode]; + + __instanceLock__.lock(); + if (_subnodes == nil) { + _subnodes = [[NSMutableArray alloc] init]; + } + [_subnodes insertObject:subnode atIndex:subnodeIndex]; + _cachedSubnodes = nil; + __instanceLock__.unlock(); + + // This call will apply our .hierarchyState to the new subnode. + // If we are a managed hierarchy, as in ASCellNode trees, it will also apply our .interfaceState. + [subnode _setSupernode:self]; + + // If this subnode will be rasterized, enter hierarchy if needed + // TODO: Move this into _setSupernode: ? + if (isRasterized) { + if (self.inHierarchy) { + [subnode __enterHierarchy]; + } + } else if (self.nodeLoaded) { + // If not rasterizing, and node is loaded insert the subview/sublayer now. + [self _insertSubnodeSubviewOrSublayer:subnode atIndex:sublayerIndex]; + } // Otherwise we will insert subview/sublayer when we get loaded + + ASDisplayNodeAssert(disableNotifications == shouldDisableNotificationsForMovingBetweenParents(oldParent, self), @"Invariant violated"); + if (disableNotifications) { + [subnode __decrementVisibilityNotificationsDisabled]; + } +} + +/* + * Inserts the view or layer of the given node at the given index + * + * @param subnode The subnode to insert + * @param idx The index in _view.subviews or _layer.sublayers at which to insert the subnode.view or + * subnode.layer of the subnode + */ +- (void)_insertSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(self.nodeLoaded, @"_insertSubnodeSubviewOrSublayer:atIndex: should never be called before our own view is created"); + + ASDisplayNodeAssert(idx != NSNotFound, @"Try to insert node on an index that was not found"); + if (idx == NSNotFound) { + return; + } + + // Because the view and layer can only be created and destroyed on Main, that is also the only thread + // where the view and layer can change. We can avoid locking. + + // If we can use view API, do. Due to an apple bug, -insertSubview:atIndex: actually wants a LAYER index, + // which we pass in. + if (canUseViewAPI(self, subnode)) { + [_view insertSubview:subnode.view atIndex:idx]; + } else { + [_layer insertSublayer:subnode.layer atIndex:(unsigned int)idx]; + } +} + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeLogEvent(self, @"addSubnode: %@ with automaticallyManagesSubnodes: %@", + subnode, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _addSubnode:subnode]; +} + +- (void)_addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertThreadAffinity(self); + + ASDisplayNodeAssert(subnode, @"Cannot insert a nil subnode"); + + // Don't add if it's already a subnode + ASDisplayNode *oldParent = subnode.supernode; + if (!subnode || subnode == self || oldParent == self) { + return; + } + + NSUInteger subnodesIndex; + NSUInteger sublayersIndex; + { + ASDN::MutexLocker l(__instanceLock__); + subnodesIndex = _subnodes.count; + sublayersIndex = _layer.sublayers.count; + } + + [self _insertSubnode:subnode atSubnodeIndex:subnodesIndex sublayerIndex:sublayersIndex andRemoveSubnode:nil]; +} + +- (void)_addSubnodeViewsAndLayers +{ + ASDisplayNodeAssertMainThread(); + + TIME_SCOPED(_debugTimeToAddSubnodeViews); + + for (ASDisplayNode *node in self.subnodes) { + [self _addSubnodeSubviewOrSublayer:node]; + } +} + +- (void)_addSubnodeSubviewOrSublayer:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertMainThread(); + + // Due to a bug in Apple's framework we have to use the layer index to insert a subview + // so just use the count of the sublayers to add the subnode + NSInteger idx = _layer.sublayers.count; // No locking is needed as it's main thread only + [self _insertSubnodeSubviewOrSublayer:subnode atIndex:idx]; +} + +- (void)replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode +{ + ASDisplayNodeLogEvent(self, @"replaceSubnode: %@ withSubnode: %@ with automaticallyManagesSubnodes: %@", + oldSubnode, replacementSubnode, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _replaceSubnode:oldSubnode withSubnode:replacementSubnode]; +} + +- (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode +{ + ASDisplayNodeAssertThreadAffinity(self); + + if (replacementSubnode == nil) { + ASDisplayNodeFailAssert(@"Invalid subnode to replace"); + return; + } + + if (oldSubnode.supernode != self) { + ASDisplayNodeFailAssert(@"Old Subnode to replace must be a subnode"); + return; + } + + ASDisplayNodeAssert(!(self.nodeLoaded && !oldSubnode.nodeLoaded), @"We have view loaded, but child node does not."); + + NSInteger subnodeIndex; + NSInteger sublayerIndex = NSNotFound; + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + subnodeIndex = [_subnodes indexOfObjectIdenticalTo:oldSubnode]; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + if (_layer) { + sublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:oldSubnode.layer]; + ASDisplayNodeAssert(sublayerIndex != NSNotFound, @"Somehow oldSubnode's supernode is self, yet we could not find it in our layers to replace"); + if (sublayerIndex == NSNotFound) { + return; + } + } + } + } + + [self _insertSubnode:replacementSubnode atSubnodeIndex:subnodeIndex sublayerIndex:sublayerIndex andRemoveSubnode:oldSubnode]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below +{ + ASDisplayNodeLogEvent(self, @"insertSubnode: %@ belowSubnode: %@ with automaticallyManagesSubnodes: %@", + subnode, below, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _insertSubnode:subnode belowSubnode:below]; +} + +- (void)_insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + if (below.supernode != self) { + ASDisplayNodeFailAssert(@"Node to insert below must be a subnode"); + return; + } + + NSInteger belowSubnodeIndex; + NSInteger belowSublayerIndex = NSNotFound; + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + belowSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:below]; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + if (_layer) { + belowSublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:below.layer]; + ASDisplayNodeAssert(belowSublayerIndex != NSNotFound, @"Somehow below's supernode is self, yet we could not find it in our layers to reference"); + if (belowSublayerIndex == NSNotFound) + return; + } + + ASDisplayNodeAssert(belowSubnodeIndex != NSNotFound, @"Couldn't find above in subnodes"); + + // If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to + // insert it will mess up our calculation + if (subnode.supernode == self) { + NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode]; + if (currentIndexInSubnodes < belowSubnodeIndex) { + belowSubnodeIndex--; + } + if (_layer) { + NSInteger currentIndexInSublayers = [_layer.sublayers indexOfObjectIdenticalTo:subnode.layer]; + if (currentIndexInSublayers < belowSublayerIndex) { + belowSublayerIndex--; + } + } + } + } + } + + ASDisplayNodeAssert(belowSubnodeIndex != NSNotFound, @"Couldn't find below in subnodes"); + + [self _insertSubnode:subnode atSubnodeIndex:belowSubnodeIndex sublayerIndex:belowSublayerIndex andRemoveSubnode:nil]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above +{ + ASDisplayNodeLogEvent(self, @"insertSubnode: %@ abodeSubnode: %@ with automaticallyManagesSubnodes: %@", + subnode, above, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _insertSubnode:subnode aboveSubnode:above]; +} + +- (void)_insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + if (above.supernode != self) { + ASDisplayNodeFailAssert(@"Node to insert above must be a subnode"); + return; + } + + NSInteger aboveSubnodeIndex; + NSInteger aboveSublayerIndex = NSNotFound; + { + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodeAssert(_subnodes, @"You should have subnodes if you have a subnode"); + + aboveSubnodeIndex = [_subnodes indexOfObjectIdenticalTo:above]; + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + if (_layer) { + aboveSublayerIndex = [_layer.sublayers indexOfObjectIdenticalTo:above.layer]; + ASDisplayNodeAssert(aboveSublayerIndex != NSNotFound, @"Somehow above's supernode is self, yet we could not find it in our layers to replace"); + if (aboveSublayerIndex == NSNotFound) + return; + } + + ASDisplayNodeAssert(aboveSubnodeIndex != NSNotFound, @"Couldn't find above in subnodes"); + + // If the subnode is already in the subnodes array / sublayers and it's before the below node, removing it to + // insert it will mess up our calculation + if (subnode.supernode == self) { + NSInteger currentIndexInSubnodes = [_subnodes indexOfObjectIdenticalTo:subnode]; + if (currentIndexInSubnodes <= aboveSubnodeIndex) { + aboveSubnodeIndex--; + } + if (_layer) { + NSInteger currentIndexInSublayers = [_layer.sublayers indexOfObjectIdenticalTo:subnode.layer]; + if (currentIndexInSublayers <= aboveSublayerIndex) { + aboveSublayerIndex--; + } + } + } + } + } + + [self _insertSubnode:subnode atSubnodeIndex:incrementIfFound(aboveSubnodeIndex) sublayerIndex:incrementIfFound(aboveSublayerIndex) andRemoveSubnode:nil]; +} + +- (void)insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeLogEvent(self, @"insertSubnode: %@ atIndex: %td with automaticallyManagesSubnodes: %@", + subnode, idx, self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _insertSubnode:subnode atIndex:idx]; +} + +- (void)_insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + NSInteger sublayerIndex = NSNotFound; + { + ASDN::MutexLocker l(__instanceLock__); + + if (idx > _subnodes.count || idx < 0) { + ASDisplayNodeFailAssert(@"Cannot insert a subnode at index %ld. Count is %ld", (long)idx, (long)_subnodes.count); + return; + } + + // Don't bother figuring out the sublayerIndex if in a rasterized subtree, because there are no layers in the + // hierarchy and none of this could possibly work. + if (subtreeIsRasterized(self) == NO) { + // Account for potentially having other subviews + if (_layer && idx == 0) { + sublayerIndex = 0; + } else if (_layer) { + ASDisplayNode *positionInRelationTo = (_subnodes.count > 0 && idx > 0) ? _subnodes[idx - 1] : nil; + if (positionInRelationTo) { + sublayerIndex = incrementIfFound([_layer.sublayers indexOfObjectIdenticalTo:positionInRelationTo.layer]); + } + } + } + } + + [self _insertSubnode:subnode atSubnodeIndex:idx sublayerIndex:sublayerIndex andRemoveSubnode:nil]; +} + +- (void)_removeSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + // Don't call self.supernode here because that will retain/autorelease the supernode. This method -_removeSupernode: is often called while tearing down a node hierarchy, and the supernode in question might be in the middle of its -dealloc. The supernode is never messaged, only compared by value, so this is safe. + // The particular issue that triggers this edge case is when a node calls -removeFromSupernode on a subnode from within its own -dealloc method. + if (!subnode || subnode.supernode != self) { + return; + } + + __instanceLock__.lock(); + [_subnodes removeObjectIdenticalTo:subnode]; + _cachedSubnodes = nil; + __instanceLock__.unlock(); + + [subnode _setSupernode:nil]; +} + +- (void)removeFromSupernode +{ + ASDisplayNodeLogEvent(self, @"removeFromSupernode with automaticallyManagesSubnodes: %@", + self.automaticallyManagesSubnodes ? @"YES" : @"NO"); + [self _removeFromSupernode]; +} + +- (void)_removeFromSupernode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + __instanceLock__.lock(); + __weak ASDisplayNode *supernode = _supernode; + __weak UIView *view = _view; + __weak CALayer *layer = _layer; + __instanceLock__.unlock(); + + [self _removeFromSupernode:supernode view:view layer:layer]; +} + +- (void)_removeFromSupernodeIfEqualTo:(ASDisplayNode *)supernode +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + __instanceLock__.lock(); + + // Only remove if supernode is still the expected supernode + if (!ASObjectIsEqual(_supernode, supernode)) { + __instanceLock__.unlock(); + return; + } + + __weak UIView *view = _view; + __weak CALayer *layer = _layer; + __instanceLock__.unlock(); + + [self _removeFromSupernode:supernode view:view layer:layer]; +} + +- (void)_removeFromSupernode:(ASDisplayNode *)supernode view:(UIView *)view layer:(CALayer *)layer +{ + // Note: we continue even if supernode is nil to ensure view/layer are removed from hierarchy. + + if (supernode != nil) { + as_log_verbose(ASNodeLog(), "Remove %@ from supernode %@", self, supernode); + } + + // Clear supernode's reference to us before removing the view from the hierarchy, as _ASDisplayView + // will trigger us to clear our _supernode pointer in willMoveToSuperview:nil. + // This may result in removing the last strong reference, triggering deallocation after this method. + [supernode _removeSubnode:self]; + + if (view != nil) { + [view removeFromSuperview]; + } else if (layer != nil) { + [layer removeFromSuperlayer]; + } +} + +#pragma mark - Visibility API + +- (BOOL)__visibilityNotificationsDisabled +{ + // Currently, this method is only used by the testing infrastructure to verify this internal feature. + ASDN::MutexLocker l(__instanceLock__); + return _flags.visibilityNotificationsDisabled > 0; +} + +- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled +{ + ASDN::MutexLocker l(__instanceLock__); + return (_hierarchyState & ASHierarchyStateTransitioningSupernodes); +} + +- (void)__incrementVisibilityNotificationsDisabled +{ + __instanceLock__.lock(); + const size_t maxVisibilityIncrement = (1ULL< 0, @"Can't decrement past 0"); + if (_flags.visibilityNotificationsDisabled > 0) { + _flags.visibilityNotificationsDisabled--; + } + BOOL visibilityNotificationsDisabled = (_flags.visibilityNotificationsDisabled == 0); + __instanceLock__.unlock(); + + if (visibilityNotificationsDisabled) { + // Must have just transitioned from 1 to 0. Notify all subnodes that we are no longer in a disabled state. + // FIXME: This system should be revisited when refactoring and consolidating the implementation of the + // addSubnode: and insertSubnode:... methods. As implemented, though logically irrelevant for expected use cases, + // multiple nodes in the subtree below may have a non-zero visibilityNotification count and still have + // the ASHierarchyState bit cleared (the only value checked when reading this state). + [self exitHierarchyState:ASHierarchyStateTransitioningSupernodes]; + } +} + +#pragma mark - Placeholder + +- (void)_locked_layoutPlaceholderIfNecessary +{ + ASAssertLocked(__instanceLock__); + if ([self _locked_shouldHavePlaceholderLayer]) { + [self _locked_setupPlaceholderLayerIfNeeded]; + } + // Update the placeholderLayer size in case the node size has changed since the placeholder was added. + _placeholderLayer.frame = self.threadSafeBounds; +} + +- (BOOL)_locked_shouldHavePlaceholderLayer +{ + ASAssertLocked(__instanceLock__); + return (_placeholderEnabled && [self _implementsDisplay]); +} + +- (void)_locked_setupPlaceholderLayerIfNeeded +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + + if (!_placeholderLayer) { + _placeholderLayer = [CALayer layer]; + // do not set to CGFLOAT_MAX in the case that something needs to be overtop the placeholder + _placeholderLayer.zPosition = 9999.0; + } + + if (_placeholderLayer.contents == nil) { + if (!_placeholderImage) { + _placeholderImage = [self placeholderImage]; + } + if (_placeholderImage) { + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(_placeholderImage.capInsets, UIEdgeInsetsZero); + if (stretchable) { + ASDisplayNodeSetResizableContents(_placeholderLayer, _placeholderImage); + } else { + _placeholderLayer.contentsScale = self.contentsScale; + _placeholderLayer.contents = (id)_placeholderImage.CGImage; + } + } + } +} + +- (UIImage *)placeholderImage +{ + // Subclass hook + return nil; +} + +- (BOOL)placeholderShouldPersist +{ + // Subclass hook + return NO; +} + +#pragma mark - Hierarchy State + +- (BOOL)isInHierarchy +{ + ASDN::MutexLocker l(__instanceLock__); + return _flags.isInHierarchy; +} + +- (void)__enterHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"Should not cause recursive __enterHierarchy"); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeLogEvent(self, @"enterHierarchy"); + + // Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock. + __instanceLock__.lock(); + + if (!_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + _flags.isEnteringHierarchy = YES; + _flags.isInHierarchy = YES; + + // Don't call -willEnterHierarchy while holding __instanceLock__. + // This method and subsequent ones (i.e -interfaceState and didEnter(.*)State) + // don't expect that they are called while the lock is being held. + // More importantly, didEnter(.*)State methods are meant to be overriden by clients. + // And so they can potentially walk up the node tree and cause deadlocks, or do expensive tasks and cause the lock to be held for too long. + __instanceLock__.unlock(); + [self willEnterHierarchy]; + for (ASDisplayNode *subnode in self.subnodes) { + [subnode __enterHierarchy]; + } + __instanceLock__.lock(); + + _flags.isEnteringHierarchy = NO; + + // If we don't have contents finished drawing by the time we are on screen, immediately add the placeholder (if it is enabled and we do have something to draw). + if (self.contents == nil && [self _implementsDisplay]) { + CALayer *layer = self.layer; + [layer setNeedsDisplay]; + + if ([self _locked_shouldHavePlaceholderLayer]) { + [CATransaction begin]; + [CATransaction setDisableActions:YES]; + [self _locked_setupPlaceholderLayerIfNeeded]; + _placeholderLayer.opacity = 1.0; + [CATransaction commit]; + [layer addSublayer:_placeholderLayer]; + } + } + } + + __instanceLock__.unlock(); + + [self didEnterHierarchy]; +} + +- (void)__exitHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"Should not cause recursive __exitHierarchy"); + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeLogEvent(self, @"exitHierarchy"); + + // Profiling has shown that locking this method is beneficial, so each of the property accesses don't have to lock and unlock. + __instanceLock__.lock(); + + if (_flags.isInHierarchy && !_flags.visibilityNotificationsDisabled && ![self __selfOrParentHasVisibilityNotificationsDisabled]) { + _flags.isExitingHierarchy = YES; + _flags.isInHierarchy = NO; + + // Don't call -didExitHierarchy while holding __instanceLock__. + // This method and subsequent ones (i.e -interfaceState and didExit(.*)State) + // don't expect that they are called while the lock is being held. + // More importantly, didExit(.*)State methods are meant to be overriden by clients. + // And so they can potentially walk up the node tree and cause deadlocks, or do expensive tasks and cause the lock to be held for too long. + __instanceLock__.unlock(); + [self didExitHierarchy]; + for (ASDisplayNode *subnode in self.subnodes) { + [subnode __exitHierarchy]; + } + __instanceLock__.lock(); + + _flags.isExitingHierarchy = NO; + } + + __instanceLock__.unlock(); +} + +- (void)enterHierarchyState:(ASHierarchyState)hierarchyState +{ + if (hierarchyState == ASHierarchyStateNormal) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + + ASDisplayNodePerformBlockOnEveryNode(nil, self, NO, ^(ASDisplayNode *node) { + node.hierarchyState |= hierarchyState; + }); +} + +- (void)exitHierarchyState:(ASHierarchyState)hierarchyState +{ + if (hierarchyState == ASHierarchyStateNormal) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + ASDisplayNodePerformBlockOnEveryNode(nil, self, NO, ^(ASDisplayNode *node) { + node.hierarchyState &= (~hierarchyState); + }); +} + +- (ASHierarchyState)hierarchyState +{ + ASDN::MutexLocker l(__instanceLock__); + return _hierarchyState; +} + +- (void)setHierarchyState:(ASHierarchyState)newState +{ + ASHierarchyState oldState = ASHierarchyStateNormal; + { + ASDN::MutexLocker l(__instanceLock__); + if (_hierarchyState == newState) { + return; + } + oldState = _hierarchyState; + _hierarchyState = newState; + } + + // Entered rasterization state. + if (newState & ASHierarchyStateRasterized) { + ASDisplayNodeAssert(checkFlag(Synchronous) == NO, @"Node created using -initWithViewBlock:/-initWithLayerBlock: cannot be added to subtree of node with subtree rasterization enabled. Node: %@", self); + } + + // Entered or exited range managed state. + if ((newState & ASHierarchyStateRangeManaged) != (oldState & ASHierarchyStateRangeManaged)) { + if (newState & ASHierarchyStateRangeManaged) { + [self enterInterfaceState:self.supernode.pendingInterfaceState]; + } else { + // The case of exiting a range-managed state should be fairly rare. Adding or removing the node + // to a view hierarchy will cause its interfaceState to be either fully set or unset (all fields), + // but because we might be about to be added to a view hierarchy, exiting the interface state now + // would cause inefficient churn. The tradeoff is that we may not clear contents / fetched data + // for nodes that are removed from a managed state and then retained but not used (bad idea anyway!) + } + } + + if ((newState & ASHierarchyStateLayoutPending) != (oldState & ASHierarchyStateLayoutPending)) { + if (newState & ASHierarchyStateLayoutPending) { + // Entering layout pending state + } else { + // Leaving layout pending state, reset related properties + ASDN::MutexLocker l(__instanceLock__); + _pendingTransitionID = ASLayoutElementContextInvalidTransitionID; + _pendingLayoutTransition = nil; + } + } + + ASDisplayNodeLogEvent(self, @"setHierarchyState: %@", NSStringFromASHierarchyStateChange(oldState, newState)); + as_log_verbose(ASNodeLog(), "%s%@ %@", sel_getName(_cmd), NSStringFromASHierarchyStateChange(oldState, newState), self); +} + +- (void)willEnterHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_flags.isEnteringHierarchy, @"You should never call -willEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASAssertUnlocked(__instanceLock__); + + if (![self supportsRangeManagedInterfaceState]) { + self.interfaceState = ASInterfaceStateInHierarchy; + } else if (ASCATransactionQueue.sharedQueue.isEnabled) { + __instanceLock__.lock(); + ASInterfaceState state = _preExitingInterfaceState; + _preExitingInterfaceState = ASInterfaceStateNone; + __instanceLock__.unlock(); + // Layer thrash happened, revert to before exiting. + if (state != ASInterfaceStateNone) { + self.interfaceState = state; + } + } +} + +- (void)didEnterHierarchy { + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"You should never call -didEnterHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isExitingHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASDisplayNodeAssert(_flags.isInHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASAssertUnlocked(__instanceLock__); +} + +- (void)didExitHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(_flags.isExitingHierarchy, @"You should never call -didExitHierarchy directly. Appearance is automatically managed by ASDisplayNode"); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"ASDisplayNode inconsistency. __enterHierarchy and __exitHierarchy are mutually exclusive"); + ASAssertUnlocked(__instanceLock__); + + // This case is important when tearing down hierarchies. We must deliver a visibileStateDidChange:NO callback, as part our API guarantee that this method can be used for + // things like data analytics about user content viewing. We cannot call the method in the dealloc as any incidental retain operations in client code would fail. + // Additionally, it may be that a Standard UIView which is containing us is moving between hierarchies, and we should not send the call if we will be re-added in the + // same runloop. Strategy: strong reference (might be the last!), wait one runloop, and confirm we are still outside the hierarchy (both layer-backed and view-backed). + // TODO: This approach could be optimized by only performing the dispatch for root elements + recursively apply the interface state change. This would require a closer + // integration with _ASDisplayLayer to ensure that the superlayer pointer has been cleared by this stage (to check if we are root or not), or a different delegate call. + +#if !ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR + if (![self supportsRangeManagedInterfaceState]) { + self.interfaceState = ASInterfaceStateNone; + return; + } +#endif + if (ASInterfaceStateIncludesVisible(self.pendingInterfaceState)) { + void(^exitVisibleInterfaceState)(void) = ^{ + // This block intentionally retains self. + __instanceLock__.lock(); + unsigned isStillInHierarchy = _flags.isInHierarchy; + BOOL isVisible = ASInterfaceStateIncludesVisible(_pendingInterfaceState); + ASInterfaceState newState = (_pendingInterfaceState & ~ASInterfaceStateVisible); + // layer may be thrashed, we need to remember the state so we can reset if it enters in same runloop later. + _preExitingInterfaceState = _pendingInterfaceState; + __instanceLock__.unlock(); + if (!isStillInHierarchy && isVisible) { +#if ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR + if (![self supportsRangeManagedInterfaceState]) { + newState = ASInterfaceStateNone; + } +#endif + self.interfaceState = newState; + } + }; + + if (!ASCATransactionQueue.sharedQueue.enabled) { + dispatch_async(dispatch_get_main_queue(), exitVisibleInterfaceState); + } else { + exitVisibleInterfaceState(); + } + } +} + +#pragma mark - Interface State + +/** + * We currently only set interface state on nodes in table/collection views. For other nodes, if they are + * in the hierarchy we enable all ASInterfaceState types with `ASInterfaceStateInHierarchy`, otherwise `None`. + */ +- (BOOL)supportsRangeManagedInterfaceState +{ + ASDN::MutexLocker l(__instanceLock__); + return ASHierarchyStateIncludesRangeManaged(_hierarchyState); +} + +- (void)enterInterfaceState:(ASInterfaceState)interfaceState +{ + if (interfaceState == ASInterfaceStateNone) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + node.interfaceState |= interfaceState; + }); +} + +- (void)exitInterfaceState:(ASInterfaceState)interfaceState +{ + if (interfaceState == ASInterfaceStateNone) { + return; // This method is a no-op with a 0-bitfield argument, so don't bother recursing. + } + ASDisplayNodeLogEvent(self, @"%s %@", sel_getName(_cmd), NSStringFromASInterfaceState(interfaceState)); + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + node.interfaceState &= (~interfaceState); + }); +} + +- (void)recursivelySetInterfaceState:(ASInterfaceState)newInterfaceState +{ + as_activity_create_for_scope("Recursively set interface state"); + + // Instead of each node in the recursion assuming it needs to schedule itself for display, + // setInterfaceState: skips this when handling range-managed nodes (our whole subtree has this set). + // If our range manager intends for us to be displayed right now, and didn't before, get started! + BOOL shouldScheduleDisplay = [self supportsRangeManagedInterfaceState] && [self shouldScheduleDisplayWithNewInterfaceState:newInterfaceState]; + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode *node) { + node.interfaceState = newInterfaceState; + }); + if (shouldScheduleDisplay) { + [ASDisplayNode scheduleNodeForRecursiveDisplay:self]; + } +} + +- (ASInterfaceState)interfaceState +{ + ASDN::MutexLocker l(__instanceLock__); + return _interfaceState; +} + +- (void)setInterfaceState:(ASInterfaceState)newState +{ + if (!ASCATransactionQueue.sharedQueue.enabled) { + [self applyPendingInterfaceState:newState]; + } else { + ASDN::MutexLocker l(__instanceLock__); + if (_pendingInterfaceState != newState) { + _pendingInterfaceState = newState; + [[ASCATransactionQueue sharedQueue] enqueue:self]; + } + } +} + +- (ASInterfaceState)pendingInterfaceState +{ + ASDN::MutexLocker l(__instanceLock__); + return _pendingInterfaceState; +} + +- (void)applyPendingInterfaceState:(ASInterfaceState)newPendingState +{ + //This method is currently called on the main thread. The assert has been added here because all of the + //did(Enter|Exit)(Display|Visible|Preload)State methods currently guarantee calling on main. + ASDisplayNodeAssertMainThread(); + + // This method manages __instanceLock__ itself, to ensure the lock is not held while didEnter/Exit(.*)State methods are called, thus avoid potential deadlocks + ASAssertUnlocked(__instanceLock__); + + ASInterfaceState oldState = ASInterfaceStateNone; + ASInterfaceState newState = ASInterfaceStateNone; + { + ASDN::MutexLocker l(__instanceLock__); + // newPendingState will not be used when ASCATransactionQueue is enabled + // and use _pendingInterfaceState instead for interfaceState update. + if (!ASCATransactionQueue.sharedQueue.enabled) { + _pendingInterfaceState = newPendingState; + } + oldState = _interfaceState; + newState = _pendingInterfaceState; + if (newState == oldState) { + return; + } + _interfaceState = newState; + _preExitingInterfaceState = ASInterfaceStateNone; + } + + // It should never be possible for a node to be visible but not be allowed / expected to display. + ASDisplayNodeAssertFalse(ASInterfaceStateIncludesVisible(newState) && !ASInterfaceStateIncludesDisplay(newState)); + + // TODO: Trigger asynchronous measurement if it is not already cached or being calculated. + // if ((newState & ASInterfaceStateMeasureLayout) != (oldState & ASInterfaceStateMeasureLayout)) { + // } + + // For the Preload and Display ranges, we don't want to call -clear* if not being managed by a range controller. + // Otherwise we get flashing behavior from normal UIKit manipulations like navigation controller push / pop. + // Still, the interfaceState should be updated to the current state of the node; just don't act on the transition. + + // Entered or exited data loading state. + BOOL nowPreload = ASInterfaceStateIncludesPreload(newState); + BOOL wasPreload = ASInterfaceStateIncludesPreload(oldState); + + if (nowPreload != wasPreload) { + if (nowPreload) { + [self didEnterPreloadState]; + } else { + // We don't want to call -didExitPreloadState on nodes that aren't being managed by a range controller. + // Otherwise we get flashing behavior from normal UIKit manipulations like navigation controller push / pop. + if ([self supportsRangeManagedInterfaceState]) { + [self didExitPreloadState]; + } + } + } + + // Entered or exited contents rendering state. + BOOL nowDisplay = ASInterfaceStateIncludesDisplay(newState); + BOOL wasDisplay = ASInterfaceStateIncludesDisplay(oldState); + + if (nowDisplay != wasDisplay) { + if ([self supportsRangeManagedInterfaceState]) { + if (nowDisplay) { + // Once the working window is eliminated (ASRangeHandlerRender), trigger display directly here. + [self setDisplaySuspended:NO]; + } else { + [self setDisplaySuspended:YES]; + //schedule clear contents on next runloop + dispatch_async(dispatch_get_main_queue(), ^{ + __instanceLock__.lock(); + ASInterfaceState interfaceState = _interfaceState; + __instanceLock__.unlock(); + if (ASInterfaceStateIncludesDisplay(interfaceState) == NO) { + [self clearContents]; + } + }); + } + } else { + // NOTE: This case isn't currently supported as setInterfaceState: isn't exposed externally, and all + // internal use cases are range-managed. When a node is visible, don't mess with display - CA will start it. + if (!ASInterfaceStateIncludesVisible(newState)) { + // Check _implementsDisplay purely for efficiency - it's faster even than calling -asyncLayer. + if ([self _implementsDisplay]) { + if (nowDisplay) { + [ASDisplayNode scheduleNodeForRecursiveDisplay:self]; + } else { + [[self asyncLayer] cancelAsyncDisplay]; + //schedule clear contents on next runloop + dispatch_async(dispatch_get_main_queue(), ^{ + __instanceLock__.lock(); + ASInterfaceState interfaceState = _interfaceState; + __instanceLock__.unlock(); + if (ASInterfaceStateIncludesDisplay(interfaceState) == NO) { + [self clearContents]; + } + }); + } + } + } + } + + if (nowDisplay) { + [self didEnterDisplayState]; + } else { + [self didExitDisplayState]; + } + } + + // Became visible or invisible. When range-managed, this represents literal visibility - at least one pixel + // is onscreen. If not range-managed, we can't guarantee more than the node being present in an onscreen window. + BOOL nowVisible = ASInterfaceStateIncludesVisible(newState); + BOOL wasVisible = ASInterfaceStateIncludesVisible(oldState); + + if (nowVisible != wasVisible) { + if (nowVisible) { + [self didEnterVisibleState]; + } else { + [self didExitVisibleState]; + } + } + + // Log this change, unless it's just the node going from {} -> {Measure} because that change happens + // for all cell nodes and it isn't currently meaningful. + BOOL measureChangeOnly = ((oldState | newState) == ASInterfaceStateMeasureLayout); + if (!measureChangeOnly) { + as_log_verbose(ASNodeLog(), "%s %@ %@", sel_getName(_cmd), NSStringFromASInterfaceStateChange(oldState, newState), self); + } + + ASDisplayNodeLogEvent(self, @"interfaceStateDidChange: %@", NSStringFromASInterfaceStateChange(oldState, newState)); + [self interfaceStateDidChange:newState fromState:oldState]; +} + +- (void)prepareForCATransactionCommit +{ + // Apply _pendingInterfaceState actual _interfaceState, note that ASInterfaceStateNone is not used. + [self applyPendingInterfaceState:ASInterfaceStateNone]; +} + +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState +{ + // Subclass hook + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertMainThread(); + [self enumerateInterfaceStateDelegates:^(id del) { + [del interfaceStateDidChange:newState fromState:oldState]; + }]; +} + +- (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState +{ + BOOL willDisplay = ASInterfaceStateIncludesDisplay(newInterfaceState); + BOOL nowDisplay = ASInterfaceStateIncludesDisplay(self.interfaceState); + return willDisplay && (willDisplay != nowDisplay); +} + +- (void)addInterfaceStateDelegate:(id )interfaceStateDelegate +{ + ASDN::MutexLocker l(__instanceLock__); + _hasHadInterfaceStateDelegates = YES; + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if (_interfaceStateDelegates[i] == nil) { + _interfaceStateDelegates[i] = interfaceStateDelegate; + return; + } + } + ASDisplayNodeFailAssert(@"Exceeded interface state delegate limit: %d", AS_MAX_INTERFACE_STATE_DELEGATES); +} + +- (void)removeInterfaceStateDelegate:(id )interfaceStateDelegate +{ + ASDN::MutexLocker l(__instanceLock__); + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if (_interfaceStateDelegates[i] == interfaceStateDelegate) { + _interfaceStateDelegates[i] = nil; + break; + } + } +} + +- (BOOL)isVisible +{ + ASDN::MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesVisible(_interfaceState); +} + +- (void)didEnterVisibleState +{ + // subclass override + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterVisibleState]; + }]; +#if AS_ENABLE_TIPS + [ASTipsController.shared nodeDidAppear:self]; +#endif +} + +- (void)didExitVisibleState +{ + // subclass override + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitVisibleState]; + }]; +} + +- (BOOL)isInDisplayState +{ + ASDN::MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesDisplay(_interfaceState); +} + +- (void)didEnterDisplayState +{ + // subclass override + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterDisplayState]; + }]; +} + +- (void)didExitDisplayState +{ + // subclass override + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitDisplayState]; + }]; +} + +- (BOOL)isInPreloadState +{ + ASDN::MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesPreload(_interfaceState); +} + +- (void)setNeedsPreload +{ + if (self.isInPreloadState) { + [self recursivelyPreload]; + } +} + +- (void)recursivelyPreload +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { + [node didEnterPreloadState]; + }); + }); +} + +- (void)recursivelyClearPreloadedData +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { + [node didExitPreloadState]; + }); + }); +} + +- (void)didEnterPreloadState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + + // If this node has ASM enabled and is not yet visible, force a layout pass to apply its applicable pending layout, if any, + // so that its subnodes are inserted/deleted and start preloading right away. + // + // - If it has an up-to-date layout (and subnodes), calling -layoutIfNeeded will be fast. + // + // - If it doesn't have a calculated or pending layout that fits its current bounds, a measurement pass will occur + // (see -__layout and -_u_measureNodeWithBoundsIfNecessary:). This scenario is uncommon, + // and running a measurement pass here is a fine trade-off because preloading any time after this point would be late. + if (self.automaticallyManagesSubnodes) { + [self layoutIfNeeded]; + } + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterPreloadState]; + }]; +} + +- (void)didExitPreloadState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitPreloadState]; + }]; +} + +- (void)clearContents +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + + ASDN::MutexLocker l(__instanceLock__); + if (_flags.canClearContentsOfLayer) { + // No-op if these haven't been created yet, as that guarantees they don't have contents that needs to be released. + _layer.contents = nil; + } + + _placeholderLayer.contents = nil; + _placeholderImage = nil; +} + +- (void)recursivelyClearContents +{ + ASPerformBlockOnMainThread(^{ + ASDisplayNodePerformBlockOnEveryNode(nil, self, YES, ^(ASDisplayNode * _Nonnull node) { + [node clearContents]; + }); + }); +} + +- (void)enumerateInterfaceStateDelegates:(void (NS_NOESCAPE ^)(id))block +{ + ASAssertUnlocked(__instanceLock__); + + id dels[AS_MAX_INTERFACE_STATE_DELEGATES]; + int count = 0; + { + ASLockScopeSelf(); + // Fast path for non-delegating nodes. + if (!_hasHadInterfaceStateDelegates) { + return; + } + + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if ((dels[count] = _interfaceStateDelegates[i])) { + count++; + } + } + } + for (int i = 0; i < count; i++) { + block(dels[i]); + } +} + +#pragma mark - Gesture Recognizing + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + // Subclass hook +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + // This method is only implemented on UIView on iOS 6+. + ASDisplayNodeAssertMainThread(); + + // No locking needed as it's main thread only + UIView *view = _view; + if (view == nil) { + return YES; + } + + // If we reach the base implementation, forward up the view hierarchy. + UIView *superview = view.superview; + return [superview gestureRecognizerShouldBegin:gestureRecognizer]; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + return [_view hitTest:point withEvent:event]; +} + +- (void)setHitTestSlop:(UIEdgeInsets)hitTestSlop +{ + ASDN::MutexLocker l(__instanceLock__); + _hitTestSlop = hitTestSlop; +} + +- (UIEdgeInsets)hitTestSlop +{ + ASDN::MutexLocker l(__instanceLock__); + return _hitTestSlop; +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + UIEdgeInsets slop = self.hitTestSlop; + if (_view && UIEdgeInsetsEqualToEdgeInsets(slop, UIEdgeInsetsZero)) { + // Safer to use UIView's -pointInside:withEvent: if we can. + return [_view pointInside:point withEvent:event]; + } else { + return CGRectContainsPoint(UIEdgeInsetsInsetRect(self.bounds, slop), point); + } +} + + +#pragma mark - Pending View State + +- (void)_locked_applyPendingStateToViewOrLayer +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssert(self.nodeLoaded, @"must have a view or layer"); + + TIME_SCOPED(_debugTimeToApplyPendingState); + + // If no view/layer properties were set before the view/layer were created, _pendingViewState will be nil and the default values + // for the view/layer are still valid. + [self _locked_applyPendingViewState]; + + if (_flags.displaySuspended) { + self._locked_asyncLayer.displaySuspended = YES; + } + if (!_flags.displaysAsynchronously) { + self._locked_asyncLayer.displaysAsynchronously = NO; + } +} + +- (void)applyPendingViewState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + + ASDN::MutexLocker l(__instanceLock__); + // FIXME: Ideally we'd call this as soon as the node receives -setNeedsLayout + // but automatic subnode management would require us to modify the node tree + // in the background on a loaded node, which isn't currently supported. + if (_pendingViewState.hasSetNeedsLayout) { + // Need to unlock before calling setNeedsLayout to avoid deadlocks. + // MutexUnlocker will re-lock at the end of scope. + ASDN::MutexUnlocker u(__instanceLock__); + [self __setNeedsLayout]; + } + + [self _locked_applyPendingViewState]; +} + +- (void)_locked_applyPendingViewState +{ + ASDisplayNodeAssertMainThread(); + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssert([self _locked_isNodeLoaded], @"Expected node to be loaded before applying pending state."); + + if (_flags.layerBacked) { + [_pendingViewState applyToLayer:_layer]; + } else { + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), _flags.layerBacked); + [_pendingViewState applyToView:_view withSpecialPropertiesHandling:specialPropertiesHandling]; + } + + // _ASPendingState objects can add up very quickly when adding + // many nodes. This is especially an issue in large collection views + // and table views. This needs to be weighed against the cost of + // reallocing a _ASPendingState. So in range managed nodes we + // delete the pending state, otherwise we just clear it. + if (ASHierarchyStateIncludesRangeManaged(_hierarchyState)) { + _pendingViewState = nil; + } else { + [_pendingViewState clearChanges]; + } +} + +// This method has proved helpful in a few rare scenarios, similar to a category extension on UIView, but assumes knowledge of _ASDisplayView. +// It's considered private API for now and its use should not be encouraged. +- (ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass checkViewHierarchy:(BOOL)checkViewHierarchy +{ + ASDisplayNode *supernode = self.supernode; + while (supernode) { + if ([supernode isKindOfClass:supernodeClass]) + return supernode; + supernode = supernode.supernode; + } + if (!checkViewHierarchy) { + return nil; + } + + UIView *view = self.view.superview; + while (view) { + ASDisplayNode *viewNode = ((_ASDisplayView *)view).asyncdisplaykit_node; + if (viewNode) { + if ([viewNode isKindOfClass:supernodeClass]) + return viewNode; + } + + view = view.superview; + } + + return nil; +} + +#pragma mark - Performance Measurement + +- (void)setMeasurementOptions:(ASDisplayNodePerformanceMeasurementOptions)measurementOptions +{ + ASDN::MutexLocker l(__instanceLock__); + _measurementOptions = measurementOptions; +} + +- (ASDisplayNodePerformanceMeasurementOptions)measurementOptions +{ + ASDN::MutexLocker l(__instanceLock__); + return _measurementOptions; +} + +- (ASDisplayNodePerformanceMeasurements)performanceMeasurements +{ + ASDN::MutexLocker l(__instanceLock__); + ASDisplayNodePerformanceMeasurements measurements = { .layoutSpecNumberOfPasses = -1, .layoutSpecTotalTime = NAN, .layoutComputationNumberOfPasses = -1, .layoutComputationTotalTime = NAN }; + if (_measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec) { + measurements.layoutSpecNumberOfPasses = _layoutSpecNumberOfPasses; + measurements.layoutSpecTotalTime = _layoutSpecTotalTime; + } + if (_measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutComputation) { + measurements.layoutComputationNumberOfPasses = _layoutComputationNumberOfPasses; + measurements.layoutComputationTotalTime = _layoutComputationTotalTime; + } + return measurements; +} + +#pragma mark - Accessibility + +- (void)setIsAccessibilityContainer:(BOOL)isAccessibilityContainer +{ + ASDN::MutexLocker l(__instanceLock__); + _isAccessibilityContainer = isAccessibilityContainer; +} + +- (BOOL)isAccessibilityContainer +{ + ASDN::MutexLocker l(__instanceLock__); + return _isAccessibilityContainer; +} + +#pragma mark - Debugging (Private) + +#if ASEVENTLOG_ENABLE +- (ASEventLog *)eventLog +{ + return _eventLog; +} +#endif + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + ASPushMainThreadAssertionsDisabled(); + + NSString *debugName = self.debugName; + if (debugName.length > 0) { + [result addObject:@{ (id)kCFNull : ASStringWithQuotesIfMultiword(debugName) }]; + } + + NSString *axId = self.accessibilityIdentifier; + if (axId.length > 0) { + [result addObject:@{ (id)kCFNull : ASStringWithQuotesIfMultiword(axId) }]; + } + + ASPopMainThreadAssertionsDisabled(); + return result; +} + +- (NSMutableArray *)propertiesForDebugDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + if (self.debugName.length > 0) { + [result addObject:@{ @"debugName" : ASStringWithQuotesIfMultiword(self.debugName)}]; + } + if (self.accessibilityIdentifier.length > 0) { + [result addObject:@{ @"axId": ASStringWithQuotesIfMultiword(self.accessibilityIdentifier) }]; + } + + CGRect windowFrame = [self _frameInWindow]; + if (CGRectIsNull(windowFrame) == NO) { + [result addObject:@{ @"frameInWindow" : [NSValue valueWithCGRect:windowFrame] }]; + } + + // Attempt to find view controller. + // Note that the convenience method asdk_associatedViewController has an assertion + // that it's run on main. Since this is a debug method, let's bypass the assertion + // and run up the chain ourselves. + if (_view != nil) { + for (UIResponder *responder in [_view asdk_responderChainEnumerator]) { + UIViewController *vc = ASDynamicCast(responder, UIViewController); + if (vc) { + [result addObject:@{ @"viewController" : ASObjectDescriptionMakeTiny(vc) }]; + break; + } + } + } + + if (_view != nil) { + [result addObject:@{ @"alpha" : @(_view.alpha) }]; + [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_view.frame] }]; + } else if (_layer != nil) { + [result addObject:@{ @"alpha" : @(_layer.opacity) }]; + [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_layer.frame] }]; + } else if (_pendingViewState != nil) { + [result addObject:@{ @"alpha" : @(_pendingViewState.alpha) }]; + [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_pendingViewState.frame] }]; + } +#ifndef MINIMAL_ASDK + // Check supernode so that if we are a cell node we don't find self. + ASCellNode *cellNode = [self supernodeOfClass:[ASCellNode class] includingSelf:NO]; + if (cellNode != nil) { + [result addObject:@{ @"cellNode" : ASObjectDescriptionMakeTiny(cellNode) }]; + } +#endif + + [result addObject:@{ @"interfaceState" : NSStringFromASInterfaceState(self.interfaceState)} ]; + + if (_view != nil) { + [result addObject:@{ @"view" : ASObjectDescriptionMakeTiny(_view) }]; + } else if (_layer != nil) { + [result addObject:@{ @"layer" : ASObjectDescriptionMakeTiny(_layer) }]; + } else if (_viewClass != nil) { + [result addObject:@{ @"viewClass" : _viewClass }]; + } else if (_layerClass != nil) { + [result addObject:@{ @"layerClass" : _layerClass }]; + } else if (_viewBlock != nil) { + [result addObject:@{ @"viewBlock" : _viewBlock }]; + } else if (_layerBlock != nil) { + [result addObject:@{ @"layerBlock" : _layerBlock }]; + } + +#if TIME_DISPLAYNODE_OPS + NSString *creationTypeString = [NSString stringWithFormat:@"cr8:%.2lfms dl:%.2lfms ap:%.2lfms ad:%.2lfms", 1000 * _debugTimeToCreateView, 1000 * _debugTimeForDidLoad, 1000 * _debugTimeToApplyPendingState, 1000 * _debugTimeToAddSubnodeViews]; + [result addObject:@{ @"creationTypeString" : creationTypeString }]; +#endif + + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSString *)debugDescription +{ + ASPushMainThreadAssertionsDisabled(); + let result = ASObjectDescriptionMake(self, [self propertiesForDebugDescription]); + ASPopMainThreadAssertionsDisabled(); + return result; +} + +// This should only be called for debugging. It's not thread safe and it doesn't assert. +// NOTE: Returns CGRectNull if the node isn't in a hierarchy. +- (CGRect)_frameInWindow +{ + if (self.isNodeLoaded == NO || self.isInHierarchy == NO) { + return CGRectNull; + } + + if (self.layerBacked) { + CALayer *rootLayer = _layer; + CALayer *nextLayer = nil; + while ((nextLayer = rootLayer.superlayer) != nil) { + rootLayer = nextLayer; + } + + return [_layer convertRect:self.threadSafeBounds toLayer:rootLayer]; + } else { + return [_view convertRect:self.threadSafeBounds toView:nil]; + } +} + +#pragma mark - Trait Collection Hooks + +- (void)asyncTraitCollectionDidChange +{ + // Subclass override +} +@end + +#pragma mark - ASDisplayNode (Debugging) + +@implementation ASDisplayNode (Debugging) + ++ (void)setShouldStoreUnflattenedLayouts:(BOOL)shouldStore +{ + storesUnflattenedLayouts.store(shouldStore); +} + ++ (BOOL)shouldStoreUnflattenedLayouts +{ + return storesUnflattenedLayouts.load(); +} + +- (ASLayout *)unflattenedCalculatedLayout +{ + ASDN::MutexLocker l(__instanceLock__); + return _unflattenedLayout; +} + +- (NSString *)displayNodeRecursiveDescription +{ + return [self _recursiveDescriptionHelperWithIndent:@""]; +} + +- (NSString *)_recursiveDescriptionHelperWithIndent:(NSString *)indent +{ + NSMutableString *subtree = [[[indent stringByAppendingString:self.debugDescription] stringByAppendingString:@"\n"] mutableCopy]; + for (ASDisplayNode *n in self.subnodes) { + [subtree appendString:[n _recursiveDescriptionHelperWithIndent:[indent stringByAppendingString:@" | "]]]; + } + return subtree; +} + +- (NSString *)detailedLayoutDescription +{ + ASPushMainThreadAssertionsDisabled(); + ASDN::MutexLocker l(__instanceLock__); + let props = [[NSMutableArray alloc] init]; + + [props addObject:@{ @"layoutVersion": @(_layoutVersion.load()) }]; + [props addObject:@{ @"bounds": [NSValue valueWithCGRect:self.bounds] }]; + + if (_calculatedDisplayNodeLayout.layout) { + [props addObject:@{ @"calculatedLayout": _calculatedDisplayNodeLayout.layout }]; + [props addObject:@{ @"calculatedVersion": @(_calculatedDisplayNodeLayout.version) }]; + [props addObject:@{ @"calculatedConstrainedSize" : NSStringFromASSizeRange(_calculatedDisplayNodeLayout.constrainedSize) }]; + if (_calculatedDisplayNodeLayout.requestedLayoutFromAbove) { + [props addObject:@{ @"calculatedRequestedLayoutFromAbove": @"YES" }]; + } + } + if (_pendingDisplayNodeLayout.layout) { + [props addObject:@{ @"pendingLayout": _pendingDisplayNodeLayout.layout }]; + [props addObject:@{ @"pendingVersion": @(_pendingDisplayNodeLayout.version) }]; + [props addObject:@{ @"pendingConstrainedSize" : NSStringFromASSizeRange(_pendingDisplayNodeLayout.constrainedSize) }]; + if (_pendingDisplayNodeLayout.requestedLayoutFromAbove) { + [props addObject:@{ @"pendingRequestedLayoutFromAbove": (id)kCFNull }]; + } + } + + ASPopMainThreadAssertionsDisabled(); + return ASObjectDescriptionMake(self, props); +} + +@end + +#pragma mark - ASDisplayNode UIKit / CA Categories + +// We use associated objects as a last resort if our view is not a _ASDisplayView ie it doesn't have the _node ivar to write to + +static const char *ASDisplayNodeAssociatedNodeKey = "ASAssociatedNode"; + +@implementation UIView (ASDisplayNodeInternal) + +- (void)setAsyncdisplaykit_node:(ASDisplayNode *)node +{ + ASWeakProxy *weakProxy = [ASWeakProxy weakProxyWithTarget:node]; + objc_setAssociatedObject(self, ASDisplayNodeAssociatedNodeKey, weakProxy, OBJC_ASSOCIATION_RETAIN); // Weak reference to avoid cycle, since the node retains the view. +} + +- (ASDisplayNode *)asyncdisplaykit_node +{ + ASWeakProxy *weakProxy = objc_getAssociatedObject(self, ASDisplayNodeAssociatedNodeKey); + return weakProxy.target; +} + +@end + +@implementation CALayer (ASDisplayNodeInternal) + +- (void)setAsyncdisplaykit_node:(ASDisplayNode *)node +{ + ASWeakProxy *weakProxy = [ASWeakProxy weakProxyWithTarget:node]; + objc_setAssociatedObject(self, ASDisplayNodeAssociatedNodeKey, weakProxy, OBJC_ASSOCIATION_RETAIN); // Weak reference to avoid cycle, since the node retains the layer. +} + +- (ASDisplayNode *)asyncdisplaykit_node +{ + ASWeakProxy *weakProxy = objc_getAssociatedObject(self, ASDisplayNodeAssociatedNodeKey); + return weakProxy.target; +} + +@end + +@implementation UIView (AsyncDisplayKit) + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + if (subnode.layerBacked) { + // Call -addSubnode: so that we use the asyncdisplaykit_node path if possible. + [self.layer addSubnode:subnode]; + } else { + ASDisplayNode *selfNode = self.asyncdisplaykit_node; + if (selfNode) { + [selfNode addSubnode:subnode]; + } else { + if (subnode.supernode) { + [subnode removeFromSupernode]; + } + [self addSubview:subnode.view]; + } + } +} + +@end + +@implementation CALayer (AsyncDisplayKit) + +- (void)addSubnode:(ASDisplayNode *)subnode +{ + ASDisplayNode *selfNode = self.asyncdisplaykit_node; + if (selfNode) { + [selfNode addSubnode:subnode]; + } else { + if (subnode.supernode) { + [subnode removeFromSupernode]; + } + [self addSublayer:subnode.layer]; + } +} + +@end diff --git a/Source/ASEditableTextNode.h.orig b/Source/ASEditableTextNode.h.orig new file mode 100644 index 0000000000..ba546db317 --- /dev/null +++ b/Source/ASEditableTextNode.h.orig @@ -0,0 +1,221 @@ +// +// ASEditableTextNode.h +// 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 + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASEditableTextNodeDelegate; +@class ASTextKitComponents; + +@interface ASEditableTextNodeTargetForAction: NSObject + +@property (nonatomic, strong, readonly) id _Nullable target; + +- (instancetype)initWithTarget:(id _Nullable)target; + +@end + +/** + @abstract Implements a node that supports text editing. + @discussion Does not support layer backing. + */ +@interface ASEditableTextNode : ASDisplayNode + +/** + * @abstract Initializes an editable text node using default TextKit components. + * + * @return An initialized ASEditableTextNode. + */ +- (instancetype)init; + +/** + * @abstract Initializes an editable text node using the provided TextKit components. + * + * @param textKitComponents The TextKit stack used to render text. + * @param placeholderTextKitComponents The TextKit stack used to render placeholder text. + * + * @return An initialized ASEditableTextNode. + */ +- (instancetype)initWithTextKitComponents:(ASTextKitComponents *)textKitComponents + placeholderTextKitComponents:(ASTextKitComponents *)placeholderTextKitComponents; + +//! @abstract The text node's delegate, which must conform to the protocol. +@property (nullable, weak) id delegate; + +#pragma mark - Configuration + +/** + @abstract Enable scrolling on the textView + @default true + */ +@property (nonatomic) BOOL scrollEnabled; + +/** + @abstract Access to underlying UITextView for more configuration options. + @warning This property should only be used on the main thread and should not be accessed before the editable text node's view is created. + */ +@property (nonatomic, readonly) UITextView *textView; + +//! @abstract The attributes to apply to new text being entered by the user. +@property (nullable, nonatomic, copy) NSDictionary *typingAttributes; + +//! @abstract The range of text currently selected. If length is zero, the range is the cursor location. +@property NSRange selectedRange; + +#pragma mark - Placeholder +/** + @abstract Indicates if the receiver is displaying the placeholder text. + @discussion To update the placeholder, see the property. + @result YES if the placeholder is currently displayed; NO otherwise. + */ +- (BOOL)isDisplayingPlaceholder AS_WARN_UNUSED_RESULT; + +/** + @abstract The styled placeholder text displayed by the text node while no text is entered + @discussion The placeholder is displayed when the user has not entered any text and the keyboard is not visible. + */ +@property (nullable, nonatomic, copy) NSAttributedString *attributedPlaceholderText; + +#pragma mark - Modifying User Text +/** + @abstract The styled text displayed by the receiver. + @discussion When the placeholder is displayed (as indicated by -isDisplayingPlaceholder), this value is nil. Otherwise, this value is the attributed text the user has entered. This value can be modified regardless of whether the receiver is the first responder (and thus, editing) or not. Changing this value from nil to non-nil will result in the placeholder being hidden, and the new value being displayed. + */ +@property (nullable, nonatomic, copy) NSAttributedString *attributedText; + +#pragma mark - Managing The Keyboard +//! @abstract The text input mode used by the receiver's keyboard, if it is visible. This value is undefined if the receiver is not the first responder. +@property (nonatomic, readonly) UITextInputMode *textInputMode; + +/** + @abstract The textContainerInset of both the placeholder and typed textView. This value defaults to UIEdgeInsetsZero. + */ +@property (nonatomic) UIEdgeInsets textContainerInset; + +/** + @abstract The maximum number of lines to display. Additional lines will require scrolling. + @default 0 (No limit) + */ +@property (nonatomic) NSUInteger maximumLinesToDisplay; + +/** + @abstract Indicates whether the receiver's text view is the first responder, and thus has the keyboard visible and is prepared for editing by the user. + @result YES if the receiver's text view is the first-responder; NO otherwise. + */ +- (BOOL)isFirstResponder AS_WARN_UNUSED_RESULT; + +//! @abstract Makes the receiver's text view the first responder. +- (BOOL)becomeFirstResponder; + +//! @abstract Resigns the receiver's text view from first-responder status, if it has it. +- (BOOL)resignFirstResponder; + +#pragma mark - Geometry +/** + @abstract Returns the frame of the given range of characters. + @param textRange A range of characters. + @discussion This method raises an exception if `textRange` is not a valid range of characters within the receiver's attributed text. + @result A CGRect that is the bounding box of the glyphs covered by the given range of characters, in the coordinate system of the receiver. + */ +- (CGRect)frameForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT; + +/** + @abstract properties. + */ +@property (nonatomic) UITextAutocapitalizationType autocapitalizationType; // default is UITextAutocapitalizationTypeSentences +@property (nonatomic) UITextAutocorrectionType autocorrectionType; // default is UITextAutocorrectionTypeDefault +@property (nonatomic) UITextSpellCheckingType spellCheckingType; // default is UITextSpellCheckingTypeDefault; +@property (nonatomic) UIKeyboardType keyboardType; // default is UIKeyboardTypeDefault +@property (nonatomic) UIKeyboardAppearance keyboardAppearance; // default is UIKeyboardAppearanceDefault +@property (nonatomic) UIReturnKeyType returnKeyType; // default is UIReturnKeyDefault (See note under UIReturnKeyType enum) +@property (nonatomic) BOOL enablesReturnKeyAutomatically; // default is NO (when YES, will automatically disable return key when text widget has zero-length contents, and will automatically enable when text widget has non-zero-length contents) +@property (nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry; // default is NO + +- (void)dropAutocorrection; + + +@end + +@interface ASEditableTextNode (Unavailable) + +- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE; + +@end + +#pragma mark - +/** + * The methods declared by the ASEditableTextNodeDelegate protocol allow the adopting delegate to + * respond to notifications such as began and finished editing, selection changed and text updated; + * and manage whether a specified text should be replaced. + */ +@protocol ASEditableTextNodeDelegate + +@optional +/** + @abstract Asks the delegate if editing should begin for the text node. + @param editableTextNode An editable text node. + @discussion YES if editing should begin; NO if editing should not begin -- the default returns YES. + */ +- (BOOL)editableTextNodeShouldBeginEditing:(ASEditableTextNode *)editableTextNode; + +/** + @abstract Indicates to the delegate that the text node began editing. + @param editableTextNode An editable text node. + @discussion The invocation of this method coincides with the keyboard animating to become visible. + */ +- (void)editableTextNodeDidBeginEditing:(ASEditableTextNode *)editableTextNode; + +/** + @abstract Asks the delegate whether the specified text should be replaced in the editable text node. + @param editableTextNode An editable text node. + @param range The current selection range. If the length of the range is 0, range reflects the current insertion point. If the user presses the Delete key, the length of the range is 1 and an empty string object replaces that single character. + @param text The text to insert. + @discussion YES if the old text should be replaced by the new text; NO if the replacement operation should be aborted. + @result The text node calls this method whenever the user types a new character or deletes an existing character. Implementation of this method is optional -- the default implementation returns YES. + */ +- (BOOL)editableTextNode:(ASEditableTextNode *)editableTextNode shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text; + +/** + @abstract Indicates to the delegate that the text node's selection has changed. + @param editableTextNode An editable text node. + @param fromSelectedRange The previously selected range. + @param toSelectedRange The current selected range. Equivalent to the property. + @param dueToEditing YES if the selection change was due to editing; NO otherwise. + @discussion You can access the selection of the receiver via . + */ +- (void)editableTextNodeDidChangeSelection:(ASEditableTextNode *)editableTextNode fromSelectedRange:(NSRange)fromSelectedRange toSelectedRange:(NSRange)toSelectedRange dueToEditing:(BOOL)dueToEditing; + +/** + @abstract Indicates to the delegate that the text node's text was updated. + @param editableTextNode An editable text node. + @discussion This method is called each time the user updated the text node's text. It is not called for programmatic changes made to the text via the property. + */ +- (void)editableTextNodeDidUpdateText:(ASEditableTextNode *)editableTextNode; + +/** + @abstract Indicates to the delegate that the text node has finished editing. + @param editableTextNode An editable text node. + @discussion The invocation of this method coincides with the keyboard animating to become hidden. + */ +- (void)editableTextNodeDidFinishEditing:(ASEditableTextNode *)editableTextNode; + +<<<<<<< HEAD +- (BOOL)editableTextNodeShouldPaste:(ASEditableTextNode *)editableTextNode; +- (ASEditableTextNodeTargetForAction * _Nullable)editableTextNodeTargetForAction:(SEL)action; +- (BOOL)editableTextNodeShouldReturn:(ASEditableTextNode *)editableTextNode; + +======= +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index a36748475e..0494c90ab7 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -257,7 +257,7 @@ typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); if (_displayWithoutProcessing && ASDisplayNodeThreadIsMain()) { BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); if (stretchable) { - ASDisplayNodeSetupLayerContentsWithResizableImage(self.layer, image); + ASDisplayNodeSetResizableContents(self, image); } else { self.contents = (id)image.CGImage; } diff --git a/Source/ASImageNode.mm.orig b/Source/ASImageNode.mm.orig new file mode 100644 index 0000000000..0106295a55 --- /dev/null +++ b/Source/ASImageNode.mm.orig @@ -0,0 +1,791 @@ +// +// ASImageNode.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 +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import +#import + +// TODO: It would be nice to remove this dependency; it's the only subclass using more than +FrameworkSubclasses.h +#import + +static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; + +typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); + +@interface ASImageNodeDrawParameters : NSObject { +@package + UIImage *_image; + BOOL _opaque; + CGRect _bounds; + CGFloat _contentsScale; + UIColor *_backgroundColor; + UIViewContentMode _contentMode; + BOOL _cropEnabled; + BOOL _forceUpscaling; + CGSize _forcedSize; + CGRect _cropRect; + CGRect _cropDisplayBounds; + asimagenode_modification_block_t _imageModificationBlock; + ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext; + ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext; + ASImageNodeDrawParametersBlock _didDrawBlock; +} + +@end + +@implementation ASImageNodeDrawParameters + +@end + +/** + * Contains all data that is needed to generate the content bitmap. + */ +@interface ASImageNodeContentsKey : NSObject + +@property (nonatomic) UIImage *image; +@property CGSize backingSize; +@property CGRect imageDrawRect; +@property BOOL isOpaque; +@property (nonatomic, copy) UIColor *backgroundColor; +@property (nonatomic) ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext; +@property (nonatomic) ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext; +@property (nonatomic) asimagenode_modification_block_t imageModificationBlock; + +@end + +@implementation ASImageNodeContentsKey + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + + // Optimization opportunity: The `isKindOfClass` call here could be avoided by not using the NSObject `isEqual:` + // convention and instead using a custom comparison function that assumes all items are heterogeneous. + // However, profiling shows that our entire `isKindOfClass` expression is only ~1/40th of the total + // overheard of our caching, so it's likely not high-impact. + if ([object isKindOfClass:[ASImageNodeContentsKey class]]) { + ASImageNodeContentsKey *other = (ASImageNodeContentsKey *)object; + return [_image isEqual:other.image] + && CGSizeEqualToSize(_backingSize, other.backingSize) + && CGRectEqualToRect(_imageDrawRect, other.imageDrawRect) + && _isOpaque == other.isOpaque + && [_backgroundColor isEqual:other.backgroundColor] + && _willDisplayNodeContentWithRenderingContext == other.willDisplayNodeContentWithRenderingContext + && _didDisplayNodeContentWithRenderingContext == other.didDisplayNodeContentWithRenderingContext + && _imageModificationBlock == other.imageModificationBlock; + } else { + return NO; + } +} + +- (NSUInteger)hash +{ +#pragma clang diagnostic push +#pragma clang diagnostic warning "-Wpadded" + struct { + NSUInteger imageHash; + CGSize backingSize; + CGRect imageDrawRect; + NSInteger isOpaque; + NSUInteger backgroundColorHash; + void *willDisplayNodeContentWithRenderingContext; + void *didDisplayNodeContentWithRenderingContext; + void *imageModificationBlock; +#pragma clang diagnostic pop + } data = { + _image.hash, + _backingSize, + _imageDrawRect, + _isOpaque, + _backgroundColor.hash, + (void *)_willDisplayNodeContentWithRenderingContext, + (void *)_didDisplayNodeContentWithRenderingContext, + (void *)_imageModificationBlock + }; + return ASHashBytes(&data, sizeof(data)); +} + +@end + + +@implementation ASImageNode +{ +@private + UIImage *_image; + ASWeakMapEntry *_weakCacheEntry; // Holds a reference that keeps our contents in cache. + UIColor *_placeholderColor; + + void (^_displayCompletionBlock)(BOOL canceled); + + // Drawing + ASTextNode *_debugLabelNode; + + // Cropping. + BOOL _cropEnabled; // Defaults to YES. + BOOL _forceUpscaling; //Defaults to NO. + CGSize _forcedSize; //Defaults to CGSizeZero, indicating no forced size. + CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0) + CGRect _cropDisplayBounds; // Defaults to CGRectNull +} + +@synthesize image = _image; +@synthesize imageModificationBlock = _imageModificationBlock; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // TODO can this be removed? + self.contentsScale = ASScreenScale(); + self.contentMode = UIViewContentModeScaleAspectFill; + self.opaque = NO; + self.clipsToBounds = YES; + + // If no backgroundColor is set to the image node and it's a subview of UITableViewCell, UITableView is setting + // the opaque value of all subviews to YES if highlighting / selection is happening and does not set it back to the + // initial value. With setting a explicit backgroundColor we can prevent that change. + self.backgroundColor = [UIColor clearColor]; + + _cropEnabled = YES; + _forceUpscaling = NO; + _cropRect = CGRectMake(0.5, 0.5, 0, 0); + _cropDisplayBounds = CGRectNull; + _placeholderColor = ASDisplayNodeDefaultPlaceholderColor(); +#ifndef MINIMAL_ASDK + _animatedImageRunLoopMode = ASAnimatedImageDefaultRunLoopMode; +#endif + + return self; +} + +- (void)dealloc +{ + // Invalidate all components around animated images +#ifndef MINIMAL_ASDK + [self invalidateAnimatedImage]; +#endif +} + +#pragma mark - Placeholder + +- (UIImage *)placeholderImage +{ + // FIXME: Replace this implementation with reusable CALayers that have .backgroundColor set. + // This would completely eliminate the memory and performance cost of the backing store. + CGSize size = self.calculatedSize; + if ((size.width * size.height) < CGFLOAT_EPSILON) { + return nil; + } + + ASDN::MutexLocker l(__instanceLock__); + + ASGraphicsBeginImageContextWithOptions(size, NO, 1); + [self.placeholderColor setFill]; + UIRectFill(CGRectMake(0, 0, size.width, size.height)); + UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); + + return image; +} + +#pragma mark - Layout and Sizing + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + let image = ASLockedSelf(_image); + + if (image == nil) { + return [super calculateSizeThatFits:constrainedSize]; + } + + return image.size; +} + +#pragma mark - Setter / Getter + +- (void)setImage:(UIImage *)image +{ + ASDN::MutexLocker l(__instanceLock__); + [self _locked_setImage:image]; +} + +- (void)_locked_setImage:(UIImage *)image +{ + ASAssertLocked(__instanceLock__); + if (ASObjectIsEqual(_image, image)) { + return; + } + + UIImage *oldImage = _image; + _image = image; + + if (image != nil) { + // We explicitly call setNeedsDisplay in this case, although we know setNeedsDisplay will be called with lock held. + // Therefore we have to be careful in methods that are involved with setNeedsDisplay to not run into a deadlock + [self setNeedsDisplay]; + +<<<<<<< HEAD + if (_displayWithoutProcessing && ASDisplayNodeThreadIsMain()) { + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (stretchable) { + ASDisplayNodeSetupLayerContentsWithResizableImage(self.layer, image); + } else { + self.contents = (id)image.CGImage; + } + return; +======= + // For debugging purposes we don't care about locking for now + if ([ASImageNode shouldShowImageScalingOverlay] && _debugLabelNode == nil) { + // do not use ASPerformBlockOnMainThread here, if it performs the block synchronously it will continue + // holding the lock while calling addSubnode. + dispatch_async(dispatch_get_main_queue(), ^{ + _debugLabelNode = [[ASTextNode alloc] init]; + _debugLabelNode.layerBacked = YES; + [self addSubnode:_debugLabelNode]; + }); +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + } + } else { + self.contents = nil; + } + + // Destruction of bigger images on the main thread can be expensive + // and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + CGSize oldImageSize = oldImage.size; + BOOL shouldReleaseImageOnBackgroundThread = oldImageSize.width > kMinReleaseImageOnBackgroundSize.width + || oldImageSize.height > kMinReleaseImageOnBackgroundSize.height; + if (shouldReleaseImageOnBackgroundThread) { + ASPerformBackgroundDeallocation(&oldImage); + } +} + +- (UIImage *)image +{ + return ASLockedSelf(_image); +} + +- (UIColor *)placeholderColor +{ + return ASLockedSelf(_placeholderColor); +} + +- (void)setPlaceholderColor:(UIColor *)placeholderColor +{ + ASLockScopeSelf(); + if (ASCompareAssignCopy(_placeholderColor, placeholderColor)) { + _placeholderEnabled = (placeholderColor != nil); + } +} + +#pragma mark - Drawing + +- (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer +{ + ASLockScopeSelf(); + + ASImageNodeDrawParameters *drawParameters = [[ASImageNodeDrawParameters alloc] init]; + drawParameters->_image = _image; + drawParameters->_bounds = [self threadSafeBounds]; + drawParameters->_opaque = self.opaque; + drawParameters->_contentsScale = _contentsScaleForDisplay; + drawParameters->_backgroundColor = self.backgroundColor; + drawParameters->_contentMode = self.contentMode; + drawParameters->_cropEnabled = _cropEnabled; + drawParameters->_forceUpscaling = _forceUpscaling; + drawParameters->_forcedSize = _forcedSize; + drawParameters->_cropRect = _cropRect; + drawParameters->_cropDisplayBounds = _cropDisplayBounds; + drawParameters->_imageModificationBlock = _imageModificationBlock; + drawParameters->_willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; + drawParameters->_didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; + + // Hack for now to retain the weak entry that was created while this drawing happened + drawParameters->_didDrawBlock = ^(ASWeakMapEntry *entry){ + ASLockScopeSelf(); + _weakCacheEntry = entry; + }; + + return drawParameters; +} + ++ (UIImage *)displayWithParameters:(id)parameter isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelled +{ + ASImageNodeDrawParameters *drawParameter = (ASImageNodeDrawParameters *)parameter; + + UIImage *image = drawParameter->_image; + if (image == nil) { + return nil; + } + + if (true) { + return image; + } + + CGRect drawParameterBounds = drawParameter->_bounds; + BOOL forceUpscaling = drawParameter->_forceUpscaling; + CGSize forcedSize = drawParameter->_forcedSize; + BOOL cropEnabled = drawParameter->_cropEnabled; + BOOL isOpaque = drawParameter->_opaque; + UIColor *backgroundColor = drawParameter->_backgroundColor; + UIViewContentMode contentMode = drawParameter->_contentMode; + CGFloat contentsScale = drawParameter->_contentsScale; + CGRect cropDisplayBounds = drawParameter->_cropDisplayBounds; + CGRect cropRect = drawParameter->_cropRect; + asimagenode_modification_block_t imageModificationBlock = drawParameter->_imageModificationBlock; + ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = drawParameter->_willDisplayNodeContentWithRenderingContext; + ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = drawParameter->_didDisplayNodeContentWithRenderingContext; + + BOOL hasValidCropBounds = cropEnabled && !CGRectIsEmpty(cropDisplayBounds); + CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : drawParameterBounds); + + + ASDisplayNodeAssert(contentsScale > 0, @"invalid contentsScale at display time"); + + // if the image is resizable, bail early since the image has likely already been configured + BOOL stretchable = !UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero); + if (stretchable) { + if (imageModificationBlock != NULL) { + image = imageModificationBlock(image); + } + return image; + } + + CGSize imageSize = image.size; + CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale); + CGSize boundsSizeInPixels = CGSizeMake(std::floor(bounds.size.width * contentsScale), std::floor(bounds.size.height * contentsScale)); + + BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill || + contentMode == UIViewContentModeScaleAspectFit || + contentMode == UIViewContentModeCenter; + + CGSize backingSize = CGSizeZero; + CGRect imageDrawRect = CGRectZero; + + if (boundsSizeInPixels.width * contentsScale < 1.0f || boundsSizeInPixels.height * contentsScale < 1.0f || + imageSizeInPixels.width < 1.0f || imageSizeInPixels.height < 1.0f) { + return nil; + } + + + // If we're not supposed to do any cropping, just decode image at original size + if (!cropEnabled || !contentModeSupported || stretchable) { + backingSize = imageSizeInPixels; + imageDrawRect = (CGRect){.size = backingSize}; + } else { + if (CGSizeEqualToSize(CGSizeZero, forcedSize) == NO) { + //scale forced size + forcedSize.width *= contentsScale; + forcedSize.height *= contentsScale; + } + ASCroppedImageBackingSizeAndDrawRectInBounds(imageSizeInPixels, + boundsSizeInPixels, + contentMode, + cropRect, + forceUpscaling, + forcedSize, + &backingSize, + &imageDrawRect); + } + + if (backingSize.width <= 0.0f || backingSize.height <= 0.0f || + imageDrawRect.size.width <= 0.0f || imageDrawRect.size.height <= 0.0f) { + return nil; + } + + ASImageNodeContentsKey *contentsKey = [[ASImageNodeContentsKey alloc] init]; + contentsKey.image = image; + contentsKey.backingSize = backingSize; + contentsKey.imageDrawRect = imageDrawRect; + contentsKey.isOpaque = isOpaque; + contentsKey.backgroundColor = backgroundColor; + contentsKey.willDisplayNodeContentWithRenderingContext = willDisplayNodeContentWithRenderingContext; + contentsKey.didDisplayNodeContentWithRenderingContext = didDisplayNodeContentWithRenderingContext; + contentsKey.imageModificationBlock = imageModificationBlock; + + if (isCancelled()) { + return nil; + } + + ASWeakMapEntry *entry = [self.class contentsForkey:contentsKey + drawParameters:parameter + isCancelled:isCancelled]; + // If nil, we were cancelled. + if (entry == nil) { + return nil; + } + + if (drawParameter->_didDrawBlock) { + drawParameter->_didDrawBlock(entry); + } + + return entry.value; +} + +static ASWeakMap *cache = nil; +// Allocate cacheLock on the heap to prevent destruction at app exit (https://github.com/TextureGroup/Texture/issues/136) +static ASDN::StaticMutex& cacheLock = *new ASDN::StaticMutex; + ++ (ASWeakMapEntry *)contentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled +{ + { + ASDN::StaticMutexLocker l(cacheLock); + if (!cache) { + cache = [[ASWeakMap alloc] init]; + } + ASWeakMapEntry *entry = [cache entryForKey:key]; + if (entry != nil) { + return entry; + } + } + + // cache miss + UIImage *contents = [self createContentsForkey:key drawParameters:drawParameters isCancelled:isCancelled]; + if (contents == nil) { // If nil, we were cancelled + return nil; + } + + { + ASDN::StaticMutexLocker l(cacheLock); + return [cache setObject:contents forKey:key]; + } +} + ++ (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:(id)drawParameters isCancelled:(asdisplaynode_iscancelled_block_t)isCancelled +{ + // The following `ASGraphicsBeginImageContextWithOptions` call will sometimes take take longer than 5ms on an + // A5 processor for a 400x800 backingSize. + // Check for cancellation before we call it. + if (isCancelled()) { + return nil; + } + + // Use contentsScale of 1.0 and do the contentsScale handling in boundsSizeInPixels so ASCroppedImageBackingSizeAndDrawRectInBounds + // will do its rounding on pixel instead of point boundaries + ASGraphicsBeginImageContextWithOptions(key.backingSize, key.isOpaque, 1.0); + + BOOL contextIsClean = YES; + + CGContextRef context = UIGraphicsGetCurrentContext(); + if (context && key.willDisplayNodeContentWithRenderingContext) { + key.willDisplayNodeContentWithRenderingContext(context, drawParameters); + contextIsClean = NO; + } + + // if view is opaque, fill the context with background color + if (key.isOpaque && key.backgroundColor) { + [key.backgroundColor setFill]; + UIRectFill({ .size = key.backingSize }); + contextIsClean = NO; + } + + // iOS 9 appears to contain a thread safety regression when drawing the same CGImageRef on + // multiple threads concurrently. In fact, instead of crashing, it appears to deadlock. + // The issue is present in Mac OS X El Capitan and has been seen hanging Pro apps like Adobe Premiere, + // as well as iOS games, and a small number of ASDK apps that provide the same image reference + // to many separate ASImageNodes. A workaround is to set .displaysAsynchronously = NO for the nodes + // that may get the same pointer for a given UI asset image, etc. + // FIXME: We should replace @synchronized here, probably using a global, locked NSMutableSet, and + // only if the object already exists in the set we should create a semaphore to signal waiting threads + // upon removal of the object from the set when the operation completes. + // Another option is to have ASDisplayNode+AsyncDisplay coordinate these cases, and share the decoded buffer. + // Details tracked in https://github.com/facebook/AsyncDisplayKit/issues/1068 + + UIImage *image = key.image; + BOOL canUseCopy = (contextIsClean || ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage))); + CGBlendMode blendMode = canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal; + + @synchronized(image) { + [image drawInRect:key.imageDrawRect blendMode:blendMode alpha:1]; + } + + if (context && key.didDisplayNodeContentWithRenderingContext) { + key.didDisplayNodeContentWithRenderingContext(context, drawParameters); + } + + // Check cancellation one last time before forming image. + if (isCancelled()) { + ASGraphicsEndImageContext(); + return nil; + } + + UIImage *result = ASGraphicsGetImageAndEndCurrentContext(); + + if (key.imageModificationBlock) { + result = key.imageModificationBlock(result); + } + + return result; +} + +- (void)displayDidFinish +{ + [super displayDidFinish]; + + __instanceLock__.lock(); + void (^displayCompletionBlock)(BOOL canceled) = _displayCompletionBlock; + UIImage *image = _image; + BOOL hasDebugLabel = (_debugLabelNode != nil); + __instanceLock__.unlock(); + + // Update the debug label if necessary + if (hasDebugLabel) { + // For debugging purposes we don't care about locking for now + CGSize imageSize = image.size; + CGSize imageSizeInPixels = CGSizeMake(imageSize.width * image.scale, imageSize.height * image.scale); + CGSize boundsSizeInPixels = CGSizeMake(std::floor(self.bounds.size.width * self.contentsScale), std::floor(self.bounds.size.height * self.contentsScale)); + CGFloat pixelCountRatio = (imageSizeInPixels.width * imageSizeInPixels.height) / (boundsSizeInPixels.width * boundsSizeInPixels.height); + if (pixelCountRatio != 1.0) { + NSString *scaleString = [NSString stringWithFormat:@"%.2fx", pixelCountRatio]; + _debugLabelNode.attributedText = [[NSAttributedString alloc] initWithString:scaleString attributes:[self debugLabelAttributes]]; + _debugLabelNode.hidden = NO; + } else { + _debugLabelNode.hidden = YES; + _debugLabelNode.attributedText = nil; + } + } + + // If we've got a block to perform after displaying, do it. + if (image && displayCompletionBlock) { + + displayCompletionBlock(NO); + + __instanceLock__.lock(); + _displayCompletionBlock = nil; + __instanceLock__.unlock(); + } +} + +- (void)setNeedsDisplayWithCompletion:(void (^ _Nullable)(BOOL canceled))displayCompletionBlock +{ + if (self.displaySuspended) { + if (displayCompletionBlock) + displayCompletionBlock(YES); + return; + } + + // Stash the block and call-site queue. We'll invoke it in -displayDidFinish. + { + ASDN::MutexLocker l(__instanceLock__); + if (_displayCompletionBlock != displayCompletionBlock) { + _displayCompletionBlock = displayCompletionBlock; + } + } + + [self setNeedsDisplay]; +} + +#pragma mark Interface State + +- (void)clearContents +{ + [super clearContents]; + + ASDN::MutexLocker l(__instanceLock__); + _weakCacheEntry = nil; // release contents from the cache. +} + +#pragma mark - Cropping + +- (BOOL)isCropEnabled +{ + ASDN::MutexLocker l(__instanceLock__); + return _cropEnabled; +} + +- (void)setCropEnabled:(BOOL)cropEnabled +{ + [self setCropEnabled:cropEnabled recropImmediately:NO inBounds:self.bounds]; +} + +- (void)setCropEnabled:(BOOL)cropEnabled recropImmediately:(BOOL)recropImmediately inBounds:(CGRect)cropBounds +{ + __instanceLock__.lock(); + if (_cropEnabled == cropEnabled) { + __instanceLock__.unlock(); + return; + } + + _cropEnabled = cropEnabled; + _cropDisplayBounds = cropBounds; + + UIImage *image = _image; + __instanceLock__.unlock(); + + // If we have an image to display, display it, respecting our recrop flag. + if (image != nil) { + ASPerformBlockOnMainThread(^{ + if (recropImmediately) + [self displayImmediately]; + else + [self setNeedsDisplay]; + }); + } +} + +- (CGRect)cropRect +{ + ASDN::MutexLocker l(__instanceLock__); + return _cropRect; +} + +- (void)setCropRect:(CGRect)cropRect +{ + { + ASDN::MutexLocker l(__instanceLock__); + if (CGRectEqualToRect(_cropRect, cropRect)) { + return; + } + + _cropRect = cropRect; + } + + // TODO: this logic needs to be updated to respect cropRect. + CGSize boundsSize = self.bounds.size; + CGSize imageSize = self.image.size; + + BOOL isCroppingImage = ((boundsSize.width < imageSize.width) || (boundsSize.height < imageSize.height)); + + // Re-display if we need to. + ASPerformBlockOnMainThread(^{ + if (self.nodeLoaded && self.contentMode == UIViewContentModeScaleAspectFill && isCroppingImage) + [self setNeedsDisplay]; + }); +} + +- (BOOL)forceUpscaling +{ + ASDN::MutexLocker l(__instanceLock__); + return _forceUpscaling; +} + +- (void)setForceUpscaling:(BOOL)forceUpscaling +{ + ASDN::MutexLocker l(__instanceLock__); + _forceUpscaling = forceUpscaling; +} + +- (CGSize)forcedSize +{ + ASDN::MutexLocker l(__instanceLock__); + return _forcedSize; +} + +- (void)setForcedSize:(CGSize)forcedSize +{ + ASDN::MutexLocker l(__instanceLock__); + _forcedSize = forcedSize; +} + +- (asimagenode_modification_block_t)imageModificationBlock +{ + ASDN::MutexLocker l(__instanceLock__); + return _imageModificationBlock; +} + +- (void)setImageModificationBlock:(asimagenode_modification_block_t)imageModificationBlock +{ + ASDN::MutexLocker l(__instanceLock__); + _imageModificationBlock = imageModificationBlock; +} + +#pragma mark - Debug + +- (void)layout +{ + [super layout]; + + if (_debugLabelNode) { + CGSize boundsSize = self.bounds.size; + CGSize debugLabelSize = [_debugLabelNode layoutThatFits:ASSizeRangeMake(CGSizeZero, boundsSize)].size; + CGPoint debugLabelOrigin = CGPointMake(boundsSize.width - debugLabelSize.width, + boundsSize.height - debugLabelSize.height); + _debugLabelNode.frame = (CGRect) {debugLabelOrigin, debugLabelSize}; + } +} + +- (NSDictionary *)debugLabelAttributes +{ + return @{ + NSFontAttributeName: [UIFont systemFontOfSize:15.0], + NSForegroundColorAttributeName: [UIColor redColor] + }; +} + +@end + +#pragma mark - Extras + +asimagenode_modification_block_t ASImageNodeRoundBorderModificationBlock(CGFloat borderWidth, UIColor *borderColor) +{ + return ^(UIImage *originalImage) { + ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); + UIBezierPath *roundOutline = [UIBezierPath bezierPathWithOvalInRect:(CGRect){CGPointZero, originalImage.size}]; + + // Make the image round + [roundOutline addClip]; + + // Draw the original image + [originalImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1]; + + // Draw a border on top. + if (borderWidth > 0.0) { + [borderColor setStroke]; + [roundOutline setLineWidth:borderWidth]; + [roundOutline stroke]; + } + + return ASGraphicsGetImageAndEndCurrentContext(); + }; +} + +asimagenode_modification_block_t ASImageNodeTintColorModificationBlock(UIColor *color) +{ + return ^(UIImage *originalImage) { + ASGraphicsBeginImageContextWithOptions(originalImage.size, NO, originalImage.scale); + + // Set color and render template + [color setFill]; + UIImage *templateImage = [originalImage imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; + [templateImage drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1]; + + UIImage *modifiedImage = ASGraphicsGetImageAndEndCurrentContext(); + + // if the original image was stretchy, keep it stretchy + if (!UIEdgeInsetsEqualToEdgeInsets(originalImage.capInsets, UIEdgeInsetsZero)) { + modifiedImage = [modifiedImage resizableImageWithCapInsets:originalImage.capInsets resizingMode:originalImage.resizingMode]; + } + + return modifiedImage; + }; +} diff --git a/Source/ASMapNode.h.orig b/Source/ASMapNode.h.orig new file mode 100644 index 0000000000..9ae005aa43 --- /dev/null +++ b/Source/ASMapNode.h.orig @@ -0,0 +1,99 @@ +// +// ASMapNode.h +// 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 +// + +<<<<<<< HEAD +#ifndef MINIMAL_ASDK + +======= +#import +#import + +#if TARGET_OS_IOS && AS_USE_MAPKIT +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * Map Annotation options. + * The default behavior is to ignore the annotations' positions, use the region or options specified instead. + * Swift: to select the default behavior, use []. + */ +typedef NS_OPTIONS(NSUInteger, ASMapNodeShowAnnotationsOptions) +{ + /** The annotations' positions are ignored, use the region or options specified instead. */ + ASMapNodeShowAnnotationsOptionsIgnored = 0, + /** The annotations' positions are used to calculate the region to show in the map, equivalent to showAnnotations:animated. */ + ASMapNodeShowAnnotationsOptionsZoomed = 1 << 0, + /** This will only have an effect if combined with the Zoomed state with liveMap turned on.*/ + ASMapNodeShowAnnotationsOptionsAnimated = 1 << 1 +}; + +@interface ASMapNode : ASImageNode + +/** + The current options of ASMapNode. This can be set at any time and ASMapNode will animate the change.

This property may be set from a background thread before the node is loaded, and will automatically be applied to define the behavior of the static snapshot (if .liveMap = NO) or the internal MKMapView (otherwise).

Changes to the region and camera options will only be animated when when the liveMap mode is enabled, otherwise these options will be applied statically to the new snapshot.

The options object is used to specify properties even when the liveMap mode is enabled, allowing seamless transitions between the snapshot and liveMap (as well as back to the snapshot). + */ +@property (nonatomic) MKMapSnapshotOptions *options; + +/** The region is simply the sub-field on the options object. If the objects object is reset, + this will in effect be overwritten and become the value of the .region property on that object. + Defaults to MKCoordinateRegionForMapRect(MKMapRectWorld). + */ +@property (nonatomic) MKCoordinateRegion region; + +/** + This is the MKMapView that is the live map part of ASMapNode. This will be nil if .liveMap = NO. Note, MKMapView is *not* thread-safe. + */ +@property (nullable, readonly) MKMapView *mapView; + +/** + Set this to YES to turn the snapshot into an interactive MKMapView and vice versa. Defaults to NO. This property may be set on a background thread before the node is loaded, and will automatically be actioned, once the node is loaded. + */ +@property (getter=isLiveMap) BOOL liveMap; + +/** + @abstract Whether ASMapNode should automatically request a new map snapshot to correspond to the new node size. + @default Default value is YES. + @discussion If mapSize is set then this will be set to NO, since the size will be the same in all orientations. + */ +@property BOOL needsMapReloadOnBoundsChange; + +/** + Set the delegate of the MKMapView. This can be set even before mapView is created and will be set on the map in the case that the liveMap mode is engaged. + + If the live map view has been created, this may only be set on the main thread. + */ +@property (nonatomic, weak) id mapDelegate; + +/** + * @abstract The annotations to display on the map. + */ +@property (copy) NSArray> *annotations; + +/** + * @abstract This property specifies how to show the annotations. + * @default Default value is ASMapNodeShowAnnotationsIgnored + */ +@property ASMapNodeShowAnnotationsOptions showAnnotationsOptions; + +/** + * @abstract The block which should return annotation image for static map based on provided annotation. + * @discussion This block is executed on an arbitrary serial queue. If this block is nil, standard pin is used. + */ +@property (nullable) UIImage * _Nullable (^imageForStaticMapAnnotationBlock)(id annotation, CGPoint *centerOffset); + +@end + +NS_ASSUME_NONNULL_END + +#endif + +#endif diff --git a/Source/ASMapNode.mm.orig b/Source/ASMapNode.mm.orig new file mode 100644 index 0000000000..1014f2f32c --- /dev/null +++ b/Source/ASMapNode.mm.orig @@ -0,0 +1,456 @@ +// +// ASMapNode.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 +// + +<<<<<<< HEAD +#ifndef MINIMAL_ASDK + +#import + +#if TARGET_OS_IOS +======= +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +#import + +#if TARGET_OS_IOS && AS_USE_MAPKIT + +#import + +#import +#import +#import +#import +#import +#import +#import + +@interface ASMapNode() +{ + MKMapSnapshotter *_snapshotter; + BOOL _snapshotAfterLayout; + NSArray *_annotations; +} +@end + +@implementation ASMapNode + +@synthesize needsMapReloadOnBoundsChange = _needsMapReloadOnBoundsChange; +@synthesize mapDelegate = _mapDelegate; +@synthesize options = _options; +@synthesize liveMap = _liveMap; +@synthesize showAnnotationsOptions = _showAnnotationsOptions; + +#pragma mark - Lifecycle +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + self.backgroundColor = ASDisplayNodeDefaultPlaceholderColor(); + self.clipsToBounds = YES; + self.userInteractionEnabled = YES; + + _needsMapReloadOnBoundsChange = YES; + _liveMap = NO; + _annotations = @[]; + _showAnnotationsOptions = ASMapNodeShowAnnotationsOptionsIgnored; + return self; +} + +- (void)didLoad +{ + [super didLoad]; + if (self.isLiveMap) { + [self addLiveMap]; + } +} + +- (void)dealloc +{ + [self destroySnapshotter]; +} + +- (void)setLayerBacked:(BOOL)layerBacked +{ + ASDisplayNodeAssert(!self.isLiveMap, @"ASMapNode can not be layer backed whilst .liveMap = YES, set .liveMap = NO to use layer backing."); + [super setLayerBacked:layerBacked]; +} + +- (void)didEnterPreloadState +{ + [super didEnterPreloadState]; + ASPerformBlockOnMainThread(^{ + if (self.isLiveMap) { + [self addLiveMap]; + } else { + [self takeSnapshot]; + } + }); +} + +- (void)didExitPreloadState +{ + [super didExitPreloadState]; + ASPerformBlockOnMainThread(^{ + if (self.isLiveMap) { + [self removeLiveMap]; + } + }); +} + +#pragma mark - Settings + +- (BOOL)isLiveMap +{ + ASLockScopeSelf(); + return _liveMap; +} + +- (void)setLiveMap:(BOOL)liveMap +{ + ASDisplayNodeAssert(!self.isLayerBacked, @"ASMapNode can not use the interactive map feature whilst .isLayerBacked = YES, set .layerBacked = NO to use the interactive map feature."); + ASLockScopeSelf(); + if (liveMap == _liveMap) { + return; + } + _liveMap = liveMap; + if (self.nodeLoaded) { + liveMap ? [self addLiveMap] : [self removeLiveMap]; + } +} + +- (BOOL)needsMapReloadOnBoundsChange +{ + ASLockScopeSelf(); + return _needsMapReloadOnBoundsChange; +} + +- (void)setNeedsMapReloadOnBoundsChange:(BOOL)needsMapReloadOnBoundsChange +{ + ASLockScopeSelf(); + _needsMapReloadOnBoundsChange = needsMapReloadOnBoundsChange; +} + +- (MKMapSnapshotOptions *)options +{ + ASLockScopeSelf(); + if (!_options) { + _options = [[MKMapSnapshotOptions alloc] init]; + _options.region = MKCoordinateRegionForMapRect(MKMapRectWorld); + CGSize calculatedSize = self.calculatedSize; + if (!CGSizeEqualToSize(calculatedSize, CGSizeZero)) { + _options.size = calculatedSize; + } + } + return _options; +} + +- (void)setOptions:(MKMapSnapshotOptions *)options +{ + ASLockScopeSelf(); + if (!_options || ![options isEqual:_options]) { + _options = options; + if (self.isLiveMap) { + [self applySnapshotOptions]; + } else if (_snapshotter) { + [self destroySnapshotter]; + [self takeSnapshot]; + } + } +} + +- (MKCoordinateRegion)region +{ + return self.options.region; +} + +- (void)setRegion:(MKCoordinateRegion)region +{ + MKMapSnapshotOptions * options = [self.options copy]; + options.region = region; + self.options = options; +} + +- (id)mapDelegate +{ + return ASLockedSelf(_mapDelegate); +} + +- (void)setMapDelegate:(id)mapDelegate { + ASLockScopeSelf(); + _mapDelegate = mapDelegate; + + if (_mapView) { + ASDisplayNodeAssertMainThread(); + _mapView.delegate = mapDelegate; + } +} + +#pragma mark - Snapshotter + +- (void)takeSnapshot +{ + // If our size is zero, we want to avoid calling a default sized snapshot. Set _snapshotAfterLayout to YES + // so if layout changes in the future, we'll try snapshotting again. + ASLayout *layout = self.calculatedLayout; + if (layout == nil || CGSizeEqualToSize(CGSizeZero, layout.size)) { + _snapshotAfterLayout = YES; + return; + } + + _snapshotAfterLayout = NO; + + if (!_snapshotter) { + [self setUpSnapshotter]; + } + + if (_snapshotter.isLoading) { + return; + } + + __weak __typeof__(self) weakSelf = self; + [_snapshotter startWithQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0) + completionHandler:^(MKMapSnapshot *snapshot, NSError *error) { + __typeof__(self) strongSelf = weakSelf; + if (!strongSelf) { + return; + } + + if (!error) { + UIImage *image = snapshot.image; + NSArray *annotations = strongSelf.annotations; + if (annotations.count > 0) { + // Only create a graphics context if we have annotations to draw. + // The MKMapSnapshotter is currently not capable of rendering annotations automatically. + + CGRect finalImageRect = CGRectMake(0, 0, image.size.width, image.size.height); + + ASGraphicsBeginImageContextWithOptions(image.size, YES, image.scale); + [image drawAtPoint:CGPointZero]; + + UIImage *pinImage; + CGPoint pinCenterOffset = CGPointZero; + + // Get a standard annotation view pin if there is no custom annotation block. + if (!strongSelf.imageForStaticMapAnnotationBlock) { + pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset]; + } + + for (id annotation in annotations) { + if (strongSelf.imageForStaticMapAnnotationBlock) { + // Get custom annotation image from custom annotation block. + pinImage = strongSelf.imageForStaticMapAnnotationBlock(annotation, &pinCenterOffset); + if (!pinImage) { + // just for case block returned nil, which can happen + pinImage = [strongSelf.class defaultPinImageWithCenterOffset:&pinCenterOffset]; + } + } + + CGPoint point = [snapshot pointForCoordinate:annotation.coordinate]; + if (CGRectContainsPoint(finalImageRect, point)) { + CGSize pinSize = pinImage.size; + point.x -= pinSize.width / 2.0; + point.y -= pinSize.height / 2.0; + point.x += pinCenterOffset.x; + point.y += pinCenterOffset.y; + [pinImage drawAtPoint:point]; + } + } + + image = ASGraphicsGetImageAndEndCurrentContext(); + } + + strongSelf.image = image; + } + }]; +} + ++ (UIImage *)defaultPinImageWithCenterOffset:(CGPoint *)centerOffset NS_RETURNS_RETAINED +{ + static MKAnnotationView *pin; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pin = [[MKPinAnnotationView alloc] initWithAnnotation:nil reuseIdentifier:@""]; + }); + *centerOffset = pin.centerOffset; + return pin.image; +} + +- (void)setUpSnapshotter +{ + _snapshotter = [[MKMapSnapshotter alloc] initWithOptions:self.options]; +} + +- (void)destroySnapshotter +{ + [_snapshotter cancel]; + _snapshotter = nil; +} + +- (void)applySnapshotOptions +{ + MKMapSnapshotOptions *options = self.options; + [_mapView setCamera:options.camera animated:YES]; + [_mapView setRegion:options.region animated:YES]; + [_mapView setMapType:options.mapType]; + _mapView.showsBuildings = options.showsBuildings; + _mapView.showsPointsOfInterest = options.showsPointsOfInterest; +} + +#pragma mark - Actions +- (void)addLiveMap +{ + ASDisplayNodeAssertMainThread(); + if (!_mapView) { + __weak ASMapNode *weakSelf = self; + _mapView = [[MKMapView alloc] initWithFrame:CGRectZero]; + _mapView.delegate = weakSelf.mapDelegate; + [weakSelf applySnapshotOptions]; + [_mapView addAnnotations:_annotations]; + [weakSelf setNeedsLayout]; + [weakSelf.view addSubview:_mapView]; + + ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions; + if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { + BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated; + [_mapView showAnnotations:_mapView.annotations animated:animated]; + } + } +} + +- (void)removeLiveMap +{ + [_mapView removeFromSuperview]; + _mapView = nil; +} + +- (NSArray *)annotations +{ + ASLockScopeSelf(); + return _annotations; +} + +- (void)setAnnotations:(NSArray *)annotations +{ + annotations = [annotations copy] ? : @[]; + + ASLockScopeSelf(); + _annotations = annotations; + ASMapNodeShowAnnotationsOptions showAnnotationsOptions = self.showAnnotationsOptions; + if (self.isLiveMap) { + [_mapView removeAnnotations:_mapView.annotations]; + [_mapView addAnnotations:annotations]; + + if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { + BOOL const animated = showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsAnimated; + [_mapView showAnnotations:_mapView.annotations animated:animated]; + } + } else { + if (showAnnotationsOptions & ASMapNodeShowAnnotationsOptionsZoomed) { + self.region = [self regionToFitAnnotations:annotations]; + } + else { + [self takeSnapshot]; + } + } +} + +- (MKCoordinateRegion)regionToFitAnnotations:(NSArray> *)annotations +{ + if([annotations count] == 0) + return MKCoordinateRegionForMapRect(MKMapRectWorld); + + CLLocationCoordinate2D topLeftCoord = CLLocationCoordinate2DMake(-90, 180); + CLLocationCoordinate2D bottomRightCoord = CLLocationCoordinate2DMake(90, -180); + + for (id annotation in annotations) { + topLeftCoord = CLLocationCoordinate2DMake(std::fmax(topLeftCoord.latitude, annotation.coordinate.latitude), + std::fmin(topLeftCoord.longitude, annotation.coordinate.longitude)); + bottomRightCoord = CLLocationCoordinate2DMake(std::fmin(bottomRightCoord.latitude, annotation.coordinate.latitude), + std::fmax(bottomRightCoord.longitude, annotation.coordinate.longitude)); + } + + MKCoordinateRegion region = MKCoordinateRegionMake(CLLocationCoordinate2DMake(topLeftCoord.latitude - (topLeftCoord.latitude - bottomRightCoord.latitude) * 0.5, + topLeftCoord.longitude + (bottomRightCoord.longitude - topLeftCoord.longitude) * 0.5), + MKCoordinateSpanMake(std::fabs(topLeftCoord.latitude - bottomRightCoord.latitude) * 2, + std::fabs(bottomRightCoord.longitude - topLeftCoord.longitude) * 2)); + + return region; +} + +-(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { + return ASLockedSelf(_showAnnotationsOptions); +} + +-(void)setShowAnnotationsOptions:(ASMapNodeShowAnnotationsOptions)showAnnotationsOptions { + ASLockScopeSelf(); + _showAnnotationsOptions = showAnnotationsOptions; +} + +#pragma mark - Layout +- (void)setSnapshotSizeWithReloadIfNeeded:(CGSize)snapshotSize +{ + if (snapshotSize.height > 0 && snapshotSize.width > 0 && !CGSizeEqualToSize(self.options.size, snapshotSize)) { + _options.size = snapshotSize; + if (_snapshotter) { + [self destroySnapshotter]; + [self takeSnapshot]; + } + } +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + // FIXME: Need a better way to allow maps to take up the right amount of space in a layout (sizeRange, etc) + // These fallbacks protect against inheriting a constrainedSize that contains a CGFLOAT_MAX value. + if (!ASIsCGSizeValidForLayout(constrainedSize)) { + //ASDisplayNodeAssert(NO, @"Invalid width or height in ASMapNode"); + constrainedSize = CGSizeZero; + } + [self setSnapshotSizeWithReloadIfNeeded:constrainedSize]; + return constrainedSize; +} + +- (void)calculatedLayoutDidChange +{ + [super calculatedLayoutDidChange]; + + if (_snapshotAfterLayout) { + [self takeSnapshot]; + } +} + +// -layout isn't usually needed over -layoutSpecThatFits, but this way we can avoid a needless node wrapper for MKMapView. +- (void)layout +{ + [super layout]; + if (self.isLiveMap) { + _mapView.frame = CGRectMake(0.0f, 0.0f, self.calculatedSize.width, self.calculatedSize.height); + } else { + // If our bounds.size is different from our current snapshot size, then let's request a new image from MKMapSnapshotter. + if (_needsMapReloadOnBoundsChange) { + [self setSnapshotSizeWithReloadIfNeeded:self.bounds.size]; + // FIXME: Adding a check for Preload here seems to cause intermittent map load failures, but shouldn't. + // if (ASInterfaceStateIncludesPreload(self.interfaceState)) { + } + } +} + +- (BOOL)supportsLayerBacking +{ + return NO; +} + +@end +<<<<<<< HEAD +#endif + +#endif +======= +#endif // TARGET_OS_IOS && AS_USE_MAPKIT +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e diff --git a/Source/ASMultiplexImageNode.mm b/Source/ASMultiplexImageNode.mm index 338f408cbe..67da6e4303 100644 --- a/Source/ASMultiplexImageNode.mm +++ b/Source/ASMultiplexImageNode.mm @@ -913,3 +913,4 @@ typedef void(^ASMultiplexImageLoadCompletionBlock)(UIImage *image, id imageIdent @end #endif +#endif diff --git a/Source/Base/ASAssert.m.orig b/Source/Base/ASAssert.m.orig new file mode 100644 index 0000000000..45a3452180 --- /dev/null +++ b/Source/Base/ASAssert.m.orig @@ -0,0 +1,72 @@ +// +// ASAssert.m +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +<<<<<<< HEAD +#ifndef MINIMAL_ASDK +static _Thread_local int tls_mainThreadAssertionsDisabledCount; +#endif +======= +#if AS_TLS_AVAILABLE +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e + +static _Thread_local int tls_mainThreadAssertionsDisabledCount; +BOOL ASMainThreadAssertionsAreDisabled() { +#ifdef MINIMAL_ASDK + return false; +#else + return tls_mainThreadAssertionsDisabledCount > 0; +#endif +} + +void ASPushMainThreadAssertionsDisabled() { +#ifndef MINIMAL_ASDK + tls_mainThreadAssertionsDisabledCount += 1; +#endif +} + +void ASPopMainThreadAssertionsDisabled() { +#ifndef MINIMAL_ASDK + tls_mainThreadAssertionsDisabledCount -= 1; + ASDisplayNodeCAssert(tls_mainThreadAssertionsDisabledCount >= 0, @"Attempt to pop thread assertion-disabling without corresponding push."); +#endif +} + +#else + +#import + +static pthread_key_t ASMainThreadAssertionsDisabledKey() { + static pthread_key_t k; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pthread_key_create(&k, NULL); + }); + return k; +} + +BOOL ASMainThreadAssertionsAreDisabled() { + return (pthread_getspecific(ASMainThreadAssertionsDisabledKey()) > 0); +} + +void ASPushMainThreadAssertionsDisabled() { + let key = ASMainThreadAssertionsDisabledKey(); + let oldVal = pthread_getspecific(key); + pthread_setspecific(key, oldVal + 1); +} + +void ASPopMainThreadAssertionsDisabled() { + let key = ASMainThreadAssertionsDisabledKey(); + let oldVal = pthread_getspecific(key); + pthread_setspecific(key, oldVal - 1); + ASDisplayNodeCAssert(oldVal > 0, @"Attempt to pop thread assertion-disabling without corresponding push."); +} + +#endif // AS_TLS_AVAILABLE diff --git a/Source/Base/ASAvailability.h b/Source/Base/ASAvailability.h index 88e9e3fbe0..2cc7400018 100644 --- a/Source/Base/ASAvailability.h +++ b/Source/Base/ASAvailability.h @@ -11,11 +11,7 @@ #pragma once -#ifdef __i386__ - #define AS_TLS_AVAILABLE 0 -#else - #define AS_TLS_AVAILABLE 1 -#endif +#define AS_TLS_AVAILABLE 0 #ifndef AS_USE_PHOTOS # define AS_USE_PHOTOS 0 diff --git a/Source/Details/ASPhotosFrameworkImageRequest.h.orig b/Source/Details/ASPhotosFrameworkImageRequest.h.orig new file mode 100644 index 0000000000..bf52121f9e --- /dev/null +++ b/Source/Details/ASPhotosFrameworkImageRequest.h.orig @@ -0,0 +1,81 @@ +// +// ASPhotosFrameworkImageRequest.h +// 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 +// + +<<<<<<< HEAD +#ifndef MINIMAL_ASDK +======= +#import + +#if AS_USE_PHOTOS + +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_EXTERN NSString *const ASPhotosURLScheme; + +/** + @abstract Use ASPhotosFrameworkImageRequest to encapsulate all the information needed to request an image from + the Photos framework and store it in a URL. + */ +API_AVAILABLE(ios(8.0), tvos(10.0)) +@interface ASPhotosFrameworkImageRequest : NSObject + +- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier NS_DESIGNATED_INITIALIZER; + +/** + @return A new image request deserialized from `url`, or nil if `url` is not a valid photos URL. + */ ++ (nullable ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url; + +/** + @abstract The asset identifier for this image request provided during initialization. + */ +@property (nonatomic, readonly) NSString *assetIdentifier; + +/** + @abstract The target size for this image request. Defaults to `PHImageManagerMaximumSize`. + */ +@property (nonatomic) CGSize targetSize; + +/** + @abstract The content mode for this image request. Defaults to `PHImageContentModeDefault`. + + @see `PHImageManager` + */ +@property (nonatomic) PHImageContentMode contentMode; + +/** + @abstract The options specified for this request. Default value is the result of `[PHImageRequestOptions new]`. + + @discussion Some properties of this object are ignored when converting this request into a URL. + As of iOS SDK 9.0, these properties are `progressHandler` and `synchronous`. + */ +@property (nonatomic) PHImageRequestOptions *options; + +/** + @return A new URL converted from this request. + */ +@property (nonatomic, readonly) NSURL *url; + +- (instancetype)init NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END +<<<<<<< HEAD +#endif +======= + +#endif // AS_USE_PHOTOS +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e diff --git a/Source/Details/ASPhotosFrameworkImageRequest.m.orig b/Source/Details/ASPhotosFrameworkImageRequest.m.orig new file mode 100644 index 0000000000..e24ee882b2 --- /dev/null +++ b/Source/Details/ASPhotosFrameworkImageRequest.m.orig @@ -0,0 +1,163 @@ +// +// ASPhotosFrameworkImageRequest.m +// 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 +// +#ifndef MINIMAL_ASDK +#import + +#if AS_USE_PHOTOS + +#import + +NSString *const ASPhotosURLScheme = @"ph"; + +static NSString *const _ASPhotosURLQueryKeyWidth = @"width"; +static NSString *const _ASPhotosURLQueryKeyHeight = @"height"; + +// value is PHImageContentMode value +static NSString *const _ASPhotosURLQueryKeyContentMode = @"contentmode"; + +// value is PHImageRequestOptionsResizeMode value +static NSString *const _ASPhotosURLQueryKeyResizeMode = @"resizemode"; + +// value is PHImageRequestOptionsDeliveryMode value +static NSString *const _ASPhotosURLQueryKeyDeliveryMode = @"deliverymode"; + +// value is PHImageRequestOptionsVersion value +static NSString *const _ASPhotosURLQueryKeyVersion = @"version"; + +// value is 0 or 1 +static NSString *const _ASPhotosURLQueryKeyAllowNetworkAccess = @"network"; + +static NSString *const _ASPhotosURLQueryKeyCropOriginX = @"crop_x"; +static NSString *const _ASPhotosURLQueryKeyCropOriginY = @"crop_y"; +static NSString *const _ASPhotosURLQueryKeyCropWidth = @"crop_w"; +static NSString *const _ASPhotosURLQueryKeyCropHeight = @"crop_h"; + +@implementation ASPhotosFrameworkImageRequest + +- (instancetype)initWithAssetIdentifier:(NSString *)assetIdentifier +{ + self = [super init]; + if (self) { + _assetIdentifier = assetIdentifier; + _options = [PHImageRequestOptions new]; + _contentMode = PHImageContentModeDefault; + _targetSize = PHImageManagerMaximumSize; + } + return self; +} + +#pragma mark NSCopying + +- (id)copyWithZone:(NSZone *)zone +{ + ASPhotosFrameworkImageRequest *copy = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:self.assetIdentifier]; + copy.options = [self.options copy]; + copy.targetSize = self.targetSize; + copy.contentMode = self.contentMode; + return copy; +} + +#pragma mark Converting to URL + +- (NSURL *)url +{ + NSURLComponents *comp = [NSURLComponents new]; + comp.scheme = ASPhotosURLScheme; + comp.host = _assetIdentifier; + NSMutableArray *queryItems = [NSMutableArray arrayWithObjects: + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyWidth value:@(_targetSize.width).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyHeight value:@(_targetSize.height).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyVersion value:@(_options.version).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyContentMode value:@(_contentMode).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyAllowNetworkAccess value:@(_options.networkAccessAllowed).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyResizeMode value:@(_options.resizeMode).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyDeliveryMode value:@(_options.deliveryMode).stringValue] + , nil]; + + CGRect cropRect = _options.normalizedCropRect; + if (!CGRectIsEmpty(cropRect)) { + [queryItems addObjectsFromArray:@[ + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginX value:@(cropRect.origin.x).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropOriginY value:@(cropRect.origin.y).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropWidth value:@(cropRect.size.width).stringValue], + [NSURLQueryItem queryItemWithName:_ASPhotosURLQueryKeyCropHeight value:@(cropRect.size.height).stringValue] + ]]; + } + comp.queryItems = queryItems; + return comp.URL; +} + +#pragma mark Converting from URL + ++ (ASPhotosFrameworkImageRequest *)requestWithURL:(NSURL *)url +{ + // not a photos URL + if (![url.scheme isEqualToString:ASPhotosURLScheme]) { + return nil; + } + + NSURLComponents *comp = [NSURLComponents componentsWithURL:url resolvingAgainstBaseURL:NO]; + + ASPhotosFrameworkImageRequest *request = [[ASPhotosFrameworkImageRequest alloc] initWithAssetIdentifier:url.host]; + + CGRect cropRect = CGRectZero; + CGSize targetSize = PHImageManagerMaximumSize; + for (NSURLQueryItem *item in comp.queryItems) { + if ([_ASPhotosURLQueryKeyAllowNetworkAccess isEqualToString:item.name]) { + request.options.networkAccessAllowed = item.value.boolValue; + } else if ([_ASPhotosURLQueryKeyWidth isEqualToString:item.name]) { + targetSize.width = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyHeight isEqualToString:item.name]) { + targetSize.height = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyContentMode isEqualToString:item.name]) { + request.contentMode = (PHImageContentMode)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyVersion isEqualToString:item.name]) { + request.options.version = (PHImageRequestOptionsVersion)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyCropOriginX isEqualToString:item.name]) { + cropRect.origin.x = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropOriginY isEqualToString:item.name]) { + cropRect.origin.y = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropWidth isEqualToString:item.name]) { + cropRect.size.width = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyCropHeight isEqualToString:item.name]) { + cropRect.size.height = item.value.doubleValue; + } else if ([_ASPhotosURLQueryKeyResizeMode isEqualToString:item.name]) { + request.options.resizeMode = (PHImageRequestOptionsResizeMode)item.value.integerValue; + } else if ([_ASPhotosURLQueryKeyDeliveryMode isEqualToString:item.name]) { + request.options.deliveryMode = (PHImageRequestOptionsDeliveryMode)item.value.integerValue; + } + } + request.targetSize = targetSize; + request.options.normalizedCropRect = cropRect; + return request; +} + +#pragma mark NSObject + +- (BOOL)isEqual:(id)object +{ + if (![object isKindOfClass:ASPhotosFrameworkImageRequest.class]) { + return NO; + } + ASPhotosFrameworkImageRequest *other = object; + return [other.assetIdentifier isEqualToString:self.assetIdentifier] && + other.contentMode == self.contentMode && + CGSizeEqualToSize(other.targetSize, self.targetSize) && + CGRectEqualToRect(other.options.normalizedCropRect, self.options.normalizedCropRect) && + other.options.resizeMode == self.options.resizeMode && + other.options.version == self.options.version; +} + +@end +<<<<<<< HEAD +#endif +======= + +#endif // AS_USE_PHOTOS +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e diff --git a/Source/Layout/ASLayoutElement.mm b/Source/Layout/ASLayoutElement.mm index da5abbd623..a89ed11a8d 100644 --- a/Source/Layout/ASLayoutElement.mm +++ b/Source/Layout/ASLayoutElement.mm @@ -66,7 +66,6 @@ void ASLayoutElementPopContext() ASDisplayNodeCAssertNotNil(tls_context, @"Attempt to pop context when there wasn't a context!"); CFRelease((__bridge CFTypeRef)tls_context); tls_context = nil; -#endif } #else diff --git a/Source/Layout/ASLayoutElement.mm.orig b/Source/Layout/ASLayoutElement.mm.orig new file mode 100644 index 0000000000..3a17293bc9 --- /dev/null +++ b/Source/Layout/ASLayoutElement.mm.orig @@ -0,0 +1,869 @@ +// +// ASLayoutElement.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 +#import + +#import +#include + +#if YOGA + #import YOGA_HEADER_PATH + #import +#endif + +#pragma mark - ASLayoutElementContext + +@implementation ASLayoutElementContext + +- (instancetype)init +{ + if (self = [super init]) { + _transitionID = ASLayoutElementContextDefaultTransitionID; + } + return self; +} + +@end + +CGFloat const ASLayoutElementParentDimensionUndefined = NAN; +CGSize const ASLayoutElementParentSizeUndefined = {ASLayoutElementParentDimensionUndefined, ASLayoutElementParentDimensionUndefined}; + +int32_t const ASLayoutElementContextInvalidTransitionID = 0; +int32_t const ASLayoutElementContextDefaultTransitionID = ASLayoutElementContextInvalidTransitionID + 1; + +<<<<<<< HEAD +#ifdef MINIMAL_ASDK +static ASLayoutElementContext *mainThreadTlsContext = nil; + +static ASLayoutElementContext *get_tls_context() { + if ([NSThread isMainThread]) { + return mainThreadTlsContext; + } else { + return [NSThread currentThread].threadDictionary[@"ASDK_tls_context"]; + } +} + +static void set_tls_context(ASLayoutElementContext *value) { + if ([NSThread isMainThread]) { + mainThreadTlsContext = value; + } else { + if (value != nil) { + [NSThread currentThread].threadDictionary[@"ASDK_tls_context"] = value; + } else { + [[NSThread currentThread].threadDictionary removeObjectForKey:@"ASDK_tls_context"]; + } + } +} +#else +======= +#if AS_TLS_AVAILABLE + +>>>>>>> 565da7d4935740d12fc204aa061faf093831da1e +static _Thread_local __unsafe_unretained ASLayoutElementContext *tls_context; +#endif + +void ASLayoutElementPushContext(ASLayoutElementContext *context) +{ +#ifdef MINIMAL_ASDK + // NOTE: It would be easy to support nested contexts – just use an NSMutableArray here. + ASDisplayNodeCAssertNil(get_tls_context(), @"Nested ASLayoutElementContexts aren't supported."); + + ; + set_tls_context(context); +#else + // NOTE: It would be easy to support nested contexts – just use an NSMutableArray here. + ASDisplayNodeCAssertNil(tls_context, @"Nested ASLayoutElementContexts aren't supported."); + + tls_context = (__bridge ASLayoutElementContext *)(__bridge_retained CFTypeRef)context; +#endif +} + +ASLayoutElementContext *ASLayoutElementGetCurrentContext() +{ + // Don't retain here. Caller will retain if it wants to! + return get_tls_context(); +} + +void ASLayoutElementPopContext() +{ +#ifdef MINIMAL_ASDK + ASDisplayNodeCAssertNotNil(get_tls_context(), @"Attempt to pop context when there wasn't a context!"); + set_tls_context(nil); +#else + ASDisplayNodeCAssertNotNil(tls_context, @"Attempt to pop context when there wasn't a context!"); + CFRelease((__bridge CFTypeRef)tls_context); + tls_context = nil; +#endif +} + +#else + +static pthread_key_t ASLayoutElementContextKey() { + static pthread_key_t k; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + pthread_key_create(&k, NULL); + }); + return k; +} +void ASLayoutElementPushContext(ASLayoutElementContext *context) +{ + // NOTE: It would be easy to support nested contexts – just use an NSMutableArray here. + ASDisplayNodeCAssertNil(pthread_getspecific(ASLayoutElementContextKey()), @"Nested ASLayoutElementContexts aren't supported."); + + let cfCtx = (__bridge_retained CFTypeRef)context; + pthread_setspecific(ASLayoutElementContextKey(), cfCtx); +} + +ASLayoutElementContext *ASLayoutElementGetCurrentContext() +{ + // Don't retain here. Caller will retain if it wants to! + let ctxPtr = pthread_getspecific(ASLayoutElementContextKey()); + return (__bridge ASLayoutElementContext *)ctxPtr; +} + +void ASLayoutElementPopContext() +{ + let ctx = (CFTypeRef)pthread_getspecific(ASLayoutElementContextKey()); + ASDisplayNodeCAssertNotNil(ctx, @"Attempt to pop context when there wasn't a context!"); + CFRelease(ctx); + pthread_setspecific(ASLayoutElementContextKey(), NULL); +} + +#endif // AS_TLS_AVAILABLE + +#pragma mark - ASLayoutElementStyle + +NSString * const ASLayoutElementStyleWidthProperty = @"ASLayoutElementStyleWidthProperty"; +NSString * const ASLayoutElementStyleMinWidthProperty = @"ASLayoutElementStyleMinWidthProperty"; +NSString * const ASLayoutElementStyleMaxWidthProperty = @"ASLayoutElementStyleMaxWidthProperty"; + +NSString * const ASLayoutElementStyleHeightProperty = @"ASLayoutElementStyleHeightProperty"; +NSString * const ASLayoutElementStyleMinHeightProperty = @"ASLayoutElementStyleMinHeightProperty"; +NSString * const ASLayoutElementStyleMaxHeightProperty = @"ASLayoutElementStyleMaxHeightProperty"; + +NSString * const ASLayoutElementStyleSpacingBeforeProperty = @"ASLayoutElementStyleSpacingBeforeProperty"; +NSString * const ASLayoutElementStyleSpacingAfterProperty = @"ASLayoutElementStyleSpacingAfterProperty"; +NSString * const ASLayoutElementStyleFlexGrowProperty = @"ASLayoutElementStyleFlexGrowProperty"; +NSString * const ASLayoutElementStyleFlexShrinkProperty = @"ASLayoutElementStyleFlexShrinkProperty"; +NSString * const ASLayoutElementStyleFlexBasisProperty = @"ASLayoutElementStyleFlexBasisProperty"; +NSString * const ASLayoutElementStyleAlignSelfProperty = @"ASLayoutElementStyleAlignSelfProperty"; +NSString * const ASLayoutElementStyleAscenderProperty = @"ASLayoutElementStyleAscenderProperty"; +NSString * const ASLayoutElementStyleDescenderProperty = @"ASLayoutElementStyleDescenderProperty"; + +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(); \ + { x } \ + _size.store(newSize); \ + __instanceLock__.unlock(); + +#define ASLayoutElementStyleCallDelegate(propertyName)\ +do {\ + [self propertyDidChange:propertyName];\ + [_delegate style:self propertyDidChange:propertyName];\ +} while(0) + +@implementation ASLayoutElementStyle { + ASDN::RecursiveMutex __instanceLock__; + ASLayoutElementStyleExtensions _extensions; + + std::atomic _size; + std::atomic _spacingBefore; + std::atomic _spacingAfter; + std::atomic _flexGrow; + std::atomic _flexShrink; + std::atomic _flexBasis; + std::atomic _alignSelf; + std::atomic _ascender; + std::atomic _descender; + std::atomic _layoutPosition; + +#if YOGA + YGNodeRef _yogaNode; + std::atomic _flexWrap; + std::atomic _flexDirection; + std::atomic _direction; + std::atomic _justifyContent; + std::atomic _alignItems; + std::atomic _positionType; + std::atomic _position; + std::atomic _margin; + std::atomic _padding; + std::atomic _border; + std::atomic _aspectRatio; +#endif +} + +@dynamic width, height, minWidth, maxWidth, minHeight, maxHeight; +@dynamic preferredSize, minSize, maxSize, preferredLayoutSize, minLayoutSize, maxLayoutSize; + +#pragma mark - Lifecycle + +- (instancetype)initWithDelegate:(id)delegate +{ + self = [self init]; + if (self) { + _delegate = delegate; + } + return self; +} + +- (instancetype)init +{ + self = [super init]; + if (self) { + _size = ASLayoutElementSizeMake(); + } + return self; +} + +ASSynthesizeLockingMethodsWithMutex(__instanceLock__) + +#pragma mark - ASLayoutElementStyleSize + +- (ASLayoutElementSize)size +{ + return _size.load(); +} + +- (void)setSize:(ASLayoutElementSize)size +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize = size; + }); + // No CallDelegate method as ASLayoutElementSize is currently internal. +} + +#pragma mark - ASLayoutElementStyleSizeForwarding + +- (ASDimension)width +{ + return _size.load().width; +} + +- (void)setWidth:(ASDimension)width +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.width = width; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); +} + +- (ASDimension)height +{ + return _size.load().height; +} + +- (void)setHeight:(ASDimension)height +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.height = height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); +} + +- (ASDimension)minWidth +{ + return _size.load().minWidth; +} + +- (void)setMinWidth:(ASDimension)minWidth +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minWidth = minWidth; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); +} + +- (ASDimension)maxWidth +{ + return _size.load().maxWidth; +} + +- (void)setMaxWidth:(ASDimension)maxWidth +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxWidth = maxWidth; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); +} + +- (ASDimension)minHeight +{ + return _size.load().minHeight; +} + +- (void)setMinHeight:(ASDimension)minHeight +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minHeight = minHeight; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); +} + +- (ASDimension)maxHeight +{ + return _size.load().maxHeight; +} + +- (void)setMaxHeight:(ASDimension)maxHeight +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxHeight = maxHeight; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); +} + + +#pragma mark - ASLayoutElementStyleSizeHelpers + +- (void)setPreferredSize:(CGSize)preferredSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.width = ASDimensionMakeWithPoints(preferredSize.width); + newSize.height = ASDimensionMakeWithPoints(preferredSize.height); + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); +} + +- (CGSize)preferredSize +{ + ASLayoutElementSize size = _size.load(); + if (size.width.unit == ASDimensionUnitFraction) { + NSCAssert(NO, @"Cannot get preferredSize of element with fractional width. Width: %@.", NSStringFromASDimension(size.width)); + return CGSizeZero; + } + + if (size.height.unit == ASDimensionUnitFraction) { + NSCAssert(NO, @"Cannot get preferredSize of element with fractional height. Height: %@.", NSStringFromASDimension(size.height)); + return CGSizeZero; + } + + return CGSizeMake(size.width.value, size.height.value); +} + +- (void)setMinSize:(CGSize)minSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minWidth = ASDimensionMakeWithPoints(minSize.width); + newSize.minHeight = ASDimensionMakeWithPoints(minSize.height); + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); +} + +- (void)setMaxSize:(CGSize)maxSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxWidth = ASDimensionMakeWithPoints(maxSize.width); + newSize.maxHeight = ASDimensionMakeWithPoints(maxSize.height); + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); +} + +- (ASLayoutSize)preferredLayoutSize +{ + ASLayoutElementSize size = _size.load(); + return ASLayoutSizeMake(size.width, size.height); +} + +- (void)setPreferredLayoutSize:(ASLayoutSize)preferredLayoutSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.width = preferredLayoutSize.width; + newSize.height = preferredLayoutSize.height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); +} + +- (ASLayoutSize)minLayoutSize +{ + ASLayoutElementSize size = _size.load(); + return ASLayoutSizeMake(size.minWidth, size.minHeight); +} + +- (void)setMinLayoutSize:(ASLayoutSize)minLayoutSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.minWidth = minLayoutSize.width; + newSize.minHeight = minLayoutSize.height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); +} + +- (ASLayoutSize)maxLayoutSize +{ + ASLayoutElementSize size = _size.load(); + return ASLayoutSizeMake(size.maxWidth, size.maxHeight); +} + +- (void)setMaxLayoutSize:(ASLayoutSize)maxLayoutSize +{ + ASLayoutElementStyleSetSizeWithScope({ + newSize.maxWidth = maxLayoutSize.width; + newSize.maxHeight = maxLayoutSize.height; + }); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); +} + +#pragma mark - ASStackLayoutElement + +- (void)setSpacingBefore:(CGFloat)spacingBefore +{ + _spacingBefore.store(spacingBefore); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingBeforeProperty); +} + +- (CGFloat)spacingBefore +{ + return _spacingBefore.load(); +} + +- (void)setSpacingAfter:(CGFloat)spacingAfter +{ + _spacingAfter.store(spacingAfter); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingAfterProperty); +} + +- (CGFloat)spacingAfter +{ + return _spacingAfter.load(); +} + +- (void)setFlexGrow:(CGFloat)flexGrow +{ + _flexGrow.store(flexGrow); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexGrowProperty); +} + +- (CGFloat)flexGrow +{ + return _flexGrow.load(); +} + +- (void)setFlexShrink:(CGFloat)flexShrink +{ + _flexShrink.store(flexShrink); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexShrinkProperty); +} + +- (CGFloat)flexShrink +{ + return _flexShrink.load(); +} + +- (void)setFlexBasis:(ASDimension)flexBasis +{ + _flexBasis.store(flexBasis); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexBasisProperty); +} + +- (ASDimension)flexBasis +{ + return _flexBasis.load(); +} + +- (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf +{ + _alignSelf.store(alignSelf); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAlignSelfProperty); +} + +- (ASStackLayoutAlignSelf)alignSelf +{ + return _alignSelf.load(); +} + +- (void)setAscender:(CGFloat)ascender +{ + _ascender.store(ascender); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAscenderProperty); +} + +- (CGFloat)ascender +{ + return _ascender.load(); +} + +- (void)setDescender:(CGFloat)descender +{ + _descender.store(descender); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleDescenderProperty); +} + +- (CGFloat)descender +{ + return _descender.load(); +} + +#pragma mark - ASAbsoluteLayoutElement + +- (void)setLayoutPosition:(CGPoint)layoutPosition +{ + _layoutPosition.store(layoutPosition); + ASLayoutElementStyleCallDelegate(ASLayoutElementStyleLayoutPositionProperty); +} + +- (CGPoint)layoutPosition +{ + return _layoutPosition.load(); +} + +#pragma mark - Extensions + +- (void)setLayoutOptionExtensionBool:(BOOL)value atIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Setting index outside of max bool extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + _extensions.boolExtensions[idx] = value; +} + +- (BOOL)layoutOptionExtensionBoolAtIndex:(int)idx\ +{ + NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Accessing index outside of max bool extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + return _extensions.boolExtensions[idx]; +} + +- (void)setLayoutOptionExtensionInteger:(NSInteger)value atIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Setting index outside of max integer extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + _extensions.integerExtensions[idx] = value; +} + +- (NSInteger)layoutOptionExtensionIntegerAtIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Accessing index outside of max integer extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + return _extensions.integerExtensions[idx]; +} + +- (void)setLayoutOptionExtensionEdgeInsets:(UIEdgeInsets)value atIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Setting index outside of max edge insets extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + _extensions.edgeInsetsExtensions[idx] = value; +} + +- (UIEdgeInsets)layoutOptionExtensionEdgeInsetsAtIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Accessing index outside of max edge insets extensions space"); + + ASDN::MutexLocker l(__instanceLock__); + return _extensions.edgeInsetsExtensions[idx]; +} + +#pragma mark - Debugging + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + if ((self.minLayoutSize.width.unit != ASDimensionUnitAuto || + self.minLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"minLayoutSize" : NSStringFromASLayoutSize(self.minLayoutSize) }]; + } + + if ((self.preferredLayoutSize.width.unit != ASDimensionUnitAuto || + self.preferredLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"preferredSize" : NSStringFromASLayoutSize(self.preferredLayoutSize) }]; + } + + if ((self.maxLayoutSize.width.unit != ASDimensionUnitAuto || + self.maxLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"maxLayoutSize" : NSStringFromASLayoutSize(self.maxLayoutSize) }]; + } + + if (self.alignSelf != ASStackLayoutAlignSelfAuto) { + [result addObject:@{ @"alignSelf" : [@[@"ASStackLayoutAlignSelfAuto", + @"ASStackLayoutAlignSelfStart", + @"ASStackLayoutAlignSelfEnd", + @"ASStackLayoutAlignSelfCenter", + @"ASStackLayoutAlignSelfStretch"] objectAtIndex:self.alignSelf] }]; + } + + if (self.ascender != 0) { + [result addObject:@{ @"ascender" : @(self.ascender) }]; + } + + if (self.descender != 0) { + [result addObject:@{ @"descender" : @(self.descender) }]; + } + + if (ASDimensionEqualToDimension(self.flexBasis, ASDimensionAuto) == NO) { + [result addObject:@{ @"flexBasis" : NSStringFromASDimension(self.flexBasis) }]; + } + + if (self.flexGrow != 0) { + [result addObject:@{ @"flexGrow" : @(self.flexGrow) }]; + } + + if (self.flexShrink != 0) { + [result addObject:@{ @"flexShrink" : @(self.flexShrink) }]; + } + + if (self.spacingAfter != 0) { + [result addObject:@{ @"spacingAfter" : @(self.spacingAfter) }]; + } + + if (self.spacingBefore != 0) { + [result addObject:@{ @"spacingBefore" : @(self.spacingBefore) }]; + } + + if (CGPointEqualToPoint(self.layoutPosition, CGPointZero) == NO) { + [result addObject:@{ @"layoutPosition" : [NSValue valueWithCGPoint:self.layoutPosition] }]; + } + + 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(); } +- (ASStackLayoutJustifyContent)justifyContent { return _justifyContent.load(); } +- (ASStackLayoutAlignItems)alignItems { return _alignItems.load(); } +- (YGPositionType)positionType { return _positionType.load(); } +- (ASEdgeInsets)position { return _position.load(); } +- (ASEdgeInsets)margin { return _margin.load(); } +- (ASEdgeInsets)padding { return _padding.load(); } +- (ASEdgeInsets)border { return _border.load(); } +- (CGFloat)aspectRatio { return _aspectRatio.load(); } + +- (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 /* YOGA */ + +@end diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index a38185e006..d4fc63d689 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -839,24 +839,6 @@ static UIColor *defaultTintColor = nil; if (flags.setSublayerTransform) layer.sublayerTransform = sublayerTransform; - if (flags.setContents) - layer.contents = contents; - - if (flags.setContentsGravity) - layer.contentsGravity = contentsGravity; - - if (flags.setContentsRect) - layer.contentsRect = contentsRect; - - if (flags.setContentsCenter) - layer.contentsCenter = contentsCenter; - - if (flags.setContentsScale) - layer.contentsScale = contentsScale; - - if (flags.setRasterizationScale) - layer.rasterizationScale = rasterizationScale; - if (flags.setClipsToBounds) layer.masksToBounds = clipsToBounds; @@ -916,6 +898,24 @@ static UIColor *defaultTintColor = nil; ASPendingStateApplyMetricsToLayer(self, layer); + if (flags.setContents) + layer.contents = contents; + + if (flags.setContentsScale) + layer.contentsScale = contentsScale; + + if (flags.setRasterizationScale) + layer.rasterizationScale = rasterizationScale; + + if (flags.setContentsGravity) + layer.contentsGravity = contentsGravity; + + if (flags.setContentsRect) + layer.contentsRect = contentsRect; + + if (flags.setContentsCenter) + layer.contentsCenter = contentsCenter; + if (flags.needsLayout) [layer setNeedsLayout]; @@ -958,24 +958,6 @@ static UIColor *defaultTintColor = nil; if (flags.setSublayerTransform) layer.sublayerTransform = sublayerTransform; - if (flags.setContents) - layer.contents = contents; - - if (flags.setContentsGravity) - layer.contentsGravity = contentsGravity; - - if (flags.setContentsRect) - layer.contentsRect = contentsRect; - - if (flags.setContentsCenter) - layer.contentsCenter = contentsCenter; - - if (flags.setContentsScale) - layer.contentsScale = contentsScale; - - if (flags.setRasterizationScale) - layer.rasterizationScale = rasterizationScale; - if (flags.setClipsToBounds) view.clipsToBounds = clipsToBounds; @@ -1143,6 +1125,24 @@ static UIColor *defaultTintColor = nil; ASPendingStateApplyMetricsToLayer(self, layer); } + if (flags.setContents) + layer.contents = contents; + + if (flags.setContentsGravity) + layer.contentsGravity = contentsGravity; + + if (flags.setContentsRect) + layer.contentsRect = contentsRect; + + if (flags.setContentsCenter) + layer.contentsCenter = contentsCenter; + + if (flags.setContentsScale) + layer.contentsScale = contentsScale; + + if (flags.setRasterizationScale) + layer.rasterizationScale = rasterizationScale; + if (flags.needsLayout) [view setNeedsLayout];