From 461c278867e87193b7e6193b1a6890074a0dbe5b Mon Sep 17 00:00:00 2001 From: Ali <> Date: Thu, 27 Feb 2020 00:02:03 +0400 Subject: [PATCH] Fix linking --- .bazelrc | 3 + .gitmodules | 3 + build-system/generate-xcode-project.sh | 15 +- build-system/tulsi | 1 + build-system/xcode_version | 1 + ...bstractLayoutController+FrameworkPrivate.h | 21 + .../Source/ASAsciiArtBoxCreator.mm | 186 + submodules/AsyncDisplayKit/Source/ASAssert.mm | 58 + .../AsyncDisplayKit/Source/ASCGImageBuffer.h | 28 + .../AsyncDisplayKit/Source/ASCGImageBuffer.mm | 88 + .../AsyncDisplayKit/Source/ASCollections.mm | 61 + .../AsyncDisplayKit/Source/ASConfiguration.mm | 64 + .../Source/ASConfigurationInternal.mm | 111 + .../Source/ASControlNode+Private.h | 18 + .../AsyncDisplayKit/Source/ASControlNode.mm | 499 +++ .../Source/ASControlTargetAction.mm | 65 + .../AsyncDisplayKit/Source/ASDimension.mm | 125 + .../Source/ASDimensionInternal.mm | 65 + .../AsyncDisplayKit/Source/ASDispatch.h | 27 + .../AsyncDisplayKit/Source/ASDispatch.mm | 63 + .../Source/ASDisplayNode+Ancestry.mm | 90 + .../Source/ASDisplayNode+AsyncDisplay.mm | 493 +++ .../Source/ASDisplayNode+Convenience.mm | 40 + .../Source/ASDisplayNode+Deprecated.h | 142 + .../Source/ASDisplayNode+Layout.mm | 1036 +++++ .../Source/ASDisplayNode+LayoutSpec.mm | 145 + .../Source/ASDisplayNode+UIViewBridge.mm | 1325 ++++++ .../AsyncDisplayKit/Source/ASDisplayNode.mm | 3803 +++++++++++++++++ .../Source/ASDisplayNodeExtras.mm | 339 ++ .../Source/ASDisplayNodeInternal.h | 407 ++ .../Source/ASDisplayNodeLayout.h | 59 + .../Source/ASEditableTextNode.mm | 1172 +++++ .../Source/ASExperimentalFeatures.mm | 52 + .../Source/ASGraphicsContext.mm | 167 + .../AsyncDisplayKit/Source/ASHashing.mm | 38 + .../Source/ASInternalHelpers.mm | 233 + submodules/AsyncDisplayKit/Source/ASLayout.mm | 378 ++ .../AsyncDisplayKit/Source/ASLayoutElement.mm | 843 ++++ .../Source/ASLayoutElementStylePrivate.h | 31 + .../AsyncDisplayKit/Source/ASLayoutManager.h | 16 + .../AsyncDisplayKit/Source/ASLayoutManager.mm | 42 + .../Source/ASLayoutSpec+Subclasses.h | 59 + .../Source/ASLayoutSpec+Subclasses.mm | 87 + .../AsyncDisplayKit/Source/ASLayoutSpec.mm | 338 ++ .../Source/ASLayoutSpecPrivate.h | 37 + .../Source/ASLayoutSpecUtilities.h | 103 + .../Source/ASLayoutTransition.h | 94 + .../Source/ASLayoutTransition.mm | 298 ++ .../Source/ASMainSerialQueue.h | 19 + .../Source/ASMainSerialQueue.mm | 81 + .../Source/ASMainThreadDeallocation.mm | 199 + .../Source/ASObjectDescriptionHelpers.mm | 101 + .../Source/ASPendingStateController.h | 50 + .../Source/ASPendingStateController.mm | 102 + .../Source/ASRecursiveUnfairLock.mm | 83 + .../Source/ASResponderChainEnumerator.h | 29 + .../Source/ASResponderChainEnumerator.mm | 45 + .../AsyncDisplayKit/Source/ASRunLoopQueue.mm | 464 ++ .../Source/ASScrollDirection.mm | 64 + .../AsyncDisplayKit/Source/ASScrollNode.mm | 178 + .../AsyncDisplayKit/Source/ASSignpost.h | 94 + .../Source/ASTextKitComponents.mm | 194 + .../AsyncDisplayKit/Source/ASTextKitContext.h | 53 + .../Source/ASTextKitContext.mm | 84 + .../AsyncDisplayKit/Source/ASTextNodeCommon.h | 34 + .../Source/ASTextNodeWordKerner.h | 37 + .../Source/ASTextNodeWordKerner.mm | 130 + .../Source/ASTraitCollection.mm | 256 ++ submodules/AsyncDisplayKit/Source/ASWeakMap.h | 59 + .../AsyncDisplayKit/Source/ASWeakMap.mm | 78 + .../AsyncDisplayKit/Source/ASWeakProxy.h | 32 + .../AsyncDisplayKit/Source/ASWeakProxy.mm | 72 + .../AsyncDisplayKit/Source/ASWeakSet.mm | 84 + .../AsyncDisplayKit/Source/NSArray+Diffing.mm | 177 + .../Source/NSIndexSet+ASHelpers.h | 29 + .../Source/NSIndexSet+ASHelpers.mm | 91 + .../AsyncDisplayKit/ASDisplayNode.h | 4 + .../Source/UIResponder+AsyncDisplayKit.mm | 32 + .../Source/_ASAsyncTransaction.mm | 465 ++ .../_ASAsyncTransactionContainer+Private.h | 24 + .../Source/_ASAsyncTransactionContainer.mm | 121 + .../Source/_ASAsyncTransactionGroup.mm | 88 + .../Source/_ASCoreAnimationExtras.mm | 187 + .../AsyncDisplayKit/Source/_ASDisplayLayer.mm | 212 + .../AsyncDisplayKit/Source/_ASDisplayView.mm | 569 +++ .../Source/_ASDisplayViewAccessiblity.h | 17 + .../Source/_ASDisplayViewAccessiblity.mm | 349 ++ .../AsyncDisplayKit/Source/_ASPendingState.h | 41 + .../AsyncDisplayKit/Source/_ASPendingState.mm | 1379 ++++++ .../AsyncDisplayKit/Source/_ASScopeTimer.h | 56 + .../Source/_ASTransitionContext.mm | 104 + .../Display/Source/Nodes/ButtonNode.swift | 1 + 92 files changed, 19935 insertions(+), 2 deletions(-) create mode 160000 build-system/tulsi create mode 100644 build-system/xcode_version create mode 100644 submodules/AsyncDisplayKit/Source/ASAbstractLayoutController+FrameworkPrivate.h create mode 100644 submodules/AsyncDisplayKit/Source/ASAsciiArtBoxCreator.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASAssert.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASCGImageBuffer.h create mode 100644 submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASCollections.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASConfiguration.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASConfigurationInternal.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASControlNode+Private.h create mode 100644 submodules/AsyncDisplayKit/Source/ASControlNode.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASControlTargetAction.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDimension.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDimensionInternal.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDispatch.h create mode 100644 submodules/AsyncDisplayKit/Source/ASDispatch.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNode+Ancestry.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNode+AsyncDisplay.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNode+Deprecated.h create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNode+Layout.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNode+UIViewBridge.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNode.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNodeInternal.h create mode 100644 submodules/AsyncDisplayKit/Source/ASDisplayNodeLayout.h create mode 100644 submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASHashing.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASInternalHelpers.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASLayout.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutElement.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutElementStylePrivate.h create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutManager.h create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutManager.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutSpec+Subclasses.h create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutSpec+Subclasses.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutSpec.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutSpecPrivate.h create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutSpecUtilities.h create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutTransition.h create mode 100644 submodules/AsyncDisplayKit/Source/ASLayoutTransition.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASMainSerialQueue.h create mode 100644 submodules/AsyncDisplayKit/Source/ASMainSerialQueue.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASObjectDescriptionHelpers.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASPendingStateController.h create mode 100644 submodules/AsyncDisplayKit/Source/ASPendingStateController.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASRecursiveUnfairLock.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASResponderChainEnumerator.h create mode 100644 submodules/AsyncDisplayKit/Source/ASResponderChainEnumerator.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASRunLoopQueue.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASScrollDirection.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASScrollNode.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASSignpost.h create mode 100644 submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASTextKitContext.h create mode 100644 submodules/AsyncDisplayKit/Source/ASTextKitContext.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASTextNodeCommon.h create mode 100644 submodules/AsyncDisplayKit/Source/ASTextNodeWordKerner.h create mode 100644 submodules/AsyncDisplayKit/Source/ASTextNodeWordKerner.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASTraitCollection.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASWeakMap.h create mode 100644 submodules/AsyncDisplayKit/Source/ASWeakMap.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASWeakProxy.h create mode 100644 submodules/AsyncDisplayKit/Source/ASWeakProxy.mm create mode 100644 submodules/AsyncDisplayKit/Source/ASWeakSet.mm create mode 100644 submodules/AsyncDisplayKit/Source/NSArray+Diffing.mm create mode 100644 submodules/AsyncDisplayKit/Source/NSIndexSet+ASHelpers.h create mode 100644 submodules/AsyncDisplayKit/Source/NSIndexSet+ASHelpers.mm create mode 100644 submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.mm create mode 100644 submodules/AsyncDisplayKit/Source/_ASAsyncTransaction.mm create mode 100644 submodules/AsyncDisplayKit/Source/_ASAsyncTransactionContainer+Private.h create mode 100644 submodules/AsyncDisplayKit/Source/_ASAsyncTransactionContainer.mm create mode 100644 submodules/AsyncDisplayKit/Source/_ASAsyncTransactionGroup.mm create mode 100644 submodules/AsyncDisplayKit/Source/_ASCoreAnimationExtras.mm create mode 100644 submodules/AsyncDisplayKit/Source/_ASDisplayLayer.mm create mode 100644 submodules/AsyncDisplayKit/Source/_ASDisplayView.mm create mode 100644 submodules/AsyncDisplayKit/Source/_ASDisplayViewAccessiblity.h create mode 100644 submodules/AsyncDisplayKit/Source/_ASDisplayViewAccessiblity.mm create mode 100644 submodules/AsyncDisplayKit/Source/_ASPendingState.h create mode 100644 submodules/AsyncDisplayKit/Source/_ASPendingState.mm create mode 100644 submodules/AsyncDisplayKit/Source/_ASScopeTimer.h create mode 100644 submodules/AsyncDisplayKit/Source/_ASTransitionContext.mm diff --git a/.bazelrc b/.bazelrc index f11e3aa2f7..a19a661ff9 100644 --- a/.bazelrc +++ b/.bazelrc @@ -6,4 +6,7 @@ build --swiftcopt='-Xcc' build --swiftcopt='-w' build --spawn_strategy=local build --strategy=SwiftCompile=local +build --features=debug_prefix_map_pwd_is_dot +build --features=swift.cacheable_swiftmodules +build --features=swift.debug_prefix_map diff --git a/.gitmodules b/.gitmodules index ab5edb3778..d416d6ad33 100644 --- a/.gitmodules +++ b/.gitmodules @@ -14,3 +14,6 @@ [submodule "submodules/TgVoip/libtgvoip"] path = submodules/TgVoip/libtgvoip url = https://github.com/telegramdesktop/libtgvoip.git +[submodule "build-system/tulsi"] + path = build-system/tulsi + url = https://github.com/ali-fareed/tulsi.git diff --git a/build-system/generate-xcode-project.sh b/build-system/generate-xcode-project.sh index 4fbcc74111..d1275940d2 100755 --- a/build-system/generate-xcode-project.sh +++ b/build-system/generate-xcode-project.sh @@ -36,6 +36,17 @@ GEN_DIRECTORY="build-input/gen/project" rm -rf "$GEN_DIRECTORY" mkdir -p "$GEN_DIRECTORY" +pushd "build-system/tulsi" +"$BAZEL" build //:tulsi --xcode_version=$(cat "build-system/xcode_version") +popd + +TULSI_DIRECTORY="build-input/gen/project" +TULSI_APP="build-input/gen/project/Tulsi.app" +TULSI="$TULSI_APP/Contents/MacOS/Tulsi" +mkdir -p "$TULSI_DIRECTORY" + +unzip -oq "build-system/tulsi/bazel-bin/tulsi.zip" -d "$TULSI_DIRECTORY" + CORE_COUNT=$(sysctl -n hw.logicalcpu) CORE_COUNT_MINUS_ONE=$(expr ${CORE_COUNT} \- 1) @@ -51,7 +62,7 @@ if [ "$BAZEL_CACHE_DIR" != "" ]; then BAZEL_OPTIONS=("${BAZEL_OPTIONS[@]}" --disk_cache="$(echo $BAZEL_CACHE_DIR | sed -e 's/[\/&]/\\&/g')") fi -$HOME/Applications/Tulsi.app/Contents/MacOS/Tulsi -- \ +"$TULSI" -- \ --verbose \ --create-tulsiproj Telegram \ --workspaceroot ./ \ @@ -68,7 +79,7 @@ done sed -i "" -e '1h;2,$H;$!d;g' -e 's/\("sourceFilters" : \[\n[ ]*\)"\.\/\.\.\."/\1"Telegram\/...", "submodules\/..."/' "$GEN_DIRECTORY/Telegram.tulsiproj/Configs/Telegram.tulsigen" -${HOME}/Applications/Tulsi.app/Contents/MacOS/Tulsi -- \ +"$TULSI" -- \ --verbose \ --genconfig "$GEN_DIRECTORY/Telegram.tulsiproj:Telegram" \ --bazel "$BAZEL" \ diff --git a/build-system/tulsi b/build-system/tulsi new file mode 160000 index 0000000000..ee185c4c20 --- /dev/null +++ b/build-system/tulsi @@ -0,0 +1 @@ +Subproject commit ee185c4c20ea4384bc3cbf8ccd8705c904154abb diff --git a/build-system/xcode_version b/build-system/xcode_version new file mode 100644 index 0000000000..f226094f1f --- /dev/null +++ b/build-system/xcode_version @@ -0,0 +1 @@ +11.3.1 \ No newline at end of file diff --git a/submodules/AsyncDisplayKit/Source/ASAbstractLayoutController+FrameworkPrivate.h b/submodules/AsyncDisplayKit/Source/ASAbstractLayoutController+FrameworkPrivate.h new file mode 100644 index 0000000000..f836fa0b84 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASAbstractLayoutController+FrameworkPrivate.h @@ -0,0 +1,21 @@ +// +// ASAbstractLayoutController+FrameworkPrivate.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 +// + +// +// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode. +// These methods must never be called or overridden by other classes. +// + +#include + +@interface ASAbstractLayoutController (FrameworkPrivate) + ++ (std::vector>)defaultTuningParameters; + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASAsciiArtBoxCreator.mm b/submodules/AsyncDisplayKit/Source/ASAsciiArtBoxCreator.mm new file mode 100644 index 0000000000..78eb572ead --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASAsciiArtBoxCreator.mm @@ -0,0 +1,186 @@ +// +// ASAsciiArtBoxCreator.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 + +static const NSUInteger kDebugBoxPadding = 2; + +typedef NS_ENUM(NSUInteger, PIDebugBoxPaddingLocation) +{ + PIDebugBoxPaddingLocationFront, + PIDebugBoxPaddingLocationEnd, + PIDebugBoxPaddingLocationBoth +}; + +@interface NSString(PIDebugBox) + +@end + +@implementation NSString(PIDebugBox) + ++ (instancetype)debugbox_stringWithString:(NSString *)stringToRepeat repeatedCount:(NSUInteger)repeatCount NS_RETURNS_RETAINED +{ + NSMutableString *string = [[NSMutableString alloc] initWithCapacity:[stringToRepeat length] * repeatCount]; + for (NSUInteger index = 0; index < repeatCount; index++) { + [string appendString:stringToRepeat]; + } + return [string copy]; +} + +- (NSString *)debugbox_stringByAddingPadding:(NSString *)padding count:(NSUInteger)count location:(PIDebugBoxPaddingLocation)location +{ + NSString *paddingString = [NSString debugbox_stringWithString:padding repeatedCount:count]; + switch (location) { + case PIDebugBoxPaddingLocationFront: + return [NSString stringWithFormat:@"%@%@", paddingString, self]; + case PIDebugBoxPaddingLocationEnd: + return [NSString stringWithFormat:@"%@%@", self, paddingString]; + case PIDebugBoxPaddingLocationBoth: + return [NSString stringWithFormat:@"%@%@%@", paddingString, self, paddingString]; + } + return [self copy]; +} + +@end + +@implementation ASAsciiArtBoxCreator + ++ (NSString *)horizontalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent +{ + if ([children count] == 0) { + return parent; + } + + NSMutableArray *childrenLines = [NSMutableArray array]; + + // split the children into lines + NSUInteger lineCountPerChild = 0; + for (NSString *child in children) { + NSArray *lines = [child componentsSeparatedByString:@"\n"]; + lineCountPerChild = MAX(lineCountPerChild, [lines count]); + } + + for (NSString *child in children) { + NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy]; + NSUInteger topPadding = ceil((CGFloat)(lineCountPerChild - [lines count])/2.0); + NSUInteger bottomPadding = (lineCountPerChild - [lines count])/2.0; + NSUInteger lineLength = [lines[0] length]; + + for (NSUInteger index = 0; index < topPadding; index++) { + [lines insertObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength] atIndex:0]; + } + for (NSUInteger index = 0; index < bottomPadding; index++) { + [lines addObject:[NSString debugbox_stringWithString:@" " repeatedCount:lineLength]]; + } + [childrenLines addObject:lines]; + } + + NSMutableArray *concatenatedLines = [NSMutableArray array]; + NSString *padding = [NSString debugbox_stringWithString:@" " repeatedCount:kDebugBoxPadding]; + for (NSUInteger index = 0; index < lineCountPerChild; index++) { + NSMutableString *line = [[NSMutableString alloc] init]; + [line appendFormat:@"|%@",padding]; + for (NSArray *childLines in childrenLines) { + [line appendFormat:@"%@%@", childLines[index], padding]; + } + [line appendString:@"|"]; + [concatenatedLines addObject:line]; + } + + // surround the lines in a box + NSUInteger totalLineLength = [concatenatedLines[0] length]; + if (totalLineLength < [parent length]) { + NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - totalLineLength; + NSUInteger leftPadding = ceil((CGFloat)difference/2.0); + NSUInteger rightPadding = difference/2; + + NSString *leftString = [@"|" debugbox_stringByAddingPadding:@" " count:leftPadding location:PIDebugBoxPaddingLocationEnd]; + NSString *rightString = [@"|" debugbox_stringByAddingPadding:@" " count:rightPadding location:PIDebugBoxPaddingLocationFront]; + + NSMutableArray *paddedLines = [NSMutableArray array]; + for (NSString *line in concatenatedLines) { + NSString *paddedLine = [line stringByReplacingOccurrencesOfString:@"|" withString:leftString options:NSCaseInsensitiveSearch range:NSMakeRange(0, 1)]; + paddedLine = [paddedLine stringByReplacingOccurrencesOfString:@"|" withString:rightString options:NSCaseInsensitiveSearch range:NSMakeRange([paddedLine length] - 1, 1)]; + [paddedLines addObject:paddedLine]; + } + concatenatedLines = paddedLines; + // totalLineLength += difference; + } + concatenatedLines = [self appendTopAndBottomToBoxString:concatenatedLines parent:parent]; + return [concatenatedLines componentsJoinedByString:@"\n"]; + +} + ++ (NSString *)verticalBoxStringForChildren:(NSArray *)children parent:(NSString *)parent +{ + if ([children count] == 0) { + return parent; + } + + NSMutableArray *childrenLines = [NSMutableArray array]; + + NSUInteger maxChildLength = 0; + for (NSString *child in children) { + NSArray *lines = [child componentsSeparatedByString:@"\n"]; + maxChildLength = MAX(maxChildLength, [lines[0] length]); + } + + NSUInteger rightPadding = 0; + NSUInteger leftPadding = 0; + + if (maxChildLength < [parent length]) { + NSUInteger difference = [parent length] + (2 * kDebugBoxPadding) - maxChildLength; + leftPadding = ceil((CGFloat)difference/2.0); + rightPadding = difference/2; + } + + NSString *rightPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightPadding + kDebugBoxPadding]; + NSString *leftPaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftPadding + kDebugBoxPadding]; + + for (NSString *child in children) { + NSMutableArray *lines = [[child componentsSeparatedByString:@"\n"] mutableCopy]; + + NSUInteger leftLinePadding = ceil((CGFloat)(maxChildLength - [lines[0] length])/2.0); + NSUInteger rightLinePadding = (maxChildLength - [lines[0] length])/2.0; + + for (NSString *line in lines) { + NSString *rightLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:rightLinePadding]; + rightLinePaddingString = [NSString stringWithFormat:@"%@%@|", rightLinePaddingString, rightPaddingString]; + + NSString *leftLinePaddingString = [NSString debugbox_stringWithString:@" " repeatedCount:leftLinePadding]; + leftLinePaddingString = [NSString stringWithFormat:@"|%@%@", leftLinePaddingString, leftPaddingString]; + + NSString *paddingLine = [NSString stringWithFormat:@"%@%@%@", leftLinePaddingString, line, rightLinePaddingString]; + [childrenLines addObject:paddingLine]; + } + } + + childrenLines = [self appendTopAndBottomToBoxString:childrenLines parent:parent]; + return [childrenLines componentsJoinedByString:@"\n"]; +} + ++ (NSMutableArray *)appendTopAndBottomToBoxString:(NSMutableArray *)boxStrings parent:(NSString *)parent +{ + NSUInteger totalLineLength = [boxStrings[0] length]; + [boxStrings addObject:[NSString debugbox_stringWithString:@"-" repeatedCount:totalLineLength]]; + + NSUInteger leftPadding = ceil(((CGFloat)(totalLineLength - [parent length]))/2.0); + NSUInteger rightPadding = (totalLineLength - [parent length])/2; + + NSString *topLine = [parent debugbox_stringByAddingPadding:@"-" count:leftPadding location:PIDebugBoxPaddingLocationFront]; + topLine = [topLine debugbox_stringByAddingPadding:@"-" count:rightPadding location:PIDebugBoxPaddingLocationEnd]; + [boxStrings insertObject:topLine atIndex:0]; + + return boxStrings; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASAssert.mm b/submodules/AsyncDisplayKit/Source/ASAssert.mm new file mode 100644 index 0000000000..6a78b06eaf --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASAssert.mm @@ -0,0 +1,58 @@ +// +// ASAssert.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#if AS_TLS_AVAILABLE + +static _Thread_local int tls_mainThreadAssertionsDisabledCount; +BOOL ASMainThreadAssertionsAreDisabled() { + return tls_mainThreadAssertionsDisabledCount > 0; +} + +void ASPushMainThreadAssertionsDisabled() { + tls_mainThreadAssertionsDisabledCount += 1; +} + +void ASPopMainThreadAssertionsDisabled() { + tls_mainThreadAssertionsDisabledCount -= 1; + ASDisplayNodeCAssert(tls_mainThreadAssertionsDisabledCount >= 0, @"Attempt to pop thread assertion-disabling without corresponding push."); +} + +#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 (nullptr != pthread_getspecific(ASMainThreadAssertionsDisabledKey())); +} + +void ASPushMainThreadAssertionsDisabled() { + const auto key = ASMainThreadAssertionsDisabledKey(); + const auto oldVal = (intptr_t)pthread_getspecific(key); + pthread_setspecific(key, (void *)(oldVal + 1)); +} + +void ASPopMainThreadAssertionsDisabled() { + const auto key = ASMainThreadAssertionsDisabledKey(); + const auto oldVal = (intptr_t)pthread_getspecific(key); + pthread_setspecific(key, (void *)(oldVal - 1)); + ASDisplayNodeCAssert(oldVal > 0, @"Attempt to pop thread assertion-disabling without corresponding push."); +} + +#endif // AS_TLS_AVAILABLE diff --git a/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.h b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.h new file mode 100644 index 0000000000..a77452622b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.h @@ -0,0 +1,28 @@ +// +// ASCGImageBuffer.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +AS_SUBCLASSING_RESTRICTED +@interface ASCGImageBuffer : NSObject + +/// Init a zero-filled buffer with the given length. +- (instancetype)initWithLength:(NSUInteger)length; + +@property (readonly) void *mutableBytes NS_RETURNS_INNER_POINTER; + +/// Don't do any drawing or call any methods after calling this. +- (CGDataProviderRef)createDataProviderAndInvalidate CF_RETURNS_RETAINED; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm new file mode 100644 index 0000000000..6f05300e23 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCGImageBuffer.mm @@ -0,0 +1,88 @@ +// +// ASCGImageBuffer.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASCGImageBuffer.h" + +#import +#import +#import +#import + +/** + * The behavior of this class is modeled on the private function + * _CGDataProviderCreateWithCopyOfData, which is the function used + * by CGBitmapContextCreateImage. + * + * If the buffer is larger than a page, we use mmap and mark it as + * read-only when they are finished drawing. Then we wrap the VM + * in an NSData + */ +@implementation ASCGImageBuffer { + BOOL _createdData; + BOOL _isVM; + NSUInteger _length; +} + +- (instancetype)initWithLength:(NSUInteger)length +{ + if (self = [super init]) { + _length = length; + _isVM = (length >= vm_page_size); + if (_isVM) { + _mutableBytes = mmap(NULL, length, PROT_WRITE | PROT_READ, MAP_ANONYMOUS | MAP_PRIVATE, VM_MAKE_TAG(VM_MEMORY_COREGRAPHICS_DATA), 0); + if (_mutableBytes == MAP_FAILED) { + NSAssert(NO, @"Failed to map for CG image data."); + _isVM = NO; + } + } + + // Check the VM flag again because we may have failed above. + if (!_isVM) { + _mutableBytes = calloc(1, length); + } + } + return self; +} + +- (void)dealloc +{ + if (!_createdData) { + [ASCGImageBuffer deallocateBuffer:_mutableBytes length:_length isVM:_isVM]; + } +} + +- (CGDataProviderRef)createDataProviderAndInvalidate +{ + NSAssert(!_createdData, @"Should not create data provider from buffer multiple times."); + _createdData = YES; + + // Mark the pages as read-only. + if (_isVM) { + __unused kern_return_t result = vm_protect(mach_task_self(), (vm_address_t)_mutableBytes, _length, true, VM_PROT_READ); + NSAssert(result == noErr, @"Error marking buffer as read-only: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]); + } + + // Wrap in an NSData + BOOL isVM = _isVM; + NSData *d = [[NSData alloc] initWithBytesNoCopy:_mutableBytes length:_length deallocator:^(void * _Nonnull bytes, NSUInteger length) { + [ASCGImageBuffer deallocateBuffer:bytes length:length isVM:isVM]; + }]; + return CGDataProviderCreateWithCFData((__bridge CFDataRef)d); +} + ++ (void)deallocateBuffer:(void *)buf length:(NSUInteger)length isVM:(BOOL)isVM +{ + if (isVM) { + __unused kern_return_t result = vm_deallocate(mach_task_self(), (vm_address_t)buf, length); + NSAssert(result == noErr, @"Failed to unmap cg image buffer: %@", [NSError errorWithDomain:NSMachErrorDomain code:result userInfo:nil]); + } else { + free(buf); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASCollections.mm b/submodules/AsyncDisplayKit/Source/ASCollections.mm new file mode 100644 index 0000000000..592dee2e88 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASCollections.mm @@ -0,0 +1,61 @@ +// +// ASCollections.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +/** + * A private allocator that signals to our retain callback to skip the retain. + * It behaves the same as the default allocator, but acts as a signal that we + * are creating a transfer array so we should skip the retain. + */ +static CFAllocatorRef gTransferAllocator; + +static const void *ASTransferRetain(CFAllocatorRef allocator, const void *val) { + if (allocator == gTransferAllocator) { + // Transfer allocator. Ignore retain and pass through. + return val; + } else { + // Other allocator. Retain like normal. + // This happens when they make a mutable copy. + return (&kCFTypeArrayCallBacks)->retain(allocator, val); + } +} + +@implementation NSArray (ASCollections) + ++ (NSArray *)arrayByTransferring:(__strong id *)pointers count:(NSUInteger)count NS_RETURNS_RETAINED +{ + // Custom callbacks that point to our ASTransferRetain callback. + static CFArrayCallBacks callbacks; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + callbacks = kCFTypeArrayCallBacks; + callbacks.retain = ASTransferRetain; + CFAllocatorContext ctx; + CFAllocatorGetContext(NULL, &ctx); + gTransferAllocator = CFAllocatorCreate(NULL, &ctx); + }); + + // NSZeroArray fast path. + if (count == 0) { + return @[]; // Does not actually call +array when optimized. + } + + // NSSingleObjectArray fast path. Retain/release here is worth it. + if (count == 1) { + NSArray *result = [[NSArray alloc] initWithObjects:pointers count:1]; + pointers[0] = nil; + return result; + } + + NSArray *result = (__bridge_transfer NSArray *)CFArrayCreate(gTransferAllocator, (const void **)(void *)pointers, count, &callbacks); + memset(pointers, 0, count * sizeof(id)); + return result; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASConfiguration.mm b/submodules/AsyncDisplayKit/Source/ASConfiguration.mm new file mode 100644 index 0000000000..34f11bd366 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASConfiguration.mm @@ -0,0 +1,64 @@ +// +// ASConfiguration.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +/// Not too performance-sensitive here. + +@implementation ASConfiguration + +- (instancetype)initWithDictionary:(NSDictionary *)dictionary +{ + if (self = [super init]) { + if (dictionary != nil) { + const auto featureStrings = ASDynamicCast(dictionary[@"experimental_features"], NSArray); + const auto version = ASDynamicCast(dictionary[@"version"], NSNumber).integerValue; + if (version != ASConfigurationSchemaCurrentVersion) { + NSLog(@"Texture warning: configuration schema is old version (%ld vs %ld)", (long)version, (long)ASConfigurationSchemaCurrentVersion); + } + self.experimentalFeatures = ASExperimentalFeaturesFromArray(featureStrings); + } else { + self.experimentalFeatures = kNilOptions; + } + } + return self; +} + +- (id)copyWithZone:(NSZone *)zone +{ + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = self.experimentalFeatures; + config.delegate = self.delegate; + return config; +} + +@end + +//#define AS_FIXED_CONFIG_JSON "{ \"version\" : 1, \"experimental_features\": [ \"exp_text_node\" ] }" + +#ifdef AS_FIXED_CONFIG_JSON + +@implementation ASConfiguration (UserProvided) + ++ (ASConfiguration *)textureConfiguration NS_RETURNS_RETAINED +{ + NSData *data = [@AS_FIXED_CONFIG_JSON dataUsingEncoding:NSUTF8StringEncoding]; + NSError *error; + NSDictionary *d = [NSJSONSerialization JSONObjectWithData:data options:kNilOptions error:&error]; + if (!d) { + NSAssert(NO, @"Error parsing fixed config string '%s': %@", AS_FIXED_CONFIG_JSON, error); + return nil; + } else { + return [[ASConfiguration alloc] initWithDictionary:d]; + } +} + +@end + +#endif // AS_FIXED_CONFIG_JSON diff --git a/submodules/AsyncDisplayKit/Source/ASConfigurationInternal.mm b/submodules/AsyncDisplayKit/Source/ASConfigurationInternal.mm new file mode 100644 index 0000000000..2fb190103a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASConfigurationInternal.mm @@ -0,0 +1,111 @@ +// +// ASConfigurationInternal.mm +// Texture +// +// 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 + +static ASConfigurationManager *ASSharedConfigurationManager; +static dispatch_once_t ASSharedConfigurationManagerOnceToken; + +NS_INLINE ASConfigurationManager *ASConfigurationManagerGet() { + dispatch_once(&ASSharedConfigurationManagerOnceToken, ^{ + ASSharedConfigurationManager = [[ASConfigurationManager alloc] init]; + }); + return ASSharedConfigurationManager; +} + +@implementation ASConfigurationManager { + ASConfiguration *_config; + dispatch_queue_t _delegateQueue; + BOOL _frameworkInitialized; + _Atomic(ASExperimentalFeatures) _activatedExperiments; +} + ++ (ASConfiguration *)defaultConfiguration NS_RETURNS_RETAINED +{ + ASConfiguration *config = [[ASConfiguration alloc] init]; + // TODO(wsdwsd0829): Fix #788 before enabling it. + // config.experimentalFeatures = ASExperimentalInterfaceStateCoalescing; + return config; +} + +- (instancetype)init +{ + if (self = [super init]) { + _delegateQueue = dispatch_queue_create("org.TextureGroup.Texture.ConfigNotifyQueue", DISPATCH_QUEUE_SERIAL); + if ([ASConfiguration respondsToSelector:@selector(textureConfiguration)]) { + _config = [[ASConfiguration textureConfiguration] copy]; + } else { + _config = [ASConfigurationManager defaultConfiguration]; + } + } + return self; +} + +- (void)frameworkDidInitialize +{ + ASDisplayNodeAssertMainThread(); + if (_frameworkInitialized) { + ASDisplayNodeFailAssert(@"Framework initialized twice."); + return; + } + _frameworkInitialized = YES; + + const auto delegate = _config.delegate; + if ([delegate respondsToSelector:@selector(textureDidInitialize)]) { + [delegate textureDidInitialize]; + } +} + +- (BOOL)activateExperimentalFeature:(ASExperimentalFeatures)requested +{ + if (_config == nil) { + return NO; + } + + NSAssert(__builtin_popcountl(requested) == 1, @"Cannot activate multiple features at once with this method."); + + // We need to call out, whether it's enabled or not. + // A/B testing requires even "control" users to be activated. + ASExperimentalFeatures enabled = requested & _config.experimentalFeatures; + ASExperimentalFeatures prevTriggered = atomic_fetch_or(&_activatedExperiments, requested); + ASExperimentalFeatures newlyTriggered = requested & ~prevTriggered; + + // Notify delegate if needed. + if (newlyTriggered != 0) { + __unsafe_unretained id del = _config.delegate; + dispatch_async(_delegateQueue, ^{ + [del textureDidActivateExperimentalFeatures:newlyTriggered]; + }); + } + + return (enabled != 0); +} + +// Define this even when !DEBUG, since we may run our tests in release mode. ++ (void)test_resetWithConfiguration:(ASConfiguration *)configuration +{ + ASConfigurationManager *inst = ASConfigurationManagerGet(); + inst->_config = configuration ?: [self defaultConfiguration]; + atomic_store(&inst->_activatedExperiments, 0); +} + +@end + +BOOL _ASActivateExperimentalFeature(ASExperimentalFeatures feature) +{ + return [ASConfigurationManagerGet() activateExperimentalFeature:feature]; +} + +void ASNotifyInitialized() +{ + [ASConfigurationManagerGet() frameworkDidInitialize]; +} diff --git a/submodules/AsyncDisplayKit/Source/ASControlNode+Private.h b/submodules/AsyncDisplayKit/Source/ASControlNode+Private.h new file mode 100644 index 0000000000..02f54a20ec --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASControlNode+Private.h @@ -0,0 +1,18 @@ +// +// ASControlNode+Private.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 + +@interface ASControlNode (Private) + +#if TARGET_OS_TV +- (void)_pressDown; +#endif + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASControlNode.mm b/submodules/AsyncDisplayKit/Source/ASControlNode.mm new file mode 100644 index 0000000000..dcdb9ec829 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASControlNode.mm @@ -0,0 +1,499 @@ +// +// ASControlNode.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 "ASControlNode+Private.h" +#import +#import +#import +#import +#import +#import + +// UIControl allows dragging some distance outside of the control itself during +// tracking. This value depends on the device idiom (25 or 70 points), so +// so replicate that effect with the same values here for our own controls. +#define kASControlNodeExpandedInset (([[UIDevice currentDevice] userInterfaceIdiom] == UIUserInterfaceIdiomPad) ? -25.0f : -70.0f) + +// Initial capacities for dispatch tables. +#define kASControlNodeEventDispatchTableInitialCapacity 4 +#define kASControlNodeActionDispatchTableInitialCapacity 4 + +@interface ASControlNode () +{ +@private + // Control Attributes + BOOL _enabled; + BOOL _highlighted; + + // Tracking + BOOL _tracking; + BOOL _touchInside; + + // Target action pairs stored in an array for each event type + // ASControlEvent -> [ASTargetAction0, ASTargetAction1] + NSMutableDictionary, NSMutableArray *> *_controlEventDispatchTable; +} + +// Read-write overrides. +@property (getter=isTracking) BOOL tracking; +@property (getter=isTouchInside) BOOL touchInside; + +/** + @abstract Returns a key to be used in _controlEventDispatchTable that identifies the control event. + @param controlEvent A control event. + @result A key for use in _controlEventDispatchTable. + */ +id _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent); + +/** + @abstract Enumerates the ASControlNode events included mask, invoking the block for each event. + @param mask An ASControlNodeEvent mask. + @param block The block to be invoked for each ASControlNodeEvent included in mask. + */ +void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent)); + +/** + @abstract Returns the expanded bounds used to determine if a touch is considered 'inside' during tracking. + @param controlNode A control node. + @result The expanded bounds of the node. + */ +CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode); + + +@end + +@implementation ASControlNode +{ +} + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + _enabled = YES; + + // As we have no targets yet, we start off with user interaction off. When a target is added, it'll get turned back on. + self.userInteractionEnabled = NO; + + return self; +} + +#if TARGET_OS_TV +- (void)didLoad +{ + [super didLoad]; + + // On tvOS all controls, such as buttons, interact with the focus system even if they don't have a target set on them. + // Here we add our own internal tap gesture to handle this behaviour. + self.userInteractionEnabled = YES; + UITapGestureRecognizer *tapGestureRec = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_pressDown)]; + tapGestureRec.allowedPressTypes = @[@(UIPressTypeSelect)]; + [self.view addGestureRecognizer:tapGestureRec]; +} +#endif + +- (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled +{ + [super setUserInteractionEnabled:userInteractionEnabled]; + self.isAccessibilityElement = userInteractionEnabled; +} + +- (void)__exitHierarchy +{ + [super __exitHierarchy]; + + // If a control node is exit the hierarchy and is tracking we have to cancel it + if (self.tracking) { + [self _cancelTrackingWithEvent:nil]; + } +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wobjc-missing-super-calls" + +#pragma mark - ASDisplayNode Overrides + +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + // If we're not interested in touches, we have nothing to do. + if (!self.enabled) { + return; + } + + // Check if the tracking should start + UITouch *theTouch = [touches anyObject]; + if (![self beginTrackingWithTouch:theTouch withEvent:event]) { + return; + } + + // If we get more than one touch down on us, cancel. + // Additionally, if we're already tracking a touch, a second touch beginning is cause for cancellation. + if (touches.count > 1 || self.tracking) { + [self _cancelTrackingWithEvent:event]; + } else { + // Otherwise, begin tracking. + self.tracking = YES; + + // No need to check bounds on touchesBegan as we wouldn't get the call if it wasn't in our bounds. + self.touchInside = YES; + self.highlighted = YES; + + // Send the appropriate touch-down control event depending on how many times we've been tapped. + ASControlNodeEvent controlEventMask = (theTouch.tapCount == 1) ? ASControlNodeEventTouchDown : ASControlNodeEventTouchDownRepeat; + [self sendActionsForControlEvents:controlEventMask withEvent:event]; + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + // If we're not interested in touches, we have nothing to do. + if (!self.enabled) { + return; + } + + NSParameterAssert(touches.count == 1); + UITouch *theTouch = [touches anyObject]; + + // Check if tracking should continue + if (!self.tracking || ![self continueTrackingWithTouch:theTouch withEvent:event]) { + self.tracking = NO; + return; + } + + CGPoint touchLocation = [theTouch locationInView:self.view]; + + // Update our touchInside state. + BOOL dragIsInsideBounds = [self pointInside:touchLocation withEvent:nil]; + + // Update our highlighted state. + CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self); + BOOL dragIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation); + self.touchInside = dragIsInsideExpandedBounds; + self.highlighted = dragIsInsideExpandedBounds; + + [self sendActionsForControlEvents:(dragIsInsideBounds ? ASControlNodeEventTouchDragInside : ASControlNodeEventTouchDragOutside) + withEvent:event]; +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + // If we're not interested in touches, we have nothing to do. + if (!self.enabled) { + return; + } + + // Note that we've cancelled tracking. + [self _cancelTrackingWithEvent:event]; +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + // If we're not interested in touches, we have nothing to do. + if (!self.enabled) { + return; + } + + // On iPhone 6s, iOS 9.2 (and maybe other versions) sometimes calls -touchesEnded:withEvent: + // twice on the view for one call to -touchesBegan:withEvent:. On ASControlNode, it used to + // trigger an action twice unintentionally. Now, we ignore that event if we're not in a tracking + // state in order to have a correct behavior. + // It might be related to that issue: http://www.openradar.me/22910171 + if (!self.tracking) { + return; + } + + NSParameterAssert([touches count] == 1); + UITouch *theTouch = [touches anyObject]; + CGPoint touchLocation = [theTouch locationInView:self.view]; + + // Update state. + self.tracking = NO; + self.touchInside = NO; + self.highlighted = NO; + + // Note that we've ended tracking. + [self endTrackingWithTouch:theTouch withEvent:event]; + + // Send the appropriate touch-up control event. + CGRect expandedBounds = _ASControlNodeGetExpandedBounds(self); + BOOL touchUpIsInsideExpandedBounds = CGRectContainsPoint(expandedBounds, touchLocation); + + [self sendActionsForControlEvents:(touchUpIsInsideExpandedBounds ? ASControlNodeEventTouchUpInside : ASControlNodeEventTouchUpOutside) + withEvent:event]; +} + +- (void)_cancelTrackingWithEvent:(UIEvent *)event +{ + // We're no longer tracking and there is no touch to be inside. + self.tracking = NO; + self.touchInside = NO; + self.highlighted = NO; + + // Send the cancel event. + [self sendActionsForControlEvents:ASControlNodeEventTouchCancel withEvent:event]; +} + +#pragma clang diagnostic pop + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); + + // If not enabled we should not care about receving touches + if (! self.enabled) { + return nil; + } + + return [super hitTest:point withEvent:event]; +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + // If we're interested in touches, this is a tap (the only gesture we care about) and passed -hitTest for us, then no, you may not begin. Sir. + if (self.enabled && [gestureRecognizer isKindOfClass:[UITapGestureRecognizer class]] && gestureRecognizer.view != self.view) { + UITapGestureRecognizer *tapRecognizer = (UITapGestureRecognizer *)gestureRecognizer; + // Allow double-tap gestures + return tapRecognizer.numberOfTapsRequired != 1; + } + + // Otherwise, go ahead. :] + return YES; +} + +- (BOOL)supportsLayerBacking +{ + return super.supportsLayerBacking && !self.userInteractionEnabled; +} + +#pragma mark - Action Messages + +- (void)addTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask +{ + NSParameterAssert(action); + NSParameterAssert(controlEventMask != 0); + + // ASControlNode cannot be layer backed if adding a target + ASDisplayNodeAssert(!self.isLayerBacked, @"ASControlNode is layer backed, will never be able to call target in target:action: pair."); + + ASLockScopeSelf(); + + if (!_controlEventDispatchTable) { + _controlEventDispatchTable = [[NSMutableDictionary alloc] initWithCapacity:kASControlNodeEventDispatchTableInitialCapacity]; // enough to handle common types without re-hashing the dictionary when adding entries. + } + + // Create new target action pair + ASControlTargetAction *targetAction = [[ASControlTargetAction alloc] init]; + targetAction.action = action; + targetAction.target = target; + + // Enumerate the events in the mask, adding the target-action pair for each control event included in controlEventMask + _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^ + (ASControlNodeEvent controlEvent) + { + // Do we already have an event table for this control event? + id eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent); + NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey]; + + if (!eventTargetActionArray) { + eventTargetActionArray = [[NSMutableArray alloc] init]; + } + + // Remove any prior target-action pair for this event, as UIKit does. + [eventTargetActionArray removeObject:targetAction]; + + // Register the new target-action as the last one to be sent. + [eventTargetActionArray addObject:targetAction]; + + if (eventKey) { + [_controlEventDispatchTable setObject:eventTargetActionArray forKey:eventKey]; + } + }); + + self.userInteractionEnabled = YES; +} + +- (NSArray *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent +{ + NSParameterAssert(target); + NSParameterAssert(controlEvent != 0 && controlEvent != ASControlNodeEventAllEvents); + + ASLockScopeSelf(); + + // Grab the event target action array for this event. + NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]; + if (!eventTargetActionArray) { + return nil; + } + + NSMutableArray *actions = [[NSMutableArray alloc] init]; + + // Collect all actions for this target. + for (ASControlTargetAction *targetAction in eventTargetActionArray) { + if ((target == nil && targetAction.createdWithNoTarget) || (target != nil && target == targetAction.target)) { + [actions addObject:NSStringFromSelector(targetAction.action)]; + } + } + + return actions; +} + +- (NSSet *)allTargets +{ + ASLockScopeSelf(); + + NSMutableSet *targets = [[NSMutableSet alloc] init]; + + // Look at each event... + for (NSMutableArray *eventTargetActionArray in [_controlEventDispatchTable objectEnumerator]) { + // and each event's targets... + for (ASControlTargetAction *targetAction in eventTargetActionArray) { + [targets addObject:targetAction.target]; + } + } + + return targets; +} + +- (void)removeTarget:(id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEventMask +{ + NSParameterAssert(controlEventMask != 0); + + ASLockScopeSelf(); + + // Enumerate the events in the mask, removing the target-action pair for each control event included in controlEventMask. + _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEventMask, ^ + (ASControlNodeEvent controlEvent) + { + // Grab the dispatch table for this event (if we have it). + id eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent); + NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey]; + if (!eventTargetActionArray) { + return; + } + + NSPredicate *filterPredicate = [NSPredicate predicateWithBlock:^BOOL(ASControlTargetAction *_Nullable evaluatedObject, NSDictionary * _Nullable bindings) { + if (!target || evaluatedObject.target == target) { + if (!action) { + return NO; + } else if (evaluatedObject.action == action) { + return NO; + } + } + + return YES; + }]; + [eventTargetActionArray filterUsingPredicate:filterPredicate]; + + if (eventTargetActionArray.count == 0) { + // If there are no targets for this event anymore, remove it. + [_controlEventDispatchTable removeObjectForKey:eventKey]; + } + }); +} + +#pragma mark - + +- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(UIEvent *)event +{ + ASDisplayNodeAssertMainThread(); //We access self.view below, it's not safe to call this off of main. + NSParameterAssert(controlEvents != 0); + + NSMutableArray *resolvedEventTargetActionArray = [[NSMutableArray alloc] init]; + + { + ASLockScopeSelf(); + + // Enumerate the events in the mask, invoking the target-action pairs for each. + _ASEnumerateControlEventsIncludedInMaskWithBlock(controlEvents, ^ + (ASControlNodeEvent controlEvent) + { + // Iterate on each target action pair + for (ASControlTargetAction *targetAction in _controlEventDispatchTable[_ASControlNodeEventKeyForControlEvent(controlEvent)]) { + ASControlTargetAction *resolvedTargetAction = [[ASControlTargetAction alloc] init]; + resolvedTargetAction.action = targetAction.action; + resolvedTargetAction.target = targetAction.target; + + // NSNull means that a nil target was set, so start at self and travel the responder chain + if (!resolvedTargetAction.target && targetAction.createdWithNoTarget) { + // if the target cannot perform the action, travel the responder chain to try to find something that does + resolvedTargetAction.target = [self.view targetForAction:resolvedTargetAction.action withSender:self]; + } + + if (resolvedTargetAction.target) { + [resolvedEventTargetActionArray addObject:resolvedTargetAction]; + } + } + }); + } + + //We don't want to hold the lock while calling out, we could potentially walk up the ownership tree causing a deadlock. +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + for (ASControlTargetAction *targetAction in resolvedEventTargetActionArray) { + [targetAction.target performSelector:targetAction.action withObject:self withObject:event]; + } +#pragma clang diagnostic pop +} + +#pragma mark - Convenience + +id _ASControlNodeEventKeyForControlEvent(ASControlNodeEvent controlEvent) +{ + return @(controlEvent); +} + +void _ASEnumerateControlEventsIncludedInMaskWithBlock(ASControlNodeEvent mask, void (^block)(ASControlNodeEvent anEvent)) +{ + if (block == nil) { + return; + } + // Start with our first event (touch down) and work our way up to the last event (PrimaryActionTriggered) + for (ASControlNodeEvent thisEvent = ASControlNodeEventTouchDown; thisEvent <= ASControlNodeEventPrimaryActionTriggered; thisEvent <<= 1) { + // If it's included in the mask, invoke the block. + if ((mask & thisEvent) == thisEvent) + block(thisEvent); + } +} + +CGRect _ASControlNodeGetExpandedBounds(ASControlNode *controlNode) { + return CGRectInset(UIEdgeInsetsInsetRect(controlNode.view.bounds, controlNode.hitTestSlop), kASControlNodeExpandedInset, kASControlNodeExpandedInset); +} + +#pragma mark - For Subclasses + +- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent +{ + return YES; +} + +- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent +{ + return YES; +} + +- (void)cancelTrackingWithEvent:(UIEvent *)touchEvent +{ + // Subclass hook +} + +- (void)endTrackingWithTouch:(UITouch *)touch withEvent:(UIEvent *)touchEvent +{ + // Subclass hook +} + +#pragma mark - Debug +- (ASDisplayNode *)debugHighlightOverlay +{ + return nil; +} +@end diff --git a/submodules/AsyncDisplayKit/Source/ASControlTargetAction.mm b/submodules/AsyncDisplayKit/Source/ASControlTargetAction.mm new file mode 100644 index 0000000000..41cc113314 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASControlTargetAction.mm @@ -0,0 +1,65 @@ +// +// ASControlTargetAction.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 + +@implementation ASControlTargetAction +{ + __weak id _target; + BOOL _createdWithNoTarget; +} + +- (void)setTarget:(id)target { + _target = target; + + if (!target) { + _createdWithNoTarget = YES; + } +} + +- (id)target { + return _target; +} + +- (BOOL)isEqual:(id)object { + if (![object isKindOfClass:[ASControlTargetAction class]]) { + return NO; + } + + ASControlTargetAction *otherObject = (ASControlTargetAction *)object; + + BOOL areTargetsEqual; + + if (self.target != nil && otherObject.target != nil && self.target == otherObject.target) { + areTargetsEqual = YES; + } + else if (self.target == nil && otherObject.target == nil && self.createdWithNoTarget && otherObject.createdWithNoTarget) { + areTargetsEqual = YES; + } + else { + areTargetsEqual = NO; + } + + if (!areTargetsEqual) { + return NO; + } + + if (self.action && otherObject.action && self.action == otherObject.action) { + return YES; + } + else { + return NO; + } +} + +- (NSUInteger)hash { + return [self.target hash]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDimension.mm b/submodules/AsyncDisplayKit/Source/ASDimension.mm new file mode 100644 index 0000000000..d1a42462df --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDimension.mm @@ -0,0 +1,125 @@ +// +// ASDimension.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 + +#pragma mark - ASDimension + +ASDimension const ASDimensionAuto = {ASDimensionUnitAuto, 0}; + +ASOVERLOADABLE ASDimension ASDimensionMake(NSString *dimension) +{ + if (dimension.length > 0) { + + // Handle points + if ([dimension hasSuffix:@"pt"]) { + return ASDimensionMake(ASDimensionUnitPoints, ASCGFloatFromString(dimension)); + } + + // Handle auto + if ([dimension isEqualToString:@"auto"]) { + return ASDimensionAuto; + } + + // Handle percent + if ([dimension hasSuffix:@"%"]) { + return ASDimensionMake(ASDimensionUnitFraction, (ASCGFloatFromString(dimension) / 100.0)); + } + } + + return ASDimensionAuto; +} + +NSString *NSStringFromASDimension(ASDimension dimension) +{ + switch (dimension.unit) { + case ASDimensionUnitPoints: + return [NSString stringWithFormat:@"%.0fpt", dimension.value]; + case ASDimensionUnitFraction: + return [NSString stringWithFormat:@"%.0f%%", dimension.value * 100.0]; + case ASDimensionUnitAuto: + return @"Auto"; + } +} + +#pragma mark - ASLayoutSize + +ASLayoutSize const ASLayoutSizeAuto = {ASDimensionAuto, ASDimensionAuto}; + +#pragma mark - ASSizeRange + +ASSizeRange const ASSizeRangeZero = {}; + +ASSizeRange const ASSizeRangeUnconstrained = { {0, 0}, { INFINITY, INFINITY }}; + +struct _Range { + CGFloat min; + CGFloat max; + + /** + Intersects another dimension range. If the other range does not overlap, this size range "wins" by returning a + single point within its own range that is closest to the non-overlapping range. + */ + _Range intersect(const _Range &other) const + { + CGFloat newMin = MAX(min, other.min); + CGFloat newMax = MIN(max, other.max); + if (newMin <= newMax) { + return {newMin, newMax}; + } else { + // No intersection. If we're before the other range, return our max; otherwise our min. + if (min < other.min) { + return {max, max}; + } else { + return {min, min}; + } + } + } +}; + +ASSizeRange ASSizeRangeIntersect(ASSizeRange sizeRange, ASSizeRange otherSizeRange) +{ + const auto w = _Range({sizeRange.min.width, sizeRange.max.width}).intersect({otherSizeRange.min.width, otherSizeRange.max.width}); + const auto h = _Range({sizeRange.min.height, sizeRange.max.height}).intersect({otherSizeRange.min.height, otherSizeRange.max.height}); + return {{w.min, h.min}, {w.max, h.max}}; +} + +NSString *NSStringFromASSizeRange(ASSizeRange sizeRange) +{ + // 17 field length copied from iOS 10.3 impl of NSStringFromCGSize. + if (CGSizeEqualToSize(sizeRange.min, sizeRange.max)) { + return [NSString stringWithFormat:@"{{%.*g, %.*g}}", + 17, sizeRange.min.width, + 17, sizeRange.min.height]; + } + return [NSString stringWithFormat:@"{{%.*g, %.*g}, {%.*g, %.*g}}", + 17, sizeRange.min.width, + 17, sizeRange.min.height, + 17, sizeRange.max.width, + 17, sizeRange.max.height]; +} + +#if YOGA +#pragma mark - Yoga - ASEdgeInsets +ASEdgeInsets const ASEdgeInsetsZero = {}; + +ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets) +{ + ASEdgeInsets asEdgeInsets = ASEdgeInsetsZero; + asEdgeInsets.top = ASDimensionMake(edgeInsets.top); + asEdgeInsets.left = ASDimensionMake(edgeInsets.left); + asEdgeInsets.bottom = ASDimensionMake(edgeInsets.bottom); + asEdgeInsets.right = ASDimensionMake(edgeInsets.right); + return asEdgeInsets; +} +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASDimensionInternal.mm b/submodules/AsyncDisplayKit/Source/ASDimensionInternal.mm new file mode 100644 index 0000000000..8af3555252 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDimensionInternal.mm @@ -0,0 +1,65 @@ +// +// ASDimensionInternal.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 + +#pragma mark - ASLayoutElementSize + +NSString *NSStringFromASLayoutElementSize(ASLayoutElementSize size) +{ + return [NSString stringWithFormat: + @"", + NSStringFromASLayoutSize(ASLayoutSizeMake(size.width, size.height)), + NSStringFromASLayoutSize(ASLayoutSizeMake(size.minWidth, size.minHeight)), + NSStringFromASLayoutSize(ASLayoutSizeMake(size.maxWidth, size.maxHeight))]; +} + +ASDISPLAYNODE_INLINE void ASLayoutElementSizeConstrain(CGFloat minVal, CGFloat exactVal, CGFloat maxVal, CGFloat *outMin, CGFloat *outMax) +{ + NSCAssert(!isnan(minVal), @"minVal must not be NaN"); + NSCAssert(!isnan(maxVal), @"maxVal must not be NaN"); + // Avoid use of min/max primitives since they're harder to reason + // about in the presence of NaN (in exactVal) + // Follow CSS: min overrides max overrides exact. + + // Begin with the min/max range + *outMin = minVal; + *outMax = maxVal; + if (maxVal <= minVal) { + // min overrides max and exactVal is irrelevant + *outMax = minVal; + return; + } + if (isnan(exactVal)) { + // no exact value, so leave as a min/max range + return; + } + if (exactVal > maxVal) { + // clip to max value + *outMin = maxVal; + } else if (exactVal < minVal) { + // clip to min value + *outMax = minVal; + } else { + // use exact value + *outMin = *outMax = exactVal; + } +} + +ASSizeRange ASLayoutElementSizeResolveAutoSize(ASLayoutElementSize size, const CGSize parentSize, ASSizeRange autoASSizeRange) +{ + CGSize resolvedExact = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.width, size.height), parentSize, {NAN, NAN}); + CGSize resolvedMin = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.minWidth, size.minHeight), parentSize, autoASSizeRange.min); + CGSize resolvedMax = ASLayoutSizeResolveSize(ASLayoutSizeMake(size.maxWidth, size.maxHeight), parentSize, autoASSizeRange.max); + + CGSize rangeMin, rangeMax; + ASLayoutElementSizeConstrain(resolvedMin.width, resolvedExact.width, resolvedMax.width, &rangeMin.width, &rangeMax.width); + ASLayoutElementSizeConstrain(resolvedMin.height, resolvedExact.height, resolvedMax.height, &rangeMin.height, &rangeMax.height); + return {rangeMin, rangeMax}; +} diff --git a/submodules/AsyncDisplayKit/Source/ASDispatch.h b/submodules/AsyncDisplayKit/Source/ASDispatch.h new file mode 100644 index 0000000000..e20941806f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDispatch.h @@ -0,0 +1,27 @@ +// +// ASDispatch.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 + +/** + * Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +AS_EXTERN void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)); + +/** + * Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +AS_EXTERN void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)); diff --git a/submodules/AsyncDisplayKit/Source/ASDispatch.mm b/submodules/AsyncDisplayKit/Source/ASDispatch.mm new file mode 100644 index 0000000000..edc2feba46 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDispatch.mm @@ -0,0 +1,63 @@ +// +// ASDispatch.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import "ASDispatch.h" +#import + + +// Prefer C atomics in this file because ObjC blocks can't capture C++ atomics well. +#import + +/** + * Like dispatch_apply, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchApply(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { + if (threadCount == 0) { + if (ASActivateExperimentalFeature(ASExperimentalDispatchApply)) { + dispatch_apply(iterationCount, queue, work); + return; + } + threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; + } + dispatch_group_t group = dispatch_group_create(); + __block atomic_size_t counter = ATOMIC_VAR_INIT(0); + for (NSUInteger t = 0; t < threadCount; t++) { + dispatch_group_async(group, queue, ^{ + size_t i; + while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { + work(i); + } + }); + } + dispatch_group_wait(group, DISPATCH_TIME_FOREVER); +}; + +/** + * Like dispatch_async, but you can set the thread count. 0 means 2*active CPUs. + * + * Note: The actual number of threads may be lower than threadCount, if libdispatch + * decides the system can't handle it. In reality this rarely happens. + */ +void ASDispatchAsync(size_t iterationCount, dispatch_queue_t queue, NSUInteger threadCount, NS_NOESCAPE void(^work)(size_t i)) { + if (threadCount == 0) { + threadCount = NSProcessInfo.processInfo.activeProcessorCount * 2; + } + __block atomic_size_t counter = ATOMIC_VAR_INIT(0); + for (NSUInteger t = 0; t < threadCount; t++) { + dispatch_async(queue, ^{ + size_t i; + while ((i = atomic_fetch_add(&counter, 1)) < iterationCount) { + work(i); + } + }); + } +}; + diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Ancestry.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Ancestry.mm new file mode 100644 index 0000000000..ea1376ed54 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Ancestry.mm @@ -0,0 +1,90 @@ +// +// ASDisplayNode+Ancestry.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 + +AS_SUBCLASSING_RESTRICTED +@interface ASNodeAncestryEnumerator : NSEnumerator +@end + +@implementation ASNodeAncestryEnumerator { + ASDisplayNode *_lastNode; // This needs to be strong because enumeration will not retain the current batch of objects + BOOL _initialState; +} + +- (instancetype)initWithNode:(ASDisplayNode *)node +{ + if (self = [super init]) { + _initialState = YES; + _lastNode = node; + } + return self; +} + +- (id)nextObject +{ + if (_initialState) { + _initialState = NO; + return _lastNode; + } + + ASDisplayNode *nextNode = _lastNode.supernode; + if (nextNode == nil && ASDisplayNodeThreadIsMain()) { + CALayer *layer = _lastNode.nodeLoaded ? _lastNode.layer.superlayer : nil; + while (layer != nil) { + nextNode = ASLayerToDisplayNode(layer); + if (nextNode != nil) { + break; + } + layer = layer.superlayer; + } + } + _lastNode = nextNode; + return nextNode; +} + +@end + +@implementation ASDisplayNode (Ancestry) + +- (id)supernodes +{ + NSEnumerator *result = [[ASNodeAncestryEnumerator alloc] initWithNode:self]; + [result nextObject]; // discard first object (self) + return result; +} + +- (id)supernodesIncludingSelf +{ + return [[ASNodeAncestryEnumerator alloc] initWithNode:self]; +} + +- (nullable __kindof ASDisplayNode *)supernodeOfClass:(Class)supernodeClass includingSelf:(BOOL)includeSelf +{ + id chain = includeSelf ? self.supernodesIncludingSelf : self.supernodes; + for (ASDisplayNode *ancestor in chain) { + if ([ancestor isKindOfClass:supernodeClass]) { + return ancestor; + } + } + return nil; +} + +- (NSString *)ancestryDescription +{ + NSMutableArray *strings = [NSMutableArray array]; + for (ASDisplayNode *node in self.supernodes) { + [strings addObject:ASObjectDescriptionMakeTiny(node)]; + } + return strings.description; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+AsyncDisplay.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+AsyncDisplay.mm new file mode 100644 index 0000000000..068f5509a7 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+AsyncDisplay.mm @@ -0,0 +1,493 @@ +// +// ASDisplayNode+AsyncDisplay.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 "ASDisplayNodeInternal.h" +#import +#import +#import +#import "ASSignpost.h" +#import + +using AS::MutexLocker; + +@interface ASDisplayNode () <_ASDisplayLayerDelegate> +@end + +@implementation ASDisplayNode (AsyncDisplay) + +#if ASDISPLAYNODE_DELAY_DISPLAY + #define ASDN_DELAY_FOR_DISPLAY() usleep( (long)(0.1 * USEC_PER_SEC) ) +#else + #define ASDN_DELAY_FOR_DISPLAY() +#endif + +#define CHECK_CANCELLED_AND_RETURN_NIL(expr) if (isCancelledBlock()) { \ + expr; \ + return nil; \ + } \ + +- (NSObject *)drawParameters +{ + __instanceLock__.lock(); + BOOL implementsDrawParameters = _flags.implementsDrawParameters; + __instanceLock__.unlock(); + + if (implementsDrawParameters) { + return [self drawParametersForAsyncLayer:self.asyncLayer]; + } else { + return nil; + } +} + +- (void)_recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock displayBlocks:(NSMutableArray *)displayBlocks +{ + // Skip subtrees that are hidden or zero alpha. + if (self.isHidden || self.alpha <= 0.0) { + return; + } + + __instanceLock__.lock(); + BOOL rasterizingFromAscendent = (_hierarchyState & ASHierarchyStateRasterized); + __instanceLock__.unlock(); + + // if super node is rasterizing descendants, subnodes will not have had layout calls because they don't have layers + if (rasterizingFromAscendent) { + [self __layout]; + } + + // Capture these outside the display block so they are retained. + UIColor *backgroundColor = self.backgroundColor; + CGRect bounds = self.bounds; + CGFloat cornerRadius = self.cornerRadius; + BOOL clipsToBounds = self.clipsToBounds; + + CGRect frame; + + // If this is the root container node, use a frame with a zero origin to draw into. If not, calculate the correct frame using the node's position, transform and anchorPoint. + if (self.rasterizesSubtree) { + frame = CGRectMake(0.0f, 0.0f, bounds.size.width, bounds.size.height); + } else { + CGPoint position = self.position; + CGPoint anchorPoint = self.anchorPoint; + + // Pretty hacky since full 3D transforms aren't actually supported, but attempt to compute the transformed frame of this node so that we can composite it into approximately the right spot. + CGAffineTransform transform = CATransform3DGetAffineTransform(self.transform); + CGSize scaledBoundsSize = CGSizeApplyAffineTransform(bounds.size, transform); + CGPoint origin = CGPointMake(position.x - scaledBoundsSize.width * anchorPoint.x, + position.y - scaledBoundsSize.height * anchorPoint.y); + frame = CGRectMake(origin.x, origin.y, bounds.size.width, bounds.size.height); + } + + // Get the display block for this node. + asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:NO isCancelledBlock:isCancelledBlock rasterizing:YES]; + + // We'll display something if there is a display block, clipping, translation and/or a background color. + BOOL shouldDisplay = displayBlock || backgroundColor || CGPointEqualToPoint(CGPointZero, frame.origin) == NO || clipsToBounds; + + // If we should display, then push a transform, draw the background color, and draw the contents. + // The transform is popped in a block added after the recursion into subnodes. + if (shouldDisplay) { + dispatch_block_t pushAndDisplayBlock = ^{ + // Push transform relative to parent. + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextSaveGState(context); + + CGContextTranslateCTM(context, frame.origin.x, frame.origin.y); + + //support cornerRadius + if (rasterizingFromAscendent && clipsToBounds) { + if (cornerRadius) { + [[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius] addClip]; + } else { + CGContextClipToRect(context, bounds); + } + } + + // Fill background if any. + CGColorRef backgroundCGColor = backgroundColor.CGColor; + if (backgroundColor && CGColorGetAlpha(backgroundCGColor) > 0.0) { + CGContextSetFillColorWithColor(context, backgroundCGColor); + CGContextFillRect(context, bounds); + } + + // If there is a display block, call it to get the image, then copy the image into the current context (which is the rasterized container's backing store). + if (displayBlock) { + UIImage *image = (UIImage *)displayBlock(); + if (image) { + BOOL opaque = ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage)); + CGBlendMode blendMode = opaque ? kCGBlendModeCopy : kCGBlendModeNormal; + [image drawInRect:bounds blendMode:blendMode alpha:1]; + } + } + }; + [displayBlocks addObject:pushAndDisplayBlock]; + } + + // Recursively capture displayBlocks for all descendants. + for (ASDisplayNode *subnode in self.subnodes) { + [subnode _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks]; + } + + // If we pushed a transform, pop it by adding a display block that does nothing other than that. + if (shouldDisplay) { + // Since this block is pure, we can store it statically. + static dispatch_block_t popBlock; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + popBlock = ^{ + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextRestoreGState(context); + }; + }); + [displayBlocks addObject:popBlock]; + } +} + +- (asyncdisplaykit_async_transaction_operation_block_t)_displayBlockWithAsynchronous:(BOOL)asynchronous + isCancelledBlock:(asdisplaynode_iscancelled_block_t)isCancelledBlock + rasterizing:(BOOL)rasterizing +{ + ASDisplayNodeAssertMainThread(); + + asyncdisplaykit_async_transaction_operation_block_t displayBlock = nil; + ASDisplayNodeFlags flags; + + __instanceLock__.lock(); + + flags = _flags; + + // We always create a graphics context, unless a -display method is used, OR if we are a subnode drawing into a rasterized parent. + BOOL shouldCreateGraphicsContext = (flags.implementsImageDisplay == NO && rasterizing == NO); + BOOL shouldBeginRasterizing = (rasterizing == NO && flags.rasterizesSubtree); + BOOL usesImageDisplay = flags.implementsImageDisplay; + BOOL usesDrawRect = flags.implementsDrawRect; + + if (usesImageDisplay == NO && usesDrawRect == NO && shouldBeginRasterizing == NO) { + // Early exit before requesting more expensive properties like bounds and opaque from the layer. + __instanceLock__.unlock(); + return nil; + } + + BOOL opaque = self.opaque; + CGRect bounds = self.bounds; + UIColor *backgroundColor = self.backgroundColor; + CGColorRef borderColor = self.borderColor; + CGFloat borderWidth = self.borderWidth; + CGFloat contentsScaleForDisplay = _contentsScaleForDisplay; + + __instanceLock__.unlock(); + + // Capture drawParameters from delegate on main thread, if this node is displaying itself rather than recursively rasterizing. + id drawParameters = (shouldBeginRasterizing == NO ? [self drawParameters] : nil); + + // Only the -display methods should be called if we can't size the graphics buffer to use. + if (CGRectIsEmpty(bounds) && (shouldBeginRasterizing || shouldCreateGraphicsContext)) { + return nil; + } + + ASDisplayNodeAssert(contentsScaleForDisplay != 0.0, @"Invalid contents scale"); + ASDisplayNodeAssert(rasterizing || !(_hierarchyState & ASHierarchyStateRasterized), + @"Rasterized descendants should never display unless being drawn into the rasterized container."); + + if (shouldBeginRasterizing) { + // Collect displayBlocks for all descendants. + NSMutableArray *displayBlocks = [[NSMutableArray alloc] init]; + [self _recursivelyRasterizeSelfAndSublayersWithIsCancelledBlock:isCancelledBlock displayBlocks:displayBlocks]; + CHECK_CANCELLED_AND_RETURN_NIL(); + + // If [UIColor clearColor] or another semitransparent background color is used, include alpha channel when rasterizing. + // Unlike CALayer drawing, we include the backgroundColor as a base during rasterization. + opaque = opaque && CGColorGetAlpha(backgroundColor.CGColor) == 1.0f; + + displayBlock = ^id{ + CHECK_CANCELLED_AND_RETURN_NIL(); + + ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + + for (dispatch_block_t block in displayBlocks) { + CHECK_CANCELLED_AND_RETURN_NIL(ASGraphicsEndImageContext()); + block(); + } + + UIImage *image = ASGraphicsGetImageAndEndCurrentContext(); + + ASDN_DELAY_FOR_DISPLAY(); + return image; + }; + } else { + displayBlock = ^id{ + CHECK_CANCELLED_AND_RETURN_NIL(); + + if (shouldCreateGraphicsContext) { + ASGraphicsBeginImageContextWithOptions(bounds.size, opaque, contentsScaleForDisplay); + CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); ); + } + + CGContextRef currentContext = UIGraphicsGetCurrentContext(); + UIImage *image = nil; + + if (shouldCreateGraphicsContext && !currentContext) { + //ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size)); + return nil; + } + + // For -display methods, we don't have a context, and thus will not call the _willDisplayNodeContentWithRenderingContext or + // _didDisplayNodeContentWithRenderingContext blocks. It's up to the implementation of -display... to do what it needs. + [self __willDisplayNodeContentWithRenderingContext:currentContext drawParameters:drawParameters]; + + if (usesImageDisplay) { // If we are using a display method, we'll get an image back directly. + image = [self.class displayWithParameters:drawParameters isCancelled:isCancelledBlock]; + } else if (usesDrawRect) { // If we're using a draw method, this will operate on the currentContext. + [self.class drawRect:bounds withParameters:drawParameters isCancelled:isCancelledBlock isRasterizing:rasterizing]; + } + + [self __didDisplayNodeContentWithRenderingContext:currentContext image:&image drawParameters:drawParameters backgroundColor:backgroundColor borderWidth:borderWidth borderColor:borderColor]; + + if (shouldCreateGraphicsContext) { + CHECK_CANCELLED_AND_RETURN_NIL( ASGraphicsEndImageContext(); ); + image = ASGraphicsGetImageAndEndCurrentContext(); + } + + ASDN_DELAY_FOR_DISPLAY(); + return image; + }; + } + + /** + If we're profiling, wrap the display block with signpost start and end. + Color the interval red if cancelled, green otherwise. + */ +#if AS_KDEBUG_ENABLE + __unsafe_unretained id ptrSelf = self; + displayBlock = ^{ + ASSignpostStartCustom(ASSignpostLayerDisplay, ptrSelf, 0); + id result = displayBlock(); + ASSignpostEndCustom(ASSignpostLayerDisplay, ptrSelf, 0, isCancelledBlock() ? ASSignpostColorRed : ASSignpostColorGreen); + return result; + }; +#endif + + return displayBlock; +} + +- (void)__willDisplayNodeContentWithRenderingContext:(CGContextRef)context drawParameters:(id _Nullable)drawParameters +{ + if (context) { + __instanceLock__.lock(); + ASCornerRoundingType cornerRoundingType = _cornerRoundingType; + CGFloat cornerRadius = _cornerRadius; + ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; + __instanceLock__.unlock(); + + if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0) { + ASDisplayNodeAssert(context == UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); + // TODO: This clip path should be removed if we are rasterizing. + CGRect boundingBox = CGContextGetClipBoundingBox(context); + [[UIBezierPath bezierPathWithRoundedRect:boundingBox cornerRadius:cornerRadius] addClip]; + } + + if (willDisplayNodeContentWithRenderingContext) { + willDisplayNodeContentWithRenderingContext(context, drawParameters); + } + } + +} +- (void)__didDisplayNodeContentWithRenderingContext:(CGContextRef)context image:(UIImage **)image drawParameters:(id _Nullable)drawParameters backgroundColor:(UIColor *)backgroundColor borderWidth:(CGFloat)borderWidth borderColor:(CGColorRef)borderColor +{ + if (context == NULL && *image == NULL) { + return; + } + + __instanceLock__.lock(); + ASCornerRoundingType cornerRoundingType = _cornerRoundingType; + CGFloat cornerRadius = _cornerRadius; + CGFloat contentsScale = _contentsScaleForDisplay; + ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; + __instanceLock__.unlock(); + + if (context != NULL) { + if (didDisplayNodeContentWithRenderingContext) { + didDisplayNodeContentWithRenderingContext(context, drawParameters); + } + } + + if (cornerRoundingType == ASCornerRoundingTypePrecomposited && cornerRadius > 0.0f) { + CGRect bounds = CGRectZero; + if (context == NULL) { + bounds = self.threadSafeBounds; + bounds.size.width *= contentsScale; + bounds.size.height *= contentsScale; + CGFloat white = 0.0f, alpha = 0.0f; + [backgroundColor getWhite:&white alpha:&alpha]; + ASGraphicsBeginImageContextWithOptions(bounds.size, (alpha == 1.0f), contentsScale); + [*image drawInRect:bounds]; + } else { + bounds = CGContextGetClipBoundingBox(context); + } + + ASDisplayNodeAssert(UIGraphicsGetCurrentContext(), @"context is expected to be pushed on UIGraphics stack %@", self); + + UIBezierPath *roundedHole = [UIBezierPath bezierPathWithRect:bounds]; + [roundedHole appendPath:[UIBezierPath bezierPathWithRoundedRect:bounds cornerRadius:cornerRadius * contentsScale]]; + roundedHole.usesEvenOddFillRule = YES; + + UIBezierPath *roundedPath = nil; + if (borderWidth > 0.0f) { // Don't create roundedPath and stroke if borderWidth is 0.0 + CGFloat strokeThickness = borderWidth * contentsScale; + CGFloat strokeInset = ((strokeThickness + 1.0f) / 2.0f) - 1.0f; + roundedPath = [UIBezierPath bezierPathWithRoundedRect:CGRectInset(bounds, strokeInset, strokeInset) + cornerRadius:_cornerRadius * contentsScale]; + roundedPath.lineWidth = strokeThickness; + [[UIColor colorWithCGColor:borderColor] setStroke]; + } + + // Punch out the corners by copying the backgroundColor over them. + // This works for everything from clearColor to opaque colors. + [backgroundColor setFill]; + [roundedHole fillWithBlendMode:kCGBlendModeCopy alpha:1.0f]; + + [roundedPath stroke]; // Won't do anything if borderWidth is 0 and roundedPath is nil. + + if (*image) { + *image = ASGraphicsGetImageAndEndCurrentContext(); + } + } +} + +- (void)displayAsyncLayer:(_ASDisplayLayer *)asyncLayer asynchronously:(BOOL)asynchronously +{ + ASDisplayNodeAssertMainThread(); + + __instanceLock__.lock(); + + if (_hierarchyState & ASHierarchyStateRasterized) { + __instanceLock__.unlock(); + return; + } + + CALayer *layer = _layer; + BOOL rasterizesSubtree = _flags.rasterizesSubtree; + + __instanceLock__.unlock(); + + // for async display, capture the current displaySentinel value to bail early when the job is executed if another is + // enqueued + // for sync display, do not support cancellation + + // FIXME: what about the degenerate case where we are calling setNeedsDisplay faster than the jobs are dequeuing + // from the displayQueue? Need to not cancel early fails from displaySentinel changes. + asdisplaynode_iscancelled_block_t isCancelledBlock = nil; + if (asynchronously) { + uint displaySentinelValue = ++_displaySentinel; + __weak ASDisplayNode *weakSelf = self; + isCancelledBlock = ^BOOL{ + __strong ASDisplayNode *self = weakSelf; + return self == nil || (displaySentinelValue != self->_displaySentinel.load()); + }; + } else { + isCancelledBlock = ^BOOL{ + return NO; + }; + } + + // Set up displayBlock to call either display or draw on the delegate and return a UIImage contents + asyncdisplaykit_async_transaction_operation_block_t displayBlock = [self _displayBlockWithAsynchronous:asynchronously isCancelledBlock:isCancelledBlock rasterizing:NO]; + + if (!displayBlock) { + return; + } + + ASDisplayNodeAssert(layer, @"Expect _layer to be not nil"); + + // This block is called back on the main thread after rendering at the completion of the current async transaction, or immediately if !asynchronously + asyncdisplaykit_async_transaction_operation_completion_block_t completionBlock = ^(id value, BOOL canceled){ + ASDisplayNodeCAssertMainThread(); + if (!canceled && !isCancelledBlock()) { + UIImage *image = (UIImage *)value; + BOOL stretchable = (NO == UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero)); + if (stretchable) { + ASDisplayNodeSetResizableContents(layer, image); + } else { + layer.contentsScale = self.contentsScale; + layer.contents = (id)image.CGImage; + } + [self didDisplayAsyncLayer:self.asyncLayer]; + + if (rasterizesSubtree) { + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node didDisplayAsyncLayer:node.asyncLayer]; + }); + } + } + }; + + // Call willDisplay immediately in either case + [self willDisplayAsyncLayer:self.asyncLayer asynchronously:asynchronously]; + + if (rasterizesSubtree) { + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node willDisplayAsyncLayer:node.asyncLayer asynchronously:asynchronously]; + }); + } + + if (asynchronously) { + // Async rendering operations are contained by a transaction, which allows them to proceed and concurrently + // while synchronizing the final application of the results to the layer's contents property (completionBlock). + + // First, look to see if we are expected to join a parent's transaction container. + CALayer *containerLayer = layer.asyncdisplaykit_parentTransactionContainer ? : layer; + + // In the case that a transaction does not yet exist (such as for an individual node outside of a container), + // this call will allocate the transaction and add it to _ASAsyncTransactionGroup. + // It will automatically commit the transaction at the end of the runloop. + _ASAsyncTransaction *transaction = containerLayer.asyncdisplaykit_asyncTransaction; + + // Adding this displayBlock operation to the transaction will start it IMMEDIATELY. + // The only function of the transaction commit is to gate the calling of the completionBlock. + [transaction addOperationWithBlock:displayBlock priority:self.drawingPriority queue:[_ASDisplayLayer displayQueue] completion:completionBlock]; + } else { + UIImage *contents = (UIImage *)displayBlock(); + completionBlock(contents, NO); + } +} + +- (void)cancelDisplayAsyncLayer:(_ASDisplayLayer *)asyncLayer +{ + _displaySentinel.fetch_add(1); +} + +- (ASDisplayNodeContextModifier)willDisplayNodeContentWithRenderingContext +{ + MutexLocker l(__instanceLock__); + return _willDisplayNodeContentWithRenderingContext; +} + +- (ASDisplayNodeContextModifier)didDisplayNodeContentWithRenderingContext +{ + MutexLocker l(__instanceLock__); + return _didDisplayNodeContentWithRenderingContext; +} + +- (void)setWillDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier +{ + MutexLocker l(__instanceLock__); + _willDisplayNodeContentWithRenderingContext = contextModifier; +} + +- (void)setDidDisplayNodeContentWithRenderingContext:(ASDisplayNodeContextModifier)contextModifier; +{ + MutexLocker l(__instanceLock__); + _didDisplayNodeContentWithRenderingContext = contextModifier; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.mm new file mode 100644 index 0000000000..29d761be5a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Convenience.mm @@ -0,0 +1,40 @@ +// +// ASDisplayNode+Convenience.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 "ASResponderChainEnumerator.h" + +@implementation ASDisplayNode (Convenience) + +- (__kindof UIViewController *)closestViewController +{ + ASDisplayNodeAssertMainThread(); + + // Careful not to trigger node loading here. + if (!self.nodeLoaded) { + return nil; + } + + // Get the closest view. + UIView *view = ASFindClosestViewOfLayer(self.layer); + // Travel up the responder chain to find a view controller. + for (UIResponder *responder in [view asdk_responderChainEnumerator]) { + UIViewController *vc = ASDynamicCast(responder, UIViewController); + if (vc != nil) { + return vc; + } + } + return nil; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Deprecated.h b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Deprecated.h new file mode 100644 index 0000000000..b05390ea47 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Deprecated.h @@ -0,0 +1,142 @@ +// +// ASDisplayNode+Deprecated.h +// Texture +// +// Copyright (c) 2014-present, Facebook, Inc. All rights reserved. +// This source code is licensed under the BSD-style license found in the +// LICENSE file in the /ASDK-Licenses directory of this source tree. An additional +// grant of patent rights can be found in the PATENTS file in the same directory. +// +// Modifications to this file made after 4/13/2017 are: Copyright (c) 2017-present, +// Pinterest, Inc. Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// + +#pragma once + +#import + +@interface ASDisplayNode (Deprecated) + +/** + * @abstract The name of this node, which will be displayed in `description`. The default value is nil. + * + * @deprecated Deprecated in version 2.0: Use .debugName instead. This value will display in + * results of the -asciiArtString method (@see ASLayoutElementAsciiArtProtocol). + */ +@property (nullable, nonatomic, copy) NSString *name ASDISPLAYNODE_DEPRECATED_MSG("Use .debugName instead."); + +/** + * @abstract Provides a default intrinsic content size for calculateSizeThatFits:. This is useful when laying out + * a node that either has no intrinsic content size or should be laid out at a different size than its intrinsic content + * size. For example, this property could be set on an ASImageNode to display at a size different from the underlying + * image size. + * + * @return Try to create a CGSize for preferredFrameSize of this node from the width and height property of this node. It will return CGSizeZero if width and height dimensions are not of type ASDimensionUnitPoints. + * + * @deprecated Deprecated in version 2.0: Just calls through to set the height and width property of the node. Convert to use sizing properties instead: height, minHeight, maxHeight, width, minWidth, maxWidth. + */ +@property (nonatomic, assign, readwrite) CGSize preferredFrameSize ASDISPLAYNODE_DEPRECATED_MSG("Use .style.preferredSize instead OR set individual values with .style.height and .style.width."); + +/** + * @abstract Asks the node to measure and return the size that best fits its subnodes. + * + * @param constrainedSize The maximum size the receiver should fit in. + * + * @return A new size that fits the receiver's subviews. + * + * @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the + * constraint and the result. + * + * @warning Subclasses must not override this; it calls -measureWithSizeRange: with zero min size. + * -measureWithSizeRange: caches results from -calculateLayoutThatFits:. Calling this method may + * be expensive if result is not cached. + * + * @see measureWithSizeRange: + * @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:] + * + * @deprecated Deprecated in version 2.0: Use layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout + */ +- (CGSize)measure:(CGSize)constrainedSize/* ASDISPLAYNODE_DEPRECATED_MSG("Use layoutThatFits: with a constrained size of (CGSizeZero, constrainedSize) and call size on the returned ASLayout.")*/; + +ASLayoutElementStyleForwardingDeclaration + +/** + * @abstract Called whenever the visiblity of the node changed. + * + * @discussion Subclasses may use this to monitor when they become visible. + * + * @deprecated @see didEnterVisibleState @see didExitVisibleState + */ +- (void)visibilityDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterVisibleState / -didExitVisibleState instead."); + +/** + * @abstract Called whenever the visiblity of the node changed. + * + * @discussion Subclasses may use this to monitor when they become visible. + * + * @deprecated @see didEnterVisibleState @see didExitVisibleState + */ +- (void)visibleStateDidChange:(BOOL)isVisible ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterVisibleState / -didExitVisibleState instead."); + +/** + * @abstract Called whenever the the node has entered or exited the display state. + * + * @discussion Subclasses may use this to monitor when a node should be rendering its content. + * + * @note This method can be called from any thread and should therefore be thread safe. + * + * @deprecated @see didEnterDisplayState @see didExitDisplayState + */ +- (void)displayStateDidChange:(BOOL)inDisplayState ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterDisplayState / -didExitDisplayState instead."); + +/** + * @abstract Called whenever the the node has entered or left the load state. + * + * @discussion Subclasses may use this to monitor data for a node should be loaded, either from a local or remote source. + * + * @note This method can be called from any thread and should therefore be thread safe. + * + * @deprecated @see didEnterPreloadState @see didExitPreloadState + */ +- (void)loadStateDidChange:(BOOL)inLoadState ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterPreloadState / -didExitPreloadState instead."); + +/** + * @abstract Cancels all performing layout transitions. Can be called on any thread. + * + * @deprecated Deprecated in version 2.0: Use cancelLayoutTransition + */ +- (void)cancelLayoutTransitionsInProgress ASDISPLAYNODE_DEPRECATED_MSG("Use -cancelLayoutTransition instead."); + +/** + * @abstract A boolean that shows whether the node automatically inserts and removes nodes based on the presence or + * absence of the node and its subnodes is completely determined in its layoutSpecThatFits: method. + * + * @discussion If flag is YES the node no longer require addSubnode: or removeFromSupernode method calls. The presence + * or absence of subnodes is completely determined in its layoutSpecThatFits: method. + * + * @deprecated Deprecated in version 2.0: Use automaticallyManagesSubnodes + */ +@property (nonatomic, assign) BOOL usesImplicitHierarchyManagement ASDISPLAYNODE_DEPRECATED_MSG("Set .automaticallyManagesSubnodes instead."); + +/** + * @abstract Indicates that the node should fetch any external data, such as images. + * + * @discussion Subclasses may override this method to be notified when they should begin to preload. Fetching + * should be done asynchronously. The node is also responsible for managing the memory of any data. + * The data may be remote and accessed via the network, but could also be a local database query. + */ +- (void)fetchData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didEnterPreloadState instead."); + +/** + * Provides an opportunity to clear any fetched data (e.g. remote / network or database-queried) on the current node. + * + * @discussion This will not clear data recursively for all subnodes. Either call -recursivelyClearPreloadedData or + * selectively clear fetched data. + */ +- (void)clearFetchedData ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use -didExitPreloadState instead."); + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+Layout.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Layout.mm new file mode 100644 index 0000000000..32934a599c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+Layout.mm @@ -0,0 +1,1036 @@ +// +// ASDisplayNode+Layout.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 "ASDisplayNodeInternal.h" +#import +#import +#import +#import +#import "ASLayoutElementStylePrivate.h" +#import + +using AS::MutexLocker; + +#pragma mark - ASDisplayNode (ASLayoutElement) + +@implementation ASDisplayNode (ASLayoutElement) + +#pragma mark + +- (BOOL)implementsLayoutMethod +{ + MutexLocker l(__instanceLock__); + return (_methodOverrides & (ASDisplayNodeMethodOverrideLayoutSpecThatFits | + ASDisplayNodeMethodOverrideCalcLayoutThatFits | + ASDisplayNodeMethodOverrideCalcSizeThatFits)) != 0 || _layoutSpecBlock != nil; +} + + +- (ASLayoutElementStyle *)style +{ + MutexLocker l(__instanceLock__); + return [self _locked_style]; +} + +- (ASLayoutElementStyle *)_locked_style +{ + if (_style == nil) { + _style = [[ASLayoutElementStyle alloc] init]; + } + return _style; +} + +- (ASLayoutElementType)layoutElementType +{ + return ASLayoutElementTypeDisplayNode; +} + +- (NSArray> *)sublayoutElements +{ + return self.subnodes; +} + +#pragma mark Measurement Pass + +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize +{ + return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max]; +} + +- (CGSize)measure:(CGSize)constrainedSize { + return [self layoutThatFits:ASSizeRangeMake(CGSizeZero, constrainedSize)].size; +} + +- (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize +{ + ASScopedLockSelfOrToRoot(); + + // If one or multiple layout transitions are in flight it still can happen that layout information is requested + // on other threads. As the pending and calculated layout to be updated in the layout transition in here just a + // layout calculation wil be performed without side effect + if ([self _isLayoutTransitionInvalid]) { + return [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:parentSize]; + } + + ASLayout *layout = nil; + NSUInteger version = _layoutVersion; + if (_calculatedDisplayNodeLayout.isValid(constrainedSize, parentSize, version)) { + ASDisplayNodeAssertNotNil(_calculatedDisplayNodeLayout.layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _calculatedDisplayNodeLayout.layout should not be nil! %@", self); + layout = _calculatedDisplayNodeLayout.layout; + } else if (_pendingDisplayNodeLayout.isValid(constrainedSize, parentSize, version)) { + ASDisplayNodeAssertNotNil(_pendingDisplayNodeLayout.layout, @"-[ASDisplayNode layoutThatFits:parentSize:] _pendingDisplayNodeLayout.layout should not be nil! %@", self); + layout = _pendingDisplayNodeLayout.layout; + } else { + // Create a pending display node layout for the layout pass + layout = [self calculateLayoutThatFits:constrainedSize + restrictedToSize:self.style.size + relativeToParentSize:parentSize]; + _pendingDisplayNodeLayout = ASDisplayNodeLayout(layout, constrainedSize, parentSize,version); + ASDisplayNodeAssertNotNil(layout, @"-[ASDisplayNode layoutThatFits:parentSize:] newly calculated layout should not be nil! %@", self); + } + + return layout ?: [ASLayout layoutWithLayoutElement:self size:{0, 0}]; +} + +#pragma mark ASLayoutElementStyleExtensibility + +ASLayoutElementStyleExtensibilityForwarding + +#pragma mark ASPrimitiveTraitCollection + +- (ASPrimitiveTraitCollection)primitiveTraitCollection +{ + return _primitiveTraitCollection.load(); +} + +- (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection +{ + if (ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(traitCollection, _primitiveTraitCollection.load()) == NO) { + _primitiveTraitCollection = traitCollection; + ASDisplayNodeLogEvent(self, @"asyncTraitCollectionDidChange: %@", NSStringFromASPrimitiveTraitCollection(traitCollection)); + + [self asyncTraitCollectionDidChange]; + } +} + +- (ASTraitCollection *)asyncTraitCollection +{ + return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection]; +} + +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtString +{ + return [ASLayoutSpec asciiArtStringForChildren:@[] parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + NSMutableString *result = [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding]; + if (_debugName) { + [result appendFormat:@" (%@)", _debugName]; + } + return result; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayout) + +@implementation ASDisplayNode (ASLayout) + +- (ASLayoutEngineType)layoutEngineType +{ +#if YOGA + MutexLocker l(__instanceLock__); + YGNodeRef yogaNode = _style.yogaNode; + BOOL hasYogaParent = (_yogaParent != nil); + BOOL hasYogaChildren = (_yogaChildren.count > 0); + if (yogaNode != NULL && (hasYogaParent || hasYogaChildren)) { + return ASLayoutEngineTypeYoga; + } +#endif + + return ASLayoutEngineTypeLayoutSpec; +} + +- (ASLayout *)calculatedLayout +{ + MutexLocker l(__instanceLock__); + return _calculatedDisplayNodeLayout.layout; +} + +- (CGSize)calculatedSize +{ + MutexLocker l(__instanceLock__); + if (_pendingDisplayNodeLayout.isValid(_layoutVersion)) { + return _pendingDisplayNodeLayout.layout.size; + } + return _calculatedDisplayNodeLayout.layout.size; +} + +- (ASSizeRange)constrainedSizeForCalculatedLayout +{ + MutexLocker l(__instanceLock__); + return [self _locked_constrainedSizeForCalculatedLayout]; +} + +- (ASSizeRange)_locked_constrainedSizeForCalculatedLayout +{ + ASAssertLocked(__instanceLock__); + if (_pendingDisplayNodeLayout.isValid(_layoutVersion)) { + return _pendingDisplayNodeLayout.constrainedSize; + } + return _calculatedDisplayNodeLayout.constrainedSize; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutElementStylability) + +@implementation ASDisplayNode (ASLayoutElementStylability) + +- (instancetype)styledWithBlock:(AS_NOESCAPE void (^)(__kindof ASLayoutElementStyle *style))styleBlock +{ + styleBlock(self.style); + return self; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutInternal) + +@implementation ASDisplayNode (ASLayoutInternal) + +/** + * @abstract Informs the root node that the intrinsic size of the receiver is no longer valid. + * + * @discussion The size of a root node is determined by each subnode. Calling invalidateSize will let the root node know + * that the intrinsic size of the receiver node is no longer valid and a resizing of the root node needs to happen. + */ +- (void)_u_setNeedsLayoutFromAbove +{ + ASDisplayNodeAssertThreadAffinity(self); + ASAssertUnlocked(__instanceLock__); + + // Mark the node for layout in the next layout pass + [self setNeedsLayout]; + + __instanceLock__.lock(); + // Escalate to the root; entire tree must allow adjustments so the layout fits the new child. + // Much of the layout will be re-used as cached (e.g. other items in an unconstrained stack) + ASDisplayNode *supernode = _supernode; + __instanceLock__.unlock(); + + if (supernode) { + // Threading model requires that we unlock before calling a method on our parent. + [supernode _u_setNeedsLayoutFromAbove]; + } else { + // Let the root node method know that the size was invalidated + [self _rootNodeDidInvalidateSize]; + } +} + +// TODO It would be easier to work with if we could `ASAssertUnlocked` here, but we +// cannot due to locking to root in `_u_measureNodeWithBoundsIfNecessary`. +- (void)_rootNodeDidInvalidateSize +{ + ASDisplayNodeAssertThreadAffinity(self); + __instanceLock__.lock(); + + // We are the root node and need to re-flow the layout; at least one child needs a new size. + CGSize boundsSizeForLayout = ASCeilSizeValues(self.bounds.size); + + // Figure out constrainedSize to use + ASSizeRange constrainedSize = ASSizeRangeMake(boundsSizeForLayout); + if (_pendingDisplayNodeLayout.layout != nil) { + constrainedSize = _pendingDisplayNodeLayout.constrainedSize; + } else if (_calculatedDisplayNodeLayout.layout != nil) { + constrainedSize = _calculatedDisplayNodeLayout.constrainedSize; + } + + __instanceLock__.unlock(); + + // Perform a measurement pass to get the full tree layout, adapting to the child's new size. + ASLayout *layout = [self layoutThatFits:constrainedSize]; + + // Check if the returned layout has a different size than our current bounds. + if (CGSizeEqualToSize(boundsSizeForLayout, layout.size) == NO) { + // If so, inform our container we need an update (e.g Table, Collection, ViewController, etc). + [self displayNodeDidInvalidateSizeNewSize:layout.size]; + } +} + +// TODO +// We should remove this logic, which is relatively new, and instead +// rely on the parent / host of the root node to do this size change. That's always been the +// expectation with other node containers like ASTableView, ASCollectionView, ASViewController, etc. +// E.g. in ASCellNode the _interactionDelegate is a Table or Collection that will resize in this +// case. By resizing without participating with the parent, we could get cases where our parent size +// does not match, especially if there is a size constraint that is applied at that level. +// +// In general a node should never need to set its own size, instead allowing its parent to do so - +// even in the root case. Anyhow this is a separate / pre-existing issue, but I think it could be +// causing real issues in cases of resizing nodes. +- (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size +{ + ASDisplayNodeAssertThreadAffinity(self); + + // The default implementation of display node changes the size of itself to the new size + CGRect oldBounds = self.bounds; + CGSize oldSize = oldBounds.size; + CGSize newSize = size; + + if (! CGSizeEqualToSize(oldSize, newSize)) { + self.bounds = (CGRect){ oldBounds.origin, newSize }; + + // Frame's origin must be preserved. Since it is computed from bounds size, anchorPoint + // and position (see frame setter in ASDisplayNode+UIViewBridge), position needs to be adjusted. + CGPoint anchorPoint = self.anchorPoint; + CGPoint oldPosition = self.position; + CGFloat xDelta = (newSize.width - oldSize.width) * anchorPoint.x; + CGFloat yDelta = (newSize.height - oldSize.height) * anchorPoint.y; + self.position = CGPointMake(oldPosition.x + xDelta, oldPosition.y + yDelta); + } +} + +- (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds +{ + // ASAssertUnlocked(__instanceLock__); + ASScopedLockSelfOrToRoot(); + + // Check if we are a subnode in a layout transition. + // In this case no measurement is needed as it's part of the layout transition + if ([self _locked_isLayoutTransitionInvalid]) { + return; + } + + CGSize boundsSizeForLayout = ASCeilSizeValues(bounds.size); + + // Prefer a newer and not yet applied _pendingDisplayNodeLayout over _calculatedDisplayNodeLayout + // If there is no such _pending, check if _calculated is valid to reuse (avoiding recalculation below). + BOOL pendingLayoutIsPreferred = NO; + if (_pendingDisplayNodeLayout.isValid(_layoutVersion)) { + NSUInteger calculatedVersion = _calculatedDisplayNodeLayout.version; + NSUInteger pendingVersion = _pendingDisplayNodeLayout.version; + if (pendingVersion > calculatedVersion) { + pendingLayoutIsPreferred = YES; // Newer _pending + } else if (pendingVersion == calculatedVersion + && !ASSizeRangeEqualToSizeRange(_pendingDisplayNodeLayout.constrainedSize, + _calculatedDisplayNodeLayout.constrainedSize)) { + pendingLayoutIsPreferred = YES; // _pending with a different constrained size + } + } + BOOL calculatedLayoutIsReusable = (_calculatedDisplayNodeLayout.isValid(_layoutVersion) + && (_calculatedDisplayNodeLayout.requestedLayoutFromAbove + || CGSizeEqualToSize(_calculatedDisplayNodeLayout.layout.size, boundsSizeForLayout))); + if (!pendingLayoutIsPreferred && calculatedLayoutIsReusable) { + return; + } + // _calculatedDisplayNodeLayout is not reusable we need to transition to a new one + [self cancelLayoutTransition]; + + BOOL didCreateNewContext = NO; + ASLayoutElementContext *context = ASLayoutElementGetCurrentContext(); + if (context == nil) { + context = [[ASLayoutElementContext alloc] init]; + ASLayoutElementPushContext(context); + didCreateNewContext = YES; + } + + // Figure out previous and pending layouts for layout transition + ASDisplayNodeLayout nextLayout = _pendingDisplayNodeLayout; + #define layoutSizeDifferentFromBounds !CGSizeEqualToSize(nextLayout.layout.size, boundsSizeForLayout) + + // nextLayout was likely created by a call to layoutThatFits:, check if it is valid and can be applied. + // If our bounds size is different than it, or invalid, recalculate. Use #define to avoid nullptr-> + BOOL pendingLayoutApplicable = NO; + if (nextLayout.layout == nil) { + } else if (!nextLayout.isValid(_layoutVersion)) { + } else if (layoutSizeDifferentFromBounds) { + } else { + pendingLayoutApplicable = YES; + } + + if (!pendingLayoutApplicable) { + // Use the last known constrainedSize passed from a parent during layout (if never, use bounds). + NSUInteger version = _layoutVersion; + ASSizeRange constrainedSize = [self _locked_constrainedSizeForLayoutPass]; + ASLayout *layout = [self calculateLayoutThatFits:constrainedSize + restrictedToSize:self.style.size + relativeToParentSize:boundsSizeForLayout]; + nextLayout = ASDisplayNodeLayout(layout, constrainedSize, boundsSizeForLayout, version); + // Now that the constrained size of pending layout might have been reused, the layout is useless + // Release it and any orphaned subnodes it retains + _pendingDisplayNodeLayout.layout = nil; + } + + if (didCreateNewContext) { + ASLayoutElementPopContext(); + } + + // If our new layout's desired size for self doesn't match current size, ask our parent to update it. + // This can occur for either pre-calculated or newly-calculated layouts. + if (nextLayout.requestedLayoutFromAbove == NO + && CGSizeEqualToSize(boundsSizeForLayout, nextLayout.layout.size) == NO) { + // The layout that we have specifies that this node (self) would like to be a different size + // than it currently is. Because that size has been computed within the constrainedSize, we + // expect that calling setNeedsLayoutFromAbove will result in our parent resizing us to this. + // However, in some cases apps may manually interfere with this (setting a different bounds). + // In this case, we need to detect that we've already asked to be resized to match this + // particular ASLayout object, and shouldn't loop asking again unless we have a different ASLayout. + nextLayout.requestedLayoutFromAbove = YES; + + { + __instanceLock__.unlock(); + [self _u_setNeedsLayoutFromAbove]; + __instanceLock__.lock(); + } + + // Update the layout's version here because _u_setNeedsLayoutFromAbove calls __setNeedsLayout which in turn increases _layoutVersion + // Failing to do this will cause the layout to be invalid immediately + nextLayout.version = _layoutVersion; + } + + // Prepare to transition to nextLayout + ASDisplayNodeAssertNotNil(nextLayout.layout, @"nextLayout.layout should not be nil! %@", self); + _pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self + pendingLayout:nextLayout + previousLayout:_calculatedDisplayNodeLayout]; + + // If a parent is currently executing a layout transition, perform our layout application after it. + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState) == NO) { + // If no transition, apply our new layout immediately (common case). + [self _completePendingLayoutTransition]; + } +} + +- (ASSizeRange)_constrainedSizeForLayoutPass +{ + MutexLocker l(__instanceLock__); + return [self _locked_constrainedSizeForLayoutPass]; +} + +- (ASSizeRange)_locked_constrainedSizeForLayoutPass +{ + // TODO: The logic in -_u_setNeedsLayoutFromAbove seems correct and doesn't use this method. + // logic seems correct. For what case does -this method need to do the CGSizeEqual checks? + // IF WE CAN REMOVE BOUNDS CHECKS HERE, THEN WE CAN ALSO REMOVE "REQUESTED FROM ABOVE" CHECK + + ASAssertLocked(__instanceLock__); + + CGSize boundsSizeForLayout = ASCeilSizeValues(self.threadSafeBounds.size); + + // Checkout if constrained size of pending or calculated display node layout can be used + if (_pendingDisplayNodeLayout.requestedLayoutFromAbove + || CGSizeEqualToSize(_pendingDisplayNodeLayout.layout.size, boundsSizeForLayout)) { + // We assume the size from the last returned layoutThatFits: layout was applied so use the pending display node + // layout constrained size + return _pendingDisplayNodeLayout.constrainedSize; + } else if (_calculatedDisplayNodeLayout.layout != nil + && (_calculatedDisplayNodeLayout.requestedLayoutFromAbove + || CGSizeEqualToSize(_calculatedDisplayNodeLayout.layout.size, boundsSizeForLayout))) { + // We assume the _calculatedDisplayNodeLayout is still valid and the frame is not different + return _calculatedDisplayNodeLayout.constrainedSize; + } else { + // In this case neither the _pendingDisplayNodeLayout or the _calculatedDisplayNodeLayout constrained size can + // be reused, so the current bounds is used. This is usual the case if a frame was set manually that differs to + // the one returned from layoutThatFits: or layoutThatFits: was never called + return ASSizeRangeMake(boundsSizeForLayout); + } +} + +- (void)_layoutSublayouts +{ + ASDisplayNodeAssertThreadAffinity(self); + // ASAssertUnlocked(__instanceLock__); + + ASLayout *layout; + { + MutexLocker l(__instanceLock__); + if (_calculatedDisplayNodeLayout.version < _layoutVersion) { + return; + } + layout = _calculatedDisplayNodeLayout.layout; + } + + for (ASDisplayNode *node in self.subnodes) { + CGRect frame = [layout frameForElement:node]; + if (CGRectIsNull(frame)) { + // There is no frame for this node in our layout. + // This currently can happen if we get a CA layout pass + // while waiting for the client to run animateLayoutTransition: + } else { + node.frame = frame; + } + } +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASAutomatic Subnode Management) + +@implementation ASDisplayNode (ASAutomaticSubnodeManagement) + +#pragma mark Automatically Manages Subnodes + +- (BOOL)automaticallyManagesSubnodes +{ + MutexLocker l(__instanceLock__); + return _automaticallyManagesSubnodes; +} + +- (void)setAutomaticallyManagesSubnodes:(BOOL)automaticallyManagesSubnodes +{ + MutexLocker l(__instanceLock__); + _automaticallyManagesSubnodes = automaticallyManagesSubnodes; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (ASLayoutTransition) + +@implementation ASDisplayNode (ASLayoutTransition) + +- (BOOL)_isLayoutTransitionInvalid +{ + MutexLocker l(__instanceLock__); + return [self _locked_isLayoutTransitionInvalid]; +} + +- (BOOL)_locked_isLayoutTransitionInvalid +{ + ASAssertLocked(__instanceLock__); + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + ASLayoutElementContext *context = ASLayoutElementGetCurrentContext(); + if (context == nil || _pendingTransitionID != context.transitionID) { + return YES; + } + } + return NO; +} + +/// Starts a new transition and returns the transition id +- (int32_t)_startNewTransition +{ + static std::atomic gNextTransitionID; + int32_t newTransitionID = gNextTransitionID.fetch_add(1) + 1; + _transitionID = newTransitionID; + return newTransitionID; +} + +/// Returns NO if there was no transition to cancel/finish. +- (BOOL)_finishOrCancelTransition +{ + int32_t oldValue = _transitionID.exchange(ASLayoutElementContextInvalidTransitionID); + return oldValue != ASLayoutElementContextInvalidTransitionID; +} + +#pragma mark Layout Transition + +- (void)transitionLayoutWithAnimation:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + [self transitionLayoutWithSizeRange:[self _constrainedSizeForLayoutPass] + animated:animated + shouldMeasureAsync:shouldMeasureAsync + measurementCompletion:completion]; +} + +- (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize + animated:(BOOL)animated + shouldMeasureAsync:(BOOL)shouldMeasureAsync + measurementCompletion:(void(^)())completion +{ + ASDisplayNodeAssertMainThread(); + + if (constrainedSize.max.width <= 0.0 || constrainedSize.max.height <= 0.0) { + // Using CGSizeZero for the sizeRange can cause negative values in client layout code. + // Most likely called transitionLayout: without providing a size, before first layout pass. + return; + } + + { + MutexLocker l(__instanceLock__); + + // Check if we are a subnode in a layout transition. + // In this case no measurement is needed as we're part of the layout transition. + if ([self _locked_isLayoutTransitionInvalid]) { + return; + } + + if (ASHierarchyStateIncludesLayoutPending(_hierarchyState)) { + ASDisplayNodeAssert(NO, @"Can't start a transition when one of the supernodes is performing one."); + return; + } + } + + // Invalidate calculated layout because this method acts as an animated "setNeedsLayout" for nodes. + // If the user has reconfigured the node and calls this, we should never return a stale layout + // for subsequent calls to layoutThatFits: regardless of size range. We choose this method rather than + // -setNeedsLayout because that method also triggers a CA layout invalidation, which isn't necessary at this time. + // See https://github.com/TextureGroup/Texture/issues/463 + [self invalidateCalculatedLayout]; + + // Every new layout transition has a transition id associated to check in subsequent transitions for cancelling + int32_t transitionID = [self _startNewTransition]; + // NOTE: This block captures self. It's cheaper than hitting the weak table. + asdisplaynode_iscancelled_block_t isCancelled = ^{ + BOOL result = (_transitionID != transitionID); + if (result) { + } + return result; + }; + + // Move all subnodes in layout pending state for this transition + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + ASDisplayNodeAssert(node->_transitionID == ASLayoutElementContextInvalidTransitionID, @"Can't start a transition when one of the subnodes is performing one."); + node.hierarchyState |= ASHierarchyStateLayoutPending; + node->_pendingTransitionID = transitionID; + }); + + // Transition block that executes the layout transition + void (^transitionBlock)(void) = ^{ + if (isCancelled()) { + return; + } + + // Perform a full layout creation pass with passed in constrained size to create the new layout for the transition + NSUInteger newLayoutVersion = _layoutVersion; + ASLayout *newLayout; + { + ASScopedLockSelfOrToRoot(); + + ASLayoutElementContext *ctx = [[ASLayoutElementContext alloc] init]; + ctx.transitionID = transitionID; + ASLayoutElementPushContext(ctx); + + BOOL automaticallyManagesSubnodesDisabled = (self.automaticallyManagesSubnodes == NO); + self.automaticallyManagesSubnodes = YES; // Temporary flag for 1.9.x + newLayout = [self calculateLayoutThatFits:constrainedSize + restrictedToSize:self.style.size + relativeToParentSize:constrainedSize.max]; + if (automaticallyManagesSubnodesDisabled) { + self.automaticallyManagesSubnodes = NO; // Temporary flag for 1.9.x + } + + ASLayoutElementPopContext(); + } + + if (isCancelled()) { + return; + } + + ASPerformBlockOnMainThread(^{ + if (isCancelled()) { + return; + } + ASLayoutTransition *pendingLayoutTransition; + _ASTransitionContext *pendingLayoutTransitionContext; + { + // Grab __instanceLock__ here to make sure this transition isn't invalidated + // right after it passed the validation test and before it proceeds + MutexLocker l(__instanceLock__); + + // Update calculated layout + const auto previousLayout = _calculatedDisplayNodeLayout; + const auto pendingLayout = ASDisplayNodeLayout(newLayout, + constrainedSize, + constrainedSize.max, + newLayoutVersion); + [self _locked_setCalculatedDisplayNodeLayout:pendingLayout]; + + // Setup pending layout transition for animation + _pendingLayoutTransition = pendingLayoutTransition = [[ASLayoutTransition alloc] initWithNode:self + pendingLayout:pendingLayout + previousLayout:previousLayout]; + // Setup context for pending layout transition. we need to hold a strong reference to the context + _pendingLayoutTransitionContext = pendingLayoutTransitionContext = [[_ASTransitionContext alloc] initWithAnimation:animated + layoutDelegate:_pendingLayoutTransition + completionDelegate:self]; + } + + // Apply complete layout transitions for all subnodes + { + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + [node _completePendingLayoutTransition]; + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + } + + // Measurement pass completion + // Give the subclass a change to hook into before calling the completion block + [self _layoutTransitionMeasurementDidFinish]; + if (completion) { + completion(); + } + + // Apply the subnode insertion immediately to be able to animate the nodes + [pendingLayoutTransition applySubnodeInsertionsAndMoves]; + + // Kick off animating the layout transition + { + [self animateLayoutTransition:pendingLayoutTransitionContext]; + } + + // Mark transaction as finished + [self _finishOrCancelTransition]; + }); + }; + + // Start transition based on flag on current or background thread + if (shouldMeasureAsync) { + ASPerformBlockOnBackgroundThread(transitionBlock); + } else { + transitionBlock(); + } +} + +- (void)cancelLayoutTransition +{ + if ([self _finishOrCancelTransition]) { + // Tell subnodes to exit layout pending state and clear related properties + ASDisplayNodePerformBlockOnEverySubnode(self, NO, ^(ASDisplayNode * _Nonnull node) { + node.hierarchyState &= (~ASHierarchyStateLayoutPending); + }); + } +} + +- (void)setDefaultLayoutTransitionDuration:(NSTimeInterval)defaultLayoutTransitionDuration +{ + MutexLocker l(__instanceLock__); + _defaultLayoutTransitionDuration = defaultLayoutTransitionDuration; +} + +- (NSTimeInterval)defaultLayoutTransitionDuration +{ + MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionDuration; +} + +- (void)setDefaultLayoutTransitionDelay:(NSTimeInterval)defaultLayoutTransitionDelay +{ + MutexLocker l(__instanceLock__); + _defaultLayoutTransitionDelay = defaultLayoutTransitionDelay; +} + +- (NSTimeInterval)defaultLayoutTransitionDelay +{ + MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionDelay; +} + +- (void)setDefaultLayoutTransitionOptions:(UIViewAnimationOptions)defaultLayoutTransitionOptions +{ + MutexLocker l(__instanceLock__); + _defaultLayoutTransitionOptions = defaultLayoutTransitionOptions; +} + +- (UIViewAnimationOptions)defaultLayoutTransitionOptions +{ + MutexLocker l(__instanceLock__); + return _defaultLayoutTransitionOptions; +} + +#pragma mark + +/* + * Hook for subclasses to perform an animation based on the given ASContextTransitioning. By default a fade in and out + * animation is provided. + */ +- (void)animateLayoutTransition:(id)context +{ + if ([context isAnimated] == NO) { + [self _layoutSublayouts]; + [context completeTransition:YES]; + return; + } + + ASDisplayNode *node = self; + + NSAssert(node.isNodeLoaded == YES, @"Invalid node state"); + + NSArray *removedSubnodes = [context removedSubnodes]; + NSMutableArray *insertedSubnodes = [[context insertedSubnodes] mutableCopy]; + const auto movedSubnodes = [[NSMutableArray alloc] init]; + + const auto insertedSubnodeContexts = [[NSMutableArray<_ASAnimatedTransitionContext *> alloc] init]; + const auto removedSubnodeContexts = [[NSMutableArray<_ASAnimatedTransitionContext *> alloc] init]; + + for (ASDisplayNode *subnode in [context subnodesForKey:ASTransitionContextToLayoutKey]) { + if ([insertedSubnodes containsObject:subnode] == NO) { + // This is an existing subnode, check if it is resized, moved or both + CGRect fromFrame = [context initialFrameForNode:subnode]; + CGRect toFrame = [context finalFrameForNode:subnode]; + if (CGSizeEqualToSize(fromFrame.size, toFrame.size) == NO) { + [insertedSubnodes addObject:subnode]; + } + if (CGPointEqualToPoint(fromFrame.origin, toFrame.origin) == NO) { + [movedSubnodes addObject:subnode]; + } + } + } + + // Create contexts for inserted and removed subnodes + for (ASDisplayNode *insertedSubnode in insertedSubnodes) { + [insertedSubnodeContexts addObject:[_ASAnimatedTransitionContext contextForNode:insertedSubnode alpha:insertedSubnode.alpha]]; + } + for (ASDisplayNode *removedSubnode in removedSubnodes) { + [removedSubnodeContexts addObject:[_ASAnimatedTransitionContext contextForNode:removedSubnode alpha:removedSubnode.alpha]]; + } + + // Fade out inserted subnodes + for (ASDisplayNode *insertedSubnode in insertedSubnodes) { + insertedSubnode.frame = [context finalFrameForNode:insertedSubnode]; + insertedSubnode.alpha = 0; + } + + // Adjust groupOpacity for animation + BOOL originAllowsGroupOpacity = node.allowsGroupOpacity; + node.allowsGroupOpacity = YES; + + [UIView animateWithDuration:self.defaultLayoutTransitionDuration delay:self.defaultLayoutTransitionDelay options:self.defaultLayoutTransitionOptions animations:^{ + // Fade removed subnodes and views out + for (ASDisplayNode *removedSubnode in removedSubnodes) { + removedSubnode.alpha = 0; + } + + // Fade inserted subnodes in + for (_ASAnimatedTransitionContext *insertedSubnodeContext in insertedSubnodeContexts) { + insertedSubnodeContext.node.alpha = insertedSubnodeContext.alpha; + } + + // Update frame of self and moved subnodes + CGSize fromSize = [context layoutForKey:ASTransitionContextFromLayoutKey].size; + CGSize toSize = [context layoutForKey:ASTransitionContextToLayoutKey].size; + BOOL isResized = (CGSizeEqualToSize(fromSize, toSize) == NO); + if (isResized == YES) { + CGPoint position = node.frame.origin; + node.frame = CGRectMake(position.x, position.y, toSize.width, toSize.height); + } + for (ASDisplayNode *movedSubnode in movedSubnodes) { + movedSubnode.frame = [context finalFrameForNode:movedSubnode]; + } + } completion:^(BOOL finished) { + // Restore all removed subnode alpha values + for (_ASAnimatedTransitionContext *removedSubnodeContext in removedSubnodeContexts) { + removedSubnodeContext.node.alpha = removedSubnodeContext.alpha; + } + + // Restore group opacity + node.allowsGroupOpacity = originAllowsGroupOpacity; + + // Subnode removals are automatically performed + [context completeTransition:finished]; + }]; +} + +/** + * Hook for subclasses to clean up nodes after the transition happened. Furthermore this can be used from subclasses + * to manually perform deletions. + */ +- (void)didCompleteLayoutTransition:(id)context +{ + ASDisplayNodeAssertMainThread(); + + __instanceLock__.lock(); + ASLayoutTransition *pendingLayoutTransition = _pendingLayoutTransition; + __instanceLock__.unlock(); + + [pendingLayoutTransition applySubnodeRemovals]; +} + +/** + * Completes the pending layout transition immediately without going through the the Layout Transition Animation API + */ +- (void)_completePendingLayoutTransition +{ + __instanceLock__.lock(); + ASLayoutTransition *pendingLayoutTransition = _pendingLayoutTransition; + __instanceLock__.unlock(); + + if (pendingLayoutTransition != nil) { + [self _setCalculatedDisplayNodeLayout:pendingLayoutTransition.pendingLayout]; + [self _completeLayoutTransition:pendingLayoutTransition]; + [self _pendingLayoutTransitionDidComplete]; + } +} + +/** + * Can be directly called to commit the given layout transition immediately to complete without calling through to the + * Layout Transition Animation API + */ +- (void)_completeLayoutTransition:(ASLayoutTransition *)layoutTransition +{ + // Layout transition is not supported for nodes that do not have automatic subnode management enabled + if (layoutTransition == nil || self.automaticallyManagesSubnodes == NO) { + return; + } + + // Trampoline to the main thread if necessary + if (ASDisplayNodeThreadIsMain() || layoutTransition.isSynchronous == NO) { + // Committing the layout transition will result in subnode insertions and removals, both of which must be called without the lock held + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + [layoutTransition commitTransition]; + } else { + // Subnode insertions and removals need to happen always on the main thread if at least one subnode is already loaded + ASPerformBlockOnMainThread(^{ + [layoutTransition commitTransition]; + }); + } +} + +- (void)_assertSubnodeState +{ + // Verify that any orphaned nodes are removed. + // This can occur in rare cases if main thread layout is flushed while a background layout is calculating. + + if (self.automaticallyManagesSubnodes == NO) { + return; + } + + MutexLocker l(__instanceLock__); + NSArray *sublayouts = _calculatedDisplayNodeLayout.layout.sublayouts; + unowned ASLayout *cSublayouts[sublayouts.count]; + [sublayouts getObjects:cSublayouts range:NSMakeRange(0, AS_ARRAY_SIZE(cSublayouts))]; + + // Fast-path if we are in the correct state (likely). + if (_subnodes.count == AS_ARRAY_SIZE(cSublayouts)) { + NSUInteger i = 0; + BOOL matches = YES; + for (ASDisplayNode *subnode in _subnodes) { + if (subnode != cSublayouts[i].layoutElement) { + matches = NO; + } + i++; + } + if (matches) { + return; + } + } + + NSArray *layoutNodes = ASArrayByFlatMapping(sublayouts, ASLayout *layout, (ASDisplayNode *)layout.layoutElement); + NSIndexSet *insertions, *deletions; + [_subnodes asdk_diffWithArray:layoutNodes insertions:&insertions deletions:&deletions]; + if (insertions.count > 0) { + NSLog(@"Warning: node's layout includes subnode that has not been added: node = %@, subnodes = %@, subnodes in layout = %@", self, _subnodes, layoutNodes); + } + + // Remove any nodes that are in the tree but should not be. + // Go in reverse order so we don't shift our indexes. + if (deletions) { + for (NSUInteger i = deletions.lastIndex; i != NSNotFound; i = [deletions indexLessThanIndex:i]) { + NSLog(@"Automatically removing orphaned subnode %@, from parent %@", _subnodes[i], self); + [_subnodes[i] removeFromSupernode]; + } + } +} + +- (void)_pendingLayoutTransitionDidComplete +{ + // This assertion introduces a breaking behavior for nodes that has ASM enabled but also manually manage some subnodes. + // Let's gate it behind YOGA flag. +#if YOGA + [self _assertSubnodeState]; +#endif + + // Subclass hook + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + [self calculatedLayoutDidChange]; + + // Grab lock after calling out to subclass + MutexLocker l(__instanceLock__); + + // We generate placeholders at -layoutThatFits: time so that a node is guaranteed to have a placeholder ready to go. + // This is also because measurement is usually asynchronous, but placeholders need to be set up synchronously. + // First measurement is guaranteed to be before the node is onscreen, so we can create the image async. but still have it appear sync. + if (_placeholderEnabled && !_placeholderImage && [self _locked_displaysAsynchronously]) { + + // Zero-sized nodes do not require a placeholder. + CGSize layoutSize = _calculatedDisplayNodeLayout.layout.size; + if (layoutSize.width * layoutSize.height <= 0.0) { + return; + } + + // If we've displayed our contents, we don't need a placeholder. + // Contents is a thread-affined property and can't be read off main after loading. + if (self.isNodeLoaded) { + ASPerformBlockOnMainThread(^{ + if (self.contents == nil) { + _placeholderImage = [self placeholderImage]; + } + }); + } else { + if (self.contents == nil) { + _placeholderImage = [self placeholderImage]; + } + } + } + + // Cleanup pending layout transition + _pendingLayoutTransition = nil; +} + +- (void)_setCalculatedDisplayNodeLayout:(const ASDisplayNodeLayout &)displayNodeLayout +{ + MutexLocker l(__instanceLock__); + [self _locked_setCalculatedDisplayNodeLayout:displayNodeLayout]; +} + +- (void)_locked_setCalculatedDisplayNodeLayout:(const ASDisplayNodeLayout &)displayNodeLayout +{ + ASAssertLocked(__instanceLock__); + ASDisplayNodeAssertTrue(displayNodeLayout.layout.layoutElement == self); + ASDisplayNodeAssertTrue(displayNodeLayout.layout.size.width >= 0.0); + ASDisplayNodeAssertTrue(displayNodeLayout.layout.size.height >= 0.0); + + _calculatedDisplayNodeLayout = displayNodeLayout; +} + +@end + +#pragma mark - +#pragma mark - ASDisplayNode (YogaLayout) + +@implementation ASDisplayNode (YogaLayout) + +- (BOOL)locked_shouldLayoutFromYogaRoot { +#if YOGA + YGNodeRef yogaNode = _style.yogaNode; + BOOL hasYogaParent = (_yogaParent != nil); + BOOL hasYogaChildren = (_yogaChildren.count > 0); + BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); + if (usesYoga) { + if ([self shouldHaveYogaMeasureFunc] == NO) { + return YES; + } else { + return NO; + } + } else { + return NO; + } +#else + return NO; +#endif +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.mm new file mode 100644 index 0000000000..132bc666f8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+LayoutSpec.mm @@ -0,0 +1,145 @@ +// +// ASDisplayNode+LayoutSpec.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import + +#import "_ASScopeTimer.h" +#import "ASDisplayNodeInternal.h" +#import +#import +#import "ASLayoutSpec+Subclasses.h" +#import "ASLayoutSpecPrivate.h" +#import + + +@implementation ASDisplayNode (ASLayoutSpec) + +- (void)setLayoutSpecBlock:(ASLayoutSpecBlock)layoutSpecBlock +{ + // For now there should never be an override of layoutSpecThatFits: and a layoutSpecBlock together. + ASDisplayNodeAssert(!(_methodOverrides & ASDisplayNodeMethodOverrideLayoutSpecThatFits), + @"Nodes with a .layoutSpecBlock must not also implement -layoutSpecThatFits:"); + AS::MutexLocker l(__instanceLock__); + _layoutSpecBlock = layoutSpecBlock; +} + +- (ASLayoutSpecBlock)layoutSpecBlock +{ + AS::MutexLocker l(__instanceLock__); + return _layoutSpecBlock; +} + +- (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize +{ + AS::UniqueLock l(__instanceLock__); + + // 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 + { + AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + ASTraitCollectionPropagateDown(layoutElement, self.primitiveTraitCollection); + } + + BOOL measureLayoutComputation = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutComputation; + if (measureLayoutComputation) { + _layoutComputationNumberOfPasses++; + } + + // Layout element layout creation + ASLayout *layout = ({ + AS::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; +} + +- (id)_locked_layoutElementThatFits:(ASSizeRange)constrainedSize +{ + ASAssertLocked(__instanceLock__); + + BOOL measureLayoutSpec = _measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec; + + if (_layoutSpecBlock != NULL) { + return ({ + AS::MutexLocker l(__instanceLock__); + AS::SumScopeTimer t(_layoutSpecTotalTime, measureLayoutSpec); + _layoutSpecBlock(self, constrainedSize); + }); + } else { + return ({ + AS::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]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode+UIViewBridge.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode+UIViewBridge.mm new file mode 100644 index 0000000000..084800a1ed --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode+UIViewBridge.mm @@ -0,0 +1,1325 @@ +// +// ASDisplayNode+UIViewBridge.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 "_ASPendingState.h" +#import +#import "ASDisplayNodeInternal.h" +#import +#import +#import "ASPendingStateController.h" + +/** + * The following macros are conveniences to help in the common tasks related to the bridging that ASDisplayNode does to UIView and CALayer. + * In general, a property can either be: + * - Always sent to the layer or view's layer + * use _getFromLayer / _setToLayer + * - Bridged to the view if view-backed or the layer if layer-backed + * use _getFromViewOrLayer / _setToViewOrLayer / _messageToViewOrLayer + * - Only applicable if view-backed + * use _setToViewOnly / _getFromViewOnly + * - Has differing types on views and layers, or custom ASDisplayNode-specific behavior is desired + * manually implement + * + * _bridge_prologue_write is defined to take the node's property lock. Add it at the beginning of any bridged property setters. + * _bridge_prologue_read is defined to take the node's property lock and enforce thread affinity. Add it at the beginning of any bridged property getters. + */ + +#define DISPLAYNODE_USE_LOCKS 1 + +#if DISPLAYNODE_USE_LOCKS +#define _bridge_prologue_read AS::MutexLocker l(__instanceLock__); ASDisplayNodeAssertThreadAffinity(self) +#define _bridge_prologue_write AS::MutexLocker l(__instanceLock__) +#else +#define _bridge_prologue_read ASDisplayNodeAssertThreadAffinity(self) +#define _bridge_prologue_write +#endif + +/// Returns YES if the property set should be applied to view/layer immediately. +/// Side Effect: Registers the node with the shared ASPendingStateController if +/// the property cannot be immediately applied and the node does not already have pending changes. +/// This function must be called with the node's lock already held (after _bridge_prologue_write). +/// *warning* the lock should *not* be released until the pending state is updated if this method +/// returns NO. Otherwise, the pending state can be scheduled and flushed *before* you get a chance +/// to apply it. +ASDISPLAYNODE_INLINE BOOL ASDisplayNodeShouldApplyBridgedWriteToView(ASDisplayNode *node) { + BOOL loaded = _loaded(node); + if (ASDisplayNodeThreadIsMain()) { + return loaded; + } else { + if (loaded && !ASDisplayNodeGetPendingState(node).hasChanges) { + [[ASPendingStateController sharedInstance] registerNode:node]; + } + return NO; + } +}; + +#define _getFromViewOrLayer(layerProperty, viewAndPendingViewStateProperty) _loaded(self) ? \ + (_view ? _view.viewAndPendingViewStateProperty : _layer.layerProperty )\ + : ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty + +#define _setToViewOrLayer(layerProperty, layerValueExpr, viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ + if (shouldApply) { (_view ? _view.viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr) : _layer.layerProperty = (layerValueExpr)); } else { ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr); } + +#define _setToViewOnly(viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ +if (shouldApply) { _view.viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr); } else { ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty = (viewAndPendingViewStateExpr); } + +#define _getFromViewOnly(viewAndPendingViewStateProperty) _loaded(self) ? _view.viewAndPendingViewStateProperty : ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty + +#define _getFromLayer(layerProperty) _loaded(self) ? _layer.layerProperty : ASDisplayNodeGetPendingState(self).layerProperty + +#define _setToLayer(layerProperty, layerValueExpr) BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ +if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNodeGetPendingState(self).layerProperty = (layerValueExpr); } + +/** + * This category implements certain frequently-used properties and methods of UIView and CALayer so that ASDisplayNode clients can just call the view/layer methods on the node, + * with minimal loss in performance. Unlike UIView and CALayer methods, these can be called from a non-main thread until the view or layer is created. + * This allows text sizing in -calculateSizeThatFits: (essentially a simplified layout) to happen off the main thread + * without any CALayer or UIView actually existing while still being able to set and read properties from ASDisplayNode instances. + */ +@implementation ASDisplayNode (UIViewBridge) + +#if TARGET_OS_TV +// Focus Engine +- (BOOL)canBecomeFocused +{ + return NO; +} + +- (void)setNeedsFocusUpdate +{ + ASDisplayNodeAssertMainThread(); + [_view setNeedsFocusUpdate]; +} + +- (void)updateFocusIfNeeded +{ + ASDisplayNodeAssertMainThread(); + [_view updateFocusIfNeeded]; +} + +- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context +{ + return NO; +} + +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator +{ + +} + +- (UIView *)preferredFocusedView +{ + if (self.nodeLoaded) { + return _view; + } + else { + return nil; + } +} +#endif + +- (BOOL)canBecomeFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __canBecomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __canResignFirstResponder]; +} + +- (BOOL)isFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __isFirstResponder]; +} + +- (BOOL)becomeFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __becomeFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + ASDisplayNodeAssertMainThread(); + return [self __resignFirstResponder]; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + ASDisplayNodeAssertMainThread(); + return !self.layerBacked && [self.view canPerformAction:action withSender:sender]; +} + +- (CGFloat)alpha +{ + _bridge_prologue_read; + return _getFromViewOrLayer(opacity, alpha); +} + +- (void)setAlpha:(CGFloat)newAlpha +{ + _bridge_prologue_write; + _setToViewOrLayer(opacity, newAlpha, alpha, newAlpha); +} + +- (CGFloat)cornerRadius +{ + AS::MutexLocker l(__instanceLock__); + return _cornerRadius; +} + +- (void)setCornerRadius:(CGFloat)newCornerRadius +{ + [self updateCornerRoundingWithType:self.cornerRoundingType cornerRadius:newCornerRadius]; +} + +- (ASCornerRoundingType)cornerRoundingType +{ + AS::MutexLocker l(__instanceLock__); + return _cornerRoundingType; +} + +- (void)setCornerRoundingType:(ASCornerRoundingType)newRoundingType +{ + [self updateCornerRoundingWithType:newRoundingType cornerRadius:self.cornerRadius]; +} + +- (NSString *)contentsGravity +{ + _bridge_prologue_read; + return _getFromLayer(contentsGravity); +} + +- (void)setContentsGravity:(NSString *)newContentsGravity +{ + _bridge_prologue_write; + _setToLayer(contentsGravity, newContentsGravity); +} + +- (CGRect)contentsRect +{ + _bridge_prologue_read; + return _getFromLayer(contentsRect); +} + +- (void)setContentsRect:(CGRect)newContentsRect +{ + _bridge_prologue_write; + _setToLayer(contentsRect, newContentsRect); +} + +- (CGRect)contentsCenter +{ + _bridge_prologue_read; + return _getFromLayer(contentsCenter); +} + +- (void)setContentsCenter:(CGRect)newContentsCenter +{ + _bridge_prologue_write; + _setToLayer(contentsCenter, newContentsCenter); +} + +- (CGFloat)contentsScale +{ + _bridge_prologue_read; + return _getFromLayer(contentsScale); +} + +- (void)setContentsScale:(CGFloat)newContentsScale +{ + _bridge_prologue_write; + _setToLayer(contentsScale, newContentsScale); +} + +- (CGFloat)rasterizationScale +{ + _bridge_prologue_read; + return _getFromLayer(rasterizationScale); +} + +- (void)setRasterizationScale:(CGFloat)newRasterizationScale +{ + _bridge_prologue_write; + _setToLayer(rasterizationScale, newRasterizationScale); +} + +- (CGRect)bounds +{ + _bridge_prologue_read; + return _getFromViewOrLayer(bounds, bounds); +} + +- (void)setBounds:(CGRect)newBounds +{ + _bridge_prologue_write; + _setToViewOrLayer(bounds, newBounds, bounds, newBounds); + self.threadSafeBounds = newBounds; +} + +- (CGRect)frame +{ + _bridge_prologue_read; + + // Frame is only defined when transform is identity. +//#if DEBUG +// // Checking if the transform is identity is expensive, so disable when unnecessary. We have assertions on in Release, so DEBUG is the only way I know of. +// ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"-[ASDisplayNode frame] - self.transform must be identity in order to use the frame property. (From Apple's UIView documentation: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.)"); +//#endif + + CGPoint position = self.position; + CGRect bounds = self.bounds; + CGPoint anchorPoint = self.anchorPoint; + CGPoint origin = CGPointMake(position.x - bounds.size.width * anchorPoint.x, + position.y - bounds.size.height * anchorPoint.y); + return CGRectMake(origin.x, origin.y, bounds.size.width, bounds.size.height); +} + +- (void)setFrame:(CGRect)rect +{ + BOOL setToView = NO; + BOOL setToLayer = NO; + CGRect newBounds = CGRectZero; + CGPoint newPosition = CGPointZero; + BOOL nodeLoaded = NO; + BOOL isMainThread = ASDisplayNodeThreadIsMain(); + { + _bridge_prologue_write; + + // For classes like ASTableNode, ASCollectionNode, ASScrollNode and similar - make sure UIView gets setFrame: + struct ASDisplayNodeFlags flags = _flags; + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), flags.layerBacked); + + nodeLoaded = _loaded(self); + if (!specialPropertiesHandling) { + BOOL canReadProperties = isMainThread || !nodeLoaded; + if (canReadProperties) { + // We don't have to set frame directly, and we can read current properties. + // Compute a new bounds and position and set them on self. + CALayer *layer = _layer; + CGPoint origin = (nodeLoaded ? layer.bounds.origin : self.bounds.origin); + CGPoint anchorPoint = (nodeLoaded ? layer.anchorPoint : self.anchorPoint); + + ASBoundsAndPositionForFrame(rect, origin, anchorPoint, &newBounds, &newPosition); + + if (ASIsCGRectValidForLayout(newBounds) == NO || ASIsCGPositionValidForLayout(newPosition) == NO) { + ASDisplayNodeAssertNonFatal(NO, @"-[ASDisplayNode setFrame:] - The new frame (%@) is invalid and unsafe to be set.", NSStringFromCGRect(rect)); + return; + } + + if (nodeLoaded) { + setToLayer = YES; + } else { + self.bounds = newBounds; + self.position = newPosition; + } + } else { + // We don't have to set frame directly, but we can't read properties. + // Store the frame in our pending state, and it'll get decomposed into + // bounds and position when the pending state is applied. + _ASPendingState *pendingState = ASDisplayNodeGetPendingState(self); + if (nodeLoaded && !pendingState.hasChanges) { + [[ASPendingStateController sharedInstance] registerNode:self]; + } + pendingState.frame = rect; + } + } else { + if (nodeLoaded && isMainThread) { + // We do have to set frame directly, and we're on main thread with a loaded node. + // Just set the frame on the view. + // NOTE: Frame is only defined when transform is identity because we explicitly diverge from CALayer behavior and define frame without transform. + setToView = YES; + } else { + // We do have to set frame directly, but either the node isn't loaded or we're on a non-main thread. + // Set the frame on the pending state, and it'll call setFrame: when applied. + _ASPendingState *pendingState = ASDisplayNodeGetPendingState(self); + if (nodeLoaded && !pendingState.hasChanges) { + [[ASPendingStateController sharedInstance] registerNode:self]; + } + pendingState.frame = rect; + } + } + } + + if (setToView) { + ASDisplayNodeAssertTrue(nodeLoaded && isMainThread); + _view.frame = rect; + } else if (setToLayer) { + ASDisplayNodeAssertTrue(nodeLoaded && isMainThread); + _layer.bounds = newBounds; + _layer.position = newPosition; + } +} + +- (void)setNeedsDisplay +{ + BOOL isRasterized = NO; + BOOL shouldApply = NO; + id viewOrLayer = nil; + { + _bridge_prologue_write; + isRasterized = _hierarchyState & ASHierarchyStateRasterized; + shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + viewOrLayer = _view ?: _layer; + + if (isRasterized == NO && shouldApply == NO) { + // We can't release the lock before applying to pending state, or it may be flushed before it can be applied. + [ASDisplayNodeGetPendingState(self) setNeedsDisplay]; + } + } + + if (isRasterized) { + ASPerformBlockOnMainThread(^{ + // The below operation must be performed on the main thread to ensure against an extremely rare deadlock, where a parent node + // begins materializing the view / layer hierarchy (locking itself or a descendant) while this node walks up + // the tree and requires locking that node to access .rasterizesSubtree. + // For this reason, this method should be avoided when possible. Use _hierarchyState & ASHierarchyStateRasterized. + ASDisplayNodeAssertMainThread(); + ASDisplayNode *rasterizedContainerNode = self.supernode; + while (rasterizedContainerNode) { + if (rasterizedContainerNode.rasterizesSubtree) { + break; + } + rasterizedContainerNode = rasterizedContainerNode.supernode; + } + [rasterizedContainerNode setNeedsDisplay]; + }); + } else { + if (shouldApply) { + // If not rasterized, and the node is loaded (meaning we certainly have a view or layer), send a + // message to the view/layer first. This is because __setNeedsDisplay calls as scheduleNodeForDisplay, + // which may call -displayIfNeeded. We want to ensure the needsDisplay flag is set now, and then cleared. + [viewOrLayer setNeedsDisplay]; + } + [self __setNeedsDisplay]; + } +} + +- (void)setNeedsLayout +{ + BOOL shouldApply = NO; + BOOL loaded = NO; + id viewOrLayer = nil; + { + _bridge_prologue_write; + shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + loaded = _loaded(self); + viewOrLayer = _view ?: _layer; + if (shouldApply == NO && loaded) { + // The node is loaded but we're not on main. + // We will call [self __setNeedsLayout] when we apply the pending state. + // We need to call it on main if the node is loaded to support automatic subnode management. + // We can't release the lock before applying to pending state, or it may be flushed before it can be applied. + [ASDisplayNodeGetPendingState(self) setNeedsLayout]; + } + } + + if (shouldApply) { + // The node is loaded and we're on main. + // Quite the opposite of setNeedsDisplay, we must call __setNeedsLayout before messaging + // the view or layer to ensure that measurement and implicitly added subnodes have been handled. + [self __setNeedsLayout]; + [viewOrLayer setNeedsLayout]; + } else if (loaded == NO) { + // The node is not loaded and we're not on main. + [self __setNeedsLayout]; + } +} + +- (void)layoutIfNeeded +{ + BOOL shouldApply = NO; + BOOL loaded = NO; + id viewOrLayer = nil; + { + _bridge_prologue_write; + shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + loaded = _loaded(self); + viewOrLayer = _view ?: _layer; + if (shouldApply == NO && loaded) { + // The node is loaded but we're not on main. + // We will call layoutIfNeeded on the view or layer when we apply the pending state. __layout will in turn be called on us (see -[_ASDisplayLayer layoutSublayers]). + // We need to call it on main if the node is loaded to support automatic subnode management. + // We can't release the lock before applying to pending state, or it may be flushed before it can be applied. + [ASDisplayNodeGetPendingState(self) layoutIfNeeded]; + } + } + + if (shouldApply) { + // The node is loaded and we're on main. + // Message the view or layer which in turn will call __layout on us (see -[_ASDisplayLayer layoutSublayers]). + [viewOrLayer layoutIfNeeded]; + } else if (loaded == NO) { + // The node is not loaded and we're not on main. + [self __layout]; + } +} + +- (BOOL)isOpaque +{ + _bridge_prologue_read; + return _getFromLayer(opaque); +} + +- (void)setOpaque:(BOOL)newOpaque +{ + _bridge_prologue_write; + + BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + + if (shouldApply) { + BOOL oldOpaque = _layer.opaque; + _layer.opaque = newOpaque; + if (oldOpaque != newOpaque) { + [self setNeedsDisplay]; + } + } else { + // NOTE: If we're in the background, we cannot read the current value of self.opaque (if loaded). + // When the pending state is applied to the view on main, we will call `setNeedsDisplay` if + // the new opaque value doesn't match the one on the layer. + ASDisplayNodeGetPendingState(self).opaque = newOpaque; + } +} + +- (BOOL)isUserInteractionEnabled +{ + _bridge_prologue_read; + if (_flags.layerBacked) return NO; + return _getFromViewOnly(userInteractionEnabled); +} + +- (void)setUserInteractionEnabled:(BOOL)enabled +{ + _bridge_prologue_write; + _setToViewOnly(userInteractionEnabled, enabled); +} +#if TARGET_OS_IOS +- (BOOL)isExclusiveTouch +{ + _bridge_prologue_read; + return _getFromViewOnly(exclusiveTouch); +} + +- (void)setExclusiveTouch:(BOOL)exclusiveTouch +{ + _bridge_prologue_write; + _setToViewOnly(exclusiveTouch, exclusiveTouch); +} +#endif +- (BOOL)clipsToBounds +{ + _bridge_prologue_read; + return _getFromViewOrLayer(masksToBounds, clipsToBounds); +} + +- (void)setClipsToBounds:(BOOL)clips +{ + _bridge_prologue_write; + _setToViewOrLayer(masksToBounds, clips, clipsToBounds, clips); +} + +- (CGPoint)anchorPoint +{ + _bridge_prologue_read; + return _getFromLayer(anchorPoint); +} + +- (void)setAnchorPoint:(CGPoint)newAnchorPoint +{ + _bridge_prologue_write; + _setToLayer(anchorPoint, newAnchorPoint); +} + +- (CGPoint)position +{ + _bridge_prologue_read; + return _getFromLayer(position); +} + +- (void)setPosition:(CGPoint)newPosition +{ + _bridge_prologue_write; + _setToLayer(position, newPosition); +} + +- (CGFloat)zPosition +{ + _bridge_prologue_read; + return _getFromLayer(zPosition); +} + +- (void)setZPosition:(CGFloat)newPosition +{ + _bridge_prologue_write; + _setToLayer(zPosition, newPosition); +} + +- (CATransform3D)transform +{ + _bridge_prologue_read; + return _getFromLayer(transform); +} + +- (void)setTransform:(CATransform3D)newTransform +{ + _bridge_prologue_write; + _setToLayer(transform, newTransform); +} + +- (CATransform3D)subnodeTransform +{ + _bridge_prologue_read; + return _getFromLayer(sublayerTransform); +} + +- (void)setSubnodeTransform:(CATransform3D)newSubnodeTransform +{ + _bridge_prologue_write; + _setToLayer(sublayerTransform, newSubnodeTransform); +} + +- (id)contents +{ + _bridge_prologue_read; + return _getFromLayer(contents); +} + +- (void)setContents:(id)newContents +{ + _bridge_prologue_write; + _setToLayer(contents, newContents); +} + +- (BOOL)isHidden +{ + _bridge_prologue_read; + return _getFromViewOrLayer(hidden, hidden); +} + +- (void)setHidden:(BOOL)flag +{ + _bridge_prologue_write; + _setToViewOrLayer(hidden, flag, hidden, flag); +} + +- (BOOL)needsDisplayOnBoundsChange +{ + _bridge_prologue_read; + return _getFromLayer(needsDisplayOnBoundsChange); +} + +- (void)setNeedsDisplayOnBoundsChange:(BOOL)flag +{ + _bridge_prologue_write; + _setToLayer(needsDisplayOnBoundsChange, flag); +} + +- (BOOL)autoresizesSubviews +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(autoresizesSubviews); +} + +- (void)setAutoresizesSubviews:(BOOL)flag +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(autoresizesSubviews, flag); +} + +- (UIViewAutoresizing)autoresizingMask +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(autoresizingMask); +} + +- (void)setAutoresizingMask:(UIViewAutoresizing)mask +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(autoresizingMask, mask); +} + +- (UIViewContentMode)contentMode +{ + _bridge_prologue_read; + if (_loaded(self)) { + if (_flags.layerBacked) { + return ASDisplayNodeUIContentModeFromCAContentsGravity(_layer.contentsGravity); + } else { + return _view.contentMode; + } + } else { + return ASDisplayNodeGetPendingState(self).contentMode; + } +} + +- (void)setContentMode:(UIViewContentMode)contentMode +{ + _bridge_prologue_write; + BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + if (shouldApply) { + if (_flags.layerBacked) { + _layer.contentsGravity = ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode); + } else { + _view.contentMode = contentMode; + } + } else { + ASDisplayNodeGetPendingState(self).contentMode = contentMode; + } +} + +- (void)setAccessibilityCustomActions:(NSArray *)accessibilityCustomActions +{ + _bridge_prologue_write; + BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + if (shouldApply) { + if (_flags.layerBacked) { + } else { + _view.accessibilityCustomActions = accessibilityCustomActions; + } + } else { + ASDisplayNodeGetPendingState(self).accessibilityCustomActions = accessibilityCustomActions; + } +} + +- (UIColor *)backgroundColor +{ + _bridge_prologue_read; + return [UIColor colorWithCGColor:_getFromLayer(backgroundColor)]; +} + +- (void)setBackgroundColor:(UIColor *)newBackgroundColor +{ + _bridge_prologue_write; + + CGColorRef newBackgroundCGColor = CGColorRetain([newBackgroundColor CGColor]); + BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); + + if (shouldApply) { + CGColorRef oldBackgroundCGColor = CGColorRetain(_layer.backgroundColor); + + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), _flags.layerBacked); + if (specialPropertiesHandling) { + _view.backgroundColor = newBackgroundColor; + } else { + _layer.backgroundColor = newBackgroundCGColor; + } + + if (!CGColorEqualToColor(oldBackgroundCGColor, newBackgroundCGColor)) { + [self setNeedsDisplay]; + } + + CGColorRelease(oldBackgroundCGColor); + } else { + // NOTE: If we're in the background, we cannot read the current value of bgcolor (if loaded). + // When the pending state is applied to the view on main, we will call `setNeedsDisplay` if + // the new background color doesn't match the one on the layer. + ASDisplayNodeGetPendingState(self).backgroundColor = newBackgroundCGColor; + } + CGColorRelease(newBackgroundCGColor); +} + +- (UIColor *)tintColor +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(tintColor); +} + +- (void)setTintColor:(UIColor *)color +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(tintColor, color); +} + +- (void)tintColorDidChange +{ + // ignore this, allow subclasses to be notified +} + +- (CGColorRef)shadowColor +{ + _bridge_prologue_read; + return _getFromLayer(shadowColor); +} + +- (void)setShadowColor:(CGColorRef)colorValue +{ + _bridge_prologue_write; + _setToLayer(shadowColor, colorValue); +} + +- (CGFloat)shadowOpacity +{ + _bridge_prologue_read; + return _getFromLayer(shadowOpacity); +} + +- (void)setShadowOpacity:(CGFloat)opacity +{ + _bridge_prologue_write; + _setToLayer(shadowOpacity, opacity); +} + +- (CGSize)shadowOffset +{ + _bridge_prologue_read; + return _getFromLayer(shadowOffset); +} + +- (void)setShadowOffset:(CGSize)offset +{ + _bridge_prologue_write; + _setToLayer(shadowOffset, offset); +} + +- (CGFloat)shadowRadius +{ + _bridge_prologue_read; + return _getFromLayer(shadowRadius); +} + +- (void)setShadowRadius:(CGFloat)radius +{ + _bridge_prologue_write; + _setToLayer(shadowRadius, radius); +} + +- (CGFloat)borderWidth +{ + _bridge_prologue_read; + return _getFromLayer(borderWidth); +} + +- (void)setBorderWidth:(CGFloat)width +{ + _bridge_prologue_write; + _setToLayer(borderWidth, width); +} + +- (CGColorRef)borderColor +{ + _bridge_prologue_read; + return _getFromLayer(borderColor); +} + +- (void)setBorderColor:(CGColorRef)colorValue +{ + _bridge_prologue_write; + _setToLayer(borderColor, colorValue); +} + +- (BOOL)allowsGroupOpacity +{ + _bridge_prologue_read; + return _getFromLayer(allowsGroupOpacity); +} + +- (void)setAllowsGroupOpacity:(BOOL)allowsGroupOpacity +{ + _bridge_prologue_write; + _setToLayer(allowsGroupOpacity, allowsGroupOpacity); +} + +- (BOOL)allowsEdgeAntialiasing +{ + _bridge_prologue_read; + return _getFromLayer(allowsEdgeAntialiasing); +} + +- (void)setAllowsEdgeAntialiasing:(BOOL)allowsEdgeAntialiasing +{ + _bridge_prologue_write; + _setToLayer(allowsEdgeAntialiasing, allowsEdgeAntialiasing); +} + +- (unsigned int)edgeAntialiasingMask +{ + _bridge_prologue_read; + return _getFromLayer(edgeAntialiasingMask); +} + +- (void)setEdgeAntialiasingMask:(unsigned int)edgeAntialiasingMask +{ + _bridge_prologue_write; + _setToLayer(edgeAntialiasingMask, edgeAntialiasingMask); +} + +- (UISemanticContentAttribute)semanticContentAttribute +{ + _bridge_prologue_read; + return _getFromViewOnly(semanticContentAttribute); +} + +- (void)setSemanticContentAttribute:(UISemanticContentAttribute)semanticContentAttribute +{ + _bridge_prologue_write; + _setToViewOnly(semanticContentAttribute, semanticContentAttribute); +#if YOGA + [self semanticContentAttributeDidChange:semanticContentAttribute]; +#endif +} + +- (UIEdgeInsets)layoutMargins +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + UIEdgeInsets margins = _getFromViewOnly(layoutMargins); + + if (!AS_AT_LEAST_IOS11 && self.insetsLayoutMarginsFromSafeArea) { + UIEdgeInsets safeArea = self.safeAreaInsets; + margins = ASConcatInsets(margins, safeArea); + } + + return margins; +} + +- (void)setLayoutMargins:(UIEdgeInsets)layoutMargins +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(layoutMargins, layoutMargins); +} + +- (BOOL)preservesSuperviewLayoutMargins +{ + _bridge_prologue_read; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + return _getFromViewOnly(preservesSuperviewLayoutMargins); +} + +- (void)setPreservesSuperviewLayoutMargins:(BOOL)preservesSuperviewLayoutMargins +{ + _bridge_prologue_write; + ASDisplayNodeAssert(!_flags.layerBacked, @"Danger: this property is undefined on layer-backed nodes."); + _setToViewOnly(preservesSuperviewLayoutMargins, preservesSuperviewLayoutMargins); +} + +- (void)layoutMarginsDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (self.automaticallyRelayoutOnLayoutMarginsChanges) { + [self setNeedsLayout]; + } +} + +- (UIEdgeInsets)safeAreaInsets +{ + _bridge_prologue_read; + + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked && _loaded(self)) { + return self.view.safeAreaInsets; + } + } + return _fallbackSafeAreaInsets; +} + +- (BOOL)insetsLayoutMarginsFromSafeArea +{ + _bridge_prologue_read; + + return [self _locked_insetsLayoutMarginsFromSafeArea]; +} + +- (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)insetsLayoutMarginsFromSafeArea +{ + ASDisplayNodeAssertThreadAffinity(self); + BOOL shouldNotifyAboutUpdate; + { + _bridge_prologue_write; + + _fallbackInsetsLayoutMarginsFromSafeArea = insetsLayoutMarginsFromSafeArea; + + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked) { + _setToViewOnly(insetsLayoutMarginsFromSafeArea, insetsLayoutMarginsFromSafeArea); + } + } + + shouldNotifyAboutUpdate = _loaded(self) && (!AS_AT_LEAST_IOS11 || _flags.layerBacked); + } + + if (shouldNotifyAboutUpdate) { + [self layoutMarginsDidChange]; + } +} + +- (void)safeAreaInsetsDidChange +{ + ASDisplayNodeAssertMainThread(); + + if (self.automaticallyRelayoutOnSafeAreaChanges) { + [self setNeedsLayout]; + } + + [self _fallbackUpdateSafeAreaOnChildren]; +} + +@end + +@implementation ASDisplayNode (InternalPropertyBridge) + +- (CGFloat)layerCornerRadius +{ + _bridge_prologue_read; + return _getFromLayer(cornerRadius); +} + +- (void)setLayerCornerRadius:(CGFloat)newLayerCornerRadius +{ + _bridge_prologue_write; + _setToLayer(cornerRadius, newLayerCornerRadius); +} + +- (BOOL)_locked_insetsLayoutMarginsFromSafeArea +{ + ASAssertLocked(__instanceLock__); + if (AS_AVAILABLE_IOS(11.0)) { + if (!_flags.layerBacked) { + return _getFromViewOnly(insetsLayoutMarginsFromSafeArea); + } + } + return _fallbackInsetsLayoutMarginsFromSafeArea; +} + +@end + +#pragma mark - UIViewBridgeAccessibility + +// ASDK supports accessibility for view or layer backed nodes. To be able to provide support for layer backed +// nodes, properties for all of the UIAccessibility protocol defined properties need to be provided an held in sync +// between node and view + +// Helper function with following logic: +// - If the node is not loaded yet use the property from the pending state +// - In case the node is loaded +// - Check if the node has a view and get the value from the view if loaded or from the pending state +// - If view is not available, e.g. the node is layer backed return the property value +#define _getAccessibilityFromViewOrProperty(nodeProperty, viewAndPendingViewStateProperty) _loaded(self) ? \ +(_view ? _view.viewAndPendingViewStateProperty : nodeProperty )\ +: ASDisplayNodeGetPendingState(self).viewAndPendingViewStateProperty + +// Helper function to set property values on pending state or view and property if loaded +#define _setAccessibilityToViewAndProperty(nodeProperty, nodeValueExpr, viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) \ +nodeProperty = nodeValueExpr; _setToViewOnly(viewAndPendingViewStateProperty, viewAndPendingViewStateExpr) + +@implementation ASDisplayNode (UIViewBridgeAccessibility) + +// iOS 11 only properties. Add this to silence "unimplemented selector" warnings +// in old SDKs. If the caller doesn't respect our API_AVAILABLE attributes, then they +// get an appropriate "unrecognized selector" runtime error. +#if __IPHONE_OS_VERSION_MAX_ALLOWED < __IPHONE_11_0 +@dynamic accessibilityAttributedLabel, accessibilityAttributedHint, accessibilityAttributedValue; +#endif + +- (BOOL)isAccessibilityElement +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_isAccessibilityElement, isAccessibilityElement); +} + +- (void)setIsAccessibilityElement:(BOOL)isAccessibilityElement +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_isAccessibilityElement, isAccessibilityElement, isAccessibilityElement, isAccessibilityElement); +} + +- (NSString *)accessibilityLabel +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityLabel, accessibilityLabel); +} + +- (void)setAccessibilityLabel:(NSString *)accessibilityLabel +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityLabel, accessibilityLabel, accessibilityLabel); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + NSAttributedString *accessibilityAttributedLabel = accessibilityLabel ? [[NSAttributedString alloc] initWithString:accessibilityLabel] : nil; + _setAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel); + } +#endif +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 +- (NSAttributedString *)accessibilityAttributedLabel +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel); +} + +- (void)setAccessibilityAttributedLabel:(NSAttributedString *)accessibilityAttributedLabel +{ + _bridge_prologue_write; + { _setAccessibilityToViewAndProperty(_accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel, accessibilityAttributedLabel); } + { _setAccessibilityToViewAndProperty(_accessibilityLabel, accessibilityAttributedLabel.string, accessibilityLabel, accessibilityAttributedLabel.string); } +} +#endif + +- (NSString *)accessibilityHint +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityHint, accessibilityHint); +} + +- (void)setAccessibilityHint:(NSString *)accessibilityHint +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityHint, accessibilityHint, accessibilityHint); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + NSAttributedString *accessibilityAttributedHint = accessibilityHint ? [[NSAttributedString alloc] initWithString:accessibilityHint] : nil; + _setAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint); + } +#endif +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 +- (NSAttributedString *)accessibilityAttributedHint +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedHint, accessibilityAttributedHint); +} + +- (void)setAccessibilityAttributedHint:(NSAttributedString *)accessibilityAttributedHint +{ + _bridge_prologue_write; + { _setAccessibilityToViewAndProperty(_accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint, accessibilityAttributedHint); } + + { _setAccessibilityToViewAndProperty(_accessibilityHint, accessibilityAttributedHint.string, accessibilityHint, accessibilityAttributedHint.string); } +} +#endif + +- (NSString *)accessibilityValue +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityValue, accessibilityValue); +} + +- (void)setAccessibilityValue:(NSString *)accessibilityValue +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityValue, accessibilityValue, accessibilityValue); +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + NSAttributedString *accessibilityAttributedValue = accessibilityValue ? [[NSAttributedString alloc] initWithString:accessibilityValue] : nil; + _setAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue); + } +#endif +} + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 +- (NSAttributedString *)accessibilityAttributedValue +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityAttributedValue, accessibilityAttributedValue); +} + +- (void)setAccessibilityAttributedValue:(NSAttributedString *)accessibilityAttributedValue +{ + _bridge_prologue_write; + { _setAccessibilityToViewAndProperty(_accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue, accessibilityAttributedValue); } + { _setAccessibilityToViewAndProperty(_accessibilityValue, accessibilityAttributedValue.string, accessibilityValue, accessibilityAttributedValue.string); } +} +#endif + +- (UIAccessibilityTraits)accessibilityTraits +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityTraits, accessibilityTraits); +} + +- (void)setAccessibilityTraits:(UIAccessibilityTraits)accessibilityTraits +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityTraits, accessibilityTraits, accessibilityTraits, accessibilityTraits); +} + +- (CGRect)accessibilityFrame +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityFrame, accessibilityFrame); +} + +- (void)setAccessibilityFrame:(CGRect)accessibilityFrame +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityFrame, accessibilityFrame, accessibilityFrame, accessibilityFrame); +} + +- (NSString *)accessibilityLanguage +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityLanguage, accessibilityLanguage); +} + +- (void)setAccessibilityLanguage:(NSString *)accessibilityLanguage +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityLanguage, accessibilityLanguage, accessibilityLanguage, accessibilityLanguage); +} + +- (BOOL)accessibilityElementsHidden +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityElementsHidden, accessibilityElementsHidden); +} + +- (void)setAccessibilityElementsHidden:(BOOL)accessibilityElementsHidden +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityElementsHidden, accessibilityElementsHidden, accessibilityElementsHidden, accessibilityElementsHidden); +} + +- (BOOL)accessibilityViewIsModal +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityViewIsModal, accessibilityViewIsModal); +} + +- (void)setAccessibilityViewIsModal:(BOOL)accessibilityViewIsModal +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityViewIsModal, accessibilityViewIsModal, accessibilityViewIsModal, accessibilityViewIsModal); +} + +- (BOOL)shouldGroupAccessibilityChildren +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_shouldGroupAccessibilityChildren, shouldGroupAccessibilityChildren); +} + +- (void)setShouldGroupAccessibilityChildren:(BOOL)shouldGroupAccessibilityChildren +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_shouldGroupAccessibilityChildren, shouldGroupAccessibilityChildren, shouldGroupAccessibilityChildren, shouldGroupAccessibilityChildren); +} + +- (NSString *)accessibilityIdentifier +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityIdentifier, accessibilityIdentifier); +} + +- (void)setAccessibilityIdentifier:(NSString *)accessibilityIdentifier +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityIdentifier, accessibilityIdentifier, accessibilityIdentifier, accessibilityIdentifier); +} + +- (void)setAccessibilityNavigationStyle:(UIAccessibilityNavigationStyle)accessibilityNavigationStyle +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityNavigationStyle, accessibilityNavigationStyle, accessibilityNavigationStyle, accessibilityNavigationStyle); +} + +- (UIAccessibilityNavigationStyle)accessibilityNavigationStyle +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityNavigationStyle, accessibilityNavigationStyle); +} + +#if TARGET_OS_TV +- (void)setAccessibilityHeaderElements:(NSArray *)accessibilityHeaderElements +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityHeaderElements, accessibilityHeaderElements, accessibilityHeaderElements, accessibilityHeaderElements); +} + +- (NSArray *)accessibilityHeaderElements +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityHeaderElements, accessibilityHeaderElements); +} +#endif + +- (void)setAccessibilityActivationPoint:(CGPoint)accessibilityActivationPoint +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityActivationPoint, accessibilityActivationPoint, accessibilityActivationPoint, accessibilityActivationPoint); +} + +- (CGPoint)accessibilityActivationPoint +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityActivationPoint, accessibilityActivationPoint); +} + +- (void)setAccessibilityPath:(UIBezierPath *)accessibilityPath +{ + _bridge_prologue_write; + _setAccessibilityToViewAndProperty(_accessibilityPath, accessibilityPath, accessibilityPath, accessibilityPath); +} + +- (UIBezierPath *)accessibilityPath +{ + _bridge_prologue_read; + return _getAccessibilityFromViewOrProperty(_accessibilityPath, accessibilityPath); +} + +- (NSInteger)accessibilityElementCount +{ + _bridge_prologue_read; + return _getFromViewOnly(accessibilityElementCount); +} + +@end + + +#pragma mark - ASAsyncTransactionContainer + +@implementation ASDisplayNode (ASAsyncTransactionContainer) + +- (BOOL)asyncdisplaykit_isAsyncTransactionContainer +{ + _bridge_prologue_read; + return _getFromViewOrLayer(asyncdisplaykit_isAsyncTransactionContainer, asyncdisplaykit_isAsyncTransactionContainer); +} + +- (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)asyncTransactionContainer +{ + _bridge_prologue_write; + _setToViewOrLayer(asyncdisplaykit_asyncTransactionContainer, asyncTransactionContainer, asyncdisplaykit_asyncTransactionContainer, asyncTransactionContainer); +} + +- (ASAsyncTransactionContainerState)asyncdisplaykit_asyncTransactionContainerState +{ + ASDisplayNodeAssertMainThread(); + return [_layer asyncdisplaykit_asyncTransactionContainerState]; +} + +- (void)asyncdisplaykit_cancelAsyncTransactions +{ + ASDisplayNodeAssertMainThread(); + [_layer asyncdisplaykit_cancelAsyncTransactions]; +} + +- (void)asyncdisplaykit_setCurrentAsyncTransaction:(_ASAsyncTransaction *)transaction +{ + _layer.asyncdisplaykit_currentAsyncTransaction = transaction; +} + +- (_ASAsyncTransaction *)asyncdisplaykit_currentAsyncTransaction +{ + return _layer.asyncdisplaykit_currentAsyncTransaction; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm new file mode 100644 index 0000000000..e8607e273c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNode.mm @@ -0,0 +1,3803 @@ +// +// 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 "ASDisplayNodeInternal.h" + +#import +#import +#import +#import "ASLayoutSpec+Subclasses.h" + +#import +#include + +#import +#import "_ASAsyncTransactionContainer+Private.h" +#import +#import +#import +#import "_ASPendingState.h" +#import "_ASScopeTimer.h" +#import +#import +#import +#import +#import +#import +#import +#import +#import "ASLayoutElementStylePrivate.h" +#import +#import "ASLayoutSpecPrivate.h" +#import +#import +#import "ASSignpost.h" +#import +#import "ASWeakProxy.h" +#import "ASResponderChainEnumerator.h" + +// Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) +#if TIME_DISPLAYNODE_OPS + #define TIME_SCOPED(outVar) AS::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 + +using AS::MutexLocker; + +static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil; + +// 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 suppressesInvalidCollectionUpdateExceptions = ATOMIC_VAR_INIT(NO); +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; +} + +void StubImplementationWithNoArgs(id receiver) {} +void StubImplementationWithSizeRange(id receiver, ASSizeRange sr) {} +void StubImplementationWithTwoInterfaceStates(id receiver, ASInterfaceState s0, ASInterfaceState s1) {} + +/** + * 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:@"); + + // Add stub implementations for global methods that the client didn't + // implement in a category. We do this instead of hard-coding empty + // implementations to avoid a linker warning when it merges categories. + // Note: addMethod will not do anything if a method already exists. + if (self == ASDisplayNode.class) { + IMP noArgsImp = (IMP)StubImplementationWithNoArgs; + class_addMethod(self, @selector(baseDidInit), noArgsImp, "v@:"); + class_addMethod(self, @selector(baseWillDealloc), noArgsImp, "v@:"); + class_addMethod(self, @selector(didLoad), noArgsImp, "v@:"); + class_addMethod(self, @selector(layoutDidFinish), noArgsImp, "v@:"); + class_addMethod(self, @selector(didEnterPreloadState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didExitPreloadState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didEnterDisplayState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didExitDisplayState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didEnterVisibleState), noArgsImp, "v@:"); + class_addMethod(self, @selector(didExitVisibleState), noArgsImp, "v@:"); + class_addMethod(self, @selector(hierarchyDisplayDidFinish), noArgsImp, "v@:"); + class_addMethod(self, @selector(asyncTraitCollectionDidChange), noArgsImp, "v@:"); + class_addMethod(self, @selector(calculatedLayoutDidChange), noArgsImp, "v@:"); + + auto type0 = "v@:" + std::string(@encode(ASSizeRange)); + class_addMethod(self, @selector(willCalculateLayout:), (IMP)StubImplementationWithSizeRange, type0.c_str()); + + auto interfaceStateType = std::string(@encode(ASInterfaceState)); + auto type1 = "v@:" + interfaceStateType + interfaceStateType; + class_addMethod(self, @selector(interfaceStateDidChange:fromState:), (IMP)StubImplementationWithTwoInterfaceStates, type1.c_str()); + } +} + +#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 = ASDefaultTransactionPriority; + + _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; + + [self baseDidInit]; + 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 +{ + AS::UniqueLock l(__instanceLock__); + + if ([self _locked_isNodeLoaded]) { + ASDisplayNodeAssertThreadAffinity(self); + l.unlock(); + body(self); + return; + } else if (_onDidLoadBlocks == nil) { + _onDidLoadBlocks = [NSMutableArray arrayWithObject:body]; + } else { + [_onDidLoadBlocks addObject:body]; + } +} + +- (void)dealloc +{ + _flags.isDeallocating = YES; + [self baseWillDealloc]; + + // 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"); + TIME_SCOPED(_debugTimeForDidLoad); + + [self didLoad]; + + __instanceLock__.lock(); + const auto onDidLoadBlocks = ASTransferStrong(_onDidLoadBlocks); + __instanceLock__.unlock(); + + for (ASDisplayNodeDidLoadBlock block in onDidLoadBlocks) { + block(self); + } + [self enumerateInterfaceStateDelegates:^(id del) { + [del nodeDidLoad]; + }]; +} + +- (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 { + MutexLocker l(__instanceLock__); + return [self _locked_isNodeLoaded]; + } +} + +- (BOOL)_locked_isNodeLoaded +{ + ASAssertLocked(__instanceLock__); + return _loaded(self); +} + +#pragma mark - Misc Setter / Getter + +- (UIView *)view +{ + AS::UniqueLock 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. + l.unlock(); + [self __setNeedsLayout]; + l.lock(); + } + + [self _locked_applyPendingStateToViewOrLayer]; + + // The following methods should not be called with a lock + l.unlock(); + + // 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 +{ + AS::UniqueLock 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]; + CALayer *layer = _layer; + + // 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. + l.unlock(); + [self __setNeedsLayout]; + l.lock(); + } + + [self _locked_applyPendingStateToViewOrLayer]; + + // The following methods should not be called with a lock + l.unlock(); + + // 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 +{ + 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); + + 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 +{ + MutexLocker l(__instanceLock__); + return _flags.layerBacked; +} + +- (BOOL)supportsLayerBacking +{ + MutexLocker l(__instanceLock__); + return !checkFlag(Synchronous) && !_flags.viewEverHadAGestureRecognizerAttached && _viewClass == [_ASDisplayView class] && _layerClass == [_ASDisplayLayer class]; +} + +- (BOOL)shouldAnimateSizeChanges +{ + MutexLocker l(__instanceLock__); + return _flags.shouldAnimateSizeChanges; +} + +- (void)setShouldAnimateSizeChanges:(BOOL)shouldAnimateSizeChanges +{ + MutexLocker l(__instanceLock__); + _flags.shouldAnimateSizeChanges = shouldAnimateSizeChanges; +} + +- (CGRect)threadSafeBounds +{ + MutexLocker l(__instanceLock__); + return [self _locked_threadSafeBounds]; +} + +- (CGRect)_locked_threadSafeBounds +{ + ASAssertLocked(__instanceLock__); + return _threadSafeBounds; +} + +- (void)setThreadSafeBounds:(CGRect)newBounds +{ + MutexLocker l(__instanceLock__); + _threadSafeBounds = newBounds; +} + +- (void)nodeViewDidAddGestureRecognizer +{ + MutexLocker l(__instanceLock__); + _flags.viewEverHadAGestureRecognizerAttached = YES; +} + +- (UIEdgeInsets)fallbackSafeAreaInsets +{ + MutexLocker l(__instanceLock__); + return _fallbackSafeAreaInsets; +} + +- (void)setFallbackSafeAreaInsets:(UIEdgeInsets)insets +{ + BOOL needsManualUpdate; + BOOL updatesLayoutMargins; + + { + 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 +{ + MutexLocker l(__instanceLock__); + return _isViewControllerRoot; +} + +- (void)setViewControllerRoot:(BOOL)flag +{ + MutexLocker l(__instanceLock__); + _isViewControllerRoot = flag; +} + +- (BOOL)automaticallyRelayoutOnSafeAreaChanges +{ + MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnSafeAreaChanges; +} + +- (void)setAutomaticallyRelayoutOnSafeAreaChanges:(BOOL)flag +{ + MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnSafeAreaChanges = flag; +} + +- (BOOL)automaticallyRelayoutOnLayoutMarginsChanges +{ + MutexLocker l(__instanceLock__); + return _automaticallyRelayoutOnLayoutMarginsChanges; +} + +- (void)setAutomaticallyRelayoutOnLayoutMarginsChanges:(BOOL)flag +{ + MutexLocker l(__instanceLock__); + _automaticallyRelayoutOnLayoutMarginsChanges = flag; +} + +#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 +{ + MutexLocker l(__instanceLock__); + return _debugName; +} + +- (void)setDebugName:(NSString *)debugName +{ + MutexLocker l(__instanceLock__); + if (!ASObjectIsEqual(_debugName, debugName)) { + _debugName = [debugName copy]; + } +} + +#pragma mark - Layout + +#pragma mark + +- (BOOL)canLayoutAsynchronous +{ + return !self.isNodeLoaded; +} + +#pragma mark Layout Pass + +- (void)__setNeedsLayout +{ + [self invalidateCalculatedLayout]; +} + +- (void)invalidateCalculatedLayout +{ + MutexLocker l(__instanceLock__); + + _layoutVersion++; + + _unflattenedLayout = nil; + +#if YOGA + [self invalidateCalculatedYogaLayout]; +#endif +} + +- (void)__layout +{ + ASDisplayNodeAssertThreadAffinity(self); + // ASAssertUnlocked(__instanceLock__); + + BOOL loaded = NO; + { + AS::UniqueLock 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. + 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; + } + + // 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). + l.unlock(); + [self _u_measureNodeWithBoundsIfNecessary:bounds]; + l.lock(); + + [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 +{ + ASDisplayNodeAssertMainThread(); + // ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertTrue(self.isNodeLoaded); + [self layoutDidFinish]; +} + +#pragma mark Calculation + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize +{ +#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]; + +#if AS_KDEBUG_ENABLE + if (--tls_callDepth == 0) { + ASSignpostEnd(ASSignpostCalculateLayout); + } +#endif + + return result; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + switch (self.layoutEngineType) { + case ASLayoutEngineTypeLayoutSpec: + return [self calculateLayoutLayoutSpec:constrainedSize]; +#if YOGA + case ASLayoutEngineTypeYoga: + return [self calculateLayoutYoga:constrainedSize]; +#endif + // If YOGA is not defined but for some reason the layout type engine is Yoga + // we explicitly fallthrough here + default: + break; + } + + // If this case is reached a layout type engine was defined for a node that is currently + // not supported. + ASDisplayNodeAssert(NO, @"No layout type determined"); + return nil; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + __ASDisplayNodeCheckForLayoutMethodOverrides; + + ASDisplayNodeLogEvent(self, @"calculateSizeThatFits: with constrainedSize: %@", NSStringFromCGSize(constrainedSize)); + + return ASIsCGSizeValidForSize(constrainedSize) ? constrainedSize : CGSizeZero; +} + +- (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]; +} + +#pragma mark - Display + +NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification = @"ASRenderingEngineDidDisplayScheduledNodes"; +NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp = @"ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp"; + +- (BOOL)displaysAsynchronously +{ + 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); + + 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 +{ + MutexLocker l(__instanceLock__); + return _flags.rasterizesSubtree; +} + +- (void)enableSubtreeRasterization +{ + 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 +{ + MutexLocker l(__instanceLock__); + + return _contentsScaleForDisplay; +} + +- (void)setContentsScaleForDisplay:(CGFloat)contentsScaleForDisplay +{ + 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; + { + 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)}]; + } + }]; + }); + + [renderQueue enqueue:node]; +} + +/// Helper method to summarize whether or not the node run through the display process +- (BOOL)_implementsDisplay +{ + 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(); + } +} + +// 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 +{ + 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 +{ + MutexLocker l(__instanceLock__); + _flags.shouldBypassEnsureDisplay = shouldBypassEnsureDisplay; +} + +- (BOOL)shouldBypassEnsureDisplay +{ + MutexLocker l(__instanceLock__); + return _flags.shouldBypassEnsureDisplay; +} + +- (void)setNeedsDisplayAtScale:(CGFloat)contentsScale +{ + { + 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 +{ +} + +- (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 +{ + 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]; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (void)displayWillStart {} +#pragma clang diagnostic pop +- (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 +{ + MutexLocker l(__instanceLock__); + return _supernode; +} + +- (void)_setSupernode:(ASDisplayNode *)newSupernode +{ + BOOL supernodeDidChange = NO; + ASDisplayNode *oldSupernode = nil; + { + 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) { + + // Now that we have a supernode, propagate its traits to self. + // This should be done before possibly forcing self to load so we have traits in -didLoad + ASTraitCollectionPropagateDown(self, newSupernode.primitiveTraitCollection); + + if (!parentWasOrIsRasterized && newSupernode.nodeLoaded) { + // Trigger the subnode to load its layer, which will create its view if it needs one. + // By doing this prior to the downward propagation of newSupernode's interface state, + // we can guarantee that -didEnterVisibleState is only called with .isNodeLoaded = YES. + [self layer]; + } + + [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; + }); + } + } + } + } 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 +{ + 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); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + + 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; + { + 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; + { + 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); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // 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; + { + 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); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // 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; + { + 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); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // ASAssertUnlocked(__instanceLock__); + + if (subnode == nil) { + ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); + return; + } + + NSInteger sublayerIndex = NSNotFound; + { + 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); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // 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); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // 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); + // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 + // 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) { + } + + // 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. + MutexLocker l(__instanceLock__); + return _flags.visibilityNotificationsDisabled > 0; +} + +- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled +{ + 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 +{ + MutexLocker l(__instanceLock__); + return _flags.isInHierarchy; +} + +- (void)__enterHierarchy +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(!_flags.isEnteringHierarchy, @"Should not cause recursive __enterHierarchy"); + 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"); + 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 +{ + MutexLocker l(__instanceLock__); + return _hierarchyState; +} + +- (void)setHierarchyState:(ASHierarchyState)newState +{ + ASHierarchyState oldState = ASHierarchyStateNormal; + { + 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 + MutexLocker l(__instanceLock__); + _pendingTransitionID = ASLayoutElementContextInvalidTransitionID; + _pendingLayoutTransition = nil; + } + } + + ASDisplayNodeLogEvent(self, @"setHierarchyState: %@", NSStringFromASHierarchyStateChange(oldState, newState)); +} + +- (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 (ASCATransactionQueueGet().enabled) { + __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 (!ASCATransactionQueueGet().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 +{ + 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 +{ + // 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 +{ + MutexLocker l(__instanceLock__); + return _interfaceState; +} + +- (void)setInterfaceState:(ASInterfaceState)newState +{ + if (!ASCATransactionQueueGet().enabled) { + [self applyPendingInterfaceState:newState]; + } else { + MutexLocker l(__instanceLock__); + if (_pendingInterfaceState != newState) { + _pendingInterfaceState = newState; + [ASCATransactionQueueGet() enqueue:self]; + } + } +} + +- (ASInterfaceState)pendingInterfaceState +{ + 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; + { + MutexLocker l(__instanceLock__); + // newPendingState will not be used when ASCATransactionQueue is enabled + // and use _pendingInterfaceState instead for interfaceState update. + if (!ASCATransactionQueueGet().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) { + } + + 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 +{ + ASAssertUnlocked(__instanceLock__); + ASDisplayNodeAssertMainThread(); + [self interfaceStateDidChange:newState fromState:oldState]; + [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 +{ + 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 +{ + MutexLocker l(__instanceLock__); + for (int i = 0; i < AS_MAX_INTERFACE_STATE_DELEGATES; i++) { + if (_interfaceStateDelegates[i] == interfaceStateDelegate) { + _interfaceStateDelegates[i] = nil; + break; + } + } +} + +- (BOOL)isVisible +{ + MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesVisible(_interfaceState); +} + +- (void)_didEnterVisibleState +{ + ASDisplayNodeAssertMainThread(); + +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + // Rasterized node's loading state is merged with root node of rasterized tree. + if (!(self.hierarchyState & ASHierarchyStateRasterized)) { + ASDisplayNodeAssert(self.isNodeLoaded, @"Node should be loaded before entering visible state."); + } +#endif + + ASAssertUnlocked(__instanceLock__); + [self didEnterVisibleState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterVisibleState]; + }]; + +#if AS_ENABLE_TIPS + [ASTipsController.shared nodeDidAppear:self]; +#endif +} + +- (void)_didExitVisibleState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self didExitVisibleState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitVisibleState]; + }]; +} + +- (BOOL)isInDisplayState +{ + MutexLocker l(__instanceLock__); + return ASInterfaceStateIncludesDisplay(_interfaceState); +} + +- (void)_didEnterDisplayState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self didEnterDisplayState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterDisplayState]; + }]; +} + +- (void)_didExitDisplayState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self didExitDisplayState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitDisplayState]; + }]; +} + +- (BOOL)isInPreloadState +{ + 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__); + [self didEnterPreloadState]; + + // 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 && !ASActivateExperimentalFeature(ASExperimentalDidEnterPreloadSkipASMLayout)) { + [self layoutIfNeeded]; + } + [self enumerateInterfaceStateDelegates:^(id del) { + [del didEnterPreloadState]; + }]; +} + +- (void)_didExitPreloadState +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + [self didExitPreloadState]; + [self enumerateInterfaceStateDelegates:^(id del) { + [del didExitPreloadState]; + }]; +} + +- (void)clearContents +{ + ASDisplayNodeAssertMainThread(); + ASAssertUnlocked(__instanceLock__); + + 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 +{ + MutexLocker l(__instanceLock__); + _hitTestSlop = hitTestSlop; +} + +- (UIEdgeInsets)hitTestSlop +{ + 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__); + + AS::UniqueLock 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. + l.unlock(); + [self __setNeedsLayout]; + l.lock(); + } + + [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 +{ + MutexLocker l(__instanceLock__); + _measurementOptions = measurementOptions; +} + +- (ASDisplayNodePerformanceMeasurementOptions)measurementOptions +{ + MutexLocker l(__instanceLock__); + return _measurementOptions; +} + +- (ASDisplayNodePerformanceMeasurements)performanceMeasurements +{ + 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 +{ + MutexLocker l(__instanceLock__); + _isAccessibilityContainer = isAccessibilityContainer; +} + +- (BOOL)isAccessibilityContainer +{ + MutexLocker l(__instanceLock__); + return _isAccessibilityContainer; +} + +- (NSString *)defaultAccessibilityLabel +{ + return nil; +} + +- (NSString *)defaultAccessibilityHint +{ + return nil; +} + +- (NSString *)defaultAccessibilityValue +{ + return nil; +} + +- (NSString *)defaultAccessibilityIdentifier +{ + return nil; +} + +- (UIAccessibilityTraits)defaultAccessibilityTraits +{ + return UIAccessibilityTraitNone; +} + +#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(); + const auto 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]; + } +} + +@end + +#pragma mark - ASDisplayNode (Debugging) + +@implementation ASDisplayNode (Debugging) + ++ (void)setShouldStoreUnflattenedLayouts:(BOOL)shouldStore +{ + storesUnflattenedLayouts.store(shouldStore); +} + ++ (BOOL)shouldStoreUnflattenedLayouts +{ + return storesUnflattenedLayouts.load(); +} + +- (ASLayout *)unflattenedCalculatedLayout +{ + MutexLocker l(__instanceLock__); + return _unflattenedLayout; +} + ++ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses +{ + suppressesInvalidCollectionUpdateExceptions.store(suppresses); +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" ++ (BOOL)suppressesInvalidCollectionUpdateExceptions +{ + return suppressesInvalidCollectionUpdateExceptions.load(); +} +#pragma clang diagnostic pop + +- (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(); + MutexLocker l(__instanceLock__); + const auto 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/submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.mm b/submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.mm new file mode 100644 index 0000000000..02871409ca --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNodeExtras.mm @@ -0,0 +1,339 @@ +// +// ASDisplayNodeExtras.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 "ASDisplayNodeInternal.h" +#import +#import +#import + +#import +#import + +void ASPerformMainThreadDeallocation(id _Nullable __strong * _Nonnull objectPtr) { + /** + * UIKit components must be deallocated on the main thread. We use this shared + * run loop queue to gradually deallocate them across many turns of the main run loop. + */ + static ASRunLoopQueue *queue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + queue = [[ASRunLoopQueue alloc] initWithRunLoop:CFRunLoopGetMain() retainObjects:YES handler:nil]; + queue.batchSize = 10; + }); + + if (objectPtr != NULL && *objectPtr != nil) { + // TODO: If ASRunLoopQueue supported an "unsafe_unretained" mode, we could + // transfer the caller's +1 into it and save the retain/release pair. + + // Lock queue while enqueuing and releasing, so that there's no risk + // that the queue will release before we get a chance to release. + [queue lock]; + [queue enqueue:*objectPtr]; // Retain, +1 + *objectPtr = nil; // Release, +0 + [queue unlock]; // (After queue drains), release, -1 + } +} + +void _ASSetDebugNames(Class _Nonnull owningClass, NSString * _Nonnull names, ASDisplayNode * _Nullable object, ...) +{ + NSString *owningClassName = NSStringFromClass(owningClass); + NSArray *nameArray = [names componentsSeparatedByString:@", "]; + va_list args; + va_start(args, object); + NSInteger i = 0; + for (ASDisplayNode *node = object; node != nil; node = va_arg(args, id), i++) { + NSMutableString *symbolName = [nameArray[i] mutableCopy]; + // Remove any `self.` or `_` prefix + [symbolName replaceOccurrencesOfString:@"self." withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, symbolName.length)]; + [symbolName replaceOccurrencesOfString:@"_" withString:@"" options:NSAnchoredSearch range:NSMakeRange(0, symbolName.length)]; + node.debugName = [NSString stringWithFormat:@"%@.%@", owningClassName, symbolName]; + } + ASDisplayNodeCAssert(nameArray.count == i, @"Malformed call to ASSetDebugNames: %@", names); + va_end(args); +} + +ASInterfaceState ASInterfaceStateForDisplayNode(ASDisplayNode *displayNode, UIWindow *window) +{ + ASDisplayNodeCAssert(![displayNode isLayerBacked], @"displayNode must not be layer backed as it may have a nil window"); + if (displayNode && [displayNode supportsRangeManagedInterfaceState]) { + // Directly clear the visible bit if we are not in a window. This means that the interface state is, + // if not already, about to be set to invisible as it is not possible for an element to be visible + // while outside of a window. + ASInterfaceState interfaceState = displayNode.pendingInterfaceState; + return (window == nil ? (interfaceState &= (~ASInterfaceStateVisible)) : interfaceState); + } else { + // For not range managed nodes we might be on our own to try to guess if we're visible. + return (window == nil ? ASInterfaceStateNone : (ASInterfaceStateVisible | ASInterfaceStateDisplay)); + } +} + +ASDisplayNode *ASLayerToDisplayNode(CALayer *layer) +{ + return layer.asyncdisplaykit_node; +} + +ASDisplayNode *ASViewToDisplayNode(UIView *view) +{ + return view.asyncdisplaykit_node; +} + +void ASDisplayNodePerformBlockOnEveryNode(CALayer * _Nullable layer, ASDisplayNode * _Nullable node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node)) +{ + if (!node) { + ASDisplayNodeCAssertNotNil(layer, @"Cannot recursively perform with nil node and nil layer"); + ASDisplayNodeCAssertMainThread(); + node = ASLayerToDisplayNode(layer); + } + + if (node) { + block(node); + } + if (traverseSublayers && !layer && [node isNodeLoaded] && ASDisplayNodeThreadIsMain()) { + layer = node.layer; + } + + if (traverseSublayers && layer && node.rasterizesSubtree == NO) { + /// NOTE: The docs say `sublayers` returns a copy, but it does not. + /// See: http://stackoverflow.com/questions/14854480/collection-calayerarray-0x1ed8faa0-was-mutated-while-being-enumerated + for (CALayer *sublayer in [[layer sublayers] copy]) { + ASDisplayNodePerformBlockOnEveryNode(sublayer, nil, traverseSublayers, block); + } + } else if (node) { + for (ASDisplayNode *subnode in [node subnodes]) { + ASDisplayNodePerformBlockOnEveryNode(nil, subnode, traverseSublayers, block); + } + } +} + +void ASDisplayNodePerformBlockOnEveryNodeBFS(ASDisplayNode *node, void(^block)(ASDisplayNode *node)) +{ + // Queue used to keep track of subnodes while traversing this layout in a BFS fashion. + std::queue queue; + queue.push(node); + + while (!queue.empty()) { + node = queue.front(); + queue.pop(); + + block(node); + + // Add all subnodes to process in next step + for (ASDisplayNode *subnode in node.subnodes) { + queue.push(subnode); + } + } +} + +void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node)) +{ + for (ASDisplayNode *subnode in node.subnodes) { + ASDisplayNodePerformBlockOnEveryNode(nil, subnode, YES, block); + } +} + +ASDisplayNode *ASDisplayNodeFindFirstSupernode(ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) +{ + // This function has historically started with `self` but the name suggests + // that it wouldn't. Perhaps we should change the behavior. + for (ASDisplayNode *ancestor in node.supernodesIncludingSelf) { + if (block(ancestor)) { + return ancestor; + } + } + return nil; +} + +__kindof ASDisplayNode *ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c) +{ + // This function has historically started with `self` but the name suggests + // that it wouldn't. Perhaps we should change the behavior. + return [start supernodeOfClass:c includingSelf:YES]; +} + +static void _ASCollectDisplayNodes(NSMutableArray *array, CALayer *layer) +{ + ASDisplayNode *node = ASLayerToDisplayNode(layer); + + if (nil != node) { + [array addObject:node]; + } + + for (CALayer *sublayer in layer.sublayers) + _ASCollectDisplayNodes(array, sublayer); +} + +NSArray *ASCollectDisplayNodes(ASDisplayNode *node) +{ + NSMutableArray *list = [[NSMutableArray alloc] init]; + for (CALayer *sublayer in node.layer.sublayers) { + _ASCollectDisplayNodes(list, sublayer); + } + return list; +} + +#pragma mark - Find all subnodes + +static void _ASDisplayNodeFindAllSubnodes(NSMutableArray *array, ASDisplayNode *node, BOOL (^block)(ASDisplayNode *node)) +{ + if (!node) + return; + + for (ASDisplayNode *subnode in node.subnodes) { + if (block(subnode)) { + [array addObject:subnode]; + } + + _ASDisplayNodeFindAllSubnodes(array, subnode, block); + } +} + +NSArray *ASDisplayNodeFindAllSubnodes(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)) +{ + NSMutableArray *list = [[NSMutableArray alloc] init]; + _ASDisplayNodeFindAllSubnodes(list, start, block); + return list; +} + +NSArray<__kindof ASDisplayNode *> *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c) +{ + return ASDisplayNodeFindAllSubnodes(start, ^(ASDisplayNode *n) { + return [n isKindOfClass:c]; + }); +} + +#pragma mark - Find first subnode + +static ASDisplayNode *_ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL includeStartNode, BOOL (^block)(ASDisplayNode *node)) +{ + for (ASDisplayNode *subnode in startNode.subnodes) { + ASDisplayNode *foundNode = _ASDisplayNodeFindFirstNode(subnode, YES, block); + if (foundNode) { + return foundNode; + } + } + + if (includeStartNode && block(startNode)) + return startNode; + + return nil; +} + +__kindof ASDisplayNode *ASDisplayNodeFindFirstNode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) +{ + return _ASDisplayNodeFindFirstNode(startNode, YES, block); +} + +__kindof ASDisplayNode *ASDisplayNodeFindFirstSubnode(ASDisplayNode *startNode, BOOL (^block)(ASDisplayNode *node)) +{ + return _ASDisplayNodeFindFirstNode(startNode, NO, block); +} + +__kindof ASDisplayNode *ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) +{ + return ASDisplayNodeFindFirstSubnode(start, ^(ASDisplayNode *n) { + return [n isKindOfClass:c]; + }); +} + +static inline BOOL _ASDisplayNodeIsAncestorOfDisplayNode(ASDisplayNode *possibleAncestor, ASDisplayNode *possibleDescendant) +{ + ASDisplayNode *supernode = possibleDescendant; + while (supernode) { + if (supernode == possibleAncestor) { + return YES; + } + supernode = supernode.supernode; + } + + return NO; +} + +UIWindow * _Nullable ASFindWindowOfLayer(CALayer *layer) +{ + UIView *view = ASFindClosestViewOfLayer(layer); + if (UIWindow *window = ASDynamicCast(view, UIWindow)) { + return window; + } else { + return view.window; + } +} + +UIView * _Nullable ASFindClosestViewOfLayer(CALayer *layer) +{ + while (layer != nil) { + if (UIView *view = ASDynamicCast(layer.delegate, UIView)) { + return view; + } + layer = layer.superlayer; + } + return nil; +} + +ASDisplayNode *ASDisplayNodeFindClosestCommonAncestor(ASDisplayNode *node1, ASDisplayNode *node2) +{ + ASDisplayNode *possibleAncestor = node1; + while (possibleAncestor) { + if (_ASDisplayNodeIsAncestorOfDisplayNode(possibleAncestor, node2)) { + break; + } + possibleAncestor = possibleAncestor.supernode; + } + + ASDisplayNodeCAssertNotNil(possibleAncestor, @"Could not find a common ancestor between node1: %@ and node2: %@", node1, node2); + return possibleAncestor; +} + +ASDisplayNode *ASDisplayNodeUltimateParentOfNode(ASDisplayNode *node) +{ + // node <- supernode on each loop + // previous <- node on each loop where node is not nil + // previous is the final non-nil value of supernode, i.e. the root node + ASDisplayNode *previousNode = node; + while ((node = [node supernode])) { + previousNode = node; + } + return previousNode; +} + +#pragma mark - Placeholders + +UIColor *ASDisplayNodeDefaultPlaceholderColor() +{ + static UIColor *defaultPlaceholderColor; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultPlaceholderColor = [UIColor colorWithWhite:0.95 alpha:1.0]; + }); + return defaultPlaceholderColor; +} + +UIColor *ASDisplayNodeDefaultTintColor() +{ + static UIColor *defaultTintColor; + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + defaultTintColor = [UIColor colorWithRed:0.0 green:0.478 blue:1.0 alpha:1.0]; + }); + return defaultTintColor; +} + +#pragma mark - Hierarchy Notifications + +void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node) +{ + [node __incrementVisibilityNotificationsDisabled]; +} + +void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node) +{ + [node __decrementVisibilityNotificationsDisabled]; +} diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNodeInternal.h b/submodules/AsyncDisplayKit/Source/ASDisplayNodeInternal.h new file mode 100644 index 0000000000..88b268c696 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNodeInternal.h @@ -0,0 +1,407 @@ +// +// ASDisplayNodeInternal.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 +// + +// +// The following methods are ONLY for use by _ASDisplayLayer, _ASDisplayView, and ASDisplayNode. +// These methods must never be called or overridden by other classes. +// + +#import +#import +#import +#import +#import "ASLayoutTransition.h" +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol _ASDisplayLayerDelegate; +@class _ASDisplayLayer; +@class _ASPendingState; +@class ASNodeController; +struct ASDisplayNodeFlags; + +BOOL ASDisplayNodeSubclassOverridesSelector(Class subclass, SEL selector); +BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLayerBacked); + +/// Get the pending view state for the node, creating one if needed. +_ASPendingState * ASDisplayNodeGetPendingState(ASDisplayNode * node); + +typedef NS_OPTIONS(NSUInteger, ASDisplayNodeMethodOverrides) +{ + ASDisplayNodeMethodOverrideNone = 0, + ASDisplayNodeMethodOverrideTouchesBegan = 1 << 0, + ASDisplayNodeMethodOverrideTouchesCancelled = 1 << 1, + ASDisplayNodeMethodOverrideTouchesEnded = 1 << 2, + ASDisplayNodeMethodOverrideTouchesMoved = 1 << 3, + ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4, + ASDisplayNodeMethodOverrideCalcLayoutThatFits = 1 << 5, + ASDisplayNodeMethodOverrideCalcSizeThatFits = 1 << 6, + ASDisplayNodeMethodOverrideCanBecomeFirstResponder= 1 << 7, + ASDisplayNodeMethodOverrideBecomeFirstResponder = 1 << 8, + ASDisplayNodeMethodOverrideCanResignFirstResponder= 1 << 9, + ASDisplayNodeMethodOverrideResignFirstResponder = 1 << 10, + ASDisplayNodeMethodOverrideIsFirstResponder = 1 << 11, +}; + +typedef NS_OPTIONS(uint_least32_t, ASDisplayNodeAtomicFlags) +{ + Synchronous = 1 << 0, + YogaLayoutInProgress = 1 << 1, +}; + +// Can be called without the node's lock. Client is responsible for thread safety. +#define _loaded(node) (node->_layer != nil) + +#define checkFlag(flag) ((_atomicFlags.load() & flag) != 0) +// Returns the old value of the flag as a BOOL. +#define setFlag(flag, x) (((x ? _atomicFlags.fetch_or(flag) \ + : _atomicFlags.fetch_and(~flag)) & flag) != 0) + +AS_EXTERN NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification; +AS_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp; + +// Allow 2^n increments of begin disabling hierarchy notifications +#define VISIBILITY_NOTIFICATIONS_DISABLED_BITS 4 + +#define TIME_DISPLAYNODE_OPS 0 // If you're using this information frequently, try: (DEBUG || PROFILE) + +#define NUM_CLIP_CORNER_LAYERS 4 + +@interface ASDisplayNode () <_ASTransitionContextCompletionDelegate> +{ +@package + AS::RecursiveMutex __instanceLock__; + + _ASPendingState *_pendingViewState; + ASInterfaceState _pendingInterfaceState; + ASInterfaceState _preExitingInterfaceState; + + UIView *_view; + CALayer *_layer; + + std::atomic _atomicFlags; + + struct ASDisplayNodeFlags { + // public properties + unsigned viewEverHadAGestureRecognizerAttached:1; + unsigned layerBacked:1; + unsigned displaysAsynchronously:1; + unsigned rasterizesSubtree:1; + unsigned shouldBypassEnsureDisplay:1; + unsigned displaySuspended:1; + unsigned shouldAnimateSizeChanges:1; + + // Wrapped view handling + + // The layer contents should not be cleared in case the node is wrapping a UIImageView.UIImageView is specifically + // optimized for performance and does not use the usual way to provide the contents of the CALayer via the + // CALayerDelegate method that backs the UIImageView. + unsigned canClearContentsOfLayer:1; + + // Prevent calling setNeedsDisplay on a layer that backs a UIImageView. Usually calling setNeedsDisplay on a CALayer + // triggers a recreation of the contents of layer unfortunately calling it on a CALayer that backs a UIImageView + // it goes through the normal flow to assign the contents to a layer via the CALayerDelegate methods. Unfortunately + // UIImageView does not do recreate the layer contents the usual way, it actually does not implement some of the + // methods at all instead it throws away the contents of the layer and nothing will show up. + unsigned canCallSetNeedsDisplayOfLayer:1; + + unsigned implementsDrawRect:1; + unsigned implementsImageDisplay:1; + unsigned implementsDrawParameters:1; + + // internal state + unsigned isEnteringHierarchy:1; + unsigned isExitingHierarchy:1; + unsigned isInHierarchy:1; + unsigned visibilityNotificationsDisabled:VISIBILITY_NOTIFICATIONS_DISABLED_BITS; + unsigned isDeallocating:1; + } _flags; + +@protected + ASDisplayNode * __weak _supernode; + NSMutableArray *_subnodes; + + ASNodeController *_strongNodeController; + __weak ASNodeController *_weakNodeController; + + // Set this to nil whenever you modify _subnodes + NSArray *_cachedSubnodes; + + std::atomic_uint _displaySentinel; + + // This is the desired contentsScale, not the scale at which the layer's contents should be displayed + CGFloat _contentsScaleForDisplay; + ASDisplayNodeMethodOverrides _methodOverrides; + + UIEdgeInsets _hitTestSlop; + +#if ASEVENTLOG_ENABLE + ASEventLog *_eventLog; +#endif + + + // Layout support + ASLayoutElementStyle *_style; + std::atomic _primitiveTraitCollection; + + // Layout Spec + ASLayoutSpecBlock _layoutSpecBlock; + NSString *_debugName; + +#if YOGA + // Only ASDisplayNodes are supported in _yogaChildren currently. This means that it is necessary to + // create ASDisplayNodes to make a stack layout when using Yoga. + // However, the implementation is mostly ready for id , with a few areas requiring updates. + NSMutableArray *_yogaChildren; + __weak ASDisplayNode *_yogaParent; + ASLayout *_yogaCalculatedLayout; +#endif + + // Automatically manages subnodes + BOOL _automaticallyManagesSubnodes; // Main thread only + + // Layout Transition + _ASTransitionContext *_pendingLayoutTransitionContext; + NSTimeInterval _defaultLayoutTransitionDuration; + NSTimeInterval _defaultLayoutTransitionDelay; + UIViewAnimationOptions _defaultLayoutTransitionOptions; + + std::atomic _transitionID; + std::atomic _pendingTransitionID; + ASLayoutTransition *_pendingLayoutTransition; + ASDisplayNodeLayout _calculatedDisplayNodeLayout; + ASDisplayNodeLayout _pendingDisplayNodeLayout; + + /// Sentinel for layout data. Incremented when we get -setNeedsLayout / -invalidateCalculatedLayout. + /// Starts at 1. + std::atomic _layoutVersion; + + + // Layout Spec performance measurement + ASDisplayNodePerformanceMeasurementOptions _measurementOptions; + NSTimeInterval _layoutSpecTotalTime; + NSInteger _layoutSpecNumberOfPasses; + NSTimeInterval _layoutComputationTotalTime; + NSInteger _layoutComputationNumberOfPasses; + + + // View Loading + ASDisplayNodeViewBlock _viewBlock; + ASDisplayNodeLayerBlock _layerBlock; + NSMutableArray *_onDidLoadBlocks; + Class _viewClass; // nil -> _ASDisplayView + Class _layerClass; // nil -> _ASDisplayLayer + + + // Placeholder support + UIImage *_placeholderImage; + BOOL _placeholderEnabled; + CALayer *_placeholderLayer; + + // keeps track of nodes/subnodes that have not finished display, used with placeholders + ASWeakSet *_pendingDisplayNodes; + + + // Corner Radius support + CGFloat _cornerRadius; + ASCornerRoundingType _cornerRoundingType; + CALayer *_clipCornerLayers[NUM_CLIP_CORNER_LAYERS]; + + ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext; + ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext; + + + // Accessibility support + BOOL _isAccessibilityElement; + NSString *_accessibilityLabel; + NSAttributedString *_accessibilityAttributedLabel; + NSString *_accessibilityHint; + NSAttributedString *_accessibilityAttributedHint; + NSString *_accessibilityValue; + NSAttributedString *_accessibilityAttributedValue; + UIAccessibilityTraits _accessibilityTraits; + CGRect _accessibilityFrame; + NSString *_accessibilityLanguage; + BOOL _accessibilityElementsHidden; + BOOL _accessibilityViewIsModal; + BOOL _shouldGroupAccessibilityChildren; + NSString *_accessibilityIdentifier; + UIAccessibilityNavigationStyle _accessibilityNavigationStyle; + NSArray *_accessibilityHeaderElements; + CGPoint _accessibilityActivationPoint; + UIBezierPath *_accessibilityPath; + BOOL _isAccessibilityContainer; + + + // Safe Area support + // These properties are used on iOS 10 and lower, where safe area is not supported by UIKit. + UIEdgeInsets _fallbackSafeAreaInsets; + BOOL _fallbackInsetsLayoutMarginsFromSafeArea; + + BOOL _automaticallyRelayoutOnSafeAreaChanges; + BOOL _automaticallyRelayoutOnLayoutMarginsChanges; + + BOOL _isViewControllerRoot; + + +#pragma mark - ASDisplayNode (Debugging) + ASLayout *_unflattenedLayout; + +#if TIME_DISPLAYNODE_OPS +@public + NSTimeInterval _debugTimeToCreateView; + NSTimeInterval _debugTimeToApplyPendingState; + NSTimeInterval _debugTimeToAddSubnodeViews; + NSTimeInterval _debugTimeForDidLoad; +#endif + + /// Fast path: tells whether we've ever had an interface state delegate before. + BOOL _hasHadInterfaceStateDelegates; + __weak id _interfaceStateDelegates[AS_MAX_INTERFACE_STATE_DELEGATES]; +} + ++ (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node; + +/// The _ASDisplayLayer backing the node, if any. +@property (nullable, nonatomic, readonly) _ASDisplayLayer *asyncLayer; + +/// Bitmask to check which methods an object overrides. +- (ASDisplayNodeMethodOverrides)methodOverrides; + +/** + * Invoked before a call to setNeedsLayout to the underlying view + */ +- (void)__setNeedsLayout; + +/** + * Invoked after a call to setNeedsDisplay to the underlying view + */ +- (void)__setNeedsDisplay; + +/** + * Called whenever the node needs to layout its subnodes and, if it's already loaded, its subviews. Executes the layout pass for the node + * + * This method is thread-safe but asserts thread affinity. + */ +- (void)__layout; + +/** + * Internal method to add / replace / insert subnode and remove from supernode without checking if + * node has automaticallyManagesSubnodes set to YES. + */ +- (void)_addSubnode:(ASDisplayNode *)subnode; +- (void)_replaceSubnode:(ASDisplayNode *)oldSubnode withSubnode:(ASDisplayNode *)replacementSubnode; +- (void)_insertSubnode:(ASDisplayNode *)subnode belowSubnode:(ASDisplayNode *)below; +- (void)_insertSubnode:(ASDisplayNode *)subnode aboveSubnode:(ASDisplayNode *)above; +- (void)_insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx; +- (void)_removeFromSupernodeIfEqualTo:(ASDisplayNode *)supernode; +- (void)_removeFromSupernode; + +// Private API for helper functions / unit tests. Use ASDisplayNodeDisableHierarchyNotifications() to control this. +- (BOOL)__visibilityNotificationsDisabled; +- (BOOL)__selfOrParentHasVisibilityNotificationsDisabled; +- (void)__incrementVisibilityNotificationsDisabled; +- (void)__decrementVisibilityNotificationsDisabled; + +// Helper methods for UIResponder forwarding +- (BOOL)__canBecomeFirstResponder; +- (BOOL)__becomeFirstResponder; +- (BOOL)__canResignFirstResponder; +- (BOOL)__resignFirstResponder; +- (BOOL)__isFirstResponder; + +/// Helper method to summarize whether or not the node run through the display process +- (BOOL)_implementsDisplay; + +/// Display the node's view/layer immediately on the current thread, bypassing the background thread rendering. Will be deprecated. +- (void)displayImmediately; + +/// Refreshes any precomposited or drawn clip corners, setting up state as required to transition radius or rounding type. +- (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType cornerRadius:(CGFloat)newCornerRadius; + +/// Alternative initialiser for backing with a custom view class. Supports asynchronous display with _ASDisplayView subclasses. +- (instancetype)initWithViewClass:(Class)viewClass; + +/// Alternative initialiser for backing with a custom layer class. Supports asynchronous display with _ASDisplayLayer subclasses. +- (instancetype)initWithLayerClass:(Class)layerClass; + +@property (nonatomic) CGFloat contentsScaleForDisplay; + +- (void)applyPendingViewState; + +/** + * Makes a local copy of the interface state delegates then calls the block on each. + * + * Lock is not held during block invocation. Method must not be called with the lock held. + */ +- (void)enumerateInterfaceStateDelegates:(void(NS_NOESCAPE ^)(id delegate))block; + +/** + * // TODO: NOT YET IMPLEMENTED + * + * @abstract Prevents interface state changes from affecting the node, until disabled. + * + * @discussion Useful to avoid flashing after removing a node from the hierarchy and re-adding it. + * Removing a node from the hierarchy will cause it to exit the Display state, clearing its contents. + * For some animations, it's desirable to be able to remove a node without causing it to re-display. + * Once re-enabled, the interface state will be updated to the same value it would have been. + * + * @see ASInterfaceState + */ +@property (nonatomic) BOOL interfaceStateSuspended; + +/** + * This method has proven helpful in a few rare scenarios, similar to a category extension on UIView, + * but it's considered private API for now and its use should not be encouraged. + * @param checkViewHierarchy If YES, and no supernode can be found, method will walk up from `self.view` to find a supernode. + * If YES, this method must be called on the main thread and the node must not be layer-backed. + */ +- (nullable ASDisplayNode *)_supernodeWithClass:(Class)supernodeClass checkViewHierarchy:(BOOL)checkViewHierarchy; + +/** + * Whether this node rasterizes its descendants. See -enableSubtreeRasterization. + */ +@property (readonly) BOOL rasterizesSubtree; + +/** + * Called if a gesture recognizer was attached to an _ASDisplayView + */ +- (void)nodeViewDidAddGestureRecognizer; + +// Recalculates fallbackSafeAreaInsets for the subnodes +- (void)_fallbackUpdateSafeAreaOnChildren; + +@end + +@interface ASDisplayNode (InternalPropertyBridge) + +@property (nonatomic) CGFloat layerCornerRadius; + +- (BOOL)_locked_insetsLayoutMarginsFromSafeArea; + +@end + +@interface ASDisplayNode (ASLayoutElementPrivate) + +/** + * Returns the internal style object or creates a new if no exists. Need to be called with lock held. + */ +- (ASLayoutElementStyle *)_locked_style; + +/** + * Returns the current layout element. Need to be called with lock held. + */ +- (id)_locked_layoutElementThatFits:(ASSizeRange)constrainedSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASDisplayNodeLayout.h b/submodules/AsyncDisplayKit/Source/ASDisplayNodeLayout.h new file mode 100644 index 0000000000..af905a813a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASDisplayNodeLayout.h @@ -0,0 +1,59 @@ +// +// ASDisplayNodeLayout.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 +// + +#pragma once + +#import +#import + +@class ASLayout; + +/* + * Represents a connection between an ASLayout and a ASDisplayNode + * ASDisplayNode uses this to store additional information that are necessary besides the layout + */ +struct ASDisplayNodeLayout { + ASLayout *layout; + ASSizeRange constrainedSize; + CGSize parentSize; + BOOL requestedLayoutFromAbove; + NSUInteger version; + + /* + * Create a new display node layout with + * @param layout The layout to associate, usually returned from a call to -layoutThatFits:parentSize: + * @param constrainedSize Constrained size used to create the layout + * @param parentSize Parent size used to create the layout + * @param version The version of the source layout data – see ASDisplayNode's _layoutVersion. + */ + ASDisplayNodeLayout(ASLayout *layout, ASSizeRange constrainedSize, CGSize parentSize, NSUInteger version) + : layout(layout), constrainedSize(constrainedSize), parentSize(parentSize), requestedLayoutFromAbove(NO), version(version) {}; + + /* + * Creates a layout without any layout associated. By default this display node layout is dirty. + */ + ASDisplayNodeLayout() + : layout(nil), constrainedSize({{0, 0}, {0, 0}}), parentSize({0, 0}), requestedLayoutFromAbove(NO), version(0) {}; + + /** + * Returns whether this is valid for a given version + */ + BOOL isValid(NSUInteger versionArg) { + return layout != nil && version >= versionArg; + } + + /** + * Returns whether this is valid for a given constrained size, parent size, and version + */ + BOOL isValid(ASSizeRange theConstrainedSize, CGSize theParentSize, NSUInteger versionArg) { + return isValid(versionArg) + && CGSizeEqualToSize(parentSize, theParentSize) + && ASSizeRangeEqualToSizeRange(constrainedSize, theConstrainedSize); + } +}; diff --git a/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm new file mode 100644 index 0000000000..87baeb09ac --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASEditableTextNode.mm @@ -0,0 +1,1172 @@ +// +// ASEditableTextNode.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 "ASTextNodeWordKerner.h" +#import + +@implementation ASEditableTextNodeTargetForAction + +- (instancetype)initWithTarget:(id _Nullable)target { + self = [super init]; + if (self != nil) { + _target = target; + } + return self; +} + +@end + +/** + @abstract Object to hold UITextView's pending UITextInputTraits +**/ +@interface _ASTextInputTraitsPendingState : NSObject + +@property UITextAutocapitalizationType autocapitalizationType; +@property UITextAutocorrectionType autocorrectionType; +@property UITextSpellCheckingType spellCheckingType; +@property UIKeyboardAppearance keyboardAppearance; +@property UIKeyboardType keyboardType; +@property UIReturnKeyType returnKeyType; +@property BOOL enablesReturnKeyAutomatically; +@property (getter=isSecureTextEntry) BOOL secureTextEntry; + +@end + +@implementation _ASTextInputTraitsPendingState + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + // set default values, as defined in Apple's comments in UITextInputTraits.h + _autocapitalizationType = UITextAutocapitalizationTypeSentences; + _autocorrectionType = UITextAutocorrectionTypeDefault; + _spellCheckingType = UITextSpellCheckingTypeDefault; + _keyboardAppearance = UIKeyboardAppearanceDefault; + _keyboardType = UIKeyboardTypeDefault; + _returnKeyType = UIReturnKeyDefault; + + return self; +} + +@end + +/** + @abstract As originally reported in rdar://14729288, when scrollEnabled = NO, + UITextView does not calculate its contentSize. This makes it difficult + for a client to embed a UITextView inside a different scroll view with + other content (setting scrollEnabled = NO on the UITextView itself, + because the containing scroll view will handle the gesture)... + because accessing contentSize is typically necessary to perform layout. + Apple later closed the issue as expected behavior. This works around + the issue by ensuring that contentSize is always calculated, while + still providing control over the UITextView's scrolling. + + See issue: https://github.com/facebook/AsyncDisplayKit/issues/1063 + */ + +@interface ASPanningOverriddenUITextView : ASTextKitComponentsTextView +{ + BOOL _shouldBlockPanGesture; +} + +@property (nonatomic, copy) bool (^shouldCopy)(); +@property (nonatomic, copy) bool (^shouldPaste)(); +@property (nonatomic, copy) ASEditableTextNodeTargetForAction *(^targetForActionImpl)(SEL); +@property (nonatomic, copy) bool (^shouldReturn)(); +@property (nonatomic, copy) void (^backspaceWhileEmpty)(); + +@property (nonatomic, strong) NSString * _Nullable initialPrimaryLanguage; +@property (nonatomic) bool initializedPrimaryInputLanguage; + +@end + +@implementation ASPanningOverriddenUITextView + +#if TARGET_OS_IOS + // tvOS doesn't support self.scrollsToTop +- (BOOL)scrollEnabled +{ + return _shouldBlockPanGesture; +} + +- (void)setScrollEnabled:(BOOL)scrollEnabled +{ + _shouldBlockPanGesture = !scrollEnabled; + self.scrollsToTop = scrollEnabled; + + [super setScrollEnabled:YES]; +} + +- (void)setContentSize:(CGSize)contentSize { + [super setContentSize:contentSize]; +} + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + if (_targetForActionImpl) { + ASEditableTextNodeTargetForAction *result = _targetForActionImpl(action); + if (result) { + return result.target != nil; + } + } + + if (action == @selector(paste:)) { + NSArray *items = [UIMenuController sharedMenuController].menuItems; + if (((UIMenuItem *)items.firstObject).action == @selector(toggleBoldface:)) { + return false; + } + return true; + } + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wundeclared-selector" + static SEL promptForReplaceSelector; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + promptForReplaceSelector = NSSelectorFromString(@"_promptForReplace:"); + }); + if (action == promptForReplaceSelector) { + return false; + } +#pragma clang diagnostic pop + + if (action == @selector(toggleUnderline:)) { + return false; + } + + return [super canPerformAction:action withSender:sender]; +} + +- (id)targetForAction:(SEL)action withSender:(id)__unused sender +{ + if (_targetForActionImpl) { + ASEditableTextNodeTargetForAction *result = _targetForActionImpl(action); + if (result) { + return result.target; + } + } + return [super targetForAction:action withSender:sender]; +} + +- (void)copy:(id)sender { + if (_shouldCopy == nil || _shouldCopy()) { + [super copy:sender]; + } +} + +- (void)paste:(id)sender +{ + if (_shouldPaste == nil || _shouldPaste()) { + [super paste:sender]; + } +} + +- (NSArray *)keyCommands { + UIKeyCommand *plainReturn = [UIKeyCommand keyCommandWithInput:@"\r" modifierFlags:kNilOptions action:@selector(handlePlainReturn:)]; + return @[ + plainReturn + ]; +} + +- (void)handlePlainReturn:(id)__unused sender { + if (_shouldReturn) { + _shouldReturn(); + } +} + +- (void)deleteBackward { + bool notify = self.text.length == 0; + [super deleteBackward]; + if (notify) { + if (_backspaceWhileEmpty) { + _backspaceWhileEmpty(); + } + } +} + +- (UIKeyboardAppearance)keyboardAppearance { + return [super keyboardAppearance]; +} + +- (UITextInputMode *)textInputMode { + if (!_initializedPrimaryInputLanguage) { + _initializedPrimaryInputLanguage = true; + if (_initialPrimaryLanguage != nil) { + for (UITextInputMode *inputMode in [UITextInputMode activeInputModes]) { + NSString *primaryLanguage = inputMode.primaryLanguage; + if (primaryLanguage != nil && [primaryLanguage isEqualToString:_initialPrimaryLanguage]) { + return inputMode; + } + } + } + } + return [super textInputMode]; +} + +#endif + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + // Never allow our pans to begin when _shouldBlockPanGesture is true. + if (_shouldBlockPanGesture && gestureRecognizer == self.panGestureRecognizer) + return NO; + + // Otherwise, proceed as usual. + if ([UITextView instancesRespondToSelector:_cmd]) + return [super gestureRecognizerShouldBegin:gestureRecognizer]; + return YES; +} + +@end + +#pragma mark - +@interface ASEditableTextNode () +{ + @private + // Configuration. + NSDictionary *_typingAttributes; + + // Core. + id __weak _delegate; + BOOL _delegateDidUpdateEnqueued; + + // TextKit. + AS::RecursiveMutex _textKitLock; + ASTextKitComponents *_textKitComponents; + ASTextKitComponents *_placeholderTextKitComponents; + // Forwards NSLayoutManagerDelegate methods related to word kerning + ASTextNodeWordKerner *_wordKerner; + + // UITextInputTraits + AS::RecursiveMutex _textInputTraitsLock; + _ASTextInputTraitsPendingState *_textInputTraits; + + // Misc. State. + BOOL _displayingPlaceholder; // Defaults to YES. + BOOL _isPreservingSelection; + BOOL _isPreservingText; + BOOL _selectionChangedForEditedText; + NSRange _previousSelectedRange; +} + +@property (nonatomic, readonly) _ASTextInputTraitsPendingState *textInputTraits; + +@end + +@implementation ASEditableTextNode + +#pragma mark - NSObject Overrides +- (instancetype)init +{ + return [self initWithTextKitComponents:[ASTextKitComponents componentsWithAttributedSeedString:nil textContainerSize:CGSizeZero] + placeholderTextKitComponents:[ASTextKitComponents componentsWithAttributedSeedString:nil textContainerSize:CGSizeZero]]; +} + +- (instancetype)initWithTextKitComponents:(ASTextKitComponents *)textKitComponents + placeholderTextKitComponents:(ASTextKitComponents *)placeholderTextKitComponents +{ + if (!(self = [super init])) + return nil; + + _displayingPlaceholder = YES; + _scrollEnabled = YES; + + // Create the scaffolding for the text view. + _textKitComponents = textKitComponents; + _textKitComponents.layoutManager.delegate = self; + _wordKerner = [[ASTextNodeWordKerner alloc] init]; + _textContainerInset = UIEdgeInsetsZero; + + // Create the placeholder scaffolding. + _placeholderTextKitComponents = placeholderTextKitComponents; + _placeholderTextKitComponents.layoutManager.delegate = self; + + return self; +} + +#pragma mark - ASDisplayNode Overrides +- (void)didLoad +{ + [super didLoad]; + + void (^configureTextView)(UITextView *) = ^(UITextView *textView) { + if (!_displayingPlaceholder || textView != _textKitComponents.textView) { + // If showing the placeholder, don't propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and if it's opaque/colored then it'll obscure the placeholder. + textView.backgroundColor = self.backgroundColor; + textView.opaque = self.opaque; + } else if (_displayingPlaceholder && textView == _textKitComponents.textView) { + // The default backgroundColor for a textView is white. Due to the reason described above, make sure the editable textView starts out transparent. + textView.backgroundColor = nil; + textView.opaque = NO; + } + textView.textContainerInset = self.textContainerInset; + + // Configure textView with UITextInputTraits + { + AS::MutexLocker l(_textInputTraitsLock); + if (_textInputTraits) { + textView.autocapitalizationType = _textInputTraits.autocapitalizationType; + textView.autocorrectionType = _textInputTraits.autocorrectionType; + textView.spellCheckingType = _textInputTraits.spellCheckingType; + textView.keyboardType = _textInputTraits.keyboardType; + textView.keyboardAppearance = _textInputTraits.keyboardAppearance; + textView.returnKeyType = _textInputTraits.returnKeyType; + textView.enablesReturnKeyAutomatically = _textInputTraits.enablesReturnKeyAutomatically; + textView.secureTextEntry = _textInputTraits.isSecureTextEntry; + } + } + + [self.view addSubview:textView]; + }; + + AS::MutexLocker l(_textKitLock); + + // Create and configure the placeholder text view. + _placeholderTextKitComponents.textView = [[ASTextKitComponentsTextView alloc] initWithFrame:CGRectZero textContainer:_placeholderTextKitComponents.textContainer]; + _placeholderTextKitComponents.textView.userInteractionEnabled = NO; + _placeholderTextKitComponents.textView.accessibilityElementsHidden = YES; + configureTextView(_placeholderTextKitComponents.textView); + + // Create and configure our text view. + ASPanningOverriddenUITextView *textView = [[ASPanningOverriddenUITextView alloc] initWithFrame:CGRectZero textContainer:_textKitComponents.textContainer]; + textView.initialPrimaryLanguage = _initialPrimaryLanguage; + __weak ASEditableTextNode *weakSelf = self; + textView.shouldCopy = ^bool{ + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeShouldCopy:)]) { + return [strongSelf->_delegate editableTextNodeShouldCopy:self]; + } + } + return true; + }; + textView.shouldPaste = ^bool{ + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeShouldPaste:)]) { + return [strongSelf->_delegate editableTextNodeShouldPaste:self]; + } + } + return true; + }; + textView.targetForActionImpl = ^id(SEL action) { + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeTargetForAction:)]) { + return [strongSelf->_delegate editableTextNodeTargetForAction:action]; + } + } + return nil; + }; + textView.shouldReturn = ^bool { + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeShouldReturn:)]) { + return [strongSelf->_delegate editableTextNodeShouldReturn:strongSelf]; + } + } + return true; + }; + textView.backspaceWhileEmpty = ^{ + __strong ASEditableTextNode *strongSelf = weakSelf; + if (strongSelf != nil) { + if ([strongSelf->_delegate respondsToSelector:@selector(editableTextNodeBackspaceWhileEmpty:)]) { + [strongSelf->_delegate editableTextNodeBackspaceWhileEmpty:strongSelf]; + } + } + }; + _textKitComponents.textView = textView; + _textKitComponents.textView.scrollEnabled = _scrollEnabled; + _textKitComponents.textView.delegate = self; + #if TARGET_OS_IOS + _textKitComponents.textView.editable = YES; + #endif + _textKitComponents.textView.typingAttributes = _typingAttributes; + _textKitComponents.textView.accessibilityHint = _placeholderTextKitComponents.textStorage.string; + configureTextView(_textKitComponents.textView); + + [self _updateDisplayingPlaceholder]; + + // once view is loaded, setters set directly on view + _textInputTraits = nil; + + UITapGestureRecognizer *tapRecognizer = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(tapGesture:)]; + tapRecognizer.cancelsTouchesInView = false; + tapRecognizer.delaysTouchesBegan = false; + tapRecognizer.delaysTouchesEnded = false; + tapRecognizer.delegate = self; + [self.view addGestureRecognizer:tapRecognizer]; +} + +- (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { + return true; +} + +- (void)tapGesture:(UITapGestureRecognizer *)recognizer { + static Class promptClass = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + promptClass = NSClassFromString([[NSString alloc] initWithFormat:@"%@AutocorrectInlinePrompt", @"UI"]); + }); + + if (recognizer.state == UIGestureRecognizerStateEnded) { + UIView *result = [self hitTest:[recognizer locationInView:self.view] withEvent:nil]; + if (result != nil && [result class] == promptClass) { + [self dropAutocorrection]; + } + } +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize +{ + ASTextKitComponents *displayedComponents = [self isDisplayingPlaceholder] ? _placeholderTextKitComponents : _textKitComponents; + + CGSize textSize; + + if (_maximumLinesToDisplay > 0) { + textSize = [displayedComponents sizeForConstrainedWidth:constrainedSize.width + forMaxNumberOfLines: _maximumLinesToDisplay]; + } else { + textSize = [displayedComponents sizeForConstrainedWidth:constrainedSize.width]; + } + + CGFloat width = std::ceil(constrainedSize.width); + CGFloat height = std::ceil(textSize.height + _textContainerInset.top + _textContainerInset.bottom); + return CGSizeMake(std::fmin(width, constrainedSize.width), std::fmin(height, constrainedSize.height)); +} + +- (void)layout +{ + ASDisplayNodeAssertMainThread(); + + [super layout]; + [self _layoutTextView]; +} + +- (void)setBackgroundColor:(UIColor *)backgroundColor +{ + [super setBackgroundColor:backgroundColor]; + + AS::MutexLocker l(_textKitLock); + + // If showing the placeholder, don't propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and if it's opaque/colored then it'll obscure the placeholder. + // The backgroundColor/opaque will be propagated to the editable textView when editing begins. + if (!_displayingPlaceholder) { + _textKitComponents.textView.backgroundColor = backgroundColor; + } + _placeholderTextKitComponents.textView.backgroundColor = backgroundColor; +} + +- (void)setTextContainerInset:(UIEdgeInsets)textContainerInset +{ + AS::MutexLocker l(_textKitLock); + + _textContainerInset = textContainerInset; + _textKitComponents.textView.textContainerInset = textContainerInset; + _placeholderTextKitComponents.textView.textContainerInset = textContainerInset; +} + +- (void)setOpaque:(BOOL)opaque +{ + [super setOpaque:opaque]; + + AS::MutexLocker l(_textKitLock); + + // If showing the placeholder, don't propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and if it's opaque/colored then it'll obscure the placeholder. + // The backgroundColor/opaque will be propagated to the editable textView when editing begins. + if (!_displayingPlaceholder) { + _textKitComponents.textView.opaque = opaque; + } + _placeholderTextKitComponents.textView.opaque = opaque; +} + +- (void)setLayerBacked:(BOOL)layerBacked +{ + ASDisplayNodeAssert(!layerBacked, @"Cannot set layerBacked to YES on ASEditableTextNode – instances must be view-backed in order to ensure touch events can be passed to the internal UITextView during editing."); + [super setLayerBacked:layerBacked]; +} + +- (BOOL)supportsLayerBacking +{ + return NO; +} + +#pragma mark - Configuration +@synthesize delegate = _delegate; + +- (void)setScrollEnabled:(BOOL)scrollEnabled +{ + AS::MutexLocker l(_textKitLock); + _scrollEnabled = scrollEnabled; + [_textKitComponents.textView setScrollEnabled:_scrollEnabled]; +} + +- (UITextView *)textView +{ + ASDisplayNodeAssertMainThread(); + [self view]; + ASDisplayNodeAssert(_textKitComponents.textView != nil, @"UITextView must be created in -[ASEditableTextNode didLoad]"); + return _textKitComponents.textView; +} + +- (void)setMaximumLinesToDisplay:(NSUInteger)maximumLines +{ + _maximumLinesToDisplay = maximumLines; + [self setNeedsLayout]; +} + +#pragma mark - +@dynamic typingAttributes; + +- (NSDictionary *)typingAttributes +{ + return _typingAttributes; +} + +- (void)setTypingAttributes:(NSDictionary *)typingAttributes +{ + if (ASObjectIsEqual(typingAttributes, _typingAttributes)) + return; + + _typingAttributes = [typingAttributes copy]; + + AS::MutexLocker l(_textKitLock); + + _textKitComponents.textView.typingAttributes = _typingAttributes; +} + +#pragma mark - +@dynamic selectedRange; + +- (NSRange)selectedRange +{ + AS::MutexLocker l(_textKitLock); + return _textKitComponents.textView.selectedRange; +} + +- (void)setSelectedRange:(NSRange)selectedRange +{ + AS::MutexLocker l(_textKitLock); + _textKitComponents.textView.selectedRange = selectedRange; +} + +- (CGRect)selectionRect { + UITextRange *range = [_textKitComponents.textView selectedTextRange]; + if (range != nil) { + return [_textKitComponents.textView firstRectForRange:range]; + } else { + return [_textKitComponents.textView bounds]; + } +} + +#pragma mark - Placeholder +- (BOOL)isDisplayingPlaceholder +{ + return _displayingPlaceholder; +} + +#pragma mark - +@dynamic attributedPlaceholderText; +- (NSAttributedString *)attributedPlaceholderText +{ + AS::MutexLocker l(_textKitLock); + + return [_placeholderTextKitComponents.textStorage copy]; +} + +- (void)setAttributedPlaceholderText:(NSAttributedString *)attributedPlaceholderText +{ + AS::MutexLocker l(_textKitLock); + + if (ASObjectIsEqual(_placeholderTextKitComponents.textStorage, attributedPlaceholderText)) + return; + + [_placeholderTextKitComponents.textStorage setAttributedString:attributedPlaceholderText ? : [[NSAttributedString alloc] initWithString:@""]]; + _textKitComponents.textView.accessibilityHint = attributedPlaceholderText.string; +} + +#pragma mark - Modifying User Text +@dynamic attributedText; +- (NSAttributedString *)attributedText +{ + // Per contract in our header, this value is nil when the placeholder is displayed. + if ([self isDisplayingPlaceholder]) + return nil; + + AS::MutexLocker l(_textKitLock); + + return [_textKitComponents.textStorage copy]; +} + +- (void)setAttributedText:(NSAttributedString *)attributedText +{ + AS::MutexLocker l(_textKitLock); + + // If we (_cmd) are called while the text view itself is updating (-textViewDidUpdate:), you cannot update the text storage and expect perfect propagation to the text view. + // Thus, we always update the textview directly if it's been created already. + if (ASObjectIsEqual((_textKitComponents.textView.attributedText ? : _textKitComponents.textStorage), attributedText)) + return; + + // If the cursor isn't at the end of the text, we need to preserve the selected range to avoid moving the cursor. + NSRange selectedRange = _textKitComponents.textView.selectedRange; + BOOL preserveSelectedRange = (selectedRange.location != _textKitComponents.textStorage.length); + + NSAttributedString *attributedStringToDisplay = nil; + + if (attributedText) + attributedStringToDisplay = attributedText; + // Otherwise, note that we don't simply nil out attributed text. Because the insertion point is guided by the attributes at index 0, we need to attribute an empty string to ensure the insert point obeys our typing attributes. + else + attributedStringToDisplay = [[NSAttributedString alloc] initWithString:@"" attributes:self.typingAttributes]; + + // Always prefer updating the text view directly if it's been created (see above). + if (_textKitComponents.textView) + [_textKitComponents.textView setAttributedText:attributedStringToDisplay]; + else + [_textKitComponents.textStorage setAttributedString:attributedStringToDisplay]; + + // Calculated size depends on the seeded text. + [self setNeedsLayout]; + + // Update if placeholder is shown. + [self _updateDisplayingPlaceholder]; + + // Preserve cursor range, if necessary. + if (preserveSelectedRange) { + _isPreservingSelection = YES; // Used in -textViewDidChangeSelection: to avoid informing our delegate about our preservation. + [_textKitComponents.textView setSelectedRange:selectedRange]; + _isPreservingSelection = NO; + } +} + +- (void)setInitialPrimaryLanguage:(NSString *)initialPrimaryLanguage { + _initialPrimaryLanguage = initialPrimaryLanguage; + ((ASPanningOverriddenUITextView *)_textKitComponents.textView).initialPrimaryLanguage = initialPrimaryLanguage; +} + +- (void)resetInitialPrimaryLanguage { + ((ASPanningOverriddenUITextView *)_textKitComponents.textView).initializedPrimaryInputLanguage = false; +} + +- (void)dropAutocorrection { + _isPreservingSelection = YES; // Used in -textViewDidChangeSelection: to avoid informing our delegate about our preservation. + _isPreservingText = YES; + + UITextView *textView = _textKitComponents.textView; + + NSRange rangeCopy = textView.selectedRange; + NSRange fakeRange = rangeCopy; + if (fakeRange.location != 0) { + fakeRange.location--; + } + [textView unmarkText]; + [textView setSelectedRange:fakeRange]; + [textView setSelectedRange:rangeCopy]; + + //[_textKitComponents.textView.inputDelegate textWillChange:_textKitComponents.textView]; + //[_textKitComponents.textView.inputDelegate textDidChange:_textKitComponents.textView]; + + _isPreservingSelection = NO; + _isPreservingText = NO; +} + +- (bool)isCurrentlyEmoji { + NSString *value = [[UITextInputMode currentInputMode] primaryLanguage]; + if ([value isEqualToString:@"emoji"]) { + return true; + } else { + return false; + } +} + +#pragma mark - Core +- (void)_updateDisplayingPlaceholder +{ + AS::MutexLocker l(_textKitLock); + + // Show the placeholder if necessary. + _displayingPlaceholder = (_textKitComponents.textStorage.length == 0); + _placeholderTextKitComponents.textView.hidden = !_displayingPlaceholder; + + // If hiding the placeholder, propagate backgroundColor/opaque to the editable textView. It is positioned over the placeholder to accept taps to begin editing, and was kept transparent so it doesn't obscure the placeholder text. Now that we're editing it and the placeholder is hidden, we can make it opaque to avoid unnecessary blending. + if (!_displayingPlaceholder) { + _textKitComponents.textView.opaque = self.isOpaque; + _textKitComponents.textView.backgroundColor = self.backgroundColor; + } else { + _textKitComponents.textView.opaque = NO; + _textKitComponents.textView.backgroundColor = nil; + } +} + +- (void)_layoutTextView +{ + AS::MutexLocker l(_textKitLock); + + // Layout filling our bounds. + _textKitComponents.textView.frame = self.bounds; + _placeholderTextKitComponents.textView.frame = self.bounds; + + // Note that both of these won't be necessary once we can disable scrolling, pending rdar://14729288 + // When we resize to fit (above) the prior layout becomes invalid. For whatever reason, UITextView doesn't invalidate its layout when its frame changes on its own, so we have to do so ourselves. + [_textKitComponents.layoutManager invalidateLayoutForCharacterRange:NSMakeRange(0, [_textKitComponents.textStorage length]) actualCharacterRange:NULL]; + + // When you type beyond UITextView's bounds it scrolls you down a line. We need to remain at the top. + [_textKitComponents.textView setContentOffset:CGPointZero animated:NO]; + [_textKitComponents.layoutManager ensureGlyphsForCharacterRange:NSMakeRange(0, [_textKitComponents.textStorage length])]; + NSRange range = [self selectedRange]; + range.location = range.location + range.length - 1; + range.length = 1; + [self.textView scrollRangeToVisible:range]; + + CGPoint bottomOffset = CGPointMake(0, self.textView.contentSize.height - self.textView.bounds.size.height); + //[self.textView setContentOffset:bottomOffset animated:NO]; +} + +#pragma mark - Keyboard +@dynamic textInputMode; +- (UITextInputMode *)textInputMode +{ + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView textInputMode]; +} + +- (BOOL)isFirstResponder +{ + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView isFirstResponder]; +} + +- (BOOL)canBecomeFirstResponder { + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView canBecomeFirstResponder]; +} + +- (BOOL)becomeFirstResponder +{ + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView becomeFirstResponder]; +} + +- (BOOL)canResignFirstResponder { + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView canResignFirstResponder]; +} + +- (BOOL)resignFirstResponder +{ + AS::MutexLocker l(_textKitLock); + return [_textKitComponents.textView resignFirstResponder]; +} + +#pragma mark - UITextInputTraits + +- (_ASTextInputTraitsPendingState *)textInputTraits +{ + if (!_textInputTraits) { + _textInputTraits = [[_ASTextInputTraitsPendingState alloc] init]; + } + return _textInputTraits; +} + +- (void)setAutocapitalizationType:(UITextAutocapitalizationType)autocapitalizationType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setAutocapitalizationType:autocapitalizationType]; + } else { + [self.textInputTraits setAutocapitalizationType:autocapitalizationType]; + } +} + +- (UITextAutocapitalizationType)autocapitalizationType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView autocapitalizationType]; + } else { + return [self.textInputTraits autocapitalizationType]; + } +} + +- (void)setAutocorrectionType:(UITextAutocorrectionType)autocorrectionType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setAutocorrectionType:autocorrectionType]; + } else { + [self.textInputTraits setAutocorrectionType:autocorrectionType]; + } +} + +- (UITextAutocorrectionType)autocorrectionType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView autocorrectionType]; + } else { + return [self.textInputTraits autocorrectionType]; + } +} + +- (void)setSpellCheckingType:(UITextSpellCheckingType)spellCheckingType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setSpellCheckingType:spellCheckingType]; + } else { + [self.textInputTraits setSpellCheckingType:spellCheckingType]; + } +} + +- (UITextSpellCheckingType)spellCheckingType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView spellCheckingType]; + } else { + return [self.textInputTraits spellCheckingType]; + } +} + +- (void)setEnablesReturnKeyAutomatically:(BOOL)enablesReturnKeyAutomatically +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setEnablesReturnKeyAutomatically:enablesReturnKeyAutomatically]; + } else { + [self.textInputTraits setEnablesReturnKeyAutomatically:enablesReturnKeyAutomatically]; + } +} + +- (BOOL)enablesReturnKeyAutomatically +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView enablesReturnKeyAutomatically]; + } else { + return [self.textInputTraits enablesReturnKeyAutomatically]; + } +} + +- (void)setKeyboardAppearance:(UIKeyboardAppearance)setKeyboardAppearance +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setKeyboardAppearance:setKeyboardAppearance]; + } else { + [self.textInputTraits setKeyboardAppearance:setKeyboardAppearance]; + } +} + +- (UIKeyboardAppearance)keyboardAppearance +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView keyboardAppearance]; + } else { + return [self.textInputTraits keyboardAppearance]; + } +} + +- (void)setKeyboardType:(UIKeyboardType)keyboardType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setKeyboardType:keyboardType]; + } else { + [self.textInputTraits setKeyboardType:keyboardType]; + } +} + +- (UIKeyboardType)keyboardType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView keyboardType]; + } else { + return [self.textInputTraits keyboardType]; + } +} + +- (void)setReturnKeyType:(UIReturnKeyType)returnKeyType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setReturnKeyType:returnKeyType]; + } else { + [self.textInputTraits setReturnKeyType:returnKeyType]; + } +} + +- (UIReturnKeyType)returnKeyType +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView returnKeyType]; + } else { + return [self.textInputTraits returnKeyType]; + } +} + +- (void)setSecureTextEntry:(BOOL)secureTextEntry +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + [self.textView setSecureTextEntry:secureTextEntry]; + } else { + [self.textInputTraits setSecureTextEntry:secureTextEntry]; + } +} + +- (BOOL)isSecureTextEntry +{ + AS::MutexLocker l(_textInputTraitsLock); + if (self.isNodeLoaded) { + return [self.textView isSecureTextEntry]; + } else { + return [self.textInputTraits isSecureTextEntry]; + } +} + +#pragma mark - UITextView Delegate +- (BOOL)textViewShouldBeginEditing:(UITextView *)textView +{ + // Delegateify. + return [self _delegateShouldBeginEditing]; +} + +- (void)textViewDidBeginEditing:(UITextView *)textView +{ + // Delegateify. + [self _delegateDidBeginEditing]; +} + +- (BOOL)textView:(UITextView *)textView shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +{ + if (_isPreservingText) { + return false; + } + // Delegateify. + return [self _delegateShouldChangeTextInRange:range replacementText:text]; +} + +- (void)textViewDidChange:(UITextView *)textView +{ + AS::MutexLocker l(_textKitLock); + + // Note we received a text changed event. + // This is used by _delegateDidChangeSelectionFromSelectedRange:toSelectedRange: to distinguish between selection changes that happen because of editing or pure selection changes. + _selectionChangedForEditedText = YES; + + // Update if the placeholder is visible. + [self _updateDisplayingPlaceholder]; + + // Invalidate, as our calculated size depends on the textview's seeded text. + [self invalidateCalculatedLayout]; + + // Delegateify. + [self _delegateDidUpdateText]; +} + +- (void)textViewDidChangeSelection:(UITextView *)textView +{ + // Typing attributes get reset when selection changes. Reapply them so they actually obey our header. + _textKitComponents.textView.typingAttributes = _typingAttributes; + + // If we're only changing selection to preserve it, don't notify about anything. + if (_isPreservingSelection) + return; + + // Note if we receive a -textDidChange: between now and when we delegatify. + // This is used by _delegateDidChangeSelectionFromSelectedRange:toSelectedRange: to distinguish between selection changes that happen because of editing or pure selection changes. + _selectionChangedForEditedText = NO; + + NSRange fromSelectedRange = _previousSelectedRange; + NSRange toSelectedRange = self.selectedRange; + _previousSelectedRange = toSelectedRange; + + // Delegateify. + [self _delegateDidChangeSelectionFromSelectedRange:fromSelectedRange toSelectedRange:toSelectedRange]; +} + +- (void)textViewDidEndEditing:(UITextView *)textView +{ + // Delegateify. + [self _delegateDidFinishEditing]; +} + +#pragma mark - NSLayoutManager Delegate + +- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)properties characterIndexes:(const NSUInteger *)characterIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange +{ + return [_wordKerner layoutManager:layoutManager shouldGenerateGlyphs:glyphs properties:properties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange]; +} + +- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)defaultAction forControlCharacterAtIndex:(NSUInteger)characterIndex +{ + return [_wordKerner layoutManager:layoutManager shouldUseAction:defaultAction forControlCharacterAtIndex:characterIndex]; +} + +- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)characterIndex +{ + return [_wordKerner layoutManager:layoutManager boundingBoxForControlGlyphAtIndex:glyphIndex forTextContainer:textContainer proposedLineFragment:proposedRect glyphPosition:glyphPosition characterIndex:characterIndex]; +} + +- (BOOL)layoutManager:(NSLayoutManager *)layoutManager shouldSetLineFragmentRect:(inout CGRect *)lineFragmentRect lineFragmentUsedRect:(inout CGRect *)lineFragmentUsedRect baselineOffset:(inout CGFloat *)baselineOffset inTextContainer:(NSTextContainer *)textContainer forGlyphRange:(NSRange)glyphRange { + CGFloat fontLineHeight; + UIFont *baseFont = _baseFont; + if (_typingAttributes[NSFontAttributeName] != nil) { + baseFont = _typingAttributes[NSFontAttributeName]; + } + if (baseFont == nil) { + fontLineHeight = 20.0; + } else { + CGFloat fontAscent = baseFont.ascender; + CGFloat fontDescent = ABS(baseFont.descender); + fontLineHeight = floor(fontAscent + fontDescent); + } + CGFloat lineHeight = fontLineHeight * 1.0; + CGFloat baselineNudge = (lineHeight - fontLineHeight) * 0.6f; + + CGRect rect = *lineFragmentRect; + rect.size.height = lineHeight; + + CGRect usedRect = *lineFragmentUsedRect; + usedRect.size.height = MAX(lineHeight, usedRect.size.height); + + *lineFragmentRect = rect; + *lineFragmentUsedRect = usedRect; + *baselineOffset = *baselineOffset + baselineNudge; + + return true; +} + +#pragma mark - Geometry +- (CGRect)frameForTextRange:(NSRange)textRange +{ + AS::MutexLocker l(_textKitLock); + + // Bail on invalid range. + if (NSMaxRange(textRange) > [_textKitComponents.textStorage length]) { + ASDisplayNodeAssert(NO, @"Invalid range"); + return CGRectZero; + } + + // Force glyph generation and layout. + [_textKitComponents.layoutManager ensureLayoutForTextContainer:_textKitComponents.textContainer]; + + NSRange glyphRange = [_textKitComponents.layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL]; + CGRect textRect = [_textKitComponents.layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:_textKitComponents.textContainer]; + return [_textKitComponents.textView convertRect:textRect toView:self.view]; +} + +#pragma mark - +- (BOOL)_delegateShouldBeginEditing +{ + if ([_delegate respondsToSelector:@selector(editableTextNodeShouldBeginEditing:)]) { + return [_delegate editableTextNodeShouldBeginEditing:self]; + } + return YES; +} + +- (void)_delegateDidBeginEditing +{ + if ([_delegate respondsToSelector:@selector(editableTextNodeDidBeginEditing:)]) + [_delegate editableTextNodeDidBeginEditing:self]; +} + +- (BOOL)_delegateShouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text +{ + if ([_delegate respondsToSelector:@selector(editableTextNode:shouldChangeTextInRange:replacementText:)]) { + return [_delegate editableTextNode:self shouldChangeTextInRange:range replacementText:text]; + } + + return YES; +} + +- (void)_delegateDidChangeSelectionFromSelectedRange:(NSRange)fromSelectedRange toSelectedRange:(NSRange)toSelectedRange +{ + // There are two reasons we're invoking the delegate on the next run of the runloop. + // 1. UITextView invokes its delegate methods when it's in the middle of text-processing. For example, -textViewDidChange: is invoked before you can truly rely on the changes being propagated throughout the Text Kit hierarchy. + // 2. This delegate method (-textViewDidChangeSelection:) is called both before -textViewDidChange: and before the layout manager/etc. has necessarily generated+laid out its glyphs. Because of the former, we need to wait until -textViewDidChange: has had an opportunity to be called so can accurately determine whether this selection change is due to editing (_selectionChangedForEditedText). + // Thus, to avoid calling out to client code in the middle of UITextView's processing, we call the delegate on the next run of the runloop, when all such internal processing is surely done. + dispatch_async(dispatch_get_main_queue(), ^{ + if ([_delegate respondsToSelector:@selector(editableTextNodeDidChangeSelection:fromSelectedRange:toSelectedRange:dueToEditing:)]) + [_delegate editableTextNodeDidChangeSelection:self fromSelectedRange:fromSelectedRange toSelectedRange:toSelectedRange dueToEditing:_selectionChangedForEditedText]; + }); +} + +- (void)_delegateDidUpdateText +{ + // Note that because -editableTextNodeDidUpdateText: passes no state, the current state of the receiver will be accessed. Thus, it's not useful to enqueue a second delegation call if the first hasn't happened yet -- doing so will result in the delegate receiving -editableTextNodeDidUpdateText: when the "updated text" has already been processed. This may sound innocuous, but because our delegation may cause additional updates to the textview's string, and because such updates discard spelling suggestions and autocompletions (like double-space to `.`), it can actually be quite dangerous! + if (_delegateDidUpdateEnqueued) + return; + + _delegateDidUpdateEnqueued = YES; + + // UITextView invokes its delegate methods when it's in the middle of text-processing. For example, -textViewDidChange: is invoked before you can truly rely on the changes being propagated throughout the Text Kit hierarchy. + // Thus, to avoid calling out to client code in the middle of UITextView's processing, we call the delegate on the next run of the runloop, when all such internal processing is surely done. + dispatch_async(dispatch_get_main_queue(), ^{ + _delegateDidUpdateEnqueued = NO; + if ([_delegate respondsToSelector:@selector(editableTextNodeDidUpdateText:)]) + [_delegate editableTextNodeDidUpdateText:self]; + }); +} + +- (void)_delegateDidFinishEditing +{ + if ([_delegate respondsToSelector:@selector(editableTextNodeDidFinishEditing:)]) + [_delegate editableTextNodeDidFinishEditing:self]; +} + +#pragma mark - UIAccessibilityContainer + +- (NSInteger)accessibilityElementCount +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElementCount since ASEditableTextNode is not loaded"); + return 0; + } + return 1; +} + +- (NSArray *)accessibilityElements +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElements since ASEditableTextNode is not loaded"); + return @[]; + } + return @[self.textView]; +} + +- (id)accessibilityElementAtIndex:(NSInteger)index +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElementAtIndex: since ASEditableTextNode is not loaded"); + return nil; + } + return self.textView; +} + +- (NSInteger)indexOfAccessibilityElement:(id)element +{ + return 0; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.mm b/submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.mm new file mode 100644 index 0000000000..653db2c370 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASExperimentalFeatures.mm @@ -0,0 +1,52 @@ +// +// ASExperimentalFeatures.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +NSArray *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags) +{ + NSArray *allNames = ASCreateOnce((@[@"exp_graphics_contexts", + @"exp_text_node", + @"exp_interface_state_coalesce", + @"exp_unfair_lock", + @"exp_infer_layer_defaults", + @"exp_collection_teardown", + @"exp_framesetter_cache", + @"exp_skip_clear_data", + @"exp_did_enter_preload_skip_asm_layout", + @"exp_disable_a11y_cache", + @"exp_dispatch_apply", + @"exp_image_downloader_priority", + @"exp_text_drawing"])); + if (flags == ASExperimentalFeatureAll) { + return allNames; + } + + // Go through all names, testing each bit. + NSUInteger i = 0; + return ASArrayByFlatMapping(allNames, NSString *name, ({ + (flags & (1 << i++)) ? name : nil; + })); +} + +// O(N^2) but with counts this small, it's probably faster +// than hashing the strings. +ASExperimentalFeatures ASExperimentalFeaturesFromArray(NSArray *array) +{ + NSArray *allNames = ASExperimentalFeaturesGetNames(ASExperimentalFeatureAll); + ASExperimentalFeatures result = 0; + for (NSString *str in array) { + NSUInteger i = [allNames indexOfObject:str]; + if (i != NSNotFound) { + result |= (1 << i); + } + } + return result; +} diff --git a/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm b/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm new file mode 100644 index 0000000000..b181ee2728 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASGraphicsContext.mm @@ -0,0 +1,167 @@ +// +// ASGraphicsContext.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ASCGImageBuffer.h" +#import +#import +#import +#import +#import +#import + +/** + * Our version of the private CGBitmapGetAlignedBytesPerRow function. + * + * In both 32-bit and 64-bit, this function rounds up to nearest multiple of 32 + * in iOS 9, 10, and 11. We'll try to catch if this ever changes by asserting that + * the bytes-per-row for a 1x1 context from the system is 32. + */ +static size_t ASGraphicsGetAlignedBytesPerRow(size_t baseValue) { + // Add 31 then zero out low 5 bits. + return (baseValue + 31) & ~0x1F; +} + +/** + * A key used to associate CGContextRef -> NSMutableData, nonatomic retain. + * + * That way the data will be released when the context dies. If they pull an image, + * we will retain the data object (in a CGDataProvider) before releasing the context. + */ +static UInt8 __contextDataAssociationKey; + +#pragma mark - Graphics Contexts + +void ASGraphicsBeginImageContextWithOptions(CGSize size, BOOL opaque, CGFloat scale) +{ + if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { + UIGraphicsBeginImageContextWithOptions(size, opaque, scale); + return; + } + + // We use "reference contexts" to get device-specific options that UIKit + // uses. + static dispatch_once_t onceToken; + static CGContextRef refCtxOpaque; + static CGContextRef refCtxTransparent; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 1); + refCtxOpaque = CGContextRetain(UIGraphicsGetCurrentContext()); + ASDisplayNodeCAssert(CGBitmapContextGetBytesPerRow(refCtxOpaque) == 32, @"Expected bytes per row to be aligned to 32. Has CGBitmapGetAlignedBytesPerRow implementation changed?"); + UIGraphicsEndImageContext(); + + // Make transparent ref context. + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), NO, 1); + refCtxTransparent = CGContextRetain(UIGraphicsGetCurrentContext()); + UIGraphicsEndImageContext(); + }); + + // These options are taken from UIGraphicsBeginImageContext. + CGContextRef refCtx = opaque ? refCtxOpaque : refCtxTransparent; + CGBitmapInfo bitmapInfo = CGBitmapContextGetBitmapInfo(refCtx); + + if (scale == 0) { + scale = ASScreenScale(); + } + size_t intWidth = (size_t)ceil(size.width * scale); + size_t intHeight = (size_t)ceil(size.height * scale); + size_t bitsPerComponent = CGBitmapContextGetBitsPerComponent(refCtx); + size_t bytesPerRow = CGBitmapContextGetBitsPerPixel(refCtx) * intWidth / 8; + bytesPerRow = ASGraphicsGetAlignedBytesPerRow(bytesPerRow); + size_t bufferSize = bytesPerRow * intHeight; + CGColorSpaceRef colorspace = CGBitmapContextGetColorSpace(refCtx); + + // We create our own buffer, and wrap the context around that. This way we can prevent + // the copy that usually gets made when you form a CGImage from the context. + ASCGImageBuffer *buffer = [[ASCGImageBuffer alloc] initWithLength:bufferSize]; + + CGContextRef context = CGBitmapContextCreate(buffer.mutableBytes, intWidth, intHeight, bitsPerComponent, bytesPerRow, colorspace, bitmapInfo); + + // Transfer ownership of the data to the context. So that if the context + // is destroyed before we create an image from it, the data will be released. + objc_setAssociatedObject((__bridge id)context, &__contextDataAssociationKey, buffer, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + + // Set the CTM to account for iOS orientation & specified scale. + // If only we could use CGContextSetBaseCTM. It doesn't + // seem like there are any consequences for our use case + // but we'll be on the look out. The internet hinted that it + // affects shadowing but I tested and shadowing works. + CGContextTranslateCTM(context, 0, intHeight); + CGContextScaleCTM(context, scale, -scale); + + // Save the state so we can restore it and recover our scale in GetImageAndEnd + CGContextSaveGState(context); + + // Transfer context ownership to the UIKit stack. + UIGraphicsPushContext(context); + CGContextRelease(context); +} + +UIImage * _Nullable ASGraphicsGetImageAndEndCurrentContext() NS_RETURNS_RETAINED +{ + if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { + UIImage *image = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return image; + } + + // Pop the context and make sure we have one. + CGContextRef context = UIGraphicsGetCurrentContext(); + if (context == NULL) { + ASDisplayNodeCFailAssert(@"Can't end image context without having begun one."); + return nil; + } + + // Read the device-specific ICC-based color space to use for the image. + // For DeviceRGB contexts (e.g. UIGraphics), CGBitmapContextCreateImage + // generates an image in a device-specific color space (for wide color support). + // We replicate that behavior, even though at this time CA does not + // require the image to be in this space. Plain DeviceRGB images seem + // to be treated exactly the same, but better safe than sorry. + static CGColorSpaceRef imageColorSpace; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0); + UIImage *refImage = UIGraphicsGetImageFromCurrentImageContext(); + imageColorSpace = CGColorSpaceRetain(CGImageGetColorSpace(refImage.CGImage)); + ASDisplayNodeCAssertNotNil(imageColorSpace, nil); + UIGraphicsEndImageContext(); + }); + + // Retrieve our buffer and create a CGDataProvider from it. + ASCGImageBuffer *buffer = objc_getAssociatedObject((__bridge id)context, &__contextDataAssociationKey); + ASDisplayNodeCAssertNotNil(buffer, nil); + CGDataProviderRef provider = [buffer createDataProviderAndInvalidate]; + + // Create the CGImage. Options taken from CGBitmapContextCreateImage. + CGImageRef cgImg = CGImageCreate(CGBitmapContextGetWidth(context), CGBitmapContextGetHeight(context), CGBitmapContextGetBitsPerComponent(context), CGBitmapContextGetBitsPerPixel(context), CGBitmapContextGetBytesPerRow(context), imageColorSpace, CGBitmapContextGetBitmapInfo(context), provider, NULL, true, kCGRenderingIntentDefault); + CGDataProviderRelease(provider); + + // We saved our GState right after setting the CTM so that we could restore it + // here and get the original scale back. + CGContextRestoreGState(context); + CGFloat scale = CGContextGetCTM(context).a; + + // Note: popping from the UIKit stack will probably destroy the context. + context = NULL; + UIGraphicsPopContext(); + + UIImage *result = [[UIImage alloc] initWithCGImage:cgImg scale:scale orientation:UIImageOrientationUp]; + CGImageRelease(cgImg); + return result; +} + +void ASGraphicsEndImageContext() +{ + if (!ASActivateExperimentalFeature(ASExperimentalGraphicsContexts)) { + UIGraphicsEndImageContext(); + return; + } + + UIGraphicsPopContext(); +} diff --git a/submodules/AsyncDisplayKit/Source/ASHashing.mm b/submodules/AsyncDisplayKit/Source/ASHashing.mm new file mode 100644 index 0000000000..17bf66bd82 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASHashing.mm @@ -0,0 +1,38 @@ +// +// ASHashing.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#define ELF_STEP(B) T1 = (H << 4) + B; T2 = T1 & 0xF0000000; if (T2) T1 ^= (T2 >> 24); T1 &= (~T2); H = T1; + +/** + * The hashing algorithm copied from CoreFoundation CFHashBytes function. + * https://opensource.apple.com/source/CF/CF-1153.18/CFUtilities.c.auto.html + */ +NSUInteger ASHashBytes(void *bytesarg, size_t length) { + /* The ELF hash algorithm, used in the ELF object file format */ + uint8_t *bytes = (uint8_t *)bytesarg; + UInt32 H = 0, T1, T2; + SInt32 rem = (SInt32)length; + while (3 < rem) { + ELF_STEP(bytes[length - rem]); + ELF_STEP(bytes[length - rem + 1]); + ELF_STEP(bytes[length - rem + 2]); + ELF_STEP(bytes[length - rem + 3]); + rem -= 4; + } + switch (rem) { + case 3: ELF_STEP(bytes[length - 3]); + case 2: ELF_STEP(bytes[length - 2]); + case 1: ELF_STEP(bytes[length - 1]); + case 0: ; + } + return H; +} + +#undef ELF_STEP diff --git a/submodules/AsyncDisplayKit/Source/ASInternalHelpers.mm b/submodules/AsyncDisplayKit/Source/ASInternalHelpers.mm new file mode 100644 index 0000000000..a9926ccca4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASInternalHelpers.mm @@ -0,0 +1,233 @@ +// +// ASInternalHelpers.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 + +static NSNumber *allowsGroupOpacityFromUIKitOrNil; +static NSNumber *allowsEdgeAntialiasingFromUIKitOrNil; + +BOOL ASDefaultAllowsGroupOpacity() +{ + static BOOL groupOpacity; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSNumber *groupOpacityObj = allowsGroupOpacityFromUIKitOrNil ?: [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIViewGroupOpacity"]; + groupOpacity = groupOpacityObj ? groupOpacityObj.boolValue : YES; + }); + return groupOpacity; +} + +BOOL ASDefaultAllowsEdgeAntialiasing() +{ + static BOOL edgeAntialiasing; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSNumber *antialiasingObj = allowsEdgeAntialiasingFromUIKitOrNil ?: [NSBundle.mainBundle objectForInfoDictionaryKey:@"UIViewEdgeAntialiasing"]; + edgeAntialiasing = antialiasingObj ? antialiasingObj.boolValue : NO; + }); + return edgeAntialiasing; +} + +void ASInitializeFrameworkMainThread(void) +{ + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ASDisplayNodeCAssertMainThread(); + // Ensure these values are cached on the main thread before needed in the background. + if (ASActivateExperimentalFeature(ASExperimentalLayerDefaults)) { + // Nop. We will gather default values on-demand in ASDefaultAllowsGroupOpacity and ASDefaultAllowsEdgeAntialiasing + } else { + CALayer *layer = [[[UIView alloc] init] layer]; + allowsGroupOpacityFromUIKitOrNil = @(layer.allowsGroupOpacity); + allowsEdgeAntialiasingFromUIKitOrNil = @(layer.allowsEdgeAntialiasing); + } + ASNotifyInitialized(); + }); +} + +BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector) +{ + if (superclass == subclass) return NO; // Even if the class implements the selector, it doesn't override itself. + Method superclassMethod = class_getInstanceMethod(superclass, selector); + Method subclassMethod = class_getInstanceMethod(subclass, selector); + return (superclassMethod != subclassMethod); +} + +BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL selector) +{ + if (superclass == subclass) return NO; // Even if the class implements the selector, it doesn't override itself. + Method superclassMethod = class_getClassMethod(superclass, selector); + Method subclassMethod = class_getClassMethod(subclass, selector); + return (superclassMethod != subclassMethod); +} + +IMP ASReplaceMethodWithBlock(Class c, SEL origSEL, id block) +{ + NSCParameterAssert(block); + + // Get original method + Method origMethod = class_getInstanceMethod(c, origSEL); + NSCParameterAssert(origMethod); + + // Convert block to IMP trampoline and replace method implementation + IMP newIMP = imp_implementationWithBlock(block); + + // Try adding the method if not yet in the current class + if (!class_addMethod(c, origSEL, newIMP, method_getTypeEncoding(origMethod))) { + return method_setImplementation(origMethod, newIMP); + } else { + return method_getImplementation(origMethod); + } +} + +void ASPerformBlockOnMainThread(void (^block)(void)) +{ + if (block == nil){ + return; + } + if (ASDisplayNodeThreadIsMain()) { + block(); + } else { + dispatch_async(dispatch_get_main_queue(), block); + } +} + +void ASPerformBlockOnBackgroundThread(void (^block)(void)) +{ + if (block == nil){ + return; + } + if (ASDisplayNodeThreadIsMain()) { + dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), block); + } else { + block(); + } +} + +void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object) +{ + [[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object]; +} + +Class _Nullable ASGetClassFromType(const char * _Nullable type) +{ + // Class types all start with @" + if (type == NULL || strncmp(type, "@\"", 2) != 0) { + return Nil; + } + + // Ensure length >= 3 + size_t typeLength = strlen(type); + if (typeLength < 3) { + ASDisplayNodeCFailAssert(@"Got invalid type-encoding: %s", type); + return Nil; + } + + // Copy type[2..(end-1)]. So @"UIImage" -> UIImage + size_t resultLength = typeLength - 3; + char className[resultLength + 1]; + strncpy(className, type + 2, resultLength); + className[resultLength] = '\0'; + return objc_getClass(className); +} + +CGFloat ASScreenScale() +{ + static CGFloat __scale = 0.0; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + UIGraphicsBeginImageContextWithOptions(CGSizeMake(1, 1), YES, 0); + __scale = CGContextGetCTM(UIGraphicsGetCurrentContext()).a; + UIGraphicsEndImageContext(); + }); + return __scale; +} + +CGSize ASFloorSizeValues(CGSize s) +{ + return CGSizeMake(ASFloorPixelValue(s.width), ASFloorPixelValue(s.height)); +} + +// See ASCeilPixelValue for a more thoroguh explanation of (f + FLT_EPSILON), +// but here is some quick math: +// +// Imagine a layout that comes back with a height of 100.66666666663 +// for a 3x deice: +// 100.66666666663 * 3 = 301.99999999988995 +// floor(301.99999999988995) = 301 +// 301 / 3 = 100.333333333 +// +// If we add FLT_EPSILON to normalize the garbage at the end we get: +// po (100.66666666663 + FLT_EPSILON) * 3 = 302.00000035751782 +// floor(302.00000035751782) = 302 +// 302/3 = 100.66666666 +CGFloat ASFloorPixelValue(CGFloat f) +{ + CGFloat scale = ASScreenScale(); + return floor((f + FLT_EPSILON) * scale) / scale; +} + +CGPoint ASCeilPointValues(CGPoint p) +{ + return CGPointMake(ASCeilPixelValue(p.x), ASCeilPixelValue(p.y)); +} + +CGSize ASCeilSizeValues(CGSize s) +{ + return CGSizeMake(ASCeilPixelValue(s.width), ASCeilPixelValue(s.height)); +} + +// With 3x devices layouts will often to compute to pixel bounds but +// include garbage values beyond the precision of a float/double. +// This garbage can result in a pixel value being rounded up when it isn't +// necessary. +// +// For example, imagine a layout that comes back with a height of 100.666666666669 +// for a 3x device: +// 100.666666666669 * 3 = 302.00000000000699 +// ceil(302.00000000000699) = 303 +// 303/3 = 101 +// +// If we use FLT_EPSILON to get rid of the garbage at the end of the value, +// things work as expected: +// (100.666666666669 - FLT_EPSILON) * 3 = 301.99999964237912 +// ceil(301.99999964237912) = 302 +// 302/3 = 100.666666666 +// +// For even more conversation around this, see: +// https://github.com/TextureGroup/Texture/issues/838 +CGFloat ASCeilPixelValue(CGFloat f) +{ + CGFloat scale = ASScreenScale(); + return ceil((f - FLT_EPSILON) * scale) / scale; +} + +CGFloat ASRoundPixelValue(CGFloat f) +{ + CGFloat scale = ASScreenScale(); + return round(f * scale) / scale; +} + +@implementation NSIndexPath (ASInverseComparison) + +- (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath +{ + return [otherIndexPath compare:self]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASLayout.mm b/submodules/AsyncDisplayKit/Source/ASLayout.mm new file mode 100644 index 0000000000..6a6a96f24c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayout.mm @@ -0,0 +1,378 @@ +// +// ASLayout.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import +#import + +#import +#import +#import "ASLayoutSpecUtilities.h" +#import "ASLayoutSpec+Subclasses.h" + +#import +#import +#import + +NSString *const ASThreadDictMaxConstraintSizeKey = @"kASThreadDictMaxConstraintSizeKey"; + +CGPoint const ASPointNull = {NAN, NAN}; + +BOOL ASPointIsNull(CGPoint point) +{ + return isnan(point.x) && isnan(point.y); +} + +/** + * Creates an defined number of " |" indent blocks for the recursive description. + */ +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString * descriptionIndents(NSUInteger indents) +{ + NSMutableString *description = [NSMutableString string]; + for (NSUInteger i = 0; i < indents; i++) { + [description appendString:@" |"]; + } + if (indents > 0) { + [description appendString:@" "]; + } + return description; +} + +ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutIsDisplayNodeType(ASLayout *layout) +{ + return layout.type == ASLayoutElementTypeDisplayNode; +} + +@interface ASLayout () +{ + ASLayoutElementType _layoutElementType; + std::atomic_bool _retainSublayoutElements; +} +@end + +@implementation ASLayout + +@dynamic frame, type; + +static std::atomic_bool static_retainsSublayoutLayoutElements = ATOMIC_VAR_INIT(NO); + ++ (void)setShouldRetainSublayoutLayoutElements:(BOOL)shouldRetain +{ + static_retainsSublayoutLayoutElements.store(shouldRetain); +} + ++ (BOOL)shouldRetainSublayoutLayoutElements +{ + return static_retainsSublayoutLayoutElements.load(); +} + +- (instancetype)initWithLayoutElement:(id)layoutElement + size:(CGSize)size + position:(CGPoint)position + sublayouts:(nullable NSArray *)sublayouts +{ + NSParameterAssert(layoutElement); + + self = [super init]; + if (self) { +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + for (ASLayout *sublayout in sublayouts) { + ASDisplayNodeAssert(ASPointIsNull(sublayout.position) == NO, @"Invalid position is not allowed in sublayout."); + } +#endif + + _layoutElement = layoutElement; + + // Read this now to avoid @c weak overhead later. + _layoutElementType = layoutElement.layoutElementType; + + if (!ASIsCGSizeValidForSize(size)) { + //ASDisplayNodeFailAssert(@"layoutSize is invalid and unsafe to provide to Core Animation! Release configurations will force to 0, 0. Size = %@, node = %@", NSStringFromCGSize(size), layoutElement); + size = CGSizeZero; + } else { + size = CGSizeMake(ASCeilPixelValue(size.width), ASCeilPixelValue(size.height)); + } + _size = size; + + if (ASPointIsNull(position) == NO) { + _position = ASCeilPointValues(position); + } else { + _position = position; + } + + _sublayouts = [sublayouts copy] ?: @[]; + + if ([ASLayout shouldRetainSublayoutLayoutElements]) { + [self retainSublayoutElements]; + } + } + + return self; +} + +#pragma mark - Class Constructors + ++ (instancetype)layoutWithLayoutElement:(id)layoutElement + size:(CGSize)size + position:(CGPoint)position + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED +{ + return [[self alloc] initWithLayoutElement:layoutElement + size:size + position:position + sublayouts:sublayouts]; +} + ++ (instancetype)layoutWithLayoutElement:(id)layoutElement + size:(CGSize)size + sublayouts:(nullable NSArray *)sublayouts NS_RETURNS_RETAINED +{ + return [self layoutWithLayoutElement:layoutElement + size:size + position:ASPointNull + sublayouts:sublayouts]; +} + ++ (instancetype)layoutWithLayoutElement:(id)layoutElement size:(CGSize)size NS_RETURNS_RETAINED +{ + return [self layoutWithLayoutElement:layoutElement + size:size + position:ASPointNull + sublayouts:nil]; +} + +- (void)dealloc +{ + if (_retainSublayoutElements.load()) { + for (ASLayout *sublayout in _sublayouts) { + // We retained this, so there's no risk of it deallocating on us. + if (CFTypeRef cfElement = (__bridge CFTypeRef)sublayout->_layoutElement) { + CFRelease(cfElement); + } + } + } +} + +#pragma mark - Sublayout Elements Caching + +- (void)retainSublayoutElements +{ + if (_retainSublayoutElements.exchange(true)) { + return; + } + + for (ASLayout *sublayout in _sublayouts) { + // CFBridgingRetain atomically casts and retains. We need the atomicity. + CFBridgingRetain(sublayout->_layoutElement); + } +} + +#pragma mark - Layout Flattening + +- (BOOL)isFlattened +{ + // A layout is flattened if its position is null, and all of its sublayouts are of type displaynode with no sublayouts. + if (!ASPointIsNull(_position)) { + return NO; + } + + for (ASLayout *sublayout in _sublayouts) { + if (ASLayoutIsDisplayNodeType(sublayout) == NO || sublayout->_sublayouts.count > 0) { + return NO; + } + } + + return YES; +} + +- (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED +{ + if ([self isFlattened]) { + // All flattened layouts must retain sublayout elements until they are applied. + [self retainSublayoutElements]; + return self; + } + + struct Context { + unowned ASLayout *layout; + CGPoint absolutePosition; + }; + + // Queue used to keep track of sublayouts while traversing this layout in a DFS fashion. + std::deque queue; + for (ASLayout *sublayout in _sublayouts) { + queue.push_back({sublayout, sublayout.position}); + } + + std::vector flattenedSublayouts; + + while (!queue.empty()) { + const Context context = std::move(queue.front()); + queue.pop_front(); + + unowned ASLayout *layout = context.layout; + // Direct ivar access to avoid retain/release, use existing +1. + const NSUInteger sublayoutsCount = layout->_sublayouts.count; + const CGPoint absolutePosition = context.absolutePosition; + + if (ASLayoutIsDisplayNodeType(layout)) { + if (sublayoutsCount > 0 || CGPointEqualToPoint(ASCeilPointValues(absolutePosition), layout.position) == NO) { + // Only create a new layout if the existing one can't be reused, which means it has either some sublayouts or an invalid absolute position. + const auto newLayout = [ASLayout layoutWithLayoutElement:layout->_layoutElement + size:layout.size + position:absolutePosition + sublayouts:@[]]; + flattenedSublayouts.push_back(newLayout); + } else { + flattenedSublayouts.push_back(layout); + } + } else if (sublayoutsCount > 0) { + // Fast-reverse-enumerate the sublayouts array by copying it into a C-array and push_front'ing each into the queue. + unowned ASLayout *rawSublayouts[sublayoutsCount]; + [layout->_sublayouts getObjects:rawSublayouts range:NSMakeRange(0, sublayoutsCount)]; + for (NSInteger i = sublayoutsCount - 1; i >= 0; i--) { + queue.push_front({rawSublayouts[i], absolutePosition + rawSublayouts[i].position}); + } + } + } + + NSArray *array = [NSArray arrayByTransferring:flattenedSublayouts.data() count:flattenedSublayouts.size()]; + // flattenedSublayouts is now all nils. + + ASLayout *layout = [ASLayout layoutWithLayoutElement:_layoutElement size:_size sublayouts:array]; + // All flattened layouts must retain sublayout elements until they are applied. + [layout retainSublayoutElements]; + return layout; +} + +#pragma mark - Equality Checking + +- (BOOL)isEqual:(id)object +{ + if (self == object) return YES; + + ASLayout *layout = ASDynamicCast(object, ASLayout); + if (layout == nil) { + return NO; + } + + if (!CGSizeEqualToSize(_size, layout.size)) return NO; + + if (!((ASPointIsNull(self.position) && ASPointIsNull(layout.position)) + || CGPointEqualToPoint(self.position, layout.position))) return NO; + if (_layoutElement != layout.layoutElement) return NO; + + if (!ASObjectIsEqual(_sublayouts, layout.sublayouts)) { + return NO; + } + + return YES; +} + +#pragma mark - Accessors + +- (ASLayoutElementType)type +{ + return _layoutElementType; +} + +- (CGRect)frameForElement:(id)layoutElement +{ + for (ASLayout *l in _sublayouts) { + if (l->_layoutElement == layoutElement) { + return l.frame; + } + } + return CGRectNull; +} + +- (CGRect)frame +{ + CGRect subnodeFrame = CGRectZero; + CGPoint adjustedOrigin = _position; + if (isfinite(adjustedOrigin.x) == NO) { + ASDisplayNodeAssert(0, @"Layout has an invalid position"); + adjustedOrigin.x = 0; + } + if (isfinite(adjustedOrigin.y) == NO) { + ASDisplayNodeAssert(0, @"Layout has an invalid position"); + adjustedOrigin.y = 0; + } + subnodeFrame.origin = adjustedOrigin; + + CGSize adjustedSize = _size; + if (isfinite(adjustedSize.width) == NO) { + ASDisplayNodeAssert(0, @"Layout has an invalid size"); + adjustedSize.width = 0; + } + if (isfinite(adjustedSize.height) == NO) { + ASDisplayNodeAssert(0, @"Layout has an invalid position"); + adjustedSize.height = 0; + } + subnodeFrame.size = adjustedSize; + + return subnodeFrame; +} + +#pragma mark - Description + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + [result addObject:@{ @"size" : [NSValue valueWithCGSize:self.size] }]; + + if (id layoutElement = self.layoutElement) { + [result addObject:@{ @"layoutElement" : layoutElement }]; + } + + const auto pos = self.position; + if (!ASPointIsNull(pos)) { + [result addObject:@{ @"position" : [NSValue valueWithCGPoint:pos] }]; + } + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSString *)recursiveDescription +{ + return [self _recursiveDescriptionForLayout:self level:0]; +} + +- (NSString *)_recursiveDescriptionForLayout:(ASLayout *)layout level:(NSUInteger)level +{ + NSMutableString *description = [NSMutableString string]; + [description appendString:descriptionIndents(level)]; + [description appendString:[layout description]]; + for (ASLayout *sublayout in layout.sublayouts) { + [description appendString:@"\n"]; + [description appendString:[self _recursiveDescriptionForLayout:sublayout level:level + 1]]; + } + return description; +} + +@end + +ASLayout *ASCalculateLayout(id layoutElement, const ASSizeRange sizeRange, const CGSize parentSize) +{ + NSCParameterAssert(layoutElement != nil); + + return [layoutElement layoutThatFits:sizeRange parentSize:parentSize]; +} + +ASLayout *ASCalculateRootLayout(id rootLayoutElement, const ASSizeRange sizeRange) +{ + ASLayout *layout = ASCalculateLayout(rootLayoutElement, sizeRange, sizeRange.max); + // Here could specific verfication happen + return layout; +} diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutElement.mm b/submodules/AsyncDisplayKit/Source/ASLayoutElement.mm new file mode 100644 index 0000000000..887f609e99 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutElement.mm @@ -0,0 +1,843 @@ +// +// 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 + +using AS::MutexLocker; + +#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; + +#if AS_TLS_AVAILABLE + +static _Thread_local __unsafe_unretained ASLayoutElementContext *tls_context; + +void ASLayoutElementPushContext(ASLayoutElementContext *context) +{ + // 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; +} + +ASLayoutElementContext *ASLayoutElementGetCurrentContext() +{ + // Don't retain here. Caller will retain if it wants to! + return tls_context; +} + +void ASLayoutElementPopContext() +{ + ASDisplayNodeCAssertNotNil(tls_context, @"Attempt to pop context when there wasn't a context!"); + CFRelease((__bridge CFTypeRef)tls_context); + tls_context = nil; +} + +#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."); + + const auto cfCtx = (__bridge_retained CFTypeRef)context; + pthread_setspecific(ASLayoutElementContextKey(), cfCtx); +} + +ASLayoutElementContext *ASLayoutElementGetCurrentContext() +{ + // Don't retain here. Caller will retain if it wants to! + const auto ctxPtr = pthread_getspecific(ASLayoutElementContextKey()); + return (__bridge ASLayoutElementContext *)ctxPtr; +} + +void ASLayoutElementPopContext() +{ + const auto 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 { + AS::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; + ASStackLayoutAlignItems _parentAlignStyle; +#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(); +#if YOGA + _parentAlignStyle = ASStackLayoutAlignItemsNotSet; +#endif + } + 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"); + + MutexLocker l(__instanceLock__); + _extensions.boolExtensions[idx] = value; +} + +- (BOOL)layoutOptionExtensionBoolAtIndex:(int)idx\ +{ + NSCAssert(idx < kMaxLayoutElementBoolExtensions, @"Accessing index outside of max bool extensions space"); + + 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"); + + MutexLocker l(__instanceLock__); + _extensions.integerExtensions[idx] = value; +} + +- (NSInteger)layoutOptionExtensionIntegerAtIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateIntegerExtensions, @"Accessing index outside of max integer extensions space"); + + 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"); + + MutexLocker l(__instanceLock__); + _extensions.edgeInsetsExtensions[idx] = value; +} + +- (UIEdgeInsets)layoutOptionExtensionEdgeInsetsAtIndex:(int)idx +{ + NSCAssert(idx < kMaxLayoutElementStateEdgeInsetExtensions, @"Accessing index outside of max edge insets extensions space"); + + 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(); } +// private (ASLayoutElementStylePrivate.h) +- (ASStackLayoutAlignItems)parentAlignStyle { + return _parentAlignStyle; +} + +- (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); +} +// private (ASLayoutElementStylePrivate.h) +- (void)setParentAlignStyle:(ASStackLayoutAlignItems)style { + _parentAlignStyle = style; +} + +#endif /* YOGA */ + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutElementStylePrivate.h b/submodules/AsyncDisplayKit/Source/ASLayoutElementStylePrivate.h new file mode 100644 index 0000000000..69e29824ef --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutElementStylePrivate.h @@ -0,0 +1,31 @@ +// +// ASLayoutElementStylePrivate.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 +// + +#pragma once + +#import +#import + +@interface ASLayoutElementStyle () + +/** + * @abstract The object that acts as the delegate of the style. + * + * @discussion The delegate must adopt the ASLayoutElementStyleDelegate protocol. The delegate is not retained. + */ +@property (nullable, nonatomic, weak) id delegate; + +/** + * @abstract A size constraint that should apply to this ASLayoutElement. + */ +@property (nonatomic, readonly) ASLayoutElementSize size; + +@property (nonatomic, assign) ASStackLayoutAlignItems parentAlignStyle; + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutManager.h b/submodules/AsyncDisplayKit/Source/ASLayoutManager.h new file mode 100644 index 0000000000..1396bf203e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutManager.h @@ -0,0 +1,16 @@ +// +// ASLayoutManager.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 + +AS_SUBCLASSING_RESTRICTED +@interface ASLayoutManager : NSLayoutManager + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutManager.mm b/submodules/AsyncDisplayKit/Source/ASLayoutManager.mm new file mode 100644 index 0000000000..9eca28e489 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutManager.mm @@ -0,0 +1,42 @@ +// +// ASLayoutManager.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 "ASLayoutManager.h" + +@implementation ASLayoutManager + +- (void)showCGGlyphs:(const CGGlyph *)glyphs + positions:(const CGPoint *)positions + count:(NSUInteger)glyphCount + font:(UIFont *)font + matrix:(CGAffineTransform)textMatrix + attributes:(NSDictionary *)attributes + inContext:(CGContextRef)graphicsContext +{ + + // NSLayoutManager has a hard coded internal color for hyperlinks which ignores + // NSForegroundColorAttributeName. To get around this, we force the fill color + // in the current context to match NSForegroundColorAttributeName. + UIColor *foregroundColor = attributes[NSForegroundColorAttributeName]; + + if (foregroundColor) + { + CGContextSetFillColorWithColor(graphicsContext, foregroundColor.CGColor); + } + + [super showCGGlyphs:glyphs + positions:positions + count:glyphCount + font:font + matrix:textMatrix + attributes:attributes + inContext:graphicsContext]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutSpec+Subclasses.h b/submodules/AsyncDisplayKit/Source/ASLayoutSpec+Subclasses.h new file mode 100644 index 0000000000..34bf6e069f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutSpec+Subclasses.h @@ -0,0 +1,59 @@ +// +// ASLayoutSpec+Subclasses.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 +#import + +NS_ASSUME_NONNULL_BEGIN + +@protocol ASLayoutElement; + +@interface ASLayoutSpec (Subclassing) + +/** + * Adds a child with the given identifier to this layout spec. + * + * @param child A child to be added. + * + * @param index An index associated with the child. + * + * @discussion Every ASLayoutSpec must act on at least one child. The ASLayoutSpec base class takes the + * responsibility of holding on to the spec children. Some layout specs, like ASInsetLayoutSpec, + * only require a single child. + * + * For layout specs that require a known number of children (ASBackgroundLayoutSpec, for example) + * a subclass can use the setChild method to set the "primary" child. It should then use this method + * to set any other required children. Ideally a subclass would hide this from the user, and use the + * setChild:forIndex: internally. For example, ASBackgroundLayoutSpec exposes a backgroundChild + * property that behind the scenes is calling setChild:forIndex:. + */ +- (void)setChild:(id)child atIndex:(NSUInteger)index; + +/** + * Returns the child added to this layout spec using the given index. + * + * @param index An identifier associated with the the child. + */ +- (nullable id)childAtIndex:(NSUInteger)index; + +@end + +@interface ASLayout () + +/** + * Position in parent. Default to CGPointNull. + * + * @discussion When being used as a sublayout, this property must not equal CGPointNull. + */ +@property (nonatomic) CGPoint position; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutSpec+Subclasses.mm b/submodules/AsyncDisplayKit/Source/ASLayoutSpec+Subclasses.mm new file mode 100644 index 0000000000..c1e4e2496a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutSpec+Subclasses.mm @@ -0,0 +1,87 @@ +// +// ASLayoutSpec+Subclasses.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 "ASLayoutSpec+Subclasses.h" + +#import +#import "ASLayoutSpecPrivate.h" + +#pragma mark - ASNullLayoutSpec + +@interface ASNullLayoutSpec : ASLayoutSpec +- (instancetype)init NS_UNAVAILABLE; ++ (ASNullLayoutSpec *)null; +@end + +@implementation ASNullLayoutSpec : ASLayoutSpec + ++ (ASNullLayoutSpec *)null +{ + static ASNullLayoutSpec *sharedNullLayoutSpec = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + sharedNullLayoutSpec = [[self alloc] init]; + }); + return sharedNullLayoutSpec; +} + +- (BOOL)isMutable +{ + return NO; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + return [ASLayout layoutWithLayoutElement:self size:CGSizeZero]; +} + +@end + + +#pragma mark - ASLayoutSpec (Subclassing) + +@implementation ASLayoutSpec (Subclassing) + +#pragma mark - Child with index + +- (void)setChild:(id)child atIndex:(NSUInteger)index +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + + id layoutElement = child ?: [ASNullLayoutSpec null]; + + if (child) { + if (_childrenArray.count < index) { + // Fill up the array with null objects until the index + NSInteger i = _childrenArray.count; + while (i < index) { + _childrenArray[i] = [ASNullLayoutSpec null]; + i++; + } + } + } + + // Replace object at the given index with the layoutElement + _childrenArray[index] = layoutElement; +} + +- (id)childAtIndex:(NSUInteger)index +{ + id layoutElement = nil; + if (index < _childrenArray.count) { + layoutElement = _childrenArray[index]; + } + + // Null layoutElement should not be accessed + ASDisplayNodeAssert(layoutElement != [ASNullLayoutSpec null], @"Access child at index without set a child at that index"); + + return layoutElement; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutSpec.mm b/submodules/AsyncDisplayKit/Source/ASLayoutSpec.mm new file mode 100644 index 0000000000..6123e4d734 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutSpec.mm @@ -0,0 +1,338 @@ +// +// ASLayoutSpec.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 "ASLayoutSpecPrivate.h" + +#import "ASLayoutSpec+Subclasses.h" + +#import +#import "ASLayoutElementStylePrivate.h" +#import +#import +#import + +#import +#import +#import + +@implementation ASLayoutSpec + +// Dynamic properties for ASLayoutElements +@dynamic layoutElementType; +@synthesize debugName = _debugName; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + _isMutable = YES; + _primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); + _childrenArray = [[NSMutableArray alloc] init]; + + return self; +} + +- (ASLayoutElementType)layoutElementType +{ + return ASLayoutElementTypeLayoutSpec; +} + +- (BOOL)canLayoutAsynchronous +{ + return YES; +} + +- (BOOL)implementsLayoutMethod +{ + return YES; +} + +#pragma mark - Style + +- (ASLayoutElementStyle *)style +{ + AS::MutexLocker l(__instanceLock__); + if (_style == nil) { + _style = [[ASLayoutElementStyle alloc] init]; + } + return _style; +} + +- (instancetype)styledWithBlock:(AS_NOESCAPE void (^)(__kindof ASLayoutElementStyle *style))styleBlock +{ + styleBlock(self.style); + return self; +} + +#pragma mark - Layout + +ASLayoutElementLayoutCalculationDefaults + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + return [ASLayout layoutWithLayoutElement:self size:constrainedSize.min]; +} + +#pragma mark - Child + +- (void)setChild:(id)child +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + ASDisplayNodeAssert(_childrenArray.count < 2, @"This layout spec does not support more than one child. Use the setChildren: or the setChild:AtIndex: API"); + + if (child) { + _childrenArray[0] = child; + } else { + if (_childrenArray.count) { + [_childrenArray removeObjectAtIndex:0]; + } + } +} + +- (id)child +{ + ASDisplayNodeAssert(_childrenArray.count < 2, @"This layout spec does not support more than one child. Use the setChildren: or the setChild:AtIndex: API"); + + return _childrenArray.firstObject; +} + +#pragma mark - Children + +- (void)setChildren:(NSArray> *)children +{ + ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable"); + +#if ASDISPLAYNODE_ASSERTIONS_ENABLED + for (id child in children) { + ASDisplayNodeAssert([child conformsToProtocol:NSProtocolFromString(@"ASLayoutElement")], @"Child %@ of spec %@ is not an ASLayoutElement!", child, self); + } +#endif + [_childrenArray setArray:children]; +} + +- (nullable NSArray> *)children +{ + return [_childrenArray copy]; +} + +- (NSArray> *)sublayoutElements +{ + return [_childrenArray copy]; +} + +#pragma mark - NSFastEnumeration + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id __unsafe_unretained _Nullable [_Nonnull])buffer count:(NSUInteger)len +{ + return [_childrenArray countByEnumeratingWithState:state objects:buffer count:len]; +} + +#pragma mark - ASTraitEnvironment + +- (ASTraitCollection *)asyncTraitCollection +{ + AS::MutexLocker l(__instanceLock__); + return [ASTraitCollection traitCollectionWithASPrimitiveTraitCollection:self.primitiveTraitCollection]; +} + +ASPrimitiveTraitCollectionDefaults + +#pragma mark - ASLayoutElementStyleExtensibility + +ASLayoutElementStyleExtensibilityForwarding + +#pragma mark - ASDescriptionProvider + +- (NSMutableArray *)propertiesForDescription +{ + const auto result = [NSMutableArray array]; + if (NSArray *children = self.children) { + // Use tiny descriptions because these trees can get nested very deep. + const auto tinyDescriptions = ASArrayByFlatMapping(children, id object, ASObjectDescriptionMakeTiny(object)); + [result addObject:@{ @"children": tinyDescriptions }]; + } + return result; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +#pragma mark - Framework Private + +#if AS_DEDUPE_LAYOUT_SPEC_TREE +- (nullable NSHashTable> *)findDuplicatedElementsInSubtree +{ + NSHashTable *result = nil; + NSUInteger count = 0; + [self _findDuplicatedElementsInSubtreeWithWorkingSet:[NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality] workingCount:&count result:&result]; + return result; +} + +/** + * This method is extremely performance-sensitive, so we do some strange things. + * + * @param workingSet A working set of elements for use in the recursion. + * @param workingCount The current count of the set for use in the recursion. + * @param result The set into which to put the result. This initially points to @c nil to save time if no duplicates exist. + */ +- (void)_findDuplicatedElementsInSubtreeWithWorkingSet:(NSHashTable> *)workingSet workingCount:(NSUInteger *)workingCount result:(NSHashTable> * _Nullable *)result +{ + Class layoutSpecClass = [ASLayoutSpec class]; + + for (id child in self) { + // Add the object into the set. + [workingSet addObject:child]; + + // Check that addObject: caused the count to increase. + // This is faster than using containsObject. + NSUInteger oldCount = *workingCount; + NSUInteger newCount = workingSet.count; + BOOL objectAlreadyExisted = (newCount != oldCount + 1); + if (objectAlreadyExisted) { + if (*result == nil) { + *result = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + } + [*result addObject:child]; + } else { + *workingCount = newCount; + // If child is a layout spec we haven't visited, recurse its children. + if ([child isKindOfClass:layoutSpecClass]) { + [(ASLayoutSpec *)child _findDuplicatedElementsInSubtreeWithWorkingSet:workingSet workingCount:workingCount result:result]; + } + } + } +} +#endif + +#pragma mark - Debugging + +- (NSString *)debugName +{ + AS::MutexLocker l(__instanceLock__); + return _debugName; +} + +- (void)setDebugName:(NSString *)debugName +{ + AS::MutexLocker l(__instanceLock__); + if (!ASObjectIsEqual(_debugName, debugName)) { + _debugName = [debugName copy]; + } +} + +#pragma mark - ASLayoutElementAsciiArtProtocol + +- (NSString *)asciiArtString +{ + NSArray *children = self.children.count < 2 && self.child ? @[self.child] : self.children; + return [ASLayoutSpec asciiArtStringForChildren:children parentName:[self asciiArtName]]; +} + +- (NSString *)asciiArtName +{ + NSMutableString *result = [NSMutableString stringWithCString:object_getClassName(self) encoding:NSASCIIStringEncoding]; + if (_debugName) { + [result appendFormat:@" (%@)", _debugName]; + } + return result; +} + +ASSynthesizeLockingMethodsWithMutex(__instanceLock__) + +@end + +#pragma mark - ASWrapperLayoutSpec + +@implementation ASWrapperLayoutSpec + ++ (instancetype)wrapperWithLayoutElement:(id)layoutElement NS_RETURNS_RETAINED +{ + return [[self alloc] initWithLayoutElement:layoutElement]; +} + +- (instancetype)initWithLayoutElement:(id)layoutElement +{ + self = [super init]; + if (self) { + self.child = layoutElement; + } + return self; +} + ++ (instancetype)wrapperWithLayoutElements:(NSArray> *)layoutElements NS_RETURNS_RETAINED +{ + return [[self alloc] initWithLayoutElements:layoutElements]; +} + +- (instancetype)initWithLayoutElements:(NSArray> *)layoutElements +{ + self = [super init]; + if (self) { + self.children = layoutElements; + } + return self; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize +{ + NSArray *children = self.children; + const auto count = children.count; + ASLayout *rawSublayouts[count]; + int i = 0; + + CGSize size = constrainedSize.min; + for (id child in children) { + ASLayout *sublayout = [child layoutThatFits:constrainedSize parentSize:constrainedSize.max]; + sublayout.position = CGPointZero; + + size.width = MAX(size.width, sublayout.size.width); + size.height = MAX(size.height, sublayout.size.height); + + rawSublayouts[i++] = sublayout; + } + const auto sublayouts = [NSArray arrayByTransferring:rawSublayouts count:i]; + return [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts]; +} + +@end + +#pragma mark - ASLayoutSpec (Debugging) + +@implementation ASLayoutSpec (Debugging) + +#pragma mark - ASCII Art Helpers + ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName direction:(ASStackLayoutDirection)direction +{ + NSMutableArray *childStrings = [NSMutableArray array]; + for (id layoutChild in children) { + NSString *childString = [layoutChild asciiArtString]; + if (childString) { + [childStrings addObject:childString]; + } + } + if (direction == ASStackLayoutDirectionHorizontal) { + return [ASAsciiArtBoxCreator horizontalBoxStringForChildren:childStrings parent:parentName]; + } + return [ASAsciiArtBoxCreator verticalBoxStringForChildren:childStrings parent:parentName]; +} + ++ (NSString *)asciiArtStringForChildren:(NSArray *)children parentName:(NSString *)parentName +{ + return [self asciiArtStringForChildren:children parentName:parentName direction:ASStackLayoutDirectionHorizontal]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutSpecPrivate.h b/submodules/AsyncDisplayKit/Source/ASLayoutSpecPrivate.h new file mode 100644 index 0000000000..930232096c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutSpecPrivate.h @@ -0,0 +1,37 @@ +// +// ASLayoutSpecPrivate.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 + +#if DEBUG + #define AS_DEDUPE_LAYOUT_SPEC_TREE 1 +#else + #define AS_DEDUPE_LAYOUT_SPEC_TREE 0 +#endif + +NS_ASSUME_NONNULL_BEGIN + +@interface ASLayoutSpec() { + AS::RecursiveMutex __instanceLock__; + std::atomic _primitiveTraitCollection; + ASLayoutElementStyle *_style; + NSMutableArray *_childrenArray; +} + +#if AS_DEDUPE_LAYOUT_SPEC_TREE +/** + * Recursively search the subtree for elements that occur more than once. + */ +- (nullable NSHashTable> *)findDuplicatedElementsInSubtree; +#endif + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutSpecUtilities.h b/submodules/AsyncDisplayKit/Source/ASLayoutSpecUtilities.h new file mode 100644 index 0000000000..62a3d9177b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutSpecUtilities.h @@ -0,0 +1,103 @@ +// +// ASLayoutSpecUtilities.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 +#import +#import +#import + +namespace AS { + // adopted from http://stackoverflow.com/questions/14945223/map-function-with-c11-constructs + // Takes an iterable, applies a function to every element, + // and returns a vector of the results + // + template + auto map(const T &iterable, Func &&func) -> std::vector()))> + { + // Some convenience type definitions + typedef decltype(func(std::declval())) value_type; + typedef std::vector result_type; + + // Prepares an output vector of the appropriate size + result_type res(iterable.size()); + + // Let std::transform apply `func` to all elements + // (use perfect forwarding for the function object) + std::transform( + begin(iterable), end(iterable), res.begin(), + std::forward(func) + ); + + return res; + } + + template + auto map(id collection, Func &&func) -> std::vector()))> + { + std::vector()))> to; + for (id obj in collection) { + to.push_back(func(obj)); + } + return to; + } + + template + auto filter(const T &iterable, Func &&func) -> std::vector + { + std::vector to; + for (auto obj : iterable) { + if (func(obj)) { + to.push_back(obj); + } + } + return to; + } +}; + +inline CGPoint operator+(const CGPoint &p1, const CGPoint &p2) +{ + return { p1.x + p2.x, p1.y + p2.y }; +} + +inline CGPoint operator-(const CGPoint &p1, const CGPoint &p2) +{ + return { p1.x - p2.x, p1.y - p2.y }; +} + +inline CGSize operator+(const CGSize &s1, const CGSize &s2) +{ + return { s1.width + s2.width, s1.height + s2.height }; +} + +inline CGSize operator-(const CGSize &s1, const CGSize &s2) +{ + return { s1.width - s2.width, s1.height - s2.height }; +} + +inline UIEdgeInsets operator+(const UIEdgeInsets &e1, const UIEdgeInsets &e2) +{ + return { e1.top + e2.top, e1.left + e2.left, e1.bottom + e2.bottom, e1.right + e2.right }; +} + +inline UIEdgeInsets operator-(const UIEdgeInsets &e1, const UIEdgeInsets &e2) +{ + return { e1.top - e2.top, e1.left - e2.left, e1.bottom - e2.bottom, e1.right - e2.right }; +} + +inline UIEdgeInsets operator*(const UIEdgeInsets &e1, const UIEdgeInsets &e2) +{ + return { e1.top * e2.top, e1.left * e2.left, e1.bottom * e2.bottom, e1.right * e2.right }; +} + +inline UIEdgeInsets operator-(const UIEdgeInsets &e) +{ + return { -e.top, -e.left, -e.bottom, -e.right }; +} diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutTransition.h b/submodules/AsyncDisplayKit/Source/ASLayoutTransition.h new file mode 100644 index 0000000000..d11eb65fb1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutTransition.h @@ -0,0 +1,94 @@ +// +// ASLayoutTransition.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 +#import +#import "ASDisplayNodeLayout.h" +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +#pragma mark - ASLayoutElementTransition + +/** + * Objects conform to this project returns if it's possible to layout asynchronous + */ +@protocol ASLayoutElementTransition + +/** + * @abstract Returns if the layoutElement can be used to layout in an asynchronous way on a background thread. + */ +@property (nonatomic, readonly) BOOL canLayoutAsynchronous; + +@end + +@interface ASDisplayNode () +@end +@interface ASLayoutSpec () +@end + + +#pragma mark - ASLayoutTransition + +AS_SUBCLASSING_RESTRICTED +@interface ASLayoutTransition : NSObject <_ASTransitionContextLayoutDelegate> + +/** + * Node to apply layout transition on + */ +@property (nonatomic, weak, readonly) ASDisplayNode *node; + +/** + * Previous layout to transition from + */ +@property (nonatomic, readonly) const ASDisplayNodeLayout &previousLayout NS_RETURNS_INNER_POINTER; + +/** + * Pending layout to transition to + */ +@property (nonatomic, readonly) const ASDisplayNodeLayout &pendingLayout NS_RETURNS_INNER_POINTER; + +/** + * Returns if the layout transition needs to happen synchronously + */ +@property (nonatomic, readonly) BOOL isSynchronous; + +/** + * Returns a newly initialized layout transition + */ +- (instancetype)initWithNode:(ASDisplayNode *)node + pendingLayout:(const ASDisplayNodeLayout &)pendingLayout + previousLayout:(const ASDisplayNodeLayout &)previousLayout NS_DESIGNATED_INITIALIZER; + +/** + * Insert and remove subnodes that were added or removed between the previousLayout and the pendingLayout + */ +- (void)commitTransition; + +/** + * Insert all new subnodes that were added and move the subnodes that moved between the previous layout and + * the pending layout. + */ +- (void)applySubnodeInsertionsAndMoves; + +/** + * Remove all subnodes that are removed between the previous layout and the pending layout + */ +- (void)applySubnodeRemovals; + +- (instancetype)init NS_UNAVAILABLE; +- (instancetype)new NS_UNAVAILABLE; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASLayoutTransition.mm b/submodules/AsyncDisplayKit/Source/ASLayoutTransition.mm new file mode 100644 index 0000000000..0956943cc0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASLayoutTransition.mm @@ -0,0 +1,298 @@ +// +// ASLayoutTransition.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 "ASLayoutTransition.h" + +#import + +#import +#import "ASDisplayNodeInternal.h" // Required for _insertSubnode... / _removeFromSupernode. + +#import + +#if AS_IG_LIST_KIT +#import +#import +#endif + +using AS::MutexLocker; + +/** + * Search the whole layout stack if at least one layout has a layoutElement object that can not be layed out asynchronous. + * This can be the case for example if a node was already loaded + */ +static inline BOOL ASLayoutCanTransitionAsynchronous(ASLayout *layout) { + // Queue used to keep track of sublayouts while traversing this layout in a BFS fashion. + std::queue queue; + queue.push(layout); + + while (!queue.empty()) { + layout = queue.front(); + queue.pop(); + +#if DEBUG + ASDisplayNodeCAssert([layout.layoutElement conformsToProtocol:@protocol(ASLayoutElementTransition)], @"ASLayoutElement in a layout transition needs to conforms to the ASLayoutElementTransition protocol."); +#endif + if (((id)layout.layoutElement).canLayoutAsynchronous == NO) { + return NO; + } + + // Add all sublayouts to process in next step + for (ASLayout *sublayout in layout.sublayouts) { + queue.push(sublayout); + } + } + + return YES; +} + +@implementation ASLayoutTransition { + std::shared_ptr __instanceLock__; + + BOOL _calculatedSubnodeOperations; + NSArray *_insertedSubnodes; + NSArray *_removedSubnodes; + std::vector _insertedSubnodePositions; + std::vector> _subnodeMoves; + ASDisplayNodeLayout _pendingLayout; + ASDisplayNodeLayout _previousLayout; +} + +- (instancetype)initWithNode:(ASDisplayNode *)node + pendingLayout:(const ASDisplayNodeLayout &)pendingLayout + previousLayout:(const ASDisplayNodeLayout &)previousLayout +{ + self = [super init]; + if (self) { + __instanceLock__ = std::make_shared(); + + _node = node; + _pendingLayout = pendingLayout; + _previousLayout = previousLayout; + } + return self; +} + +- (BOOL)isSynchronous +{ + MutexLocker l(*__instanceLock__); + return !ASLayoutCanTransitionAsynchronous(_pendingLayout.layout); +} + +- (void)commitTransition +{ + [self applySubnodeRemovals]; + [self applySubnodeInsertionsAndMoves]; +} + +- (void)applySubnodeInsertionsAndMoves +{ + MutexLocker l(*__instanceLock__); + [self calculateSubnodeOperationsIfNeeded]; + + // Create an activity even if no subnodes affected. + if (_insertedSubnodePositions.size() == 0 && _subnodeMoves.size() == 0) { + return; + } + + ASDisplayNodeLogEvent(_node, @"insertSubnodes: %@", _insertedSubnodes); + NSUInteger i = 0; + NSUInteger j = 0; + for (auto const &move : _subnodeMoves) { + [move.first _removeFromSupernodeIfEqualTo:_node]; + } + j = 0; + while (i < _insertedSubnodePositions.size() && j < _subnodeMoves.size()) { + NSUInteger p = _insertedSubnodePositions[i]; + NSUInteger q = _subnodeMoves[j].second; + if (p < q) { + [_node _insertSubnode:_insertedSubnodes[i] atIndex:p]; + i++; + } else { + [_node _insertSubnode:_subnodeMoves[j].first atIndex:q]; + j++; + } + } + for (; i < _insertedSubnodePositions.size(); ++i) { + [_node _insertSubnode:_insertedSubnodes[i] atIndex:_insertedSubnodePositions[i]]; + } + for (; j < _subnodeMoves.size(); ++j) { + [_node _insertSubnode:_subnodeMoves[j].first atIndex:_subnodeMoves[j].second]; + } +} + +- (void)applySubnodeRemovals +{ + MutexLocker l(*__instanceLock__); + [self calculateSubnodeOperationsIfNeeded]; + + if (_removedSubnodes.count == 0) { + return; + } + + ASDisplayNodeLogEvent(_node, @"removeSubnodes: %@", _removedSubnodes); + for (ASDisplayNode *subnode in _removedSubnodes) { + // In this case we should only remove the subnode if it's still a subnode of the _node that executes a layout transition. + // It can happen that a node already did a layout transition and added this subnode, in this case the subnode + // would be removed from the new node instead of _node + if (_node.automaticallyManagesSubnodes) { + [subnode _removeFromSupernodeIfEqualTo:_node]; + } + } +} + +- (void)calculateSubnodeOperationsIfNeeded +{ + MutexLocker l(*__instanceLock__); + if (_calculatedSubnodeOperations) { + return; + } + + // Create an activity even if no subnodes affected. + ASLayout *previousLayout = _previousLayout.layout; + ASLayout *pendingLayout = _pendingLayout.layout; + + if (previousLayout) { +#if AS_IG_LIST_KIT + // IGListDiff completes in linear time O(m+n), so use it if we have it: + IGListIndexSetResult *result = IGListDiff(previousLayout.sublayouts, pendingLayout.sublayouts, IGListDiffEquality); + _insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, result.inserts, &_insertedSubnodes); + findNodesInLayoutAtIndexes(previousLayout, result.deletes, &_removedSubnodes); + for (IGListMoveIndex *move in result.moves) { + _subnodeMoves.push_back(std::make_pair(previousLayout.sublayouts[move.from].layoutElement, move.to)); + } + + // Sort by ascending order of move destinations, this will allow easy loop of `insertSubnode:AtIndex` later. + std::sort(_subnodeMoves.begin(), _subnodeMoves.end(), [](std::pair, NSUInteger> a, + std::pair b) { + return a.second < b.second; + }); +#else + NSIndexSet *insertions, *deletions; + NSArray *moves; + NSArray *previousNodes = [previousLayout.sublayouts valueForKey:@"layoutElement"]; + NSArray *pendingNodes = [pendingLayout.sublayouts valueForKey:@"layoutElement"]; + [previousNodes asdk_diffWithArray:pendingNodes + insertions:&insertions + deletions:&deletions + moves:&moves]; + + _insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, insertions, &_insertedSubnodes); + _removedSubnodes = [previousNodes objectsAtIndexes:deletions]; + // These should arrive sorted in ascending order of move destinations. + for (NSIndexPath *move in moves) { + _subnodeMoves.push_back(std::make_pair(previousLayout.sublayouts[([move indexAtPosition:0])].layoutElement, + [move indexAtPosition:1])); + } +#endif + } else { + NSIndexSet *indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [pendingLayout.sublayouts count])]; + _insertedSubnodePositions = findNodesInLayoutAtIndexes(pendingLayout, indexes, &_insertedSubnodes); + _removedSubnodes = nil; + } + _calculatedSubnodeOperations = YES; +} + +#pragma mark - _ASTransitionContextDelegate + +- (NSArray *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + MutexLocker l(*__instanceLock__); + return _node.subnodes; +} + +- (NSArray *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + MutexLocker l(*__instanceLock__); + [self calculateSubnodeOperationsIfNeeded]; + return _insertedSubnodes; +} + +- (NSArray *)removedSubnodesWithTransitionContext:(_ASTransitionContext *)context +{ + MutexLocker l(*__instanceLock__); + [self calculateSubnodeOperationsIfNeeded]; + return _removedSubnodes; +} + +- (ASLayout *)transitionContext:(_ASTransitionContext *)context layoutForKey:(NSString *)key +{ + MutexLocker l(*__instanceLock__); + if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { + return _previousLayout.layout; + } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { + return _pendingLayout.layout; + } else { + return nil; + } +} + +- (ASSizeRange)transitionContext:(_ASTransitionContext *)context constrainedSizeForKey:(NSString *)key +{ + MutexLocker l(*__instanceLock__); + if ([key isEqualToString:ASTransitionContextFromLayoutKey]) { + return _previousLayout.constrainedSize; + } else if ([key isEqualToString:ASTransitionContextToLayoutKey]) { + return _pendingLayout.constrainedSize; + } else { + return ASSizeRangeMake(CGSizeZero, CGSizeZero); + } +} + +#pragma mark - Filter helpers + +/** + * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. + */ +static inline std::vector findNodesInLayoutAtIndexes(ASLayout *layout, + NSIndexSet *indexes, + NSArray * __strong *storedNodes) +{ + return findNodesInLayoutAtIndexesWithFilteredNodes(layout, indexes, nil, storedNodes); +} + +/** + * @abstract Stores the nodes at the given indexes in the `storedNodes` array, storing indexes in a `storedPositions` c++ vector. + * Call only with a flattened layout. + * @discussion If the node exists in the `filteredNodes` array, the node is not added to `storedNodes`. + */ +static inline std::vector findNodesInLayoutAtIndexesWithFilteredNodes(ASLayout *layout, + NSIndexSet *indexes, + NSArray *filteredNodes, + NSArray * __strong *storedNodes) +{ + NSMutableArray *nodes = [NSMutableArray arrayWithCapacity:indexes.count]; + std::vector positions = std::vector(); + + // From inspection, this is how enumerateObjectsAtIndexes: works under the hood + NSUInteger firstIndex = indexes.firstIndex; + NSUInteger lastIndex = indexes.lastIndex; + NSUInteger idx = 0; + for (ASLayout *sublayout in layout.sublayouts) { + if (idx > lastIndex) { break; } + if (idx >= firstIndex && [indexes containsIndex:idx]) { + ASDisplayNode *node = (ASDisplayNode *)(sublayout.layoutElement); + ASDisplayNodeCAssert(node, @"ASDisplayNode was deallocated before it was added to a subnode. It's likely the case that you use automatically manages subnodes and allocate a ASDisplayNode in layoutSpecThatFits: and don't have any strong reference to it."); + ASDisplayNodeCAssert([node isKindOfClass:[ASDisplayNode class]], @"sublayout is an ASLayout, but not an ASDisplayNode - only call findNodesInLayoutAtIndexesWithFilteredNodes with a flattened layout (all sublayouts are ASDisplayNodes)."); + if (node != nil) { + BOOL notFiltered = (filteredNodes == nil || [filteredNodes indexOfObjectIdenticalTo:node] == NSNotFound); + if (notFiltered) { + [nodes addObject:node]; + positions.push_back(idx); + } + } + } + idx += 1; + } + *storedNodes = nodes; + + return positions; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASMainSerialQueue.h b/submodules/AsyncDisplayKit/Source/ASMainSerialQueue.h new file mode 100644 index 0000000000..405164f75d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMainSerialQueue.h @@ -0,0 +1,19 @@ +// +// ASMainSerialQueue.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 + +AS_SUBCLASSING_RESTRICTED +@interface ASMainSerialQueue : NSObject + +@property (nonatomic, readonly) NSUInteger numberOfScheduledBlocks; +- (void)performBlockOnMainThread:(dispatch_block_t)block; + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASMainSerialQueue.mm b/submodules/AsyncDisplayKit/Source/ASMainSerialQueue.mm new file mode 100644 index 0000000000..60bf8d7f67 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMainSerialQueue.mm @@ -0,0 +1,81 @@ +// +// ASMainSerialQueue.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 "ASMainSerialQueue.h" + +#import +#import + +@interface ASMainSerialQueue () +{ + AS::Mutex _serialQueueLock; + NSMutableArray *_blocks; +} + +@end + +@implementation ASMainSerialQueue + +- (instancetype)init +{ + if (!(self = [super init])) { + return nil; + } + + _blocks = [[NSMutableArray alloc] init]; + return self; +} + +- (NSUInteger)numberOfScheduledBlocks +{ + AS::MutexLocker l(_serialQueueLock); + return _blocks.count; +} + +- (void)performBlockOnMainThread:(dispatch_block_t)block +{ + + AS::UniqueLock l(_serialQueueLock); + [_blocks addObject:block]; + { + l.unlock(); + [self runBlocks]; + l.lock(); + } +} + +- (void)runBlocks +{ + dispatch_block_t mainThread = ^{ + AS::UniqueLock l(self->_serialQueueLock); + do { + dispatch_block_t block; + if (self->_blocks.count > 0) { + block = _blocks[0]; + [self->_blocks removeObjectAtIndex:0]; + } else { + break; + } + { + l.unlock(); + block(); + l.lock(); + } + } while (true); + }; + + ASPerformBlockOnMainThread(mainThread); +} + +- (NSString *)description +{ + return [[super description] stringByAppendingFormat:@" Blocks: %@", _blocks]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.mm b/submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.mm new file mode 100644 index 0000000000..4b16c932d2 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASMainThreadDeallocation.mm @@ -0,0 +1,199 @@ +// +// ASMainThreadDeallocation.mm +// Texture +// +// 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 + +@implementation NSObject (ASMainThreadIvarTeardown) + +- (void)scheduleIvarsForMainThreadDeallocation +{ + if (ASDisplayNodeThreadIsMain()) { + return; + } + + NSValue *ivarsObj = [[self class] _ivarsThatMayNeedMainDeallocation]; + + // Unwrap the ivar array + unsigned int count = 0; + // Will be unused if assertions are disabled. + __unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); + ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType); + Ivar ivars[count]; + [ivarsObj getValue:ivars]; + + for (Ivar ivar : ivars) { + id value = object_getIvar(self, ivar); + if (value == nil) { + continue; + } + + if ([object_getClass(value) needsMainThreadDeallocation]) { + // Release the ivar's reference before handing the object to the queue so we + // don't risk holding onto it longer than the queue does. + object_setIvar(self, ivar, nil); + + ASPerformMainThreadDeallocation(&value); + } else { + } + } +} + +/** + * Returns an NSValue-wrapped array of all the ivars in this class or its superclasses + * up through ASDisplayNode, that we expect may need to be deallocated on main. + * + * This method caches its results. + * + * Result is of type NSValue<[Ivar]> + */ ++ (NSValue * _Nonnull)_ivarsThatMayNeedMainDeallocation NS_RETURNS_RETAINED +{ + static NSCache *ivarsCache; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + ivarsCache = [[NSCache alloc] init]; + }); + + NSValue *result = [ivarsCache objectForKey:self]; + if (result != nil) { + return result; + } + + // Cache miss. + unsigned int resultCount = 0; + static const int kMaxDealloc2MainIvarsPerClassTree = 64; + Ivar resultIvars[kMaxDealloc2MainIvarsPerClassTree]; + + // Get superclass results first. + Class c = class_getSuperclass(self); + if (c != [NSObject class]) { + NSValue *ivarsObj = [c _ivarsThatMayNeedMainDeallocation]; + // Unwrap the ivar array and append it to our working array + unsigned int count = 0; + // Will be unused if assertions are disabled. + __unused int scanResult = sscanf(ivarsObj.objCType, "[%u^{objc_ivar}]", &count); + ASDisplayNodeAssert(scanResult == 1, @"Unexpected type in NSValue: %s", ivarsObj.objCType); + ASDisplayNodeCAssert(resultCount + count < kMaxDealloc2MainIvarsPerClassTree, @"More than %d dealloc2main ivars are not supported. Count: %d", kMaxDealloc2MainIvarsPerClassTree, resultCount + count); + [ivarsObj getValue:resultIvars + resultCount]; + resultCount += count; + } + + // Now gather ivars from this particular class. + unsigned int allMyIvarsCount; + Ivar *allMyIvars = class_copyIvarList(self, &allMyIvarsCount); + + for (NSUInteger i = 0; i < allMyIvarsCount; i++) { + Ivar ivar = allMyIvars[i]; + + // NOTE: Would be great to exclude weak/unowned ivars, since we don't + // release them. Unfortunately the objc_ivar_management access is private and + // class_getWeakIvarLayout does not have a well-defined structure. + + const char *type = ivar_getTypeEncoding(ivar); + + if (type != NULL && strcmp(type, @encode(id)) == 0) { + // If it's `id` we have to include it just in case. + resultIvars[resultCount] = ivar; + resultCount += 1; + } else { + // If it's an ivar with a static type, check the type. + Class c = ASGetClassFromType(type); + if ([c needsMainThreadDeallocation]) { + resultIvars[resultCount] = ivar; + resultCount += 1; + } else { + } + } + } + free(allMyIvars); + + // Encode the type (array of Ivars) into a string and wrap it in an NSValue + char arrayType[32]; + snprintf(arrayType, 32, "[%u^{objc_ivar}]", resultCount); + result = [NSValue valueWithBytes:resultIvars objCType:arrayType]; + + [ivarsCache setObject:result forKey:self]; + return result; +} + +@end + +@implementation NSObject (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + const auto name = class_getName(self); + if (0 == strncmp(name, "AV", 2) || 0 == strncmp(name, "UI", 2) || 0 == strncmp(name, "CA", 2)) { + return YES; + } + return NO; +} + +@end + +@implementation CALayer (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return YES; +} + +@end + +@implementation UIColor (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return NO; +} + +@end + +@implementation UIGestureRecognizer (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return YES; +} + +@end + +@implementation UIImage (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return NO; +} + +@end + +@implementation UIResponder (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return YES; +} + +@end + +@implementation NSProxy (ASNeedsMainThreadDeallocation) + ++ (BOOL)needsMainThreadDeallocation +{ + return NO; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASObjectDescriptionHelpers.mm b/submodules/AsyncDisplayKit/Source/ASObjectDescriptionHelpers.mm new file mode 100644 index 0000000000..2b5d94492c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASObjectDescriptionHelpers.mm @@ -0,0 +1,101 @@ +// +// ASObjectDescriptionHelpers.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 "NSIndexSet+ASHelpers.h" + +NSString *ASGetDescriptionValueString(id object) +{ + if ([object isKindOfClass:[NSValue class]]) { + // Use shortened NSValue descriptions + NSValue *value = object; + const char *type = value.objCType; + + if (strcmp(type, @encode(CGRect)) == 0) { + CGRect rect = [value CGRectValue]; + return [NSString stringWithFormat:@"(%g %g; %g %g)", rect.origin.x, rect.origin.y, rect.size.width, rect.size.height]; + } else if (strcmp(type, @encode(CGSize)) == 0) { + return NSStringFromCGSize(value.CGSizeValue); + } else if (strcmp(type, @encode(CGPoint)) == 0) { + return NSStringFromCGPoint(value.CGPointValue); + } + + } else if ([object isKindOfClass:[NSIndexSet class]]) { + return [object as_smallDescription]; + } else if ([object isKindOfClass:[NSIndexPath class]]) { + // index paths like (0, 7) + NSIndexPath *indexPath = object; + NSMutableArray *strings = [NSMutableArray array]; + for (NSUInteger i = 0; i < indexPath.length; i++) { + [strings addObject:[NSString stringWithFormat:@"%lu", (unsigned long)[indexPath indexAtPosition:i]]]; + } + return [NSString stringWithFormat:@"(%@)", [strings componentsJoinedByString:@", "]]; + } else if ([object respondsToSelector:@selector(componentsJoinedByString:)]) { + // e.g. "[ ]" + return [NSString stringWithFormat:@"[ %@ ]", [object componentsJoinedByString:@" "]]; + } + return [object description]; +} + +NSString *_ASObjectDescriptionMakePropertyList(NSArray * _Nullable propertyGroups) +{ + NSMutableArray *components = [NSMutableArray array]; + for (NSDictionary *properties in propertyGroups) { + [properties enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { + NSString *str; + if (key == (id)kCFNull) { + str = ASGetDescriptionValueString(obj); + } else { + str = [NSString stringWithFormat:@"%@ = %@", key, ASGetDescriptionValueString(obj)]; + } + [components addObject:str]; + }]; + } + return [components componentsJoinedByString:@"; "]; +} + +NSString *ASObjectDescriptionMakeWithoutObject(NSArray * _Nullable propertyGroups) +{ + return [NSString stringWithFormat:@"{ %@ }", _ASObjectDescriptionMakePropertyList(propertyGroups)]; +} + +NSString *ASObjectDescriptionMake(__autoreleasing id object, NSArray *propertyGroups) +{ + if (object == nil) { + return @"(null)"; + } + + NSMutableString *str = [NSMutableString stringWithFormat:@"<%s: %p", object_getClassName(object), object]; + + NSString *propList = _ASObjectDescriptionMakePropertyList(propertyGroups); + if (propList.length > 0) { + [str appendFormat:@"; %@", propList]; + } + [str appendString:@">"]; + return str; +} + +NSString *ASObjectDescriptionMakeTiny(__autoreleasing id object) { + return ASObjectDescriptionMake(object, nil); +} + +NSString *ASStringWithQuotesIfMultiword(NSString *string) { + if (string == nil) { + return nil; + } + + if ([string rangeOfCharacterFromSet:[NSCharacterSet whitespaceCharacterSet]].location != NSNotFound) { + return [NSString stringWithFormat:@"\"%@\"", string]; + } else { + return string; + } +} diff --git a/submodules/AsyncDisplayKit/Source/ASPendingStateController.h b/submodules/AsyncDisplayKit/Source/ASPendingStateController.h new file mode 100644 index 0000000000..b50a7c1f94 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASPendingStateController.h @@ -0,0 +1,50 @@ +// +// ASPendingStateController.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 + +@class ASDisplayNode; + +NS_ASSUME_NONNULL_BEGIN + +/** + A singleton that is responsible for applying changes to + UIView/CALayer properties of display nodes when they + have been set on background threads. + + This controller will enqueue run-loop events to flush changes + but if you need them flushed now you can call `flush` from the main thread. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASPendingStateController : NSObject + ++ (ASPendingStateController *)sharedInstance; + +@property (nonatomic, readonly) BOOL hasChanges; + +/** + Flush all pending states for nodes now. Any UIView/CALayer properties + that have been set in the background will be applied to their + corresponding views/layers before this method returns. + + You must call this method on the main thread. + */ +- (void)flush; + +/** + Register this node as having pending state that needs to be copied + over to the view/layer. This is called automatically by display nodes + when their view/layer properties are set post-load on background threads. + */ +- (void)registerNode:(ASDisplayNode *)node; + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASPendingStateController.mm b/submodules/AsyncDisplayKit/Source/ASPendingStateController.mm new file mode 100644 index 0000000000..e7ca4a71de --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASPendingStateController.mm @@ -0,0 +1,102 @@ +// +// ASPendingStateController.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 "ASPendingStateController.h" +#import +#import +#import "ASDisplayNodeInternal.h" // Required for -applyPendingViewState; consider moving this to +FrameworkPrivate + +@interface ASPendingStateController() +{ + AS::Mutex _lock; + + struct ASPendingStateControllerFlags { + unsigned pendingFlush:1; + } _flags; +} + +@property (nonatomic, readonly) ASWeakSet *dirtyNodes; +@end + +@implementation ASPendingStateController + +#pragma mark Lifecycle & Singleton + +- (instancetype)init +{ + self = [super init]; + if (self) { + _dirtyNodes = [[ASWeakSet alloc] init]; + } + return self; +} + ++ (ASPendingStateController *)sharedInstance +{ + static dispatch_once_t onceToken; + static ASPendingStateController *controller = nil; + dispatch_once(&onceToken, ^{ + controller = [[ASPendingStateController alloc] init]; + }); + return controller; +} + +#pragma mark External API + +- (void)registerNode:(ASDisplayNode *)node +{ + ASDisplayNodeAssert(node.nodeLoaded, @"Expected display node to be loaded before it was registered with ASPendingStateController. Node: %@", node); + AS::MutexLocker l(_lock); + [_dirtyNodes addObject:node]; + + [self scheduleFlushIfNeeded]; +} + +- (void)flush +{ + ASDisplayNodeAssertMainThread(); + _lock.lock(); + ASWeakSet *dirtyNodes = _dirtyNodes; + _dirtyNodes = [[ASWeakSet alloc] init]; + _flags.pendingFlush = NO; + _lock.unlock(); + + for (ASDisplayNode *node in dirtyNodes) { + [node applyPendingViewState]; + } +} + + +#pragma mark Private Methods + +/** + This method is assumed to be called with the lock held. + */ +- (void)scheduleFlushIfNeeded +{ + if (_flags.pendingFlush) { + return; + } + + _flags.pendingFlush = YES; + dispatch_async(dispatch_get_main_queue(), ^{ + [self flush]; + }); +} + +@end + +@implementation ASPendingStateController (Testing) + +- (BOOL)test_isFlushScheduled +{ + return _flags.pendingFlush; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASRecursiveUnfairLock.mm b/submodules/AsyncDisplayKit/Source/ASRecursiveUnfairLock.mm new file mode 100644 index 0000000000..e44eec76d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASRecursiveUnfairLock.mm @@ -0,0 +1,83 @@ +// +// ASRecursiveUnfairLock.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +/** + * For our atomic _thread, we use acquire/release memory order so that we can have + * the minimum possible constraint on the hardware. The default, `memory_order_seq_cst` + * demands that there be a total order of all such modifications as seen by all threads. + * Acquire/release only requires that modifications to this specific atomic are + * synchronized across acquire/release pairs. + * http://en.cppreference.com/w/cpp/atomic/memory_order + * + * Note also that the unfair_lock involves a thread fence as well, so we don't need to + * take care of synchronizing other values. Just the thread value. + */ +#define rul_set_thread(l, t) atomic_store_explicit(&l->_thread, t, memory_order_release) +#define rul_get_thread(l) atomic_load_explicit(&l->_thread, memory_order_acquire) + +void ASRecursiveUnfairLockLock(ASRecursiveUnfairLock *l) +{ + // Try to lock without blocking. If we fail, check what thread owns it. + // Note that the owning thread CAN CHANGE freely, but it can't become `self` + // because only we are `self`. And if it's already `self` then we already have + // the lock, because we reset it to NULL before we unlock. So (thread == self) is + // invariant. + + const pthread_t s = pthread_self(); + if (os_unfair_lock_trylock(&l->_lock)) { + // Owned by nobody. We now have the lock. Assign self. + rul_set_thread(l, s); + } else if (rul_get_thread(l) == s) { + // Owned by self (recursive lock). nop. + } else { + // Owned by other thread. Block and then set thread to self. + os_unfair_lock_lock(&l->_lock); + rul_set_thread(l, s); + } + + l->_count++; +} + +BOOL ASRecursiveUnfairLockTryLock(ASRecursiveUnfairLock *l) +{ + // Same as Lock above. See comments there. + + const pthread_t s = pthread_self(); + if (os_unfair_lock_trylock(&l->_lock)) { + // Owned by nobody. We now have the lock. Assign self. + rul_set_thread(l, s); + } else if (rul_get_thread(l) == s) { + // Owned by self (recursive lock). nop. + } else { + // Owned by other thread. Fail. + return NO; + } + + l->_count++; + return YES; +} + +void ASRecursiveUnfairLockUnlock(ASRecursiveUnfairLock *l) +{ + // Ensure we have the lock. This check may miss some pathological cases, + // but it'll catch 99.999999% of this serious programmer error. + NSCAssert(rul_get_thread(l) == pthread_self(), @"Unlocking from a different thread than locked."); + + if (0 == --l->_count) { + // Note that we have to clear this before unlocking because, if another thread + // succeeds in locking above, but hasn't managed to update _thread, and we + // try to re-lock, and fail the -tryLock, and read _thread, then we'll mistakenly + // think that we still own the lock and proceed without blocking. + rul_set_thread(l, NULL); + os_unfair_lock_unlock(&l->_lock); + } +} diff --git a/submodules/AsyncDisplayKit/Source/ASResponderChainEnumerator.h b/submodules/AsyncDisplayKit/Source/ASResponderChainEnumerator.h new file mode 100644 index 0000000000..dcd2e1c4c4 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASResponderChainEnumerator.h @@ -0,0 +1,29 @@ +// +// ASResponderChainEnumerator.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 + +AS_SUBCLASSING_RESTRICTED +@interface ASResponderChainEnumerator : NSEnumerator + +- (instancetype)initWithResponder:(UIResponder *)responder; + +@end + +@interface UIResponder (ASResponderChainEnumerator) + +- (ASResponderChainEnumerator *)asdk_responderChainEnumerator; + +@end + + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASResponderChainEnumerator.mm b/submodules/AsyncDisplayKit/Source/ASResponderChainEnumerator.mm new file mode 100644 index 0000000000..2d94c99945 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASResponderChainEnumerator.mm @@ -0,0 +1,45 @@ +// +// ASResponderChainEnumerator.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 "ASResponderChainEnumerator.h" +#import + +@implementation ASResponderChainEnumerator { + UIResponder *_currentResponder; +} + +- (instancetype)initWithResponder:(UIResponder *)responder +{ + ASDisplayNodeAssertMainThread(); + if (self = [super init]) { + _currentResponder = responder; + } + return self; +} + +#pragma mark - NSEnumerator + +- (id)nextObject +{ + ASDisplayNodeAssertMainThread(); + id result = [_currentResponder nextResponder]; + _currentResponder = result; + return result; +} + +@end + +@implementation UIResponder (ASResponderChainEnumerator) + +- (NSEnumerator *)asdk_responderChainEnumerator +{ + return [[ASResponderChainEnumerator alloc] initWithResponder:self]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASRunLoopQueue.mm b/submodules/AsyncDisplayKit/Source/ASRunLoopQueue.mm new file mode 100644 index 0000000000..2d2415e2b1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASRunLoopQueue.mm @@ -0,0 +1,464 @@ +// +// ASRunLoopQueue.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 "ASSignpost.h" +#import +#import +#import +#import + +#define ASRunLoopQueueLoggingEnabled 0 +#define ASRunLoopQueueVerboseLoggingEnabled 0 + +using AS::MutexLocker; + +static void runLoopSourceCallback(void *info) { + // No-op +#if ASRunLoopQueueVerboseLoggingEnabled + NSLog(@"<%@> - Called runLoopSourceCallback", info); +#endif +} + +#pragma mark - ASDeallocQueue + +@implementation ASDeallocQueue { + std::vector _queue; + AS::Mutex _lock; +} + ++ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED +{ + static ASDeallocQueue *deallocQueue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + deallocQueue = [[ASDeallocQueue alloc] init]; + }); + return deallocQueue; +} + +- (void)dealloc +{ + ASDisplayNodeFailAssert(@"Singleton should not dealloc."); +} + +- (void)releaseObjectInBackground:(id _Nullable __strong *)objectPtr +{ + NSParameterAssert(objectPtr != NULL); + + // Cast to CFType so we can manipulate retain count manually. + const auto cfPtr = (CFTypeRef *)(void *)objectPtr; + if (!cfPtr || !*cfPtr) { + return; + } + + _lock.lock(); + const auto isFirstEntry = _queue.empty(); + // Push the pointer into our queue and clear their pointer. + // This "steals" the +1 from ARC and nils their pointer so they can't + // access or release the object. + _queue.push_back(*cfPtr); + *cfPtr = NULL; + _lock.unlock(); + + if (isFirstEntry) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.100 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + [self drain]; + }); + } +} + +- (void)drain +{ + _lock.lock(); + const auto q = std::move(_queue); + _lock.unlock(); + for (CFTypeRef ref : q) { + // NOTE: Could check that retain count is 1 and retry later if not. + CFRelease(ref); + } +} + +@end + +@implementation ASAbstractRunLoopQueue + +- (instancetype)init +{ + self = [super init]; + if (self == nil) { + return nil; + } + ASDisplayNodeAssert(self.class != [ASAbstractRunLoopQueue class], @"Should never create instances of abstract class ASAbstractRunLoopQueue."); + return self; +} + +@end + +#pragma mark - ASRunLoopQueue + +@interface ASRunLoopQueue () { + CFRunLoopRef _runLoop; + CFRunLoopSourceRef _runLoopSource; + CFRunLoopObserverRef _runLoopObserver; + NSPointerArray *_internalQueue; // Use NSPointerArray so we can decide __strong or __weak per-instance. + AS::RecursiveMutex _internalQueueLock; + +#if ASRunLoopQueueLoggingEnabled + NSTimer *_runloopQueueLoggingTimer; +#endif +} + +@property (nonatomic) void (^queueConsumer)(id dequeuedItem, BOOL isQueueDrained); + +@end + +@implementation ASRunLoopQueue + +- (instancetype)initWithRunLoop:(CFRunLoopRef)runloop retainObjects:(BOOL)retainsObjects handler:(void (^)(id _Nullable, BOOL))handlerBlock +{ + if (self = [super init]) { + _runLoop = runloop; + NSPointerFunctionsOptions options = retainsObjects ? NSPointerFunctionsStrongMemory : NSPointerFunctionsWeakMemory; + _internalQueue = [[NSPointerArray alloc] initWithOptions:options]; + _queueConsumer = handlerBlock; + _batchSize = 1; + _ensureExclusiveMembership = YES; + + // Self is guaranteed to outlive the observer. Without the high cost of a weak pointer, + // __unsafe_unretained allows us to avoid flagging the memory cycle detector. + __unsafe_unretained __typeof__(self) weakSelf = self; + void (^handlerBlock) (CFRunLoopObserverRef observer, CFRunLoopActivity activity) = ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + [weakSelf processQueue]; + }; + _runLoopObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, 0, handlerBlock); + CFRunLoopAddObserver(_runLoop, _runLoopObserver, kCFRunLoopCommonModes); + + // It is not guaranteed that the runloop will turn if it has no scheduled work, and this causes processing of + // the queue to stop. Attaching a custom loop source to the run loop and signal it if new work needs to be done + CFRunLoopSourceContext sourceContext = {}; + sourceContext.perform = runLoopSourceCallback; +#if ASRunLoopQueueLoggingEnabled + sourceContext.info = (__bridge void *)self; +#endif + _runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext); + CFRunLoopAddSource(runloop, _runLoopSource, kCFRunLoopCommonModes); + +#if ASRunLoopQueueLoggingEnabled + _runloopQueueLoggingTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(checkRunLoop) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:_runloopQueueLoggingTimer forMode:NSRunLoopCommonModes]; +#endif + } + return self; +} + +- (void)dealloc +{ + if (CFRunLoopContainsSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes)) { + CFRunLoopRemoveSource(_runLoop, _runLoopSource, kCFRunLoopCommonModes); + } + CFRelease(_runLoopSource); + _runLoopSource = nil; + + if (CFRunLoopObserverIsValid(_runLoopObserver)) { + CFRunLoopObserverInvalidate(_runLoopObserver); + } + CFRelease(_runLoopObserver); + _runLoopObserver = nil; +} + +#if ASRunLoopQueueLoggingEnabled +- (void)checkRunLoop +{ + NSLog(@"<%@> - Jobs: %ld", self, _internalQueue.count); +} +#endif + +- (void)processQueue +{ + BOOL hasExecutionBlock = (_queueConsumer != nil); + + // If we have an execution block, this vector will be populated, otherwise remains empty. + // This is to avoid needlessly retaining/releasing the objects if we don't have a block. + std::vector itemsToProcess; + + BOOL isQueueDrained = NO; + { + MutexLocker l(_internalQueueLock); + + NSInteger internalQueueCount = _internalQueue.count; + // Early-exit if the queue is empty. + if (internalQueueCount == 0) { + return; + } + + ASSignpostStart(ASSignpostRunLoopQueueBatch); + + // Snatch the next batch of items. + NSInteger maxCountToProcess = MIN(internalQueueCount, self.batchSize); + + /** + * For each item in the next batch, if it's non-nil then NULL it out + * and if we have an execution block then add it in. + * This could be written a bunch of different ways but + * this particular one nicely balances readability, safety, and efficiency. + */ + NSInteger foundItemCount = 0; + for (NSInteger i = 0; i < internalQueueCount && foundItemCount < maxCountToProcess; i++) { + /** + * It is safe to use unsafe_unretained here. If the queue is weak, the + * object will be added to the autorelease pool. If the queue is strong, + * it will retain the object until we transfer it (retain it) in itemsToProcess. + */ + __unsafe_unretained id ptr = (__bridge id)[_internalQueue pointerAtIndex:i]; + if (ptr != nil) { + foundItemCount++; + if (hasExecutionBlock) { + itemsToProcess.push_back(ptr); + } + [_internalQueue replacePointerAtIndex:i withPointer:NULL]; + } + } + + if (foundItemCount == 0) { + // If _internalQueue holds weak references, and all of them just become NULL, then the array + // is never marked as needsCompletion, and compact will return early, not removing the NULL's. + // Inserting a NULL here ensures the compaction will take place. + // See http://www.openradar.me/15396578 and https://stackoverflow.com/a/40274426/1136669 + [_internalQueue addPointer:NULL]; + } + + [_internalQueue compact]; + if (_internalQueue.count == 0) { + isQueueDrained = YES; + } + } + + // itemsToProcess will be empty if _queueConsumer == nil so no need to check again. + const auto count = itemsToProcess.size(); + if (count > 0) { + const auto itemsEnd = itemsToProcess.cend(); + for (auto iterator = itemsToProcess.begin(); iterator < itemsEnd; iterator++) { + __unsafe_unretained id value = *iterator; + _queueConsumer(value, isQueueDrained && iterator == itemsEnd - 1); + } + } + + // If the queue is not fully drained yet force another run loop to process next batch of items + if (!isQueueDrained) { + CFRunLoopSourceSignal(_runLoopSource); + CFRunLoopWakeUp(_runLoop); + } + + ASSignpostEnd(ASSignpostRunLoopQueueBatch); +} + +- (void)enqueue:(id)object +{ + if (!object) { + return; + } + + MutexLocker l(_internalQueueLock); + + // Check if the object exists. + BOOL foundObject = NO; + + if (_ensureExclusiveMembership) { + for (id currentObject in _internalQueue) { + if (currentObject == object) { + foundObject = YES; + break; + } + } + } + + if (!foundObject) { + [_internalQueue addPointer:(__bridge void *)object]; + if (_internalQueue.count == 1) { + CFRunLoopSourceSignal(_runLoopSource); + CFRunLoopWakeUp(_runLoop); + } + } +} + +- (BOOL)isEmpty +{ + MutexLocker l(_internalQueueLock); + return _internalQueue.count == 0; +} + +ASSynthesizeLockingMethodsWithMutex(_internalQueueLock) + +@end + +#pragma mark - ASCATransactionQueue + +@interface ASCATransactionQueue () { + CFRunLoopSourceRef _runLoopSource; + CFRunLoopObserverRef _preTransactionObserver; + + // Current buffer for new entries, only accessed from within its mutex. + std::vector> _internalQueue; + + // No retain, no release, pointer hash, pointer equality. + // Enforce uniqueness in our queue. std::unordered_set does a heap allocation for each entry – not good. + CFMutableSetRef _internalQueueHashSet; + + // Temporary buffer, only accessed from the main thread in -process. + std::vector> _batchBuffer; + + AS::Mutex _internalQueueLock; + + // In order to not pollute the top-level activities, each queue has 1 root activity. + +#if ASRunLoopQueueLoggingEnabled + NSTimer *_runloopQueueLoggingTimer; +#endif +} + +@end + +@implementation ASCATransactionQueue + +// CoreAnimation commit order is 2000000, the goal of this is to process shortly beforehand +// but after most other scheduled work on the runloop has processed. +static int const kASASCATransactionQueueOrder = 1000000; + +ASCATransactionQueue *_ASSharedCATransactionQueue; +dispatch_once_t _ASSharedCATransactionQueueOnceToken; + +- (instancetype)init +{ + if (self = [super init]) { + _internalQueueHashSet = CFSetCreateMutable(NULL, 0, NULL); + + // This is going to be a very busy queue – every node in the preload range will enter this queue. + // Save some time on first render by reserving space up front. + static constexpr int kInternalQueueInitialCapacity = 64; + _internalQueue.reserve(kInternalQueueInitialCapacity); + _batchBuffer.reserve(kInternalQueueInitialCapacity); + + // Self is guaranteed to outlive the observer. Without the high cost of a weak pointer, + // __unsafe_unretained allows us to avoid flagging the memory cycle detector. + __unsafe_unretained __typeof__(self) weakSelf = self; + _preTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASASCATransactionQueueOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + while (!weakSelf->_internalQueue.empty()) { + [weakSelf processQueue]; + } + }); + + CFRunLoopAddObserver(CFRunLoopGetMain(), _preTransactionObserver, kCFRunLoopCommonModes); + + // It is not guaranteed that the runloop will turn if it has no scheduled work, and this causes processing of + // the queue to stop. Attaching a custom loop source to the run loop and signal it if new work needs to be done + CFRunLoopSourceContext sourceContext = {}; + sourceContext.perform = runLoopSourceCallback; +#if ASRunLoopQueueLoggingEnabled + sourceContext.info = (__bridge void *)self; +#endif + _runLoopSource = CFRunLoopSourceCreate(NULL, 0, &sourceContext); + CFRunLoopAddSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes); + +#if ASRunLoopQueueLoggingEnabled + _runloopQueueLoggingTimer = [NSTimer timerWithTimeInterval:1.0 target:self selector:@selector(checkRunLoop) userInfo:nil repeats:YES]; + [[NSRunLoop mainRunLoop] addTimer:_runloopQueueLoggingTimer forMode:NSRunLoopCommonModes]; +#endif + } + return self; +} + +- (void)dealloc +{ + ASDisplayNodeAssertMainThread(); + + CFRelease(_internalQueueHashSet); + CFRunLoopRemoveSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes); + CFRelease(_runLoopSource); + _runLoopSource = nil; + + if (CFRunLoopObserverIsValid(_preTransactionObserver)) { + CFRunLoopObserverInvalidate(_preTransactionObserver); + } + CFRelease(_preTransactionObserver); + _preTransactionObserver = nil; +} + +#if ASRunLoopQueueLoggingEnabled +- (void)checkRunLoop +{ + NSLog(@"<%@> - Jobs: %ld", self, _internalQueue.count); +} +#endif + +- (void)processQueue +{ + ASDisplayNodeAssertMainThread(); + + AS::UniqueLock l(_internalQueueLock); + NSInteger count = _internalQueue.size(); + // Early-exit if the queue is empty. + if (count == 0) { + return; + } + ASSignpostStart(ASSignpostRunLoopQueueBatch); + + // Swap buffers, clear our hash table. + _internalQueue.swap(_batchBuffer); + CFSetRemoveAllValues(_internalQueueHashSet); + + // Unlock early. We are done with internal queue, and batch buffer is main-thread-only so no lock. + l.unlock(); + + for (const id &value : _batchBuffer) { + [value prepareForCATransactionCommit]; + } + _batchBuffer.clear(); + ASSignpostEnd(ASSignpostRunLoopQueueBatch); +} + +- (void)enqueue:(id)object +{ + if (!object) { + return; + } + + if (!self.enabled) { + [object prepareForCATransactionCommit]; + return; + } + + MutexLocker l(_internalQueueLock); + if (CFSetContainsValue(_internalQueueHashSet, (__bridge void *)object)) { + return; + } + CFSetAddValue(_internalQueueHashSet, (__bridge void *)object); + _internalQueue.emplace_back(object); + if (_internalQueue.size() == 1) { + CFRunLoopSourceSignal(_runLoopSource); + CFRunLoopWakeUp(CFRunLoopGetMain()); + } +} + +- (BOOL)isEmpty +{ + MutexLocker l(_internalQueueLock); + return _internalQueue.empty(); +} + +- (BOOL)isEnabled +{ + return ASActivateExperimentalFeature(ASExperimentalInterfaceStateCoalescing); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASScrollDirection.mm b/submodules/AsyncDisplayKit/Source/ASScrollDirection.mm new file mode 100644 index 0000000000..3dff6ba9b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASScrollDirection.mm @@ -0,0 +1,64 @@ +// +// ASScrollDirection.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 + +const ASScrollDirection ASScrollDirectionHorizontalDirections = ASScrollDirectionLeft | ASScrollDirectionRight; +const ASScrollDirection ASScrollDirectionVerticalDirections = ASScrollDirectionUp | ASScrollDirectionDown; + +BOOL ASScrollDirectionContainsVerticalDirection(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionVerticalDirections) != 0; +} + +BOOL ASScrollDirectionContainsHorizontalDirection(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionHorizontalDirections) != 0; +} + +BOOL ASScrollDirectionContainsRight(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionRight) != 0; +} + +BOOL ASScrollDirectionContainsLeft(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionLeft) != 0; +} + +BOOL ASScrollDirectionContainsUp(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionUp) != 0; +} + +BOOL ASScrollDirectionContainsDown(ASScrollDirection scrollDirection) { + return (scrollDirection & ASScrollDirectionDown) != 0; +} + +ASScrollDirection ASScrollDirectionInvertHorizontally(ASScrollDirection scrollDirection) { + if (scrollDirection == ASScrollDirectionRight) { + return ASScrollDirectionLeft; + } else if (scrollDirection == ASScrollDirectionLeft) { + return ASScrollDirectionRight; + } + return scrollDirection; +} + +ASScrollDirection ASScrollDirectionInvertVertically(ASScrollDirection scrollDirection) { + if (scrollDirection == ASScrollDirectionUp) { + return ASScrollDirectionDown; + } else if (scrollDirection == ASScrollDirectionDown) { + return ASScrollDirectionUp; + } + return scrollDirection; +} + +ASScrollDirection ASScrollDirectionApplyTransform(ASScrollDirection scrollDirection, CGAffineTransform transform) { + if ((transform.a < 0) && ASScrollDirectionContainsHorizontalDirection(scrollDirection)) { + return ASScrollDirectionInvertHorizontally(scrollDirection); + } else if ((transform.d < 0) && ASScrollDirectionContainsVerticalDirection(scrollDirection)) { + return ASScrollDirectionInvertVertically(scrollDirection); + } + return scrollDirection; +} diff --git a/submodules/AsyncDisplayKit/Source/ASScrollNode.mm b/submodules/AsyncDisplayKit/Source/ASScrollNode.mm new file mode 100644 index 0000000000..08fd82da80 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASScrollNode.mm @@ -0,0 +1,178 @@ +// +// ASScrollNode.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 + +@interface ASScrollView : UIScrollView +@end + +@implementation ASScrollView + +// This special +layerClass allows ASScrollNode to get -layout calls from -layoutSublayers. ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +- (ASScrollNode *)scrollNode +{ + return (ASScrollNode *)ASViewToDisplayNode(self); +} + +- (BOOL)touchesShouldCancelInContentView:(UIView *)view { + if ([[self scrollNode] canCancelAllTouchesInViews]) { + return true; + } + return [super touchesShouldCancelInContentView:view]; +} + +#pragma mark - _ASDisplayView behavior substitutions +// Need these to drive interfaceState so we know when we are visible, if not nested in another range-managing element. +// Because our superclass is a true UIKit class, we cannot also subclass _ASDisplayView. +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + ASDisplayNode *node = self.scrollNode; // Create strong reference to weak ivar. + BOOL visible = (newWindow != nil); + if (visible && !node.inHierarchy) { + [node __enterHierarchy]; + } +} + +- (void)didMoveToWindow +{ + ASDisplayNode *node = self.scrollNode; // Create strong reference to weak ivar. + BOOL visible = (self.window != nil); + if (!visible && node.inHierarchy) { + [node __exitHierarchy]; + } +} + +- (NSArray *)accessibilityElements +{ + return [self.asyncdisplaykit_node accessibilityElements]; +} + +@end + +@implementation ASScrollNode +{ + ASScrollDirection _scrollableDirections; + BOOL _automaticallyManagesContentSize; + CGSize _contentCalculatedSizeFromLayout; +} +@dynamic view; + +- (instancetype)init +{ + if (self = [super init]) { + [self setViewBlock:^UIView *{ return [[ASScrollView alloc] init]; }]; + } + return self; +} + +- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize + restrictedToSize:(ASLayoutElementSize)size + relativeToParentSize:(CGSize)parentSize +{ + ASScopedLockSelfOrToRoot(); + + ASSizeRange contentConstrainedSize = constrainedSize; + if (ASScrollDirectionContainsVerticalDirection(_scrollableDirections)) { + contentConstrainedSize.max.height = CGFLOAT_MAX; + } + if (ASScrollDirectionContainsHorizontalDirection(_scrollableDirections)) { + contentConstrainedSize.max.width = CGFLOAT_MAX; + } + + ASLayout *layout = [super calculateLayoutThatFits:contentConstrainedSize + restrictedToSize:size + relativeToParentSize:parentSize]; + + if (_automaticallyManagesContentSize) { + // To understand this code, imagine we're containing a horizontal stack set within a vertical table node. + // Our parentSize is fixed ~375pt width, but 0 - INF height. Our stack measures 1000pt width, 50pt height. + // In this case, we want our scrollNode.bounds to be 375pt wide, and 50pt high. ContentSize 1000pt, 50pt. + // We can achieve this behavior by: + // 1. Always set contentSize to layout.size. + // 2. Set bounds to a size that is calculated by clamping parentSize against constrained size, + // unless one dimension is not defined, in which case adopt the contentSize for that dimension. + _contentCalculatedSizeFromLayout = layout.size; + CGSize selfSize = ASSizeRangeClamp(constrainedSize, parentSize); + if (ASPointsValidForLayout(selfSize.width) == NO) { + selfSize.width = _contentCalculatedSizeFromLayout.width; + } + if (ASPointsValidForLayout(selfSize.height) == NO) { + selfSize.height = _contentCalculatedSizeFromLayout.height; + } + // Don't provide a position, as that should be set by the parent. + layout = [ASLayout layoutWithLayoutElement:self + size:selfSize + sublayouts:layout.sublayouts]; + } + return layout; +} + +- (void)layout +{ + [super layout]; + + ASLockScopeSelf(); // Lock for using our two instance variables. + + if (_automaticallyManagesContentSize) { + CGSize contentSize = _contentCalculatedSizeFromLayout; + if (ASIsCGSizeValidForLayout(contentSize) == NO) { + NSLog(@"%@ calculated a size in its layout spec that can't be applied to .contentSize: %@. Applying parentSize (scrollNode's bounds) instead: %@.", self, NSStringFromCGSize(contentSize), NSStringFromCGSize(self.calculatedSize)); + contentSize = self.calculatedSize; + } + self.view.contentSize = contentSize; + } +} + +- (BOOL)automaticallyManagesContentSize +{ + ASLockScopeSelf(); + return _automaticallyManagesContentSize; +} + +- (void)setAutomaticallyManagesContentSize:(BOOL)automaticallyManagesContentSize +{ + ASLockScopeSelf(); + _automaticallyManagesContentSize = automaticallyManagesContentSize; + if (_automaticallyManagesContentSize == YES + && ASScrollDirectionContainsVerticalDirection(_scrollableDirections) == NO + && ASScrollDirectionContainsHorizontalDirection(_scrollableDirections) == NO) { + // Set the @default value, for more user-friendly behavior of the most + // common use cases of .automaticallyManagesContentSize. + _scrollableDirections = ASScrollDirectionVerticalDirections; + } +} + +- (ASScrollDirection)scrollableDirections +{ + ASLockScopeSelf(); + return _scrollableDirections; +} + +- (void)setScrollableDirections:(ASScrollDirection)scrollableDirections +{ + ASLockScopeSelf(); + if (_scrollableDirections != scrollableDirections) { + _scrollableDirections = scrollableDirections; + [self setNeedsLayout]; + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASSignpost.h b/submodules/AsyncDisplayKit/Source/ASSignpost.h new file mode 100644 index 0000000000..a841794417 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASSignpost.h @@ -0,0 +1,94 @@ +// +// ASSignpost.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +/// The signposts we use. Signposts are grouped by color. The SystemTrace.tracetemplate file +/// should be kept up-to-date with these values. +typedef NS_ENUM(uint32_t, ASSignpostName) { + // Collection/Table (Blue) + ASSignpostDataControllerBatch = 300, // Alloc/layout nodes before collection update. + ASSignpostRangeControllerUpdate, // Ranges update pass. + ASSignpostCollectionUpdate, // Entire update process, from -endUpdates to [super perform…] + + // Rendering (Green) + ASSignpostLayerDisplay = 325, // Client display callout. + ASSignpostRunLoopQueueBatch, // One batch of ASRunLoopQueue. + + // Layout (Purple) + ASSignpostCalculateLayout = 350, // Start of calculateLayoutThatFits to end. Max 1 per thread. + + // Misc (Orange) + ASSignpostDeallocQueueDrain = 375, // One chunk of dealloc queue work. arg0 is count. + ASSignpostCATransactionLayout, // The CA transaction commit layout phase. + ASSignpostCATransactionCommit // The CA transaction commit post-layout phase. +}; + +typedef NS_ENUM(uintptr_t, ASSignpostColor) { + ASSignpostColorBlue, + ASSignpostColorGreen, + ASSignpostColorPurple, + ASSignpostColorOrange, + ASSignpostColorRed, + ASSignpostColorDefault +}; + +static inline ASSignpostColor ASSignpostGetColor(ASSignpostName name, ASSignpostColor colorPref) { + if (colorPref == ASSignpostColorDefault) { + return (ASSignpostColor)((name / 25) % 4); + } else { + return colorPref; + } +} + +#if defined(PROFILE) && __has_include() + #define AS_KDEBUG_ENABLE 1 +#else + #define AS_KDEBUG_ENABLE 0 +#endif + +#if AS_KDEBUG_ENABLE + +#import + +// These definitions are required to build the backward-compatible kdebug trace +// on the iOS 10 SDK. The kdebug_trace function crashes if run on iOS 9 and earlier. +// It's valuable to support trace signposts on iOS 9, because A5 devices don't support iOS 10. +#ifndef DBG_MACH_CHUD +#define DBG_MACH_CHUD 0x0A +#define DBG_FUNC_NONE 0 +#define DBG_FUNC_START 1 +#define DBG_FUNC_END 2 +#define DBG_APPS 33 +#define SYS_kdebug_trace 180 +#define KDBG_CODE(Class, SubClass, code) (((Class & 0xff) << 24) | ((SubClass & 0xff) << 16) | ((code & 0x3fff) << 2)) +#define APPSDBG_CODE(SubClass,code) KDBG_CODE(DBG_APPS, SubClass, code) +#endif + +// Currently we'll reserve arg3. +#define ASSignpost(name, identifier, arg2, color) \ +AS_AT_LEAST_IOS10 ? kdebug_signpost(name, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)) \ +: syscall(SYS_kdebug_trace, APPSDBG_CODE(DBG_MACH_CHUD, name) | DBG_FUNC_NONE, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)); + +#define ASSignpostStartCustom(name, identifier, arg2) \ +AS_AT_LEAST_IOS10 ? kdebug_signpost_start(name, (uintptr_t)identifier, (uintptr_t)arg2, 0, 0) \ +: syscall(SYS_kdebug_trace, APPSDBG_CODE(DBG_MACH_CHUD, name) | DBG_FUNC_START, (uintptr_t)identifier, (uintptr_t)arg2, 0, 0); +#define ASSignpostStart(name) ASSignpostStartCustom(name, self, 0) + +#define ASSignpostEndCustom(name, identifier, arg2, color) \ +AS_AT_LEAST_IOS10 ? kdebug_signpost_end(name, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)) \ +: syscall(SYS_kdebug_trace, APPSDBG_CODE(DBG_MACH_CHUD, name) | DBG_FUNC_END, (uintptr_t)identifier, (uintptr_t)arg2, 0, ASSignpostGetColor(name, color)); +#define ASSignpostEnd(name) ASSignpostEndCustom(name, self, 0, ASSignpostColorDefault) + +#else + +#define ASSignpost(name, identifier, arg2, color) +#define ASSignpostStartCustom(name, identifier, arg2) +#define ASSignpostStart(name) +#define ASSignpostEndCustom(name, identifier, arg2, color) +#define ASSignpostEnd(name) + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm new file mode 100644 index 0000000000..eeff17607d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextKitComponents.mm @@ -0,0 +1,194 @@ +// +// ASTextKitComponents.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 + +@interface ASTextKitComponentsTextView () { + // Prevent UITextView from updating contentOffset while deallocating: https://github.com/TextureGroup/Texture/issues/860 + BOOL _deallocating; +} +@property CGRect threadSafeBounds; +@end + +@implementation ASTextKitComponentsTextView + +- (instancetype)initWithFrame:(CGRect)frame textContainer:(NSTextContainer *)textContainer +{ + self = [super initWithFrame:frame textContainer:textContainer]; + if (self) { + _threadSafeBounds = self.bounds; + _deallocating = NO; + } + return self; +} + +- (void)dealloc +{ + _deallocating = YES; +} + +- (void)setFrame:(CGRect)frame +{ + ASDisplayNodeAssertMainThread(); + [super setFrame:frame]; + self.threadSafeBounds = self.bounds; +} + +- (void)setBounds:(CGRect)bounds +{ + ASDisplayNodeAssertMainThread(); + [super setBounds:bounds]; + self.threadSafeBounds = bounds; +} + +- (void)setContentOffset:(CGPoint)contentOffset +{ + if (_deallocating) { + return; + } + + [super setContentOffset:contentOffset]; +} + + +@end + +@interface ASTextKitComponents () + +// read-write redeclarations +@property (nonatomic) NSTextStorage *textStorage; +@property (nonatomic) NSTextContainer *textContainer; +@property (nonatomic) NSLayoutManager *layoutManager; + +@end + +@implementation ASTextKitComponents + +#pragma mark - Class + ++ (instancetype)componentsWithAttributedSeedString:(NSAttributedString *)attributedSeedString + textContainerSize:(CGSize)textContainerSize NS_RETURNS_RETAINED +{ + NSTextStorage *textStorage = attributedSeedString ? [[NSTextStorage alloc] initWithAttributedString:attributedSeedString] : [[NSTextStorage alloc] init]; + + return [self componentsWithTextStorage:textStorage + textContainerSize:textContainerSize + layoutManager:[[NSLayoutManager alloc] init]]; +} + ++ (instancetype)componentsWithTextStorage:(NSTextStorage *)textStorage + textContainerSize:(CGSize)textContainerSize + layoutManager:(NSLayoutManager *)layoutManager NS_RETURNS_RETAINED +{ + ASTextKitComponents *components = [[self alloc] init]; + + components.textStorage = textStorage; + + components.layoutManager = layoutManager; + [components.textStorage addLayoutManager:components.layoutManager]; + + components.textContainer = [[NSTextContainer alloc] initWithSize:textContainerSize]; + components.textContainer.lineFragmentPadding = 0.0; // We want the text laid out up to the very edges of the text-view. + [components.layoutManager addTextContainer:components.textContainer]; + + return components; +} + ++ (BOOL)needsMainThreadDeallocation +{ + return YES; +} + +#pragma mark - Lifecycle + +- (void)dealloc +{ + // Nil out all delegates to prevent crash + if (_textView) { + ASDisplayNodeAssertMainThread(); + _textView.delegate = nil; + } + _layoutManager.delegate = nil; +} + +#pragma mark - Sizing + +- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth +{ + ASTextKitComponents *components = self; + + // If our text-view's width is already the constrained width, we can use our existing TextKit stack for this sizing calculation. + // Otherwise, we create a temporary stack to size for `constrainedWidth`. + if (CGRectGetWidth(components.textView.threadSafeBounds) != constrainedWidth) { + components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; + } + + // Force glyph generation and layout, which may not have happened yet (and isn't triggered by -usedRectForTextContainer:). + [components.layoutManager ensureLayoutForTextContainer:components.textContainer]; + CGSize textSize = [components.layoutManager usedRectForTextContainer:components.textContainer].size; + + return textSize; +} + +- (CGSize)sizeForConstrainedWidth:(CGFloat)constrainedWidth + forMaxNumberOfLines:(NSInteger)maxNumberOfLines +{ + if (maxNumberOfLines == 0) { + return [self sizeForConstrainedWidth:constrainedWidth]; + } + + ASTextKitComponents *components = self; + + // Always use temporary stack in case of threading issues + components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth, CGFLOAT_MAX)]; + + // Force glyph generation and layout, which may not have happened yet (and isn't triggered by - usedRectForTextContainer:). + [components.layoutManager ensureLayoutForTextContainer:components.textContainer]; + + CGFloat width = [components.layoutManager usedRectForTextContainer:components.textContainer].size.width; + + // Calculate height based on line fragments + // Based on calculating number of lines from: http://asciiwwdc.com/2013/sessions/220 + NSRange glyphRange, lineRange = NSMakeRange(0, 0); + CGRect rect = CGRectZero; + CGFloat height = 0; + CGFloat lastOriginY = -1.0; + NSUInteger numberOfLines = 0; + + glyphRange = [components.layoutManager glyphRangeForTextContainer:components.textContainer]; + + while (lineRange.location < NSMaxRange(glyphRange)) { + rect = [components.layoutManager lineFragmentRectForGlyphAtIndex:lineRange.location + effectiveRange:&lineRange]; + + if (CGRectGetMinY(rect) > lastOriginY) { + ++numberOfLines; + if (numberOfLines == maxNumberOfLines) { + height = rect.origin.y + rect.size.height; + break; + } + } + + lastOriginY = CGRectGetMinY(rect); + lineRange.location = NSMaxRange(lineRange); + } + + CGFloat fragmentHeight = rect.origin.y + rect.size.height; + CGFloat finalHeight = std::ceil(std::fmax(height, fragmentHeight)); + + CGSize size = CGSizeMake(width, finalHeight); + + return size; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitContext.h b/submodules/AsyncDisplayKit/Source/ASTextKitContext.h new file mode 100644 index 0000000000..82a40b7d8d --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextKitContext.h @@ -0,0 +1,53 @@ +// +// ASTextKitContext.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 + +#if AS_ENABLE_TEXTNODE + +#import + +/** + A threadsafe container for the TextKit components that ASTextKit uses to lay out and truncate its text. + + This container is the sole owner and manager of the TextKit classes. This is an important model because of major + thread safety issues inside vanilla TextKit. It provides a central locking location for accessing TextKit methods. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASTextKitContext : NSObject + +/** + Initializes a context and its associated TextKit components. + + Initialization of TextKit components is a globally locking operation so be careful of bottlenecks with this class. + */ +- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString + lineBreakMode:(NSLineBreakMode)lineBreakMode + maximumNumberOfLines:(NSUInteger)maximumNumberOfLines + exclusionPaths:(NSArray *)exclusionPaths + constrainedSize:(CGSize)constrainedSize; + +/** + All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to + TextKit components may cause crashes. + + The block provided MUST not call out to client code from within its scope or it is possible for this to cause deadlocks + in your application. Use with EXTREME care. + + Callers MUST NOT keep a ref to these internal objects and use them later. This WILL cause crashes in your application. + */ +- (void)performBlockWithLockedTextKitComponents:(AS_NOESCAPE void (^)(NSLayoutManager *layoutManager, + NSTextStorage *textStorage, + NSTextContainer *textContainer))block; + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm b/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm new file mode 100644 index 0000000000..42f4a1a45c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextKitContext.mm @@ -0,0 +1,84 @@ +// +// ASTextKitContext.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 "ASTextKitContext.h" + +#if AS_ENABLE_TEXTNODE + +#import "ASLayoutManager.h" +#import + +#include + +@implementation ASTextKitContext +{ + // All TextKit operations (even non-mutative ones) must be executed serially. + std::shared_ptr __instanceLock__; + + NSLayoutManager *_layoutManager; + NSTextStorage *_textStorage; + NSTextContainer *_textContainer; +} + +- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString + lineBreakMode:(NSLineBreakMode)lineBreakMode + maximumNumberOfLines:(NSUInteger)maximumNumberOfLines + exclusionPaths:(NSArray *)exclusionPaths + constrainedSize:(CGSize)constrainedSize + +{ + if (self = [super init]) { + static dispatch_once_t onceToken; + static AS::Mutex *mutex; + dispatch_once(&onceToken, ^{ + mutex = new AS::Mutex(); + }); + + // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. + AS::MutexLocker l(*mutex); + + __instanceLock__ = std::make_shared(); + + // Create the TextKit component stack with our default configuration. + + _textStorage = [[NSTextStorage alloc] init]; + _layoutManager = [[ASLayoutManager alloc] init]; + _layoutManager.usesFontLeading = NO; + [_textStorage addLayoutManager:_layoutManager]; + + // Instead of calling [NSTextStorage initWithAttributedString:], setting attributedString just after calling addlayoutManager can fix CJK language layout issues. + // See https://github.com/facebook/AsyncDisplayKit/issues/2894 + if (attributedString) { + [_textStorage setAttributedString:attributedString]; + } + + _textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize]; + // We want the text laid out up to the very edges of the container. + _textContainer.lineFragmentPadding = 0; + _textContainer.lineBreakMode = lineBreakMode; + _textContainer.maximumNumberOfLines = maximumNumberOfLines; + _textContainer.exclusionPaths = exclusionPaths; + [_layoutManager addTextContainer:_textContainer]; + } + return self; +} + +- (void)performBlockWithLockedTextKitComponents:(NS_NOESCAPE void (^)(NSLayoutManager *, + NSTextStorage *, + NSTextContainer *))block +{ + AS::MutexLocker l(*__instanceLock__); + if (block) { + block(_layoutManager, _textStorage, _textContainer); + } +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/ASTextNodeCommon.h b/submodules/AsyncDisplayKit/Source/ASTextNodeCommon.h new file mode 100644 index 0000000000..765f401c2a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextNodeCommon.h @@ -0,0 +1,34 @@ +// +// ASTextNodeCommon.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +#import + +#define AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE() { \ + static dispatch_once_t onceToken; \ + dispatch_once(&onceToken, ^{ \ + NSLog(@"[Texture] Warning: Feature %@ is unimplemented in %@.", NSStringFromSelector(_cmd), NSStringFromClass(self.class)); \ + });\ +} + +/** + * Highlight styles. + */ +typedef NS_ENUM(NSUInteger, ASTextNodeHighlightStyle) { + /** + * Highlight style for text on a light background. + */ + ASTextNodeHighlightStyleLight, + + /** + * Highlight style for text on a dark background. + */ + ASTextNodeHighlightStyleDark +}; + diff --git a/submodules/AsyncDisplayKit/Source/ASTextNodeWordKerner.h b/submodules/AsyncDisplayKit/Source/ASTextNodeWordKerner.h new file mode 100644 index 0000000000..795c4ea099 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextNodeWordKerner.h @@ -0,0 +1,37 @@ +// +// ASTextNodeWordKerner.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 + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + @abstract This class acts as the NSLayoutManagerDelegate for ASTextNode. + @discussion Its current job is word kerning, i.e. adjusting the width of spaces to match the set + wordKernedSpaceWidth. If word kerning is not needed, set the layoutManager's delegate to nil. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASTextNodeWordKerner : NSObject + +/** + The following @optional NSLayoutManagerDelegate methods are implemented: + +- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)props characterIndexes:(const NSUInteger *)charIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange NS_AVAILABLE_IOS(7_0); + +- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)action forControlCharacterAtIndex:(NSUInteger)charIndex NS_AVAILABLE_IOS(7_0); + +- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)charIndex NS_AVAILABLE_IOS(7_0); + */ + +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASTextNodeWordKerner.mm b/submodules/AsyncDisplayKit/Source/ASTextNodeWordKerner.mm new file mode 100644 index 0000000000..e1d0c73c0e --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTextNodeWordKerner.mm @@ -0,0 +1,130 @@ +// +// ASTextNodeWordKerner.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 "ASTextNodeWordKerner.h" + +#import + +#import + +@implementation ASTextNodeWordKerner + +#pragma mark - NSLayoutManager Delegate +- (NSUInteger)layoutManager:(NSLayoutManager *)layoutManager shouldGenerateGlyphs:(const CGGlyph *)glyphs properties:(const NSGlyphProperty *)properties characterIndexes:(const NSUInteger *)characterIndexes font:(UIFont *)aFont forGlyphRange:(NSRange)glyphRange +{ + NSUInteger glyphCount = glyphRange.length; + NSGlyphProperty *newGlyphProperties = NULL; + + BOOL usesWordKerning = NO; + + // If our typing attributes specify word kerning, specify the spaces as whitespace control characters so we can customize their width. + // Are any of the characters spaces? + NSString *textStorageString = layoutManager.textStorage.string; + for (NSUInteger arrayIndex = 0; arrayIndex < glyphCount; arrayIndex++) { + NSUInteger characterIndex = characterIndexes[arrayIndex]; + if ([textStorageString characterAtIndex:characterIndex] != ' ') + continue; + + // If we've set the whitespace control character for this space already, we have nothing to do. + if (properties[arrayIndex] == NSGlyphPropertyControlCharacter) { + usesWordKerning = YES; + continue; + } + + // Create new glyph properties, if necessary. + if (!newGlyphProperties) { + newGlyphProperties = (NSGlyphProperty *)malloc(sizeof(NSGlyphProperty) * glyphCount); + memcpy(newGlyphProperties, properties, (sizeof(NSGlyphProperty) * glyphCount)); + } + + // It's a space. Make it a whitespace control character. + newGlyphProperties[arrayIndex] = NSGlyphPropertyControlCharacter; + } + + // If we don't have any custom glyph properties, return 0 to indicate to the layout manager that it should use the standard glyphs+properties. + if (!newGlyphProperties) { + if (usesWordKerning) { + // If the text does use word kerning we have to make sure we return the correct glyphCount, or the layout manager will just use the default properties and ignore our kerning. + [layoutManager setGlyphs:glyphs properties:properties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange]; + return glyphCount; + } else { + return 0; + } + } + + // Otherwise, use our custom glyph properties. + [layoutManager setGlyphs:glyphs properties:newGlyphProperties characterIndexes:characterIndexes font:aFont forGlyphRange:glyphRange]; + free(newGlyphProperties); + + return glyphCount; +} + +- (NSControlCharacterAction)layoutManager:(NSLayoutManager *)layoutManager shouldUseAction:(NSControlCharacterAction)defaultAction forControlCharacterAtIndex:(NSUInteger)characterIndex +{ + // If it's a space character and we have custom word kerning, use the whitespace action control character. + if ([layoutManager.textStorage.string characterAtIndex:characterIndex] == ' ') + return NSControlCharacterActionWhitespace; + + return defaultAction; +} + +- (CGRect)layoutManager:(NSLayoutManager *)layoutManager boundingBoxForControlGlyphAtIndex:(NSUInteger)glyphIndex forTextContainer:(NSTextContainer *)textContainer proposedLineFragment:(CGRect)proposedRect glyphPosition:(CGPoint)glyphPosition characterIndex:(NSUInteger)characterIndex +{ + CGFloat wordKernedSpaceWidth = [self _wordKernedSpaceWidthForCharacterAtIndex:characterIndex atGlyphPosition:glyphPosition forTextContainer:textContainer layoutManager:layoutManager]; + return CGRectMake(glyphPosition.x, glyphPosition.y, wordKernedSpaceWidth, CGRectGetHeight(proposedRect)); +} + +- (CGFloat)_wordKernedSpaceWidthForCharacterAtIndex:(NSUInteger)characterIndex atGlyphPosition:(CGPoint)glyphPosition forTextContainer:(NSTextContainer *)textContainer layoutManager:(NSLayoutManager *)layoutManager +{ + // We use a map table for pointer equality and non-copying keys. + static NSMapTable *spaceSizes; + // NSMapTable is a defined thread unsafe class, so we need to synchronize + // access in a light manner. So we use dispatch_sync on this queue for all + // access to the map table. + static dispatch_queue_t mapQueue; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + spaceSizes = [[NSMapTable alloc] initWithKeyOptions:NSMapTableStrongMemory valueOptions:NSMapTableStrongMemory capacity:1]; + mapQueue = dispatch_queue_create("org.AsyncDisplayKit.wordKerningQueue", DISPATCH_QUEUE_SERIAL); + }); + CGFloat ordinarySpaceWidth; + UIFont *font = [layoutManager.textStorage attribute:NSFontAttributeName atIndex:characterIndex effectiveRange:NULL]; + CGFloat wordKerning = [[layoutManager.textStorage attribute:ASTextNodeWordKerningAttributeName atIndex:characterIndex effectiveRange:NULL] floatValue]; + __block NSNumber *ordinarySpaceSizeValue; + dispatch_sync(mapQueue, ^{ + ordinarySpaceSizeValue = [spaceSizes objectForKey:font]; + }); + if (ordinarySpaceSizeValue == nil) { + ordinarySpaceWidth = [@" " sizeWithAttributes:@{ NSFontAttributeName : font }].width; + dispatch_async(mapQueue, ^{ + [spaceSizes setObject:@(ordinarySpaceWidth) forKey:font]; + }); + } else { + ordinarySpaceWidth = [ordinarySpaceSizeValue floatValue]; + } + + CGFloat totalKernedWidth = (ordinarySpaceWidth + wordKerning); + + // TextKit normally handles whitespace by increasing the advance of the previous glyph, rather than displaying an + // actual glyph for the whitespace itself. However, in order to implement word kerning, we explicitly require a + // discrete glyph whose bounding box we can specify. The problem is that TextKit does not know this glyph is + // invisible. From TextKit's perspective, this whitespace glyph is a glyph that MUST be displayed. Thus when it + // comes to determining linebreaks, the width of this trailing whitespace glyph is considered. This causes + // our text to wrap sooner than it otherwise would, as room is allocated at the end of each line for a glyph that + // isn't actually visible. To implement our desired behavior, we check to see if the current whitespace glyph + // would break to the next line. If it breaks to the next line, then this constitutes trailing whitespace, and + // we specify enough room to fill up the remainder of the line, but nothing more. + if (glyphPosition.x + totalKernedWidth > textContainer.size.width) { + return (textContainer.size.width - glyphPosition.x); + } + + return totalKernedWidth; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASTraitCollection.mm b/submodules/AsyncDisplayKit/Source/ASTraitCollection.mm new file mode 100644 index 0000000000..e885239211 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASTraitCollection.mm @@ -0,0 +1,256 @@ +// +// ASTraitCollection.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 + +#pragma mark - ASPrimitiveTraitCollection + +void ASTraitCollectionPropagateDown(id element, ASPrimitiveTraitCollection traitCollection) { + if (element) { + element.primitiveTraitCollection = traitCollection; + } + + for (id subelement in element.sublayoutElements) { + ASTraitCollectionPropagateDown(subelement, traitCollection); + } +} + +ASPrimitiveTraitCollection ASPrimitiveTraitCollectionMakeDefault() { + ASPrimitiveTraitCollection tc = {}; + tc.userInterfaceIdiom = UIUserInterfaceIdiomUnspecified; + tc.forceTouchCapability = UIForceTouchCapabilityUnknown; + tc.displayScale = 0.0; + tc.horizontalSizeClass = UIUserInterfaceSizeClassUnspecified; + tc.verticalSizeClass = UIUserInterfaceSizeClassUnspecified; + tc.containerSize = CGSizeZero; + if (AS_AVAILABLE_IOS(10)) { + tc.displayGamut = UIDisplayGamutUnspecified; + tc.preferredContentSizeCategory = UIContentSizeCategoryUnspecified; + tc.layoutDirection = UITraitEnvironmentLayoutDirectionUnspecified; + } +#if AS_BUILD_UIUSERINTERFACESTYLE + if (AS_AVAILABLE_IOS_TVOS(12, 10)) { + tc.userInterfaceStyle = UIUserInterfaceStyleUnspecified; + } +#endif + return tc; +} + +ASPrimitiveTraitCollection ASPrimitiveTraitCollectionFromUITraitCollection(UITraitCollection *traitCollection) { + ASPrimitiveTraitCollection environmentTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); + environmentTraitCollection.horizontalSizeClass = traitCollection.horizontalSizeClass; + environmentTraitCollection.verticalSizeClass = traitCollection.verticalSizeClass; + environmentTraitCollection.displayScale = traitCollection.displayScale; + environmentTraitCollection.userInterfaceIdiom = traitCollection.userInterfaceIdiom; + environmentTraitCollection.forceTouchCapability = traitCollection.forceTouchCapability; + if (AS_AVAILABLE_IOS(10)) { + environmentTraitCollection.displayGamut = traitCollection.displayGamut; + environmentTraitCollection.layoutDirection = traitCollection.layoutDirection; + + ASDisplayNodeCAssertPermanent(traitCollection.preferredContentSizeCategory); + environmentTraitCollection.preferredContentSizeCategory = traitCollection.preferredContentSizeCategory; + } +#if AS_BUILD_UIUSERINTERFACESTYLE + if (AS_AVAILABLE_IOS_TVOS(12, 10)) { + environmentTraitCollection.userInterfaceStyle = traitCollection.userInterfaceStyle; + } +#endif + return environmentTraitCollection; +} + +BOOL ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(ASPrimitiveTraitCollection lhs, ASPrimitiveTraitCollection rhs) { + return !memcmp(&lhs, &rhs, sizeof(ASPrimitiveTraitCollection)); +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceIdiom(UIUserInterfaceIdiom idiom) { + switch (idiom) { + case UIUserInterfaceIdiomTV: + return @"TV"; + case UIUserInterfaceIdiomPad: + return @"Pad"; + case UIUserInterfaceIdiomPhone: + return @"Phone"; + case UIUserInterfaceIdiomCarPlay: + return @"CarPlay"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIForceTouchCapability(UIForceTouchCapability capability) { + switch (capability) { + case UIForceTouchCapabilityAvailable: + return @"Available"; + case UIForceTouchCapabilityUnavailable: + return @"Unavailable"; + default: + return @"Unknown"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceSizeClass(UIUserInterfaceSizeClass sizeClass) { + switch (sizeClass) { + case UIUserInterfaceSizeClassCompact: + return @"Compact"; + case UIUserInterfaceSizeClassRegular: + return @"Regular"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +API_AVAILABLE(ios(10)) +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIDisplayGamut(UIDisplayGamut displayGamut) { + switch (displayGamut) { + case UIDisplayGamutSRGB: + return @"sRGB"; + case UIDisplayGamutP3: + return @"P3"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +API_AVAILABLE(ios(10)) +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUITraitEnvironmentLayoutDirection(UITraitEnvironmentLayoutDirection layoutDirection) { + switch (layoutDirection) { + case UITraitEnvironmentLayoutDirectionLeftToRight: + return @"LeftToRight"; + case UITraitEnvironmentLayoutDirectionRightToLeft: + return @"RightToLeft"; + default: + return @"Unspecified"; + } +} + +// Named so as not to conflict with a hidden Apple function, in case compiler decides not to inline +#if AS_BUILD_UIUSERINTERFACESTYLE +API_AVAILABLE(tvos(10.0), ios(12.0)) +ASDISPLAYNODE_INLINE NSString *AS_NSStringFromUIUserInterfaceStyle(UIUserInterfaceStyle userInterfaceStyle) { + switch (userInterfaceStyle) { + case UIUserInterfaceStyleLight: + return @"Light"; + case UIUserInterfaceStyleDark: + return @"Dark"; + default: + return @"Unspecified"; + } +} +#endif + +NSString *NSStringFromASPrimitiveTraitCollection(ASPrimitiveTraitCollection traits) { + NSMutableArray *props = [NSMutableArray array]; + [props addObject:@{ @"verticalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.verticalSizeClass) }]; + [props addObject:@{ @"horizontalSizeClass": AS_NSStringFromUIUserInterfaceSizeClass(traits.horizontalSizeClass) }]; + [props addObject:@{ @"displayScale": [NSString stringWithFormat: @"%.0lf", (double)traits.displayScale] }]; + [props addObject:@{ @"userInterfaceIdiom": AS_NSStringFromUIUserInterfaceIdiom(traits.userInterfaceIdiom) }]; + [props addObject:@{ @"forceTouchCapability": AS_NSStringFromUIForceTouchCapability(traits.forceTouchCapability) }]; +#if AS_BUILD_UIUSERINTERFACESTYLE + if (AS_AVAILABLE_IOS_TVOS(12, 10)) { + [props addObject:@{ @"userInterfaceStyle": AS_NSStringFromUIUserInterfaceStyle(traits.userInterfaceStyle) }]; + } +#endif + if (AS_AVAILABLE_IOS(10)) { + [props addObject:@{ @"layoutDirection": AS_NSStringFromUITraitEnvironmentLayoutDirection(traits.layoutDirection) }]; + [props addObject:@{ @"preferredContentSizeCategory": traits.preferredContentSizeCategory }]; + [props addObject:@{ @"displayGamut": AS_NSStringFromUIDisplayGamut(traits.displayGamut) }]; + } + [props addObject:@{ @"containerSize": NSStringFromCGSize(traits.containerSize) }]; + return ASObjectDescriptionMakeWithoutObject(props); +} + +#pragma mark - ASTraitCollection + +@implementation ASTraitCollection { + ASPrimitiveTraitCollection _prim; +} + ++ (ASTraitCollection *)traitCollectionWithASPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traits NS_RETURNS_RETAINED { + ASTraitCollection *tc = [[ASTraitCollection alloc] init]; + if (AS_AVAILABLE_IOS(10)) { + ASDisplayNodeCAssertPermanent(traits.preferredContentSizeCategory); + } + tc->_prim = traits; + return tc; +} + +- (ASPrimitiveTraitCollection)primitiveTraitCollection { + return _prim; +} +- (UIUserInterfaceSizeClass)horizontalSizeClass +{ + return _prim.horizontalSizeClass; +} +-(UIUserInterfaceSizeClass)verticalSizeClass +{ + return _prim.verticalSizeClass; +} +- (CGFloat)displayScale +{ + return _prim.displayScale; +} +- (UIDisplayGamut)displayGamut +{ + return _prim.displayGamut; +} +- (UIForceTouchCapability)forceTouchCapability +{ + return _prim.forceTouchCapability; +} +- (UITraitEnvironmentLayoutDirection)layoutDirection +{ + return _prim.layoutDirection; +} +- (CGSize)containerSize +{ + return _prim.containerSize; +} +#if AS_BUILD_UIUSERINTERFACESTYLE +- (UIUserInterfaceStyle)userInterfaceStyle +{ + return _prim.userInterfaceStyle; +} +#endif +- (UIContentSizeCategory)preferredContentSizeCategory +{ + return _prim.preferredContentSizeCategory; +} +- (NSUInteger)hash { + return ASHashBytes(&_prim, sizeof(ASPrimitiveTraitCollection)); +} + +- (BOOL)isEqual:(id)object { + if (!object || ![object isKindOfClass:ASTraitCollection.class]) { + return NO; + } + return [self isEqualToTraitCollection:object]; +} + +- (BOOL)isEqualToTraitCollection:(ASTraitCollection *)traitCollection +{ + if (traitCollection == nil) { + return NO; + } + + if (self == traitCollection) { + return YES; + } + return ASPrimitiveTraitCollectionIsEqualToASPrimitiveTraitCollection(_prim, traitCollection->_prim); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASWeakMap.h b/submodules/AsyncDisplayKit/Source/ASWeakMap.h new file mode 100644 index 0000000000..1f413ca7ae --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASWeakMap.h @@ -0,0 +1,59 @@ +// +// ASWeakMap.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 + + +/** + * This class is used in conjunction with ASWeakMap. Instances of this type are returned by an ASWeakMap, + * must retain this value for as long as they want the entry to exist in the map. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASWeakMapEntry : NSObject + +@property (readonly) Value value; + +@end + + +/** + * This is not a full-featured map. It does not support features like `count` and FastEnumeration because there + * is not currently a need. + * + * This is a map that does not retain keys or values added to it. When both getting and setting, the caller is + * returned a ASWeakMapEntry and must retain it for as long as it wishes the key/value to remain in the map. + * We return a single Entry value to the caller to avoid two potential problems: + * + * 1) It's easier for callers to retain one value (the Entry) and not two (a key and a value). + * 2) Key values are tested for `isEqual` equality. If if a caller asks for a key "A" that is equal to a key "B" + * already in the map, then we need the caller to retain key "B" and not key "A". Returning an Entry simplifies + * the semantics. + * + * The underlying storage is a hash table and the Key type should implement `hash` and `isEqual:`. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASWeakMap<__covariant Key, Value> : NSObject + +/** + * Read from the cache. The Value object is accessible from the returned ASWeakMapEntry. + */ +- (nullable ASWeakMapEntry *)entryForKey:(Key)key AS_WARN_UNUSED_RESULT; + +/** + * Put a value into the cache. If an entry with an equal key already exists, then the value is updated on the existing entry. + */ +- (ASWeakMapEntry *)setObject:(Value)value forKey:(Key)key AS_WARN_UNUSED_RESULT; + +@end + + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/ASWeakMap.mm b/submodules/AsyncDisplayKit/Source/ASWeakMap.mm new file mode 100644 index 0000000000..1267d8ff69 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASWeakMap.mm @@ -0,0 +1,78 @@ +// +// ASWeakMap.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 "ASWeakMap.h" + +@interface ASWeakMapEntry () +@property (nonatomic, readonly) id key; +@property id value; +@end + +@implementation ASWeakMapEntry + +- (instancetype)initWithKey:(id)key value:(id)value +{ + self = [super init]; + if (self) { + _key = key; + _value = value; + } + return self; +} + +@end + + +@interface ASWeakMap () +@property (nonatomic, readonly) NSMapTable *hashTable; +@end + +/** + * Implementation details: + * + * The retained size of our keys is potentially very large (for example, a UIImage is commonly part of a key). + * Unfortunately, NSMapTable does not make guarantees about how quickly it will dispose of entries where + * either the key or the value is weak and has been disposed. So, a NSMapTable with "strong key to weak value" is + * unsuitable for our purpose because the strong keys are retained longer than the value and for an indefininte period of time. + * More details here: http://cocoamine.net/blog/2013/12/13/nsmaptable-and-zeroing-weak-references/ + * + * Our NSMapTable is "weak key to weak value" where each key maps to an Entry. The Entry object is responsible + * for retaining both the key and value. Our convention is that the caller must retain the Entry object + * in order to keep the key and the value in the cache. + */ +@implementation ASWeakMap + +- (instancetype)init +{ + self = [super init]; + if (self) { + _hashTable = [NSMapTable weakToWeakObjectsMapTable]; + } + return self; +} + +- (ASWeakMapEntry *)entryForKey:(id)key +{ + return [self.hashTable objectForKey:key]; +} + +- (ASWeakMapEntry *)setObject:(id)value forKey:(id)key +{ + ASWeakMapEntry *entry = [self.hashTable objectForKey:key]; + if (entry != nil) { + // Update the value in the existing entry. + entry.value = value; + } else { + entry = [[ASWeakMapEntry alloc] initWithKey:key value:value]; + [self.hashTable setObject:entry forKey:key]; + } + return entry; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASWeakProxy.h b/submodules/AsyncDisplayKit/Source/ASWeakProxy.h new file mode 100644 index 0000000000..7396474b1c --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASWeakProxy.h @@ -0,0 +1,32 @@ +// +// ASWeakProxy.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 + +AS_SUBCLASSING_RESTRICTED +@interface ASWeakProxy : NSProxy + +/** + * @return target The target which will be forwarded all messages sent to the weak proxy. + */ +@property (nonatomic, weak, readonly) id target; + +/** + * An object which forwards messages to a target which it weakly references + * + * @discussion This class is useful for breaking retain cycles. You can pass this in place + * of the target to something which creates a strong reference. All messages sent to the + * proxy will be passed onto the target. + * + * @return an instance of ASWeakProxy + */ ++ (instancetype)weakProxyWithTarget:(id)target NS_RETURNS_RETAINED; + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASWeakProxy.mm b/submodules/AsyncDisplayKit/Source/ASWeakProxy.mm new file mode 100644 index 0000000000..4a3b5c8a2a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASWeakProxy.mm @@ -0,0 +1,72 @@ +// +// ASWeakProxy.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 "ASWeakProxy.h" +#import +#import + +@implementation ASWeakProxy + +- (instancetype)initWithTarget:(id)target +{ + if (self) { + _target = target; + } + return self; +} + ++ (instancetype)weakProxyWithTarget:(id)target NS_RETURNS_RETAINED +{ + return [[ASWeakProxy alloc] initWithTarget:target]; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + return _target; +} + +- (BOOL)respondsToSelector:(SEL)aSelector +{ + return [_target respondsToSelector:aSelector]; +} + +- (BOOL)conformsToProtocol:(Protocol *)aProtocol +{ + return [_target conformsToProtocol:aProtocol]; +} + +/// Strangely, this method doesn't get forwarded by ObjC. +- (BOOL)isKindOfClass:(Class)aClass +{ + return [_target isKindOfClass:aClass]; +} + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, @[@{ @"target": _target ?: (id)kCFNull }]); +} + +- (NSMethodSignature *)methodSignatureForSelector:(SEL)sel +{ + ASDisplayNodeAssertNil(_target, @"ASWeakProxy got %@ when its target is still alive, which is unexpected.", NSStringFromSelector(_cmd)); + // Unfortunately, in order to get this object to work properly, the use of a method which creates an NSMethodSignature + // from a C string. -methodSignatureForSelector is called when a compiled definition for the selector cannot be found. + // This is the place where we have to create our own dud NSMethodSignature. This is necessary because if this method + // returns nil, a selector not found exception is raised. The string argument to -signatureWithObjCTypes: outlines + // the return type and arguments to the message. To return a dud NSMethodSignature, pretty much any signature will + // suffice. Since the -forwardInvocation call will do nothing if the target does not respond to the selector, + // the dud NSMethodSignature simply gets us around the exception. + return [NSMethodSignature signatureWithObjCTypes:"@^v^c"]; +} +- (void)forwardInvocation:(NSInvocation *)invocation +{ + ASDisplayNodeAssertNil(_target, @"ASWeakProxy got %@ when its target is still alive, which is unexpected.", NSStringFromSelector(_cmd)); +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/ASWeakSet.mm b/submodules/AsyncDisplayKit/Source/ASWeakSet.mm new file mode 100644 index 0000000000..6530271a5a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/ASWeakSet.mm @@ -0,0 +1,84 @@ +// +// ASWeakSet.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 + +@interface ASWeakSet<__covariant ObjectType> () +@property (nonatomic, readonly) NSHashTable *hashTable; +@end + +@implementation ASWeakSet + +- (instancetype)init +{ + self = [super init]; + if (self) { + _hashTable = [NSHashTable hashTableWithOptions:NSHashTableWeakMemory | NSHashTableObjectPointerPersonality]; + } + return self; +} + +- (void)addObject:(id)object +{ + [_hashTable addObject:object]; +} + +- (void)removeObject:(id)object +{ + [_hashTable removeObject:object]; +} + +- (void)removeAllObjects +{ + [_hashTable removeAllObjects]; +} + +- (NSArray *)allObjects +{ + return _hashTable.allObjects; +} + +- (BOOL)containsObject:(id)object +{ + return [_hashTable containsObject:object]; +} + +- (BOOL)isEmpty +{ + return [_hashTable anyObject] == nil; +} + +/** + Note: The `count` property of NSHashTable is unreliable + in the case of weak-memory hash tables because entries + that have been deallocated are not removed immediately. + + In order to get the true count we have to fall back to using + fast enumeration. + */ +- (NSUInteger)count +{ + NSUInteger count = 0; + for (__unused id object in _hashTable) { + count += 1; + } + return count; +} + +- (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(__unsafe_unretained id _Nonnull *)buffer count:(NSUInteger)len +{ + return [_hashTable countByEnumeratingWithState:state objects:buffer count:len]; +} + +- (NSString *)description +{ + return [[super description] stringByAppendingFormat:@" count: %tu, contents: %@", self.count, _hashTable]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/NSArray+Diffing.mm b/submodules/AsyncDisplayKit/Source/NSArray+Diffing.mm new file mode 100644 index 0000000000..83d32fea68 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/NSArray+Diffing.mm @@ -0,0 +1,177 @@ +// +// NSArray+Diffing.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 + +@implementation NSArray (Diffing) + +typedef BOOL (^compareBlock)(id _Nonnull lhs, id _Nonnull rhs); + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions +{ + [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:[NSArray defaultCompareBlock]]; +} + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions + compareBlock:(compareBlock)comparison +{ + [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:nil compareBlock:comparison]; +} + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions + moves:(NSArray **)moves +{ + [self asdk_diffWithArray:array insertions:insertions deletions:deletions moves:moves + compareBlock:[NSArray defaultCompareBlock]]; +} + +- (void)asdk_diffWithArray:(NSArray *)array insertions:(NSIndexSet **)insertions deletions:(NSIndexSet **)deletions + moves:(NSArray **)moves compareBlock:(compareBlock)comparison +{ + struct NSObjectHash + { + std::size_t operator()(id k) const { return (std::size_t) [k hash]; }; + }; + struct NSObjectCompare + { + bool operator()(id lhs, id rhs) const { return (bool) [lhs isEqual:rhs]; }; + }; + std::unordered_multimap potentialMoves; + + NSAssert(comparison != nil, @"Comparison block is required"); + NSAssert(moves == nil || comparison == [NSArray defaultCompareBlock], @"move detection requires isEqual: and hash (no custom compare)"); + NSMutableArray *moveIndexPaths = nil; + NSMutableIndexSet *insertionIndexes = nil, *deletionIndexes = nil; + if (moves) { + moveIndexPaths = [NSMutableArray new]; + } + NSMutableIndexSet *commonIndexes = [self _asdk_commonIndexesWithArray:array compareBlock:comparison]; + + if (deletions || moves) { + deletionIndexes = [NSMutableIndexSet indexSet]; + NSUInteger i = 0; + for (id element in self) { + if (![commonIndexes containsIndex:i]) { + [deletionIndexes addIndex:i]; + } + if (moves) { + potentialMoves.insert(std::pair(element, i)); + } + ++i; + } + } + + if (insertions || moves) { + insertionIndexes = [NSMutableIndexSet indexSet]; + NSArray *commonObjects = [self objectsAtIndexes:commonIndexes]; + for (NSUInteger i = 0, j = 0; j < array.count; j++) { + auto moveFound = potentialMoves.find(array[j]); + NSUInteger movedFrom = NSNotFound; + if (moveFound != potentialMoves.end() && moveFound->second != j) { + movedFrom = moveFound->second; + potentialMoves.erase(moveFound); + [moveIndexPaths addObject:[NSIndexPath indexPathForItem:j inSection:movedFrom]]; + } + if (i < commonObjects.count && j < array.count && comparison(commonObjects[i], array[j])) { + i++; + } else { + if (movedFrom != NSNotFound) { + // moves will coalesce a delete / insert - the insert is just not done, and here we remove the delete: + [deletionIndexes removeIndex:movedFrom]; + // OR a move will have come from the LCS: + if ([commonIndexes containsIndex:movedFrom]) { + [commonIndexes removeIndex:movedFrom]; + commonObjects = [self objectsAtIndexes:commonIndexes]; + } + } else { + [insertionIndexes addIndex:j]; + } + } + } + } + + if (moves) {*moves = moveIndexPaths;} + if (deletions) {*deletions = deletionIndexes;} + if (insertions) {*insertions = insertionIndexes;} +} + +// https://github.com/raywenderlich/swift-algorithm-club/tree/master/Longest%20Common%20Subsequence is not exactly this code (obviously), but +// is a good commentary on the algorithm. +- (NSMutableIndexSet *)_asdk_commonIndexesWithArray:(NSArray *)array compareBlock:(BOOL (^)(id lhs, id rhs))comparison +{ + NSAssert(comparison != nil, @"Comparison block is required"); + + NSInteger selfCount = self.count; + NSInteger arrayCount = array.count; + + // Allocate the diff map in the heap so we don't blow the stack for large arrays. + NSInteger **lengths = NULL; + lengths = (NSInteger **)malloc(sizeof(NSInteger*) * (selfCount+1)); + if (lengths == NULL) { + ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); + return nil; + } + // Fill in a LCS length matrix: + for (NSInteger i = 0; i <= selfCount; i++) { + lengths[i] = (NSInteger *)malloc(sizeof(NSInteger) * (arrayCount+1)); + if (lengths[i] == NULL) { + ASDisplayNodeFailAssert(@"Failed to allocate memory for diffing"); + return nil; + } + id selfObj = i > 0 ? self[i-1] : nil; + for (NSInteger j = 0; j <= arrayCount; j++) { + if (i == 0 || j == 0) { + lengths[i][j] = 0; + } else if (comparison(selfObj, array[j-1])) { + lengths[i][j] = 1 + lengths[i-1][j-1]; + } else { + lengths[i][j] = MAX(lengths[i-1][j], lengths[i][j-1]); + } + } + } + // Backtrack to fill in indices based on length matrix: + NSMutableIndexSet *common = [NSMutableIndexSet indexSet]; + NSInteger i = selfCount, j = arrayCount; + while(i > 0 && j > 0) { + if (comparison(self[i-1], array[j-1])) { + [common addIndex:(i-1)]; + i--; j--; + } else if (lengths[i-1][j] > lengths[i][j-1]) { + i--; + } else { + j--; + } + } + + for (NSInteger i = 0; i <= selfCount; i++) { + free(lengths[i]); + } + free(lengths); + return common; +} + +static compareBlock defaultCompare = nil; + ++ (compareBlock)defaultCompareBlock +{ + static dispatch_once_t onceToken; + + dispatch_once(&onceToken, ^{ + defaultCompare = ^BOOL(id lhs, id rhs) { + return [lhs isEqual:rhs]; + }; + }); + + return defaultCompare; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/NSIndexSet+ASHelpers.h b/submodules/AsyncDisplayKit/Source/NSIndexSet+ASHelpers.h new file mode 100644 index 0000000000..cab7c94310 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/NSIndexSet+ASHelpers.h @@ -0,0 +1,29 @@ +// +// NSIndexSet+ASHelpers.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 + +@interface NSIndexSet (ASHelpers) + +- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger idx))block; + +- (NSIndexSet *)as_intersectionWithIndexes:(NSIndexSet *)indexes; + +/// Returns all the item indexes from the given index paths that are in the given section. ++ (NSIndexSet *)as_indexSetFromIndexPaths:(NSArray *)indexPaths inSection:(NSUInteger)section; + +/// If you've got an old index, and you insert items using this index set, this returns the change to get to the new index. +- (NSUInteger)as_indexChangeByInsertingItemsBelowIndex:(NSUInteger)index; + +- (NSString *)as_smallDescription; + +/// Returns all the section indexes contained in the index paths array. ++ (NSIndexSet *)as_sectionsFromIndexPaths:(NSArray *)indexPaths; + +@end diff --git a/submodules/AsyncDisplayKit/Source/NSIndexSet+ASHelpers.mm b/submodules/AsyncDisplayKit/Source/NSIndexSet+ASHelpers.mm new file mode 100644 index 0000000000..615e1749f0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/NSIndexSet+ASHelpers.mm @@ -0,0 +1,91 @@ +// +// NSIndexSet+ASHelpers.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 +// + +// UIKit indexPath helpers +#import + +#import "NSIndexSet+ASHelpers.h" + +@implementation NSIndexSet (ASHelpers) + +- (NSIndexSet *)as_indexesByMapping:(NSUInteger (^)(NSUInteger))block +{ + NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init]; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { + NSUInteger newIndex = block(i); + if (newIndex != NSNotFound) { + [result addIndex:newIndex]; + } + } + }]; + return result; +} + +- (NSIndexSet *)as_intersectionWithIndexes:(NSIndexSet *)indexes +{ + NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init]; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [indexes enumerateRangesInRange:range options:kNilOptions usingBlock:^(NSRange range, BOOL * _Nonnull stop) { + [result addIndexesInRange:range]; + }]; + }]; + return result; +} + ++ (NSIndexSet *)as_indexSetFromIndexPaths:(NSArray *)indexPaths inSection:(NSUInteger)section +{ + NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init]; + for (NSIndexPath *indexPath in indexPaths) { + if (indexPath.section == section) { + [result addIndex:indexPath.item]; + } + } + return result; +} + +- (NSUInteger)as_indexChangeByInsertingItemsBelowIndex:(NSUInteger)index +{ + __block NSUInteger newIndex = index; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + for (NSUInteger i = range.location; i < NSMaxRange(range); i++) { + if (i <= newIndex) { + newIndex += 1; + } else { + *stop = YES; + } + } + }]; + return newIndex - index; +} + +- (NSString *)as_smallDescription +{ + NSMutableString *result = [NSMutableString stringWithString:@"{ "]; + [self enumerateRangesUsingBlock:^(NSRange range, BOOL * _Nonnull stop) { + if (range.length == 1) { + [result appendFormat:@"%tu ", range.location]; + } else { + [result appendFormat:@"%tu-%tu ", range.location, NSMaxRange(range) - 1]; + } + }]; + [result appendString:@"}"]; + return result; +} + ++ (NSIndexSet *)as_sectionsFromIndexPaths:(NSArray *)indexPaths +{ + NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init]; + for (NSIndexPath *indexPath in indexPaths) { + [result addIndex:indexPath.section]; + } + return result; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASDisplayNode.h b/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASDisplayNode.h index 6fa3718301..5b97e3fd83 100644 --- a/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASDisplayNode.h +++ b/submodules/AsyncDisplayKit/Source/PublicHeaders/AsyncDisplayKit/ASDisplayNode.h @@ -989,4 +989,8 @@ typedef NS_ENUM(NSInteger, ASLayoutEngineType) { @property (nullable, weak) ASDisplayNode *asyncdisplaykit_node; @end +@interface CALayer (ASDisplayNodeInternal) +@property (nullable, weak) ASDisplayNode *asyncdisplaykit_node; +@end + NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.mm b/submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.mm new file mode 100644 index 0000000000..9365fba5e0 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/UIResponder+AsyncDisplayKit.mm @@ -0,0 +1,32 @@ +// +// UIResponder+AsyncDisplayKit.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 +// + +#import + +#import +#import +#import "ASResponderChainEnumerator.h" + +@implementation UIResponder (AsyncDisplayKit) + +- (__kindof UIViewController *)asdk_associatedViewController +{ + ASDisplayNodeAssertMainThread(); + + for (UIResponder *responder in [self asdk_responderChainEnumerator]) { + UIViewController *vc = ASDynamicCast(responder, UIViewController); + if (vc) { + return vc; + } + } + return nil; +} + +@end + diff --git a/submodules/AsyncDisplayKit/Source/_ASAsyncTransaction.mm b/submodules/AsyncDisplayKit/Source/_ASAsyncTransaction.mm new file mode 100644 index 0000000000..c147f5f263 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASAsyncTransaction.mm @@ -0,0 +1,465 @@ +// +// _ASAsyncTransaction.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 + +#ifndef __STRICT_ANSI__ + #warning "Texture must be compiled with std=c++11 to prevent layout issues. gnu++ is not supported. This is hopefully temporary." +#endif + +AS_EXTERN NSRunLoopMode const UITrackingRunLoopMode; + +NSInteger const ASDefaultTransactionPriority = 0; + +@interface ASAsyncTransactionOperation : NSObject +- (instancetype)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock; +@property (nonatomic) asyncdisplaykit_async_transaction_operation_completion_block_t operationCompletionBlock; +@property id value; // set on bg queue by the operation block +@end + +@implementation ASAsyncTransactionOperation + +- (instancetype)initWithOperationCompletionBlock:(asyncdisplaykit_async_transaction_operation_completion_block_t)operationCompletionBlock +{ + if ((self = [super init])) { + _operationCompletionBlock = operationCompletionBlock; + } + return self; +} + +- (void)dealloc +{ + NSAssert(_operationCompletionBlock == nil, @"Should have been called and released before -dealloc"); +} + +- (void)callAndReleaseCompletionBlock:(BOOL)canceled; +{ + ASDisplayNodeAssertMainThread(); + if (_operationCompletionBlock) { + _operationCompletionBlock(self.value, canceled); + // Guarantee that _operationCompletionBlock is released on main thread + _operationCompletionBlock = nil; + } +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"", self, self.value]; +} + +@end + +// Lightweight operation queue for _ASAsyncTransaction that limits number of spawned threads +class ASAsyncTransactionQueue +{ +public: + + // Similar to dispatch_group_t + class Group + { + public: + // call when group is no longer needed; after last scheduled operation the group will delete itself + virtual void release() = 0; + + // schedule block on given queue + virtual void schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) = 0; + + // dispatch block on given queue when all previously scheduled blocks finished executing + virtual void notify(dispatch_queue_t queue, dispatch_block_t block) = 0; + + // used when manually executing blocks + virtual void enter() = 0; + virtual void leave() = 0; + + // wait until all scheduled blocks finished executing + virtual void wait() = 0; + + protected: + virtual ~Group() { }; // call release() instead + }; + + // Create new group + Group *createGroup(); + + static ASAsyncTransactionQueue &instance(); + +private: + + struct GroupNotify + { + dispatch_block_t _block; + dispatch_queue_t _queue; + }; + + class GroupImpl : public Group + { + public: + GroupImpl(ASAsyncTransactionQueue &queue) + : _pendingOperations(0) + , _releaseCalled(false) + , _queue(queue) + { + } + + virtual void release(); + virtual void schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block); + virtual void notify(dispatch_queue_t queue, dispatch_block_t block); + virtual void enter(); + virtual void leave(); + virtual void wait(); + + int _pendingOperations; + std::list _notifyList; + std::condition_variable _condition; + BOOL _releaseCalled; + ASAsyncTransactionQueue &_queue; + }; + + struct Operation + { + dispatch_block_t _block; + GroupImpl *_group; + NSInteger _priority; + }; + + struct DispatchEntry // entry for each dispatch queue + { + typedef std::list OperationQueue; + typedef std::list OperationIteratorList; // each item points to operation queue + typedef std::map OperationPriorityMap; // sorted by priority + + OperationQueue _operationQueue; + OperationPriorityMap _operationPriorityMap; + int _threadCount; + + Operation popNextOperation(bool respectPriority); // assumes locked mutex + void pushOperation(Operation operation); // assumes locked mutex + }; + + std::map _entries; + std::mutex _mutex; +}; + +ASAsyncTransactionQueue::Group* ASAsyncTransactionQueue::createGroup() +{ + Group *res = new GroupImpl(*this); + return res; +} + +void ASAsyncTransactionQueue::GroupImpl::release() +{ + std::lock_guard l(_queue._mutex); + + if (_pendingOperations == 0) { + delete this; + } else { + _releaseCalled = YES; + } +} + +ASAsyncTransactionQueue::Operation ASAsyncTransactionQueue::DispatchEntry::popNextOperation(bool respectPriority) +{ + NSCAssert(!_operationQueue.empty() && !_operationPriorityMap.empty(), @"No scheduled operations available"); + + OperationQueue::iterator queueIterator; + OperationPriorityMap::iterator mapIterator; + + if (respectPriority) { + mapIterator = --_operationPriorityMap.end(); // highest priority "bucket" + queueIterator = *mapIterator->second.begin(); + } else { + queueIterator = _operationQueue.begin(); + mapIterator = _operationPriorityMap.find(queueIterator->_priority); + } + + // no matter what, first item in "bucket" must match item in queue + NSCAssert(mapIterator->second.front() == queueIterator, @"Queue inconsistency"); + + Operation res = *queueIterator; + _operationQueue.erase(queueIterator); + + mapIterator->second.pop_front(); + if (mapIterator->second.empty()) { + _operationPriorityMap.erase(mapIterator); + } + + return res; +} + +void ASAsyncTransactionQueue::DispatchEntry::pushOperation(ASAsyncTransactionQueue::Operation operation) +{ + _operationQueue.push_back(operation); + + OperationIteratorList &list = _operationPriorityMap[operation._priority]; + list.push_back(--_operationQueue.end()); +} + +void ASAsyncTransactionQueue::GroupImpl::schedule(NSInteger priority, dispatch_queue_t queue, dispatch_block_t block) +{ + ASAsyncTransactionQueue &q = _queue; + std::lock_guard l(q._mutex); + + DispatchEntry &entry = q._entries[queue]; + + Operation operation; + operation._block = block; + operation._group = this; + operation._priority = priority; + entry.pushOperation(operation); + + ++_pendingOperations; // enter group + +#if ASDISPLAYNODE_DELAY_DISPLAY + NSUInteger maxThreads = 1; +#else + NSUInteger maxThreads = [NSProcessInfo processInfo].activeProcessorCount * 2; + + // Bit questionable maybe - we can give main thread more CPU time during tracking. + if ([[NSRunLoop mainRunLoop].currentMode isEqualToString:UITrackingRunLoopMode]) + --maxThreads; +#endif + + if (entry._threadCount < maxThreads) { // we need to spawn another thread + + // first thread will take operations in queue order (regardless of priority), other threads will respect priority + bool respectPriority = entry._threadCount > 0; + ++entry._threadCount; + + dispatch_async(queue, ^{ + std::unique_lock lock(q._mutex); + + // go until there are no more pending operations + while (!entry._operationQueue.empty()) { + Operation operation = entry.popNextOperation(respectPriority); + lock.unlock(); + if (operation._block) { + operation._block(); + } + operation._group->leave(); + operation._block = nil; // the block must be freed while mutex is unlocked + lock.lock(); + } + --entry._threadCount; + + if (entry._threadCount == 0) { + NSCAssert(entry._operationQueue.empty() || entry._operationPriorityMap.empty(), @"No working threads but operations are still scheduled"); // this shouldn't happen + q._entries.erase(queue); + } + }); + } +} + +void ASAsyncTransactionQueue::GroupImpl::notify(dispatch_queue_t queue, dispatch_block_t block) +{ + std::lock_guard l(_queue._mutex); + + if (_pendingOperations == 0) { + dispatch_async(queue, block); + } else { + GroupNotify notify; + notify._block = block; + notify._queue = queue; + _notifyList.push_back(notify); + } +} + +void ASAsyncTransactionQueue::GroupImpl::enter() +{ + std::lock_guard l(_queue._mutex); + ++_pendingOperations; +} + +void ASAsyncTransactionQueue::GroupImpl::leave() +{ + std::lock_guard l(_queue._mutex); + --_pendingOperations; + + if (_pendingOperations == 0) { + std::list notifyList; + _notifyList.swap(notifyList); + + for (GroupNotify & notify : notifyList) { + dispatch_async(notify._queue, notify._block); + } + + _condition.notify_one(); + + // there was attempt to release the group before, but we still + // had operations scheduled so now is good time + if (_releaseCalled) { + delete this; + } + } +} + +void ASAsyncTransactionQueue::GroupImpl::wait() +{ + std::unique_lock lock(_queue._mutex); + while (_pendingOperations > 0) { + _condition.wait(lock); + } +} + +ASAsyncTransactionQueue & ASAsyncTransactionQueue::instance() +{ + static ASAsyncTransactionQueue *instance = new ASAsyncTransactionQueue(); + return *instance; +} + +@interface _ASAsyncTransaction () +@property ASAsyncTransactionState state; +@end + + +@implementation _ASAsyncTransaction +{ + ASAsyncTransactionQueue::Group *_group; + NSMutableArray *_operations; +} + +#pragma mark - Lifecycle + +- (instancetype)initWithCompletionBlock:(void(^)(_ASAsyncTransaction *, BOOL))completionBlock +{ + if ((self = [self init])) { + _completionBlock = completionBlock; + self.state = ASAsyncTransactionStateOpen; + } + return self; +} + +- (void)dealloc +{ + // Uncommitted transactions break our guarantees about releasing completion blocks on callbackQueue. + NSAssert(self.state != ASAsyncTransactionStateOpen, @"Uncommitted ASAsyncTransactions are not allowed"); + if (_group) { + _group->release(); + } +} + +#pragma mark - Transaction Management + +- (void)addOperationWithBlock:(asyncdisplaykit_async_transaction_operation_block_t)block + priority:(NSInteger)priority + queue:(dispatch_queue_t)queue + completion:(asyncdisplaykit_async_transaction_operation_completion_block_t)completion +{ + ASDisplayNodeAssertMainThread(); + NSAssert(self.state == ASAsyncTransactionStateOpen, @"You can only add operations to open transactions"); + + [self _ensureTransactionData]; + + ASAsyncTransactionOperation *operation = [[ASAsyncTransactionOperation alloc] initWithOperationCompletionBlock:completion]; + [_operations addObject:operation]; + _group->schedule(priority, queue, ^{ + @autoreleasepool { + if (self.state != ASAsyncTransactionStateCanceled) { + operation.value = block(); + } + } + }); +} + +- (void)cancel +{ + ASDisplayNodeAssertMainThread(); + NSAssert(self.state != ASAsyncTransactionStateOpen, @"You can only cancel a committed or already-canceled transaction"); + self.state = ASAsyncTransactionStateCanceled; +} + +- (void)commit +{ + ASDisplayNodeAssertMainThread(); + NSAssert(self.state == ASAsyncTransactionStateOpen, @"You cannot double-commit a transaction"); + self.state = ASAsyncTransactionStateCommitted; + + if ([_operations count] == 0) { + // Fast path: if a transaction was opened, but no operations were added, execute completion block synchronously. + if (_completionBlock) { + _completionBlock(self, NO); + } + } else { + NSAssert(_group != NULL, @"If there are operations, dispatch group should have been created"); + + _group->notify(dispatch_get_main_queue(), ^{ + [self completeTransaction]; + }); + } +} + +- (void)completeTransaction +{ + ASDisplayNodeAssertMainThread(); + ASAsyncTransactionState state = self.state; + if (state != ASAsyncTransactionStateComplete) { + BOOL isCanceled = (state == ASAsyncTransactionStateCanceled); + for (ASAsyncTransactionOperation *operation in _operations) { + [operation callAndReleaseCompletionBlock:isCanceled]; + } + + // Always set state to Complete, even if we were cancelled, to block any extraneous + // calls to this method that may have been scheduled for the next runloop + // (e.g. if we needed to force one in this runloop with -waitUntilComplete, but another was already scheduled) + self.state = ASAsyncTransactionStateComplete; + + if (_completionBlock) { + _completionBlock(self, isCanceled); + } + } +} + +- (void)waitUntilComplete +{ + ASDisplayNodeAssertMainThread(); + if (self.state != ASAsyncTransactionStateComplete) { + if (_group) { + _group->wait(); + + // At this point, the asynchronous operation may have completed, but the runloop + // observer has not committed the batch of transactions we belong to. It's important to + // commit ourselves via the group to avoid double-committing the transaction. + // This is only necessary when forcing display work to complete before allowing the runloop + // to continue, e.g. in the implementation of -[ASDisplayNode recursivelyEnsureDisplay]. + if (self.state == ASAsyncTransactionStateOpen) { + [_ASAsyncTransactionGroup.mainTransactionGroup commit]; + NSAssert(self.state != ASAsyncTransactionStateOpen, @"Transaction should not be open after committing group"); + } + // If we needed to commit the group above, -completeTransaction may have already been run. + // It is designed to accommodate this by checking _state to ensure it is not complete. + [self completeTransaction]; + } + } +} + +#pragma mark - Helper Methods + +- (void)_ensureTransactionData +{ + // Lazily initialize _group and _operations to avoid overhead in the case where no operations are added to the transaction + if (_group == NULL) { + _group = ASAsyncTransactionQueue::instance().createGroup(); + } + if (_operations == nil) { + _operations = [[NSMutableArray alloc] init]; + } +} + +- (NSString *)description +{ + return [NSString stringWithFormat:@"<_ASAsyncTransaction: %p - _state = %lu, _group = %p, _operations = %@>", self, (unsigned long)self.state, _group, _operations]; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/_ASAsyncTransactionContainer+Private.h b/submodules/AsyncDisplayKit/Source/_ASAsyncTransactionContainer+Private.h new file mode 100644 index 0000000000..003184c586 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASAsyncTransactionContainer+Private.h @@ -0,0 +1,24 @@ +// +// _ASAsyncTransactionContainer+Private.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 + +@class _ASAsyncTransaction; + +@interface CALayer (ASAsyncTransactionContainerTransactions) +@property (nonatomic, nullable, setter=asyncdisplaykit_setAsyncLayerTransactions:) NSHashTable<_ASAsyncTransaction *> *asyncdisplaykit_asyncLayerTransactions; + +- (void)asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:(_ASAsyncTransaction *)transaction; +- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction; +@end + +NS_ASSUME_NONNULL_END diff --git a/submodules/AsyncDisplayKit/Source/_ASAsyncTransactionContainer.mm b/submodules/AsyncDisplayKit/Source/_ASAsyncTransactionContainer.mm new file mode 100644 index 0000000000..5bbfd95f15 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASAsyncTransactionContainer.mm @@ -0,0 +1,121 @@ +// +// _ASAsyncTransactionContainer.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 "_ASAsyncTransactionContainer+Private.h" + +#import +#import + +@implementation CALayer (ASAsyncTransactionContainerTransactions) +@dynamic asyncdisplaykit_asyncLayerTransactions; + +// No-ops in the base class. Mostly exposed for testing. +- (void)asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:(_ASAsyncTransaction *)transaction {} +- (void)asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:(_ASAsyncTransaction *)transaction {} +@end + +@implementation CALayer (ASAsyncTransactionContainer) +@dynamic asyncdisplaykit_currentAsyncTransaction; +@dynamic asyncdisplaykit_asyncTransactionContainer; + +- (ASAsyncTransactionContainerState)asyncdisplaykit_asyncTransactionContainerState +{ + return ([self.asyncdisplaykit_asyncLayerTransactions count] == 0) ? ASAsyncTransactionContainerStateNoTransactions : ASAsyncTransactionContainerStatePendingTransactions; +} + +- (void)asyncdisplaykit_cancelAsyncTransactions +{ + // If there was an open transaction, commit and clear the current transaction. Otherwise: + // (1) The run loop observer will try to commit a canceled transaction which is not allowed + // (2) We leave the canceled transaction attached to the layer, dooming future operations + _ASAsyncTransaction *currentTransaction = self.asyncdisplaykit_currentAsyncTransaction; + [currentTransaction commit]; + self.asyncdisplaykit_currentAsyncTransaction = nil; + + for (_ASAsyncTransaction *transaction in [self.asyncdisplaykit_asyncLayerTransactions copy]) { + [transaction cancel]; + } +} + +- (_ASAsyncTransaction *)asyncdisplaykit_asyncTransaction +{ + _ASAsyncTransaction *transaction = self.asyncdisplaykit_currentAsyncTransaction; + if (transaction == nil) { + NSHashTable *transactions = self.asyncdisplaykit_asyncLayerTransactions; + if (transactions == nil) { + transactions = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + self.asyncdisplaykit_asyncLayerTransactions = transactions; + } + __weak CALayer *weakSelf = self; + transaction = [[_ASAsyncTransaction alloc] initWithCompletionBlock:^(_ASAsyncTransaction *completedTransaction, BOOL cancelled) { + __strong CALayer *self = weakSelf; + if (self == nil) { + return; + } + [transactions removeObject:completedTransaction]; + [self asyncdisplaykit_asyncTransactionContainerDidCompleteTransaction:completedTransaction]; + }]; + [transactions addObject:transaction]; + self.asyncdisplaykit_currentAsyncTransaction = transaction; + [self asyncdisplaykit_asyncTransactionContainerWillBeginTransaction:transaction]; + } + [_ASAsyncTransactionGroup.mainTransactionGroup addTransactionContainer:self]; + return transaction; +} + +- (CALayer *)asyncdisplaykit_parentTransactionContainer +{ + CALayer *containerLayer = self; + while (containerLayer && !containerLayer.asyncdisplaykit_isAsyncTransactionContainer) { + containerLayer = containerLayer.superlayer; + } + return containerLayer; +} + +@end + +@implementation UIView (ASAsyncTransactionContainer) + +- (BOOL)asyncdisplaykit_isAsyncTransactionContainer +{ + return self.layer.asyncdisplaykit_isAsyncTransactionContainer; +} + +- (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)asyncTransactionContainer +{ + self.layer.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; +} + +- (ASAsyncTransactionContainerState)asyncdisplaykit_asyncTransactionContainerState +{ + return self.layer.asyncdisplaykit_asyncTransactionContainerState; +} + +- (void)asyncdisplaykit_cancelAsyncTransactions +{ + [self.layer asyncdisplaykit_cancelAsyncTransactions]; +} + +- (void)asyncdisplaykit_asyncTransactionContainerStateDidChange +{ + // No-op in the base class. +} + +- (void)asyncdisplaykit_setCurrentAsyncTransaction:(_ASAsyncTransaction *)transaction +{ + self.layer.asyncdisplaykit_currentAsyncTransaction = transaction; +} + +- (_ASAsyncTransaction *)asyncdisplaykit_currentAsyncTransaction +{ + return self.layer.asyncdisplaykit_currentAsyncTransaction; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/_ASAsyncTransactionGroup.mm b/submodules/AsyncDisplayKit/Source/_ASAsyncTransactionGroup.mm new file mode 100644 index 0000000000..ca170229b8 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASAsyncTransactionGroup.mm @@ -0,0 +1,88 @@ +// +// _ASAsyncTransactionGroup.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 "_ASAsyncTransactionContainer+Private.h" + +@implementation _ASAsyncTransactionGroup { + NSHashTable> *_containers; +} + ++ (_ASAsyncTransactionGroup *)mainTransactionGroup +{ + ASDisplayNodeAssertMainThread(); + static _ASAsyncTransactionGroup *mainTransactionGroup; + + if (mainTransactionGroup == nil) { + mainTransactionGroup = [[_ASAsyncTransactionGroup alloc] _init]; + [mainTransactionGroup registerAsMainRunloopObserver]; + } + return mainTransactionGroup; +} + +- (void)registerAsMainRunloopObserver +{ + ASDisplayNodeAssertMainThread(); + static CFRunLoopObserverRef observer; + ASDisplayNodeAssert(observer == NULL, @"A _ASAsyncTransactionGroup should not be registered on the main runloop twice"); + // defer the commit of the transaction so we can add more during the current runloop iteration + CFRunLoopRef runLoop = CFRunLoopGetCurrent(); + CFOptionFlags activities = (kCFRunLoopBeforeWaiting | // before the run loop starts sleeping + kCFRunLoopExit); // before exiting a runloop run + + observer = CFRunLoopObserverCreateWithHandler(NULL, // allocator + activities, // activities + YES, // repeats + INT_MAX, // order after CA transaction commits + ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + ASDisplayNodeCAssertMainThread(); + [self commit]; + }); + CFRunLoopAddObserver(runLoop, observer, kCFRunLoopCommonModes); + CFRelease(observer); +} + +- (instancetype)_init +{ + if ((self = [super init])) { + _containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + } + return self; +} + +- (void)addTransactionContainer:(id)container +{ + ASDisplayNodeAssertMainThread(); + ASDisplayNodeAssert(container != nil, @"No container"); + [_containers addObject:container]; +} + +- (void)commit +{ + ASDisplayNodeAssertMainThread(); + + if ([_containers count]) { + NSHashTable *containersToCommit = _containers; + _containers = [NSHashTable hashTableWithOptions:NSHashTableObjectPointerPersonality]; + + for (id container in containersToCommit) { + // Note that the act of committing a transaction may open a new transaction, + // so we must nil out the transaction we're committing first. + _ASAsyncTransaction *transaction = container.asyncdisplaykit_currentAsyncTransaction; + container.asyncdisplaykit_currentAsyncTransaction = nil; + [transaction commit]; + } + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/_ASCoreAnimationExtras.mm b/submodules/AsyncDisplayKit/Source/_ASCoreAnimationExtras.mm new file mode 100644 index 0000000000..b55bd6442f --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASCoreAnimationExtras.mm @@ -0,0 +1,187 @@ +// +// _ASCoreAnimationExtras.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 + +void ASDisplayNodeSetupLayerContentsWithResizableImage(CALayer *layer, UIImage *image) +{ + ASDisplayNodeSetResizableContents(layer, image); +} + +void ASDisplayNodeSetResizableContents(id obj, UIImage *image) +{ + // FIXME (https://github.com/TextureGroup/Texture/issues/1046): This method does not currently handle UIImageResizingModeTile, which is the default. + // See also https://developer.apple.com/documentation/uikit/uiimage/1624157-resizingmode?language=objc + // I'm not sure of a way to use CALayer directly to perform such tiling on the GPU, though the stretch is handled by the GPU, + // and CALayer.h documents the fact that contentsCenter is used to stretch the pixels. + + if (image) { + ASDisplayNodeCAssert(image.resizingMode == UIImageResizingModeStretch || UIEdgeInsetsEqualToEdgeInsets(image.capInsets, UIEdgeInsetsZero), + @"Image insets must be all-zero or resizingMode has to be UIImageResizingModeStretch. XCode assets default value is UIImageResizingModeTile which is not supported by Texture because of GPU-accelerated CALayer features."); + + // Image may not actually be stretchable in one or both dimensions; this is handled + obj.contents = (id)[image CGImage]; + obj.contentsScale = [image scale]; + obj.rasterizationScale = [image scale]; + CGSize imageSize = [image size]; + + UIEdgeInsets insets = [image capInsets]; + + // These are lifted from what UIImageView does by experimentation. Without these exact values, the stretching is slightly off. + const CGFloat halfPixelFudge = 0.49f; + const CGFloat otherPixelFudge = 0.02f; + // Convert to unit coordinates for the contentsCenter property. + CGRect contentsCenter = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); + if (insets.left > 0 || insets.right > 0) { + contentsCenter.origin.x = ((insets.left + halfPixelFudge) / imageSize.width); + contentsCenter.size.width = (imageSize.width - (insets.left + insets.right + 1.f) + otherPixelFudge) / imageSize.width; + } + if (insets.top > 0 || insets.bottom > 0) { + contentsCenter.origin.y = ((insets.top + halfPixelFudge) / imageSize.height); + contentsCenter.size.height = (imageSize.height - (insets.top + insets.bottom + 1.f) + otherPixelFudge) / imageSize.height; + } + obj.contentsGravity = kCAGravityResize; + obj.contentsCenter = contentsCenter; + + } else { + obj.contents = nil; + } +} + + +struct _UIContentModeStringLUTEntry { + UIViewContentMode contentMode; + NSString *const string; +}; + +static const _UIContentModeStringLUTEntry *UIContentModeCAGravityLUT(size_t *count) +{ + // Initialize this in a function (instead of at file level) to avoid + // startup initialization time. + static const _UIContentModeStringLUTEntry sUIContentModeCAGravityLUT[] = { + {UIViewContentModeScaleToFill, kCAGravityResize}, + {UIViewContentModeScaleAspectFit, kCAGravityResizeAspect}, + {UIViewContentModeScaleAspectFill, kCAGravityResizeAspectFill}, + {UIViewContentModeCenter, kCAGravityCenter}, + {UIViewContentModeTop, kCAGravityBottom}, + {UIViewContentModeBottom, kCAGravityTop}, + {UIViewContentModeLeft, kCAGravityLeft}, + {UIViewContentModeRight, kCAGravityRight}, + {UIViewContentModeTopLeft, kCAGravityBottomLeft}, + {UIViewContentModeTopRight, kCAGravityBottomRight}, + {UIViewContentModeBottomLeft, kCAGravityTopLeft}, + {UIViewContentModeBottomRight, kCAGravityTopRight}, + }; + *count = sizeof(sUIContentModeCAGravityLUT) / sizeof(sUIContentModeCAGravityLUT[0]); + return sUIContentModeCAGravityLUT; +} + +static const _UIContentModeStringLUTEntry *UIContentModeDescriptionLUT(size_t *count) +{ + // Initialize this in a function (instead of at file level) to avoid + // startup initialization time. + static const _UIContentModeStringLUTEntry sUIContentModeDescriptionLUT[] = { + {UIViewContentModeScaleToFill, @"scaleToFill"}, + {UIViewContentModeScaleAspectFit, @"aspectFit"}, + {UIViewContentModeScaleAspectFill, @"aspectFill"}, + {UIViewContentModeRedraw, @"redraw"}, + {UIViewContentModeCenter, @"center"}, + {UIViewContentModeTop, @"top"}, + {UIViewContentModeBottom, @"bottom"}, + {UIViewContentModeLeft, @"left"}, + {UIViewContentModeRight, @"right"}, + {UIViewContentModeTopLeft, @"topLeft"}, + {UIViewContentModeTopRight, @"topRight"}, + {UIViewContentModeBottomLeft, @"bottomLeft"}, + {UIViewContentModeBottomRight, @"bottomRight"}, + }; + *count = sizeof(sUIContentModeDescriptionLUT) / sizeof(sUIContentModeDescriptionLUT[0]); + return sUIContentModeDescriptionLUT; +} + +NSString *ASDisplayNodeNSStringFromUIContentMode(UIViewContentMode contentMode) +{ + size_t lutSize; + const _UIContentModeStringLUTEntry *lut = UIContentModeDescriptionLUT(&lutSize); + for (size_t i = 0; i < lutSize; ++i) { + if (lut[i].contentMode == contentMode) { + return lut[i].string; + } + } + return [NSString stringWithFormat:@"%d", (int)contentMode]; +} + +UIViewContentMode ASDisplayNodeUIContentModeFromNSString(NSString *string) +{ + size_t lutSize; + const _UIContentModeStringLUTEntry *lut = UIContentModeDescriptionLUT(&lutSize); + for (size_t i = 0; i < lutSize; ++i) { + if (ASObjectIsEqual(lut[i].string, string)) { + return lut[i].contentMode; + } + } + return UIViewContentModeScaleToFill; +} + +NSString *const ASDisplayNodeCAContentsGravityFromUIContentMode(UIViewContentMode contentMode) +{ + size_t lutSize; + const _UIContentModeStringLUTEntry *lut = UIContentModeCAGravityLUT(&lutSize); + for (size_t i = 0; i < lutSize; ++i) { + if (lut[i].contentMode == contentMode) { + return lut[i].string; + } + } + ASDisplayNodeCAssert(contentMode == UIViewContentModeRedraw, @"Encountered an unknown contentMode %ld. Is this a new version of iOS?", (long)contentMode); + // Redraw is ok to return nil. + return nil; +} + +#define ContentModeCacheSize 10 +UIViewContentMode ASDisplayNodeUIContentModeFromCAContentsGravity(NSString *const contentsGravity) +{ + static int currentCacheIndex = 0; + static NSMutableArray *cachedStrings = [NSMutableArray arrayWithCapacity:ContentModeCacheSize]; + static UIViewContentMode cachedModes[ContentModeCacheSize] = {}; + + NSInteger foundCacheIndex = [cachedStrings indexOfObjectIdenticalTo:contentsGravity]; + if (foundCacheIndex != NSNotFound && foundCacheIndex < ContentModeCacheSize) { + return cachedModes[foundCacheIndex]; + } + + size_t lutSize; + const _UIContentModeStringLUTEntry *lut = UIContentModeCAGravityLUT(&lutSize); + for (size_t i = 0; i < lutSize; ++i) { + if (ASObjectIsEqual(lut[i].string, contentsGravity)) { + UIViewContentMode foundContentMode = lut[i].contentMode; + + if (currentCacheIndex < ContentModeCacheSize) { + // Cache the input value. This is almost always a different pointer than in our LUT and will frequently + // be the same value for an overwhelming majority of inputs. + [cachedStrings addObject:contentsGravity]; + cachedModes[currentCacheIndex] = foundContentMode; + currentCacheIndex++; + } + + return foundContentMode; + } + } + + ASDisplayNodeCAssert(contentsGravity, @"Encountered an unknown contentsGravity \"%@\". Is this a new version of iOS?", contentsGravity); + ASDisplayNodeCAssert(!contentsGravity, @"You passed nil to ASDisplayNodeUIContentModeFromCAContentsGravity. We're falling back to resize, but this is probably a bug."); + // If asserts disabled, fall back to this + return UIViewContentModeScaleToFill; +} + +BOOL ASDisplayNodeLayerHasAnimations(CALayer *layer) +{ + return (layer.animationKeys.count != 0); +} diff --git a/submodules/AsyncDisplayKit/Source/_ASDisplayLayer.mm b/submodules/AsyncDisplayKit/Source/_ASDisplayLayer.mm new file mode 100644 index 0000000000..9ad2ca289b --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASDisplayLayer.mm @@ -0,0 +1,212 @@ +// +// _ASDisplayLayer.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 "ASDisplayNodeInternal.h" +#import +#import + +@implementation _ASDisplayLayer +{ + BOOL _attemptedDisplayWhileZeroSized; + + struct { + BOOL delegateDidChangeBounds:1; + } _delegateFlags; +} + +@dynamic displaysAsynchronously; + +#ifdef DEBUG +- (void)dealloc { + if (![NSThread isMainThread]) { + assert(true); + } +} +#endif + +#pragma mark - Properties + +- (void)setDelegate:(id)delegate +{ + [super setDelegate:delegate]; + _delegateFlags.delegateDidChangeBounds = [delegate respondsToSelector:@selector(layer:didChangeBoundsWithOldValue:newValue:)]; +} + +- (void)setDisplaySuspended:(BOOL)displaySuspended +{ + ASDisplayNodeAssertMainThread(); + if (_displaySuspended != displaySuspended) { + _displaySuspended = displaySuspended; + if (!displaySuspended) { + // If resuming display, trigger a display now. + [self setNeedsDisplay]; + } else { + // If suspending display, cancel any current async display so that we don't have contents set on us when it's finished. + [self cancelAsyncDisplay]; + } + } +} + +- (void)setBounds:(CGRect)bounds +{ + BOOL valid = ASDisplayNodeAssertNonFatal(ASIsCGRectValidForLayout(bounds), @"Caught attempt to set invalid bounds %@ on %@.", NSStringFromCGRect(bounds), self); + if (!valid) { + return; + } + if (_delegateFlags.delegateDidChangeBounds) { + CGRect oldBounds = self.bounds; + [super setBounds:bounds]; + self.asyncdisplaykit_node.threadSafeBounds = bounds; + [(id)self.delegate layer:self didChangeBoundsWithOldValue:oldBounds newValue:bounds]; + + } else { + [super setBounds:bounds]; + self.asyncdisplaykit_node.threadSafeBounds = bounds; + } + + if (_attemptedDisplayWhileZeroSized && CGRectIsEmpty(bounds) == NO && self.needsDisplayOnBoundsChange == NO) { + _attemptedDisplayWhileZeroSized = NO; + [self setNeedsDisplay]; + } +} + +#if DEBUG // These override is strictly to help detect application-level threading errors. Avoid method overhead in release. +- (void)setContents:(id)contents +{ + ASDisplayNodeAssertMainThread(); + [super setContents:contents]; +} + +- (void)setNeedsLayout +{ + ASDisplayNodeAssertMainThread(); + [super setNeedsLayout]; +} +#endif + +- (void)layoutSublayers +{ + ASDisplayNodeAssertMainThread(); + [super layoutSublayers]; + + [self.asyncdisplaykit_node __layout]; +} + +- (void)setNeedsDisplay +{ + ASDisplayNodeAssertMainThread(); + + // FIXME: Reconsider whether we should cancel a display in progress. + // We should definitely cancel a display that is scheduled, but unstarted display. + [self cancelAsyncDisplay]; + + // Short circuit if display is suspended. When resumed, we will setNeedsDisplay at that time. + if (!_displaySuspended) { + [super setNeedsDisplay]; + } +} + +#pragma mark - + ++ (dispatch_queue_t)displayQueue +{ + static dispatch_queue_t displayQueue = NULL; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + displayQueue = dispatch_queue_create("org.AsyncDisplayKit.ASDisplayLayer.displayQueue", DISPATCH_QUEUE_CONCURRENT); + // we use the highpri queue to prioritize UI rendering over other async operations + dispatch_set_target_queue(displayQueue, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)); + }); + + return displayQueue; +} + ++ (id)defaultValueForKey:(NSString *)key +{ + if ([key isEqualToString:@"displaysAsynchronously"]) { + return @YES; + } else if ([key isEqualToString:@"opaque"]) { + return @YES; + } else { + return [super defaultValueForKey:key]; + } +} + +#pragma mark - Display + +- (void)displayImmediately +{ + // This method is a low-level bypass that avoids touching CA, including any reset of the + // needsDisplay flag, until the .contents property is set with the result. + // It is designed to be able to block the thread of any caller and fully execute the display. + + ASDisplayNodeAssertMainThread(); + [self display:NO]; +} + +- (void)_hackResetNeedsDisplay +{ + ASDisplayNodeAssertMainThread(); + // Don't listen to our subclasses crazy ideas about setContents by going through super + super.contents = super.contents; +} + +- (void)display +{ + ASDisplayNodeAssertMainThread(); + [self _hackResetNeedsDisplay]; + + if (self.displaySuspended) { + return; + } + + [self display:self.displaysAsynchronously]; +} + +- (void)display:(BOOL)asynchronously +{ + if (CGRectIsEmpty(self.bounds)) { + _attemptedDisplayWhileZeroSized = YES; + } + + [self.asyncDelegate displayAsyncLayer:self asynchronously:asynchronously]; +} + +- (void)cancelAsyncDisplay +{ + ASDisplayNodeAssertMainThread(); + + [self.asyncDelegate cancelDisplayAsyncLayer:self]; +} + +// e.g. > +- (NSString *)description +{ + NSMutableString *description = [[super description] mutableCopy]; + ASDisplayNode *node = self.asyncdisplaykit_node; + if (node != nil) { + NSString *classString = [NSString stringWithFormat:@"%s-", object_getClassName(node)]; + [description replaceOccurrencesOfString:@"_ASDisplay" withString:classString options:kNilOptions range:NSMakeRange(0, description.length)]; + NSUInteger insertionIndex = [description rangeOfString:@">"].location; + if (insertionIndex != NSNotFound) { + NSString *nodeString = [NSString stringWithFormat:@"; node = %@", node]; + [description insertString:nodeString atIndex:insertionIndex]; + } + } + return description; +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/_ASDisplayView.mm b/submodules/AsyncDisplayKit/Source/_ASDisplayView.mm new file mode 100644 index 0000000000..365b141bbf --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASDisplayView.mm @@ -0,0 +1,569 @@ +// +// _ASDisplayView.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 "_ASDisplayViewAccessiblity.h" + +#import +#import +#import "ASDisplayNodeInternal.h" +#import +#import +#import +#import +#import +#import + +#pragma mark - _ASDisplayViewMethodOverrides + +typedef NS_OPTIONS(NSUInteger, _ASDisplayViewMethodOverrides) +{ + _ASDisplayViewMethodOverrideNone = 0, + _ASDisplayViewMethodOverrideCanBecomeFirstResponder = 1 << 0, + _ASDisplayViewMethodOverrideBecomeFirstResponder = 1 << 1, + _ASDisplayViewMethodOverrideCanResignFirstResponder = 1 << 2, + _ASDisplayViewMethodOverrideResignFirstResponder = 1 << 3, + _ASDisplayViewMethodOverrideIsFirstResponder = 1 << 4, +}; + +/** + * Returns _ASDisplayViewMethodOverrides for the given class + * + * @param c the class, required. + * + * @return _ASDisplayViewMethodOverrides. + */ +static _ASDisplayViewMethodOverrides GetASDisplayViewMethodOverrides(Class c) +{ + ASDisplayNodeCAssertNotNil(c, @"class is required"); + + _ASDisplayViewMethodOverrides overrides = _ASDisplayViewMethodOverrideNone; + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canBecomeFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideCanBecomeFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(becomeFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideBecomeFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(canResignFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideCanResignFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(resignFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideResignFirstResponder; + } + if (ASSubclassOverridesSelector([_ASDisplayView class], c, @selector(isFirstResponder))) { + overrides |= _ASDisplayViewMethodOverrideIsFirstResponder; + } + return overrides; +} + +#pragma mark - _ASDisplayView + +@interface _ASDisplayView () + +// Keep the node alive while its view is active. If you create a view, add its layer to a layer hierarchy, then release +// the view, the layer retains the view to prevent a crash. This replicates this behaviour for the node abstraction. +@property (nonatomic) ASDisplayNode *keepalive_node; +@end + +@implementation _ASDisplayView +{ + BOOL _inHitTest; + BOOL _inPointInside; + + NSArray *_accessibilityElements; + CGRect _lastAccessibilityElementsFrame; + + _ASDisplayViewMethodOverrides _methodOverrides; +} + +#pragma mark - Class + ++ (void)initialize +{ + __unused Class initializeSelf = self; + IMP staticInitialize = imp_implementationWithBlock(^(_ASDisplayView *view) { + ASDisplayNodeAssert(view.class == initializeSelf, @"View 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 %@", view.class, initializeSelf); + view->_methodOverrides = GetASDisplayViewMethodOverrides(view.class); + }); + + class_replaceMethod(self, @selector(_staticInitialize), staticInitialize, "v:@"); +} + ++ (Class)layerClass +{ + return [_ASDisplayLayer class]; +} + +#pragma mark - NSObject Overrides + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + [self _initializeInstance]; + + return self; +} + +- (void)_initializeInstance +{ + [self _staticInitialize]; +} + +- (void)_staticInitialize +{ + ASDisplayNodeAssert(NO, @"_staticInitialize must be overridden"); +} + +// e.g. ; frame = ...> +- (NSString *)description +{ + NSMutableString *description = [[super description] mutableCopy]; + + ASDisplayNode *node = _asyncdisplaykit_node; + + if (node != nil) { + NSString *classString = [NSString stringWithFormat:@"%s-", object_getClassName(node)]; + [description replaceOccurrencesOfString:@"_ASDisplay" withString:classString options:kNilOptions range:NSMakeRange(0, description.length)]; + NSUInteger semicolon = [description rangeOfString:@";"].location; + if (semicolon != NSNotFound) { + NSString *nodeString = [NSString stringWithFormat:@"; node = %@", node]; + [description insertString:nodeString atIndex:semicolon]; + } + // Remove layer description – it never contains valuable info and it duplicates the node info. Noisy. + NSRange layerDescriptionRange = [description rangeOfString:@"; layer = <.*>" options:NSRegularExpressionSearch]; + if (layerDescriptionRange.location != NSNotFound) { + [description replaceCharactersInRange:layerDescriptionRange withString:@""]; + // Our regex will grab the closing angle bracket and I'm not clever enough to come up with a better one, so re-add it if needed. + if ([description hasSuffix:@">"] == NO) { + [description appendString:@">"]; + } + } + } + return description; +} + +#pragma mark - UIView Overrides + +- (void)willMoveToWindow:(UIWindow *)newWindow +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + BOOL visible = (newWindow != nil); + if (visible && !node.inHierarchy) { + [node __enterHierarchy]; + } +} + +- (void)didMoveToWindow +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + BOOL visible = (self.window != nil); + if (!visible && node.inHierarchy) { + [node __exitHierarchy]; + } +} + +- (void)willMoveToSuperview:(UIView *)newSuperview +{ + // Keep the node alive while the view is in a view hierarchy. This helps ensure that async-drawing views can always + // display their contents as long as they are visible somewhere, and aids in lifecycle management because the + // lifecycle of the node can be treated as the same as the lifecycle of the view (let the view hierarchy own the + // view). + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + UIView *currentSuperview = self.superview; + if (!currentSuperview && newSuperview) { + self.keepalive_node = node; + } + + if (newSuperview) { + ASDisplayNode *supernode = node.supernode; + BOOL supernodeLoaded = supernode.nodeLoaded; + ASDisplayNodeAssert(!supernode.isLayerBacked, @"Shouldn't be possible for _ASDisplayView's supernode to be layer-backed."); + + BOOL needsSupernodeUpdate = NO; + + if (supernode) { + if (supernodeLoaded) { + if (supernode.layerBacked) { + // See comment in -didMoveToSuperview. This case should be avoided, but is possible with app-level coding errors. + needsSupernodeUpdate = (supernode.layer != newSuperview.layer); + } else { + // If we have a supernode, compensate for users directly messing with views by hitching up to any new supernode. + needsSupernodeUpdate = (supernode.view != newSuperview); + } + } else { + needsSupernodeUpdate = YES; + } + } else { + // If we have no supernode and we are now in a view hierarchy, check to see if we can hook up to a supernode. + needsSupernodeUpdate = (newSuperview != nil); + } + + if (needsSupernodeUpdate) { + // -removeFromSupernode is called by -addSubnode:, if it is needed. + // FIXME: Needs rethinking if automaticallyManagesSubnodes=YES + [newSuperview.asyncdisplaykit_node _addSubnode:node]; + } + } +} + +- (void)didMoveToSuperview +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + UIView *superview = self.superview; + if (superview == nil) { + // Clearing keepalive_node may cause deallocation of the node. In this case, __exitHierarchy may not have an opportunity (e.g. _node will be cleared + // by the time -didMoveToWindow occurs after this) to clear the Visible interfaceState, which we need to do before deallocation to meet an API guarantee. + if (node.inHierarchy) { + [node __exitHierarchy]; + } + self.keepalive_node = nil; + } + +#ifndef MINIMAL_ASDK +#if DEBUG + // This is only to help detect issues when a root-of-view-controller node is reused separately from its view controller. + // Avoid overhead in release. + if (superview && node.viewControllerRoot) { + UIViewController *vc = [node closestViewController]; + + ASDisplayNodeAssert(vc != nil && [vc isKindOfClass:[ASViewController class]] && ((ASViewController*)vc).node == node, @"This node was once used as a view controller's node. You should not reuse it without its view controller."); + } +#endif +#endif + + ASDisplayNode *supernode = node.supernode; + ASDisplayNodeAssert(!supernode.isLayerBacked, @"Shouldn't be possible for superview's node to be layer-backed."); + + if (supernode) { + ASDisplayNodeAssertTrue(node.nodeLoaded); + BOOL supernodeLoaded = supernode.nodeLoaded; + BOOL needsSupernodeRemoval = NO; + + if (superview) { + // If our new superview is not the same as the supernode's view, or the supernode has no view, disconnect. + if (supernodeLoaded) { + if (supernode.layerBacked) { + // As asserted at the top, this shouldn't be possible, but in production with assertions disabled it can happen. + // We try to make such code behave as well as feasible because it's not that hard of an error to make if some deep + // child node of a layer-backed node happens to be view-backed, but it is not supported and should be avoided. + needsSupernodeRemoval = (supernode.layer != superview.layer); + } else { + needsSupernodeRemoval = (supernode.view != superview); + } + } else { + needsSupernodeRemoval = YES; + } + } else { + // If supernode is loaded but our superview is nil, the user likely manually removed us, so disconnect supernode. + // The unlikely alternative: we are in __unloadNode, with shouldRasterizeSubnodes just having been turned on. + // In the latter case, we don't want to disassemble the node hierarchy because all views are intentionally being destroyed. + BOOL nodeIsRasterized = ((node.hierarchyState & ASHierarchyStateRasterized) == ASHierarchyStateRasterized); + needsSupernodeRemoval = (supernodeLoaded && !nodeIsRasterized); + } + + if (needsSupernodeRemoval) { + // The node will only disconnect from its supernode, not removeFromSuperview, in this condition. + // FIXME: Needs rethinking if automaticallyManagesSubnodes=YES + [node _removeFromSupernode]; + } + } +} + +- (void)insertSubview:(UIView *)view atIndex:(NSInteger)index { + [super insertSubview:view atIndex:index]; + +#ifndef ASDK_ACCESSIBILITY_DISABLE + self.accessibilityElements = nil; +#endif +} + +- (void)addSubview:(UIView *)view +{ + [super addSubview:view]; + +#ifndef ASDK_ACCESSIBILITY_DISABLE + self.accessibilityElements = nil; +#endif +} + +- (void)willRemoveSubview:(UIView *)subview +{ + [super willRemoveSubview:subview]; + +#ifndef ASDK_ACCESSIBILITY_DISABLE + self.accessibilityElements = nil; +#endif +} + +- (CGSize)sizeThatFits:(CGSize)size +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return node ? [node layoutThatFits:ASSizeRangeMake(size)].size : [super sizeThatFits:size]; +} + +- (void)setNeedsDisplay +{ + ASDisplayNodeAssertMainThread(); + // Standard implementation does not actually get to the layer, at least for views that don't implement drawRect:. + [self.layer setNeedsDisplay]; +} + +- (UIViewContentMode)contentMode +{ + return ASDisplayNodeUIContentModeFromCAContentsGravity(self.layer.contentsGravity); +} + +- (void)setContentMode:(UIViewContentMode)contentMode +{ + ASDisplayNodeAssert(contentMode != UIViewContentModeRedraw, @"Don't do this. Use needsDisplayOnBoundsChange instead."); + + // Do our own mapping so as not to call super and muck up needsDisplayOnBoundsChange. If we're in a production build, fall back to resize if we see redraw + self.layer.contentsGravity = (contentMode != UIViewContentModeRedraw) ? ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode) : kCAGravityResize; +} + +- (void)setBounds:(CGRect)bounds +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + [super setBounds:bounds]; + node.threadSafeBounds = bounds; +} + +- (void)addGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer +{ + [super addGestureRecognizer:gestureRecognizer]; + [_asyncdisplaykit_node nodeViewDidAddGestureRecognizer]; +} + +#pragma mark - Event Handling + UIResponder Overrides +- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesBegan) { + [node touchesBegan:touches withEvent:event]; + } else { + [super touchesBegan:touches withEvent:event]; + } +} + +- (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesMoved) { + [node touchesMoved:touches withEvent:event]; + } else { + [super touchesMoved:touches withEvent:event]; + } +} + +- (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesEnded) { + [node touchesEnded:touches withEvent:event]; + } else { + [super touchesEnded:touches withEvent:event]; + } +} + +- (void)touchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (node.methodOverrides & ASDisplayNodeMethodOverrideTouchesCancelled) { + [node touchesCancelled:touches withEvent:event]; + } else { + [super touchesCancelled:touches withEvent:event]; + } +} + +- (void)__forwardTouchesBegan:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesBegan:touches withEvent:event]; +} + +- (void)__forwardTouchesMoved:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesMoved:touches withEvent:event]; +} + +- (void)__forwardTouchesEnded:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesEnded:touches withEvent:event]; +} + +- (void)__forwardTouchesCancelled:(NSSet *)touches withEvent:(UIEvent *)event +{ + [super touchesCancelled:touches withEvent:event]; +} + +- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event +{ + // REVIEW: We should optimize these types of messages by setting a boolean in the associated ASDisplayNode subclass if + // they actually override the method. Same goes for -pointInside:withEvent: below. Many UIKit classes use that + // pattern for meaningful reductions of message send overhead in hot code (especially event handling). + + // Set boolean so this method can be re-entrant. If the node subclass wants to default to / make use of UIView + // hitTest:, it will call it on the view, which is _ASDisplayView. After calling into the node, any additional calls + // should use the UIView implementation of hitTest: + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (!_inHitTest) { + _inHitTest = YES; + UIView *hitView = [node hitTest:point withEvent:event]; + _inHitTest = NO; + return hitView; + } else { + return [super hitTest:point withEvent:event]; + } +} + +- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event +{ + // See comments in -hitTest:withEvent: for the strategy here. + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + if (!_inPointInside) { + _inPointInside = YES; + BOOL result = [node pointInside:point withEvent:event]; + _inPointInside = NO; + return result; + } else { + return [super pointInside:point withEvent:event]; + } +} + +- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node gestureRecognizerShouldBegin:gestureRecognizer]; +} + +- (void)tintColorDidChange +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + [super tintColorDidChange]; + + [node tintColorDidChange]; +} + +#pragma mark UIResponder Handling + +#define IMPLEMENT_RESPONDER_METHOD(__sel, __nodeMethodOverride, __viewMethodOverride) \ +- (BOOL)__sel\ +{\ + ASDisplayNode *node = _asyncdisplaykit_node; /* Create strong reference to weak ivar. */ \ + /* Check if we can call through to ASDisplayNode subclass directly */ \ + if (node.methodOverrides & __nodeMethodOverride) { \ + return [node __sel]; \ + } else { \ + /* Prevent an infinite loop in here if [super __sel] was called on a \ + / _ASDisplayView subclass */ \ + if (self->_methodOverrides & __viewMethodOverride) { \ + /* Call through to views superclass as we expect super was called from the + _ASDisplayView subclass and a node subclass does not overwrite __sel */ \ + return [self __##__sel]; \ + } else { \ + /* Call through to internal node __sel method that will consider the view in responding */ \ + return [node __##__sel]; \ + } \ + } \ +}\ +/* All __ prefixed methods are called from ASDisplayNode to let the view decide in what UIResponder state they \ +are not overridden by a ASDisplayNode subclass */ \ +- (BOOL)__##__sel \ +{ \ + return [super __sel]; \ +} \ + +IMPLEMENT_RESPONDER_METHOD(canBecomeFirstResponder, + ASDisplayNodeMethodOverrideCanBecomeFirstResponder, + _ASDisplayViewMethodOverrideCanBecomeFirstResponder); +IMPLEMENT_RESPONDER_METHOD(becomeFirstResponder, + ASDisplayNodeMethodOverrideBecomeFirstResponder, + _ASDisplayViewMethodOverrideBecomeFirstResponder); +IMPLEMENT_RESPONDER_METHOD(canResignFirstResponder, + ASDisplayNodeMethodOverrideCanResignFirstResponder, + _ASDisplayViewMethodOverrideCanResignFirstResponder); +IMPLEMENT_RESPONDER_METHOD(resignFirstResponder, + ASDisplayNodeMethodOverrideResignFirstResponder, + _ASDisplayViewMethodOverrideResignFirstResponder); +IMPLEMENT_RESPONDER_METHOD(isFirstResponder, + ASDisplayNodeMethodOverrideIsFirstResponder, + _ASDisplayViewMethodOverrideIsFirstResponder); + +- (BOOL)canPerformAction:(SEL)action withSender:(id)sender +{ + // We forward responder-chain actions to our node if we can't handle them ourselves. See -targetForAction:withSender:. + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return ([super canPerformAction:action withSender:sender] || [node respondsToSelector:action]); +} + +- (void)layoutMarginsDidChange +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + [super layoutMarginsDidChange]; + + [node layoutMarginsDidChange]; +} + +- (void)safeAreaInsetsDidChange +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + [super safeAreaInsetsDidChange]; + + [node safeAreaInsetsDidChange]; +} + +- (id)forwardingTargetForSelector:(SEL)aSelector +{ + // Ideally, we would implement -targetForAction:withSender: and simply return the node where we don't respond personally. + // Unfortunately UIResponder's default implementation of -targetForAction:withSender: doesn't follow its own documentation. It doesn't call -targetForAction:withSender: up the responder chain when -canPerformAction:withSender: fails, but instead merely calls -canPerformAction:withSender: on itself and then up the chain. rdar://20111500. + // Consequently, to forward responder-chain actions to our node, we override -canPerformAction:withSender: (used by the chain) to indicate support for responder chain-driven actions that our node supports, and then provide the node as a forwarding target here. + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return node; +} + +#if TARGET_OS_TV +#pragma mark - tvOS +- (BOOL)canBecomeFocused +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node canBecomeFocused]; +} + +- (void)didUpdateFocusInContext:(UIFocusUpdateContext *)context withAnimationCoordinator:(UIFocusAnimationCoordinator *)coordinator +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node didUpdateFocusInContext:context withAnimationCoordinator:coordinator]; +} + +- (void)setNeedsFocusUpdate +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node setNeedsFocusUpdate]; +} + +- (void)updateFocusIfNeeded +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node updateFocusIfNeeded]; +} + +- (BOOL)shouldUpdateFocusInContext:(UIFocusUpdateContext *)context +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node shouldUpdateFocusInContext:context]; +} + +- (UIView *)preferredFocusedView +{ + ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. + return [node preferredFocusedView]; +} +#endif +@end diff --git a/submodules/AsyncDisplayKit/Source/_ASDisplayViewAccessiblity.h b/submodules/AsyncDisplayKit/Source/_ASDisplayViewAccessiblity.h new file mode 100644 index 0000000000..9d0bf0719a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASDisplayViewAccessiblity.h @@ -0,0 +1,17 @@ +// +// _ASDisplayViewAccessiblity.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 + +// WARNING: When dealing with accessibility elements, please use the `accessibilityElements` +// property instead of the older methods e.g. `accessibilityElementCount()`. While the older methods +// should still work as long as accessibility is enabled, this framework provides no guarantees on +// their correctness. For details, see +// https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements diff --git a/submodules/AsyncDisplayKit/Source/_ASDisplayViewAccessiblity.mm b/submodules/AsyncDisplayKit/Source/_ASDisplayViewAccessiblity.mm new file mode 100644 index 0000000000..b24e8e9f58 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASDisplayViewAccessiblity.mm @@ -0,0 +1,349 @@ +// +// _ASDisplayViewAccessiblity.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 +// + +#ifndef ASDK_ACCESSIBILITY_DISABLE + +#import +#import +#import +#import +#import "ASDisplayNodeInternal.h" + +#import + +NS_INLINE UIAccessibilityTraits InteractiveAccessibilityTraitsMask() { + return UIAccessibilityTraitLink | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton; +} + +#pragma mark - UIAccessibilityElement + +@protocol ASAccessibilityElementPositioning + +@property (nonatomic, readonly) CGRect accessibilityFrame; + +@end + +typedef NSComparisonResult (^SortAccessibilityElementsComparator)(id, id); + +/// Sort accessiblity elements first by y and than by x origin. +static void SortAccessibilityElements(NSMutableArray *elements) +{ + ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); + + static SortAccessibilityElementsComparator comparator = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + comparator = ^NSComparisonResult(id a, id b) { + CGPoint originA = a.accessibilityFrame.origin; + CGPoint originB = b.accessibilityFrame.origin; + if (originA.y == originB.y) { + if (originA.x == originB.x) { + return NSOrderedSame; + } + return (originA.x < originB.x) ? NSOrderedAscending : NSOrderedDescending; + } + return (originA.y < originB.y) ? NSOrderedAscending : NSOrderedDescending; + }; + }); + [elements sortUsingComparator:comparator]; +} + +@interface ASAccessibilityElement : UIAccessibilityElement + +@property (nonatomic) ASDisplayNode *node; +@property (nonatomic) ASDisplayNode *containerNode; + ++ (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode; + +@end + +@implementation ASAccessibilityElement + ++ (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node containerNode:(ASDisplayNode *)containerNode +{ + ASAccessibilityElement *accessibilityElement = [[ASAccessibilityElement alloc] initWithAccessibilityContainer:container]; + accessibilityElement.node = node; + accessibilityElement.containerNode = containerNode; + accessibilityElement.accessibilityIdentifier = node.accessibilityIdentifier; + accessibilityElement.accessibilityLabel = node.accessibilityLabel; + accessibilityElement.accessibilityHint = node.accessibilityHint; + accessibilityElement.accessibilityValue = node.accessibilityValue; + accessibilityElement.accessibilityTraits = node.accessibilityTraits; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel; + accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint; + accessibilityElement.accessibilityAttributedValue = node.accessibilityAttributedValue; + } +#endif + return accessibilityElement; +} + +- (CGRect)accessibilityFrame +{ + CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node]; + accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.accessibilityContainer); + return accessibilityFrame; +} + +@end + +#pragma mark - _ASDisplayView / UIAccessibilityContainer + +@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction + +@property (nonatomic) UIView *container; +@property (nonatomic) ASDisplayNode *node; +@property (nonatomic) ASDisplayNode *containerNode; + +@end + +@implementation ASAccessibilityCustomAction + +- (CGRect)accessibilityFrame +{ + CGRect accessibilityFrame = [self.containerNode convertRect:self.node.bounds fromNode:self.node]; + accessibilityFrame = UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, self.container); + return accessibilityFrame; +} + +@end + +/// Collect all subnodes for the given node by walking down the subnode tree and calculates the screen coordinates based on the containerNode and container +static void CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements) +{ + ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); + + ASDisplayNodePerformBlockOnEveryNodeBFS(node, ^(ASDisplayNode * _Nonnull currentNode) { + // For every subnode that is layer backed or it's supernode has subtree rasterization enabled + // we have to create a UIAccessibilityElement as no view for this node exists + if (currentNode != containerNode && currentNode.isAccessibilityElement) { + UIAccessibilityElement *accessibilityElement = [ASAccessibilityElement accessibilityElementWithContainer:container node:currentNode containerNode:containerNode]; + [elements addObject:accessibilityElement]; + } + }); +} + +static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, UIView *view, NSMutableArray *elements) { + UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:container containerNode:container]; + + NSMutableArray *labeledNodes = [[NSMutableArray alloc] init]; + NSMutableArray *actions = [[NSMutableArray alloc] init]; + std::queue queue; + queue.push(container); + + // If the container does not have an accessibility label set, or if the label is meant for custom + // actions only, then aggregate its subnodes' labels. Otherwise, treat the label as an overriden + // value and do not perform the aggregation. + BOOL shouldAggregateSubnodeLabels = + (container.accessibilityLabel.length == 0) || + (container.accessibilityTraits & InteractiveAccessibilityTraitsMask()); + + ASDisplayNode *node = nil; + while (!queue.empty()) { + node = queue.front(); + queue.pop(); + + if (node != container && node.isAccessibilityContainer) { + CollectAccessibilityElementsForContainer(node, view, elements); + continue; + } + + if (node.accessibilityLabel.length > 0) { + if (node.accessibilityTraits & InteractiveAccessibilityTraitsMask()) { + ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:node.accessibilityLabel target:node selector:@selector(performAccessibilityCustomAction:)]; + action.node = node; + action.containerNode = node.supernode; + action.container = node.supernode.view; + [actions addObject:action]; + } else if (node == container || shouldAggregateSubnodeLabels) { + // Even though not surfaced to UIKit, create a non-interactive element for purposes of building sorted aggregated label. + ASAccessibilityElement *nonInteractiveElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:node containerNode:container]; + [labeledNodes addObject:nonInteractiveElement]; + } + } + + for (ASDisplayNode *subnode in node.subnodes) { + queue.push(subnode); + } + } + + SortAccessibilityElements(labeledNodes); + +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + NSArray *attributedLabels = [labeledNodes valueForKey:@"accessibilityAttributedLabel"]; + NSMutableAttributedString *attributedLabel = [NSMutableAttributedString new]; + [attributedLabels enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { + if (idx != 0) { + [attributedLabel appendAttributedString:[[NSAttributedString alloc] initWithString:@", "]]; + } + [attributedLabel appendAttributedString:(NSAttributedString *)obj]; + }]; + accessiblityElement.accessibilityAttributedLabel = attributedLabel; + } else +#endif + { + NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; + accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; + } + + SortAccessibilityElements(actions); + accessiblityElement.accessibilityCustomActions = actions; + + [elements addObject:accessiblityElement]; +} + +/// Collect all accessibliity elements for a given view and view node +static void CollectAccessibilityElementsForView(UIView *view, NSMutableArray *elements) +{ + ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); + + ASDisplayNode *node = view.asyncdisplaykit_node; + + static Class displayListViewClass = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + displayListViewClass = NSClassFromString(@"Display.ListView"); + }); + BOOL anySubNodeIsCollection = (nil != ASDisplayNodeFindFirstNode(node, + ^BOOL(ASDisplayNode *nodeToCheck) { + if (displayListViewClass != nil && [nodeToCheck isKindOfClass:displayListViewClass]) { + return true; + } + return false; + /*return ASDynamicCast(nodeToCheck, ASCollectionNode) != nil || + ASDynamicCast(nodeToCheck, ASTableNode) != nil;*/ + })); + + if (node.isAccessibilityContainer && !anySubNodeIsCollection) { + CollectAccessibilityElementsForContainer(node, view, elements); + return; + } + + // Handle rasterize case + if (node.rasterizesSubtree) { + CollectUIAccessibilityElementsForNode(node, node, view, elements); + return; + } + + for (ASDisplayNode *subnode in node.subnodes) { + if (subnode.isAccessibilityElement) { + + // An accessiblityElement can either be a UIView or a UIAccessibilityElement + if (subnode.isLayerBacked) { + // No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement that represents this node + UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:subnode containerNode:node]; + [elements addObject:accessiblityElement]; + } else { + // Accessiblity element is not layer backed just add the view as accessibility element + [elements addObject:subnode.view]; + } + } else if (subnode.isLayerBacked) { + // Go down the hierarchy of the layer backed subnode and collect all of the UIAccessibilityElement + CollectUIAccessibilityElementsForNode(subnode, node, view, elements); + } else if ([subnode accessibilityElementCount] > 0) { + // UIView is itself a UIAccessibilityContainer just add it + [elements addObject:subnode.view]; + } + } +} + +@interface _ASDisplayView () { + NSArray *_accessibilityElements; +} + +@end + +@implementation _ASDisplayView (UIAccessibilityContainer) + +- (void)accessibilityElementDidBecomeFocused { + ASDisplayNode *viewNode = self.asyncdisplaykit_node; + if ([viewNode respondsToSelector:@selector(accessibilityElementDidBecomeFocused)]) { + [viewNode accessibilityElementDidBecomeFocused]; + } +} + +/*- (bool)accessibilityActivate { + ASDisplayNode *viewNode = self.asyncdisplaykit_node; + if ([viewNode respondsToSelector:@selector(accessibilityActivate)]) { + return [viewNode accessibilityActivate]; + } + return false; +}*/ + +#pragma mark - UIAccessibility + +- (void)setAccessibilityElements:(NSArray *)accessibilityElements +{ + ASDisplayNodeAssertMainThread(); + _accessibilityElements = nil; +} + +- (NSArray *)accessibilityElements +{ + ASDisplayNodeAssertMainThread(); + + ASDisplayNode *viewNode = self.asyncdisplaykit_node; + if (viewNode == nil) { + return @[]; + } + if (true || _accessibilityElements == nil) { + _accessibilityElements = [viewNode accessibilityElements]; + } + return _accessibilityElements; +} + +@end + +@implementation ASDisplayNode (AccessibilityInternal) + +- (NSArray *)accessibilityElements +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElements since node is not loaded"); + return @[]; + } + NSMutableArray *accessibilityElements = [[NSMutableArray alloc] init]; + CollectAccessibilityElementsForView(self.view, accessibilityElements); + SortAccessibilityElements(accessibilityElements); + return accessibilityElements; +} + +@end + +@implementation _ASDisplayView (UIAccessibilityAction) + +- (BOOL)accessibilityActivate { + return [self.asyncdisplaykit_node accessibilityActivate]; +} + +- (void)accessibilityIncrement { + [self.asyncdisplaykit_node accessibilityIncrement]; +} + +- (void)accessibilityDecrement { + [self.asyncdisplaykit_node accessibilityDecrement]; +} + +- (BOOL)accessibilityScroll:(UIAccessibilityScrollDirection)direction { + return [self.asyncdisplaykit_node accessibilityScroll:direction]; +} + +- (BOOL)accessibilityPerformEscape { + return [self.asyncdisplaykit_node accessibilityPerformEscape]; +} + +- (BOOL)accessibilityPerformMagicTap { + return [self.asyncdisplaykit_node accessibilityPerformMagicTap]; +} + +@end + +#endif diff --git a/submodules/AsyncDisplayKit/Source/_ASPendingState.h b/submodules/AsyncDisplayKit/Source/_ASPendingState.h new file mode 100644 index 0000000000..0a96e7a8ad --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASPendingState.h @@ -0,0 +1,41 @@ +// +// _ASPendingState.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 + +/** + + Private header for ASDisplayNode.mm + + _ASPendingState is a proxy for a UIView that has yet to be created. + In response to its setters, it sets an internal property and a flag that indicates that that property has been set. + + When you want to configure a view from this pending state information, just call -applyToView: + */ + +@interface _ASPendingState : NSObject + +// Supports all of the properties included in the ASDisplayNodeViewProperties protocol + +- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)setFrameDirectly; +- (void)applyToLayer:(CALayer *)layer; + ++ (_ASPendingState *)pendingViewStateFromLayer:(CALayer *)layer; ++ (_ASPendingState *)pendingViewStateFromView:(UIView *)view; + +@property (nonatomic, readonly) BOOL hasSetNeedsLayout; +@property (nonatomic, readonly) BOOL hasSetNeedsDisplay; + +@property (nonatomic, readonly) BOOL hasChanges; + +- (void)clearChanges; + +@end diff --git a/submodules/AsyncDisplayKit/Source/_ASPendingState.mm b/submodules/AsyncDisplayKit/Source/_ASPendingState.mm new file mode 100644 index 0000000000..5bf75e50d1 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASPendingState.mm @@ -0,0 +1,1379 @@ +// +// _ASPendingState.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 "_ASPendingState.h" + +#import +#import +#import +#import +#import "ASDisplayNodeInternal.h" +#import + +#define __shouldSetNeedsDisplay(layer) (flags.needsDisplay \ + || (flags.setOpaque && opaque != (layer).opaque)\ + || (flags.setBackgroundColor && !CGColorEqualToColor(backgroundColor, (layer).backgroundColor))) + +typedef struct { + // Properties + int needsDisplay:1; + int needsLayout:1; + int layoutIfNeeded:1; + + // Flags indicating that a given property should be applied to the view at creation + int setClipsToBounds:1; + int setOpaque:1; + int setNeedsDisplayOnBoundsChange:1; + int setAutoresizesSubviews:1; + int setAutoresizingMask:1; + int setFrame:1; + int setBounds:1; + int setBackgroundColor:1; + int setTintColor:1; + int setHidden:1; + int setAlpha:1; + int setCornerRadius:1; + int setContentMode:1; + int setNeedsDisplay:1; + int setAnchorPoint:1; + int setPosition:1; + int setZPosition:1; + int setTransform:1; + int setSublayerTransform:1; + int setContents:1; + int setContentsGravity:1; + int setContentsRect:1; + int setContentsCenter:1; + int setContentsScale:1; + int setRasterizationScale:1; + int setUserInteractionEnabled:1; + int setExclusiveTouch:1; + int setShadowColor:1; + int setShadowOpacity:1; + int setShadowOffset:1; + int setShadowRadius:1; + int setBorderWidth:1; + int setBorderColor:1; + int setAsyncTransactionContainer:1; + int setAllowsGroupOpacity:1; + int setAllowsEdgeAntialiasing:1; + int setEdgeAntialiasingMask:1; + int setIsAccessibilityElement:1; + int setAccessibilityLabel:1; + int setAccessibilityAttributedLabel:1; + int setAccessibilityHint:1; + int setAccessibilityAttributedHint:1; + int setAccessibilityValue:1; + int setAccessibilityAttributedValue:1; + int setAccessibilityTraits:1; + int setAccessibilityFrame:1; + int setAccessibilityLanguage:1; + int setAccessibilityElementsHidden:1; + int setAccessibilityViewIsModal:1; + int setShouldGroupAccessibilityChildren:1; + int setAccessibilityIdentifier:1; + int setAccessibilityNavigationStyle:1; + int setAccessibilityHeaderElements:1; + int setAccessibilityActivationPoint:1; + int setAccessibilityPath:1; + int setSemanticContentAttribute:1; + int setLayoutMargins:1; + int setPreservesSuperviewLayoutMargins:1; + int setInsetsLayoutMarginsFromSafeArea:1; + int setAccessibilityCustomActions:1; +} ASPendingStateFlags; + +@implementation _ASPendingState +{ + @package //Expose all ivars for ASDisplayNode to bypass getters for efficiency + + UIViewAutoresizing autoresizingMask; + unsigned int edgeAntialiasingMask; + CGRect frame; // Frame is only to be used for synchronous views wrapped by nodes (see setFrame:) + CGRect bounds; + CGColorRef backgroundColor; + CGFloat alpha; + CGFloat cornerRadius; + UIViewContentMode contentMode; + CGPoint anchorPoint; + CGPoint position; + CGFloat zPosition; + CATransform3D transform; + CATransform3D sublayerTransform; + id contents; + NSString *contentsGravity; + CGRect contentsRect; + CGRect contentsCenter; + CGFloat contentsScale; + CGFloat rasterizationScale; + CGColorRef shadowColor; + CGFloat shadowOpacity; + CGSize shadowOffset; + CGFloat shadowRadius; + CGFloat borderWidth; + CGColorRef borderColor; + BOOL asyncTransactionContainer; + UIEdgeInsets layoutMargins; + BOOL preservesSuperviewLayoutMargins; + BOOL insetsLayoutMarginsFromSafeArea; + BOOL isAccessibilityElement; + NSString *accessibilityLabel; + NSAttributedString *accessibilityAttributedLabel; + NSString *accessibilityHint; + NSAttributedString *accessibilityAttributedHint; + NSString *accessibilityValue; + NSAttributedString *accessibilityAttributedValue; + UIAccessibilityTraits accessibilityTraits; + CGRect accessibilityFrame; + NSString *accessibilityLanguage; + BOOL accessibilityElementsHidden; + BOOL accessibilityViewIsModal; + BOOL shouldGroupAccessibilityChildren; + NSString *accessibilityIdentifier; + UIAccessibilityNavigationStyle accessibilityNavigationStyle; + NSArray *accessibilityHeaderElements; + CGPoint accessibilityActivationPoint; + UIBezierPath *accessibilityPath; + UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); + NSArray * accessibilityCustomActions; + + ASPendingStateFlags _flags; +} + +/** + * Apply the state's frame, bounds, and position to layer. This will not + * be called on synchronous view-backed nodes which require we directly + * call [view setFrame:]. + * + * FIXME: How should we reconcile order-of-operations between setting frame, bounds, position? + * Note we can't read bounds and position in the background, so we have to keep the frame + * value intact until application time (now). + */ +ASDISPLAYNODE_INLINE void ASPendingStateApplyMetricsToLayer(_ASPendingState *state, CALayer *layer) { + ASPendingStateFlags flags = state->_flags; + if (flags.setFrame) { + CGRect _bounds = CGRectZero; + CGPoint _position = CGPointZero; + ASBoundsAndPositionForFrame(state->frame, layer.bounds.origin, layer.anchorPoint, &_bounds, &_position); + layer.bounds = _bounds; + layer.position = _position; + } else { + if (flags.setBounds) + layer.bounds = state->bounds; + if (flags.setPosition) + layer.position = state->position; + } +} + +@synthesize clipsToBounds=clipsToBounds; +@synthesize opaque=opaque; +@synthesize frame=frame; +@synthesize bounds=bounds; +@synthesize backgroundColor=backgroundColor; +@synthesize hidden=isHidden; +@synthesize needsDisplayOnBoundsChange=needsDisplayOnBoundsChange; +@synthesize allowsGroupOpacity=allowsGroupOpacity; +@synthesize allowsEdgeAntialiasing=allowsEdgeAntialiasing; +@synthesize edgeAntialiasingMask=edgeAntialiasingMask; +@synthesize autoresizesSubviews=autoresizesSubviews; +@synthesize autoresizingMask=autoresizingMask; +@synthesize tintColor=tintColor; +@synthesize alpha=alpha; +@synthesize cornerRadius=cornerRadius; +@synthesize contentMode=contentMode; +@synthesize anchorPoint=anchorPoint; +@synthesize position=position; +@synthesize zPosition=zPosition; +@synthesize transform=transform; +@synthesize sublayerTransform=sublayerTransform; +@synthesize contents=contents; +@synthesize contentsGravity=contentsGravity; +@synthesize contentsRect=contentsRect; +@synthesize contentsCenter=contentsCenter; +@synthesize contentsScale=contentsScale; +@synthesize rasterizationScale=rasterizationScale; +@synthesize userInteractionEnabled=userInteractionEnabled; +@synthesize exclusiveTouch=exclusiveTouch; +@synthesize shadowColor=shadowColor; +@synthesize shadowOpacity=shadowOpacity; +@synthesize shadowOffset=shadowOffset; +@synthesize shadowRadius=shadowRadius; +@synthesize borderWidth=borderWidth; +@synthesize borderColor=borderColor; +@synthesize asyncdisplaykit_asyncTransactionContainer=asyncTransactionContainer; +@synthesize semanticContentAttribute=semanticContentAttribute; +@synthesize layoutMargins=layoutMargins; +@synthesize preservesSuperviewLayoutMargins=preservesSuperviewLayoutMargins; +@synthesize insetsLayoutMarginsFromSafeArea=insetsLayoutMarginsFromSafeArea; + +static CGColorRef blackColorRef = NULL; +static UIColor *defaultTintColor = nil; + +- (instancetype)init +{ + if (!(self = [super init])) + return nil; + + + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + // Default UIKit color is an RGB color + CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB(); + blackColorRef = CGColorCreate(colorSpace, (CGFloat[]){0,0,0,1} ); + CFRetain(blackColorRef); + CGColorSpaceRelease(colorSpace); + defaultTintColor = [UIColor colorWithRed:0.0 green:0.478 blue:1.0 alpha:1.0]; + }); + + // Set defaults, these come from the defaults specified in CALayer and UIView + clipsToBounds = NO; + opaque = YES; + frame = CGRectZero; + bounds = CGRectZero; + backgroundColor = nil; + tintColor = defaultTintColor; + isHidden = NO; + needsDisplayOnBoundsChange = NO; + allowsGroupOpacity = ASDefaultAllowsGroupOpacity(); + allowsEdgeAntialiasing = ASDefaultAllowsEdgeAntialiasing(); + autoresizesSubviews = YES; + alpha = 1.0f; + cornerRadius = 0.0f; + contentMode = UIViewContentModeScaleToFill; + _flags.needsDisplay = NO; + anchorPoint = CGPointMake(0.5, 0.5); + position = CGPointZero; + zPosition = 0.0; + transform = CATransform3DIdentity; + sublayerTransform = CATransform3DIdentity; + contents = nil; + contentsGravity = kCAGravityResize; + contentsRect = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); + contentsCenter = CGRectMake(0.0f, 0.0f, 1.0f, 1.0f); + contentsScale = 1.0f; + rasterizationScale = 1.0f; + userInteractionEnabled = YES; + shadowColor = blackColorRef; + shadowOpacity = 0.0; + shadowOffset = CGSizeMake(0, -3); + shadowRadius = 3; + borderWidth = 0; + borderColor = blackColorRef; + layoutMargins = UIEdgeInsetsMake(8, 8, 8, 8); + preservesSuperviewLayoutMargins = NO; + insetsLayoutMarginsFromSafeArea = YES; + isAccessibilityElement = NO; + accessibilityLabel = nil; + accessibilityAttributedLabel = nil; + accessibilityHint = nil; + accessibilityAttributedHint = nil; + accessibilityValue = nil; + accessibilityAttributedValue = nil; + accessibilityTraits = UIAccessibilityTraitNone; + accessibilityFrame = CGRectZero; + accessibilityLanguage = nil; + accessibilityElementsHidden = NO; + accessibilityViewIsModal = NO; + shouldGroupAccessibilityChildren = NO; + accessibilityIdentifier = nil; + accessibilityNavigationStyle = UIAccessibilityNavigationStyleAutomatic; + accessibilityHeaderElements = nil; + accessibilityActivationPoint = CGPointZero; + accessibilityPath = nil; + edgeAntialiasingMask = (kCALayerLeftEdge | kCALayerRightEdge | kCALayerTopEdge | kCALayerBottomEdge); + semanticContentAttribute = UISemanticContentAttributeUnspecified; + + return self; +} + +- (void)setNeedsDisplay +{ + _flags.needsDisplay = YES; +} + +- (void)setNeedsLayout +{ + _flags.needsLayout = YES; +} + +- (void)layoutIfNeeded +{ + _flags.layoutIfNeeded = YES; +} + +- (void)setClipsToBounds:(BOOL)flag +{ + clipsToBounds = flag; + _flags.setClipsToBounds = YES; +} + +- (void)setOpaque:(BOOL)flag +{ + opaque = flag; + _flags.setOpaque = YES; +} + +- (void)setNeedsDisplayOnBoundsChange:(BOOL)flag +{ + needsDisplayOnBoundsChange = flag; + _flags.setNeedsDisplayOnBoundsChange = YES; +} + +- (void)setAllowsGroupOpacity:(BOOL)flag +{ + allowsGroupOpacity = flag; + _flags.setAllowsGroupOpacity = YES; +} + +- (void)setAllowsEdgeAntialiasing:(BOOL)flag +{ + allowsEdgeAntialiasing = flag; + _flags.setAllowsEdgeAntialiasing = YES; +} + +- (void)setEdgeAntialiasingMask:(unsigned int)mask +{ + edgeAntialiasingMask = mask; + _flags.setEdgeAntialiasingMask = YES; +} + +- (void)setAutoresizesSubviews:(BOOL)flag +{ + autoresizesSubviews = flag; + _flags.setAutoresizesSubviews = YES; +} + +- (void)setAutoresizingMask:(UIViewAutoresizing)mask +{ + autoresizingMask = mask; + _flags.setAutoresizingMask = YES; +} + +- (void)setFrame:(CGRect)newFrame +{ + frame = newFrame; + _flags.setFrame = YES; +} + +- (void)setBounds:(CGRect)newBounds +{ + ASDisplayNodeAssert(!isnan(newBounds.size.width) && !isnan(newBounds.size.height), @"Invalid bounds %@ provided to %@", NSStringFromCGRect(newBounds), self); + if (isnan(newBounds.size.width)) + newBounds.size.width = 0.0; + if (isnan(newBounds.size.height)) + newBounds.size.height = 0.0; + bounds = newBounds; + _flags.setBounds = YES; +} + +- (CGColorRef)backgroundColor +{ + return backgroundColor; +} + +- (void)setBackgroundColor:(CGColorRef)color +{ + if (color == backgroundColor) { + return; + } + + CGColorRelease(backgroundColor); + backgroundColor = CGColorRetain(color); + _flags.setBackgroundColor = YES; +} + +- (void)setTintColor:(UIColor *)newTintColor +{ + tintColor = newTintColor; + _flags.setTintColor = YES; +} + +- (void)setHidden:(BOOL)flag +{ + isHidden = flag; + _flags.setHidden = YES; +} + +- (void)setAlpha:(CGFloat)newAlpha +{ + alpha = newAlpha; + _flags.setAlpha = YES; +} + +- (void)setCornerRadius:(CGFloat)newCornerRadius +{ + cornerRadius = newCornerRadius; + _flags.setCornerRadius = YES; +} + +- (void)setContentMode:(UIViewContentMode)newContentMode +{ + contentMode = newContentMode; + _flags.setContentMode = YES; +} + +- (void)setAnchorPoint:(CGPoint)newAnchorPoint +{ + anchorPoint = newAnchorPoint; + _flags.setAnchorPoint = YES; +} + +- (void)setPosition:(CGPoint)newPosition +{ + ASDisplayNodeAssert(!isnan(newPosition.x) && !isnan(newPosition.y), @"Invalid position %@ provided to %@", NSStringFromCGPoint(newPosition), self); + if (isnan(newPosition.x)) + newPosition.x = 0.0; + if (isnan(newPosition.y)) + newPosition.y = 0.0; + position = newPosition; + _flags.setPosition = YES; +} + +- (void)setZPosition:(CGFloat)newPosition +{ + zPosition = newPosition; + _flags.setZPosition = YES; +} + +- (void)setTransform:(CATransform3D)newTransform +{ + transform = newTransform; + _flags.setTransform = YES; +} + +- (void)setSublayerTransform:(CATransform3D)newSublayerTransform +{ + sublayerTransform = newSublayerTransform; + _flags.setSublayerTransform = YES; +} + +- (void)setContents:(id)newContents +{ + if (contents == newContents) { + return; + } + + contents = newContents; + _flags.setContents = YES; +} + +- (void)setContentsGravity:(NSString *)newContentsGravity +{ + contentsGravity = newContentsGravity; + _flags.setContentsGravity = YES; +} + +- (void)setContentsRect:(CGRect)newContentsRect +{ + contentsRect = newContentsRect; + _flags.setContentsRect = YES; +} + +- (void)setContentsCenter:(CGRect)newContentsCenter +{ + contentsCenter = newContentsCenter; + _flags.setContentsCenter = YES; +} + +- (void)setContentsScale:(CGFloat)newContentsScale +{ + contentsScale = newContentsScale; + _flags.setContentsScale = YES; +} + +- (void)setRasterizationScale:(CGFloat)newRasterizationScale +{ + rasterizationScale = newRasterizationScale; + _flags.setRasterizationScale = YES; +} + +- (void)setUserInteractionEnabled:(BOOL)flag +{ + userInteractionEnabled = flag; + _flags.setUserInteractionEnabled = YES; +} + +- (void)setExclusiveTouch:(BOOL)flag +{ + exclusiveTouch = flag; + _flags.setExclusiveTouch = YES; +} + +- (void)setShadowColor:(CGColorRef)color +{ + if (shadowColor == color) { + return; + } + + if (shadowColor != blackColorRef) { + CGColorRelease(shadowColor); + } + shadowColor = color; + CGColorRetain(shadowColor); + + _flags.setShadowColor = YES; +} + +- (void)setShadowOpacity:(CGFloat)newOpacity +{ + shadowOpacity = newOpacity; + _flags.setShadowOpacity = YES; +} + +- (void)setShadowOffset:(CGSize)newOffset +{ + shadowOffset = newOffset; + _flags.setShadowOffset = YES; +} + +- (void)setShadowRadius:(CGFloat)newRadius +{ + shadowRadius = newRadius; + _flags.setShadowRadius = YES; +} + +- (void)setBorderWidth:(CGFloat)newWidth +{ + borderWidth = newWidth; + _flags.setBorderWidth = YES; +} + +- (void)setBorderColor:(CGColorRef)color +{ + if (borderColor == color) { + return; + } + + if (borderColor != blackColorRef) { + CGColorRelease(borderColor); + } + borderColor = color; + CGColorRetain(borderColor); + + _flags.setBorderColor = YES; +} + +- (void)asyncdisplaykit_setAsyncTransactionContainer:(BOOL)flag +{ + asyncTransactionContainer = flag; + _flags.setAsyncTransactionContainer = YES; +} + +- (void)setLayoutMargins:(UIEdgeInsets)margins +{ + layoutMargins = margins; + _flags.setLayoutMargins = YES; +} + +- (void)setPreservesSuperviewLayoutMargins:(BOOL)flag +{ + preservesSuperviewLayoutMargins = flag; + _flags.setPreservesSuperviewLayoutMargins = YES; +} + +- (void)setInsetsLayoutMarginsFromSafeArea:(BOOL)flag +{ + insetsLayoutMarginsFromSafeArea = flag; + _flags.setInsetsLayoutMarginsFromSafeArea = YES; +} + +- (void)setSemanticContentAttribute:(UISemanticContentAttribute)attribute API_AVAILABLE(ios(9.0), tvos(9.0)) { + semanticContentAttribute = attribute; + _flags.setSemanticContentAttribute = YES; +} + +- (void)setAccessibilityCustomActions:(NSArray *)accessibilityCustomActions { + self->accessibilityCustomActions = accessibilityCustomActions; + _flags.setAccessibilityCustomActions = YES; +} + +- (BOOL)isAccessibilityElement +{ + return isAccessibilityElement; +} + +- (void)setIsAccessibilityElement:(BOOL)newIsAccessibilityElement +{ + isAccessibilityElement = newIsAccessibilityElement; + _flags.setIsAccessibilityElement = YES; +} + +- (NSString *)accessibilityLabel +{ + if (_flags.setAccessibilityAttributedLabel) { + return accessibilityAttributedLabel.string; + } + return accessibilityLabel; +} + +- (void)setAccessibilityLabel:(NSString *)newAccessibilityLabel +{ + ASCompareAssignCopy(accessibilityLabel, newAccessibilityLabel); + _flags.setAccessibilityLabel = YES; + _flags.setAccessibilityAttributedLabel = NO; +} + +- (NSAttributedString *)accessibilityAttributedLabel +{ + if (_flags.setAccessibilityLabel) { + return [[NSAttributedString alloc] initWithString:accessibilityLabel]; + } + return accessibilityAttributedLabel; +} + +- (void)setAccessibilityAttributedLabel:(NSAttributedString *)newAccessibilityAttributedLabel +{ + ASCompareAssignCopy(accessibilityAttributedLabel, newAccessibilityAttributedLabel); + _flags.setAccessibilityAttributedLabel = YES; + _flags.setAccessibilityLabel = NO; +} + +- (NSString *)accessibilityHint +{ + if (_flags.setAccessibilityAttributedHint) { + return accessibilityAttributedHint.string; + } + return accessibilityHint; +} + +- (void)setAccessibilityHint:(NSString *)newAccessibilityHint +{ + ASCompareAssignCopy(accessibilityHint, newAccessibilityHint); + _flags.setAccessibilityHint = YES; + _flags.setAccessibilityAttributedHint = NO; +} + +- (NSAttributedString *)accessibilityAttributedHint +{ + if (_flags.setAccessibilityHint) { + return [[NSAttributedString alloc] initWithString:accessibilityHint]; + } + return accessibilityAttributedHint; +} + +- (void)setAccessibilityAttributedHint:(NSAttributedString *)newAccessibilityAttributedHint +{ + ASCompareAssignCopy(accessibilityAttributedHint, newAccessibilityAttributedHint); + _flags.setAccessibilityAttributedHint = YES; + _flags.setAccessibilityHint = NO; +} + +- (NSString *)accessibilityValue +{ + if (_flags.setAccessibilityAttributedValue) { + return accessibilityAttributedValue.string; + } + return accessibilityValue; +} + +- (void)setAccessibilityValue:(NSString *)newAccessibilityValue +{ + ASCompareAssignCopy(accessibilityValue, newAccessibilityValue); + _flags.setAccessibilityValue = YES; + _flags.setAccessibilityAttributedValue = NO; +} + +- (NSAttributedString *)accessibilityAttributedValue +{ + if (_flags.setAccessibilityValue) { + return [[NSAttributedString alloc] initWithString:accessibilityValue]; + } + return accessibilityAttributedValue; +} + +- (void)setAccessibilityAttributedValue:(NSAttributedString *)newAccessibilityAttributedValue +{ + ASCompareAssignCopy(accessibilityAttributedValue, newAccessibilityAttributedValue); + _flags.setAccessibilityAttributedValue = YES; + _flags.setAccessibilityValue = NO; +} + +- (UIAccessibilityTraits)accessibilityTraits +{ + return accessibilityTraits; +} + +- (void)setAccessibilityTraits:(UIAccessibilityTraits)newAccessibilityTraits +{ + accessibilityTraits = newAccessibilityTraits; + _flags.setAccessibilityTraits = YES; +} + +- (CGRect)accessibilityFrame +{ + return accessibilityFrame; +} + +- (void)setAccessibilityFrame:(CGRect)newAccessibilityFrame +{ + accessibilityFrame = newAccessibilityFrame; + _flags.setAccessibilityFrame = YES; +} + +- (NSString *)accessibilityLanguage +{ + return accessibilityLanguage; +} + +- (void)setAccessibilityLanguage:(NSString *)newAccessibilityLanguage +{ + _flags.setAccessibilityLanguage = YES; + accessibilityLanguage = newAccessibilityLanguage; +} + +- (BOOL)accessibilityElementsHidden +{ + return accessibilityElementsHidden; +} + +- (void)setAccessibilityElementsHidden:(BOOL)newAccessibilityElementsHidden +{ + accessibilityElementsHidden = newAccessibilityElementsHidden; + _flags.setAccessibilityElementsHidden = YES; +} + +- (BOOL)accessibilityViewIsModal +{ + return accessibilityViewIsModal; +} + +- (void)setAccessibilityViewIsModal:(BOOL)newAccessibilityViewIsModal +{ + accessibilityViewIsModal = newAccessibilityViewIsModal; + _flags.setAccessibilityViewIsModal = YES; +} + +- (BOOL)shouldGroupAccessibilityChildren +{ + return shouldGroupAccessibilityChildren; +} + +- (void)setShouldGroupAccessibilityChildren:(BOOL)newShouldGroupAccessibilityChildren +{ + shouldGroupAccessibilityChildren = newShouldGroupAccessibilityChildren; + _flags.setShouldGroupAccessibilityChildren = YES; +} + +- (NSString *)accessibilityIdentifier +{ + return accessibilityIdentifier; +} + +- (void)setAccessibilityIdentifier:(NSString *)newAccessibilityIdentifier +{ + _flags.setAccessibilityIdentifier = YES; + if (accessibilityIdentifier != newAccessibilityIdentifier) { + accessibilityIdentifier = [newAccessibilityIdentifier copy]; + } +} + +- (UIAccessibilityNavigationStyle)accessibilityNavigationStyle +{ + return accessibilityNavigationStyle; +} + +- (void)setAccessibilityNavigationStyle:(UIAccessibilityNavigationStyle)newAccessibilityNavigationStyle +{ + _flags.setAccessibilityNavigationStyle = YES; + accessibilityNavigationStyle = newAccessibilityNavigationStyle; +} + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wdeprecated-implementations" +- (NSArray *)accessibilityHeaderElements +{ + return accessibilityHeaderElements; +} + +- (void)setAccessibilityHeaderElements:(NSArray *)newAccessibilityHeaderElements +{ + _flags.setAccessibilityHeaderElements = YES; + if (accessibilityHeaderElements != newAccessibilityHeaderElements) { + accessibilityHeaderElements = [newAccessibilityHeaderElements copy]; + } +} +#pragma clang diagnostic pop + +- (CGPoint)accessibilityActivationPoint +{ + if (_flags.setAccessibilityActivationPoint) { + return accessibilityActivationPoint; + } + + // Default == Mid-point of the accessibilityFrame + return CGPointMake(CGRectGetMidX(accessibilityFrame), CGRectGetMidY(accessibilityFrame)); +} + +- (void)setAccessibilityActivationPoint:(CGPoint)newAccessibilityActivationPoint +{ + _flags.setAccessibilityActivationPoint = YES; + accessibilityActivationPoint = newAccessibilityActivationPoint; +} + +- (UIBezierPath *)accessibilityPath +{ + return accessibilityPath; +} + +- (void)setAccessibilityPath:(UIBezierPath *)newAccessibilityPath +{ + _flags.setAccessibilityPath = YES; + if (accessibilityPath != newAccessibilityPath) { + accessibilityPath = newAccessibilityPath; + } +} + +- (void)applyToLayer:(CALayer *)layer +{ + ASPendingStateFlags flags = _flags; + + if (__shouldSetNeedsDisplay(layer)) { + [layer setNeedsDisplay]; + } + + if (flags.setAnchorPoint) + layer.anchorPoint = anchorPoint; + + if (flags.setZPosition) + layer.zPosition = zPosition; + + if (flags.setTransform) + layer.transform = transform; + + if (flags.setSublayerTransform) + layer.sublayerTransform = sublayerTransform; + + if (flags.setClipsToBounds) + layer.masksToBounds = clipsToBounds; + + if (flags.setBackgroundColor) + layer.backgroundColor = backgroundColor; + + if (flags.setOpaque) + layer.opaque = opaque; + + if (flags.setHidden) + layer.hidden = isHidden; + + if (flags.setAlpha) + layer.opacity = alpha; + + if (flags.setCornerRadius) + layer.cornerRadius = cornerRadius; + + if (flags.setContentMode) + layer.contentsGravity = ASDisplayNodeCAContentsGravityFromUIContentMode(contentMode); + + if (flags.setShadowColor) + layer.shadowColor = shadowColor; + + if (flags.setShadowOpacity) + layer.shadowOpacity = shadowOpacity; + + if (flags.setShadowOffset) + layer.shadowOffset = shadowOffset; + + if (flags.setShadowRadius) + layer.shadowRadius = shadowRadius; + + if (flags.setBorderWidth) + layer.borderWidth = borderWidth; + + if (flags.setBorderColor) + layer.borderColor = borderColor; + + if (flags.setNeedsDisplayOnBoundsChange) + layer.needsDisplayOnBoundsChange = needsDisplayOnBoundsChange; + + if (flags.setAllowsGroupOpacity) + layer.allowsGroupOpacity = allowsGroupOpacity; + + if (flags.setAllowsEdgeAntialiasing) + layer.allowsEdgeAntialiasing = allowsEdgeAntialiasing; + + if (flags.setEdgeAntialiasingMask) + layer.edgeAntialiasingMask = edgeAntialiasingMask; + + if (flags.setAsyncTransactionContainer) + layer.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; + + if (flags.setOpaque) + ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); + + 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]; + + if (flags.layoutIfNeeded) + [layer layoutIfNeeded]; +} + +- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPropertiesHandling +{ + /* + Use our convenience setters blah here instead of layer.blah + We were accidentally setting some properties on layer here, but view in UIViewBridgeOptimizations. + + That could easily cause bugs where it mattered whether you set something up on a bg thread on in -didLoad + because a different setter would be called. + */ + + CALayer *layer = view.layer; + + ASPendingStateFlags flags = _flags; + if (__shouldSetNeedsDisplay(layer)) { + [view setNeedsDisplay]; + } + + if (flags.setAnchorPoint) + layer.anchorPoint = anchorPoint; + + if (flags.setPosition) + layer.position = position; + + if (flags.setZPosition) + layer.zPosition = zPosition; + + if (flags.setBounds) + view.bounds = bounds; + + if (flags.setTransform) + layer.transform = transform; + + if (flags.setSublayerTransform) + layer.sublayerTransform = sublayerTransform; + + if (flags.setClipsToBounds) + view.clipsToBounds = clipsToBounds; + + if (flags.setBackgroundColor) { + // We have to make sure certain nodes get the background color call directly set + if (specialPropertiesHandling) { + view.backgroundColor = [UIColor colorWithCGColor:backgroundColor]; + } else { + // Set the background color to the layer as in the UIView bridge we use this value as background color + layer.backgroundColor = backgroundColor; + } + } + + if (flags.setTintColor) + view.tintColor = self.tintColor; + + if (flags.setOpaque) + layer.opaque = opaque; + + if (flags.setHidden) + view.hidden = isHidden; + + if (flags.setAlpha) + view.alpha = alpha; + + if (flags.setCornerRadius) + layer.cornerRadius = cornerRadius; + + if (flags.setContentMode) + view.contentMode = contentMode; + + if (flags.setUserInteractionEnabled) + view.userInteractionEnabled = userInteractionEnabled; + + #if TARGET_OS_IOS + if (flags.setExclusiveTouch) + view.exclusiveTouch = exclusiveTouch; + #endif + + if (flags.setShadowColor) + layer.shadowColor = shadowColor; + + if (flags.setShadowOpacity) + layer.shadowOpacity = shadowOpacity; + + if (flags.setShadowOffset) + layer.shadowOffset = shadowOffset; + + if (flags.setShadowRadius) + layer.shadowRadius = shadowRadius; + + if (flags.setBorderWidth) + layer.borderWidth = borderWidth; + + if (flags.setBorderColor) + layer.borderColor = borderColor; + + if (flags.setAutoresizingMask) + view.autoresizingMask = autoresizingMask; + + if (flags.setAutoresizesSubviews) + view.autoresizesSubviews = autoresizesSubviews; + + if (flags.setNeedsDisplayOnBoundsChange) + layer.needsDisplayOnBoundsChange = needsDisplayOnBoundsChange; + + if (flags.setAllowsGroupOpacity) + layer.allowsGroupOpacity = allowsGroupOpacity; + + if (flags.setAllowsEdgeAntialiasing) + layer.allowsEdgeAntialiasing = allowsEdgeAntialiasing; + + if (flags.setEdgeAntialiasingMask) + layer.edgeAntialiasingMask = edgeAntialiasingMask; + + if (flags.setAsyncTransactionContainer) + view.asyncdisplaykit_asyncTransactionContainer = asyncTransactionContainer; + + if (flags.setOpaque) + ASDisplayNodeAssert(layer.opaque == opaque, @"Didn't set opaque as desired"); + + if (flags.setLayoutMargins) + view.layoutMargins = layoutMargins; + + if (flags.setPreservesSuperviewLayoutMargins) + view.preservesSuperviewLayoutMargins = preservesSuperviewLayoutMargins; + + if (AS_AVAILABLE_IOS(11.0)) { + if (flags.setInsetsLayoutMarginsFromSafeArea) { + view.insetsLayoutMarginsFromSafeArea = insetsLayoutMarginsFromSafeArea; + } + } + + if (flags.setAccessibilityCustomActions) { + view.accessibilityCustomActions = accessibilityCustomActions; + } + + if (flags.setSemanticContentAttribute) { + view.semanticContentAttribute = semanticContentAttribute; + } + + if (flags.setIsAccessibilityElement) + view.isAccessibilityElement = isAccessibilityElement; + + if (flags.setAccessibilityLabel) + view.accessibilityLabel = accessibilityLabel; + + if (flags.setAccessibilityHint) + view.accessibilityHint = accessibilityHint; + + if (flags.setAccessibilityValue) + view.accessibilityValue = accessibilityValue; + + if (AS_AVAILABLE_IOS(11)) { + if (flags.setAccessibilityAttributedLabel) { + view.accessibilityAttributedLabel = accessibilityAttributedLabel; + } + if (flags.setAccessibilityAttributedHint) { + view.accessibilityAttributedHint = accessibilityAttributedHint; + } + if (flags.setAccessibilityAttributedValue) { + view.accessibilityAttributedValue = accessibilityAttributedValue; + } + } + + if (flags.setAccessibilityTraits) + view.accessibilityTraits = accessibilityTraits; + + if (flags.setAccessibilityFrame) + view.accessibilityFrame = accessibilityFrame; + + if (flags.setAccessibilityLanguage) + view.accessibilityLanguage = accessibilityLanguage; + + if (flags.setAccessibilityElementsHidden) + view.accessibilityElementsHidden = accessibilityElementsHidden; + + if (flags.setAccessibilityViewIsModal) + view.accessibilityViewIsModal = accessibilityViewIsModal; + + if (flags.setShouldGroupAccessibilityChildren) + view.shouldGroupAccessibilityChildren = shouldGroupAccessibilityChildren; + + if (flags.setAccessibilityIdentifier) + view.accessibilityIdentifier = accessibilityIdentifier; + + if (flags.setAccessibilityNavigationStyle) + view.accessibilityNavigationStyle = accessibilityNavigationStyle; + +#if TARGET_OS_TV + if (flags.setAccessibilityHeaderElements) + view.accessibilityHeaderElements = accessibilityHeaderElements; +#endif + + if (flags.setAccessibilityActivationPoint) + view.accessibilityActivationPoint = accessibilityActivationPoint; + + if (flags.setAccessibilityPath) + view.accessibilityPath = accessibilityPath; + + if (flags.setFrame && specialPropertiesHandling) { + // Frame is only defined when transform is identity because we explicitly diverge from CALayer behavior and define frame without transform +//#if DEBUG +// // Checking if the transform is identity is expensive, so disable when unnecessary. We have assertions on in Release, so DEBUG is the only way I know of. +// ASDisplayNodeAssert(CATransform3DIsIdentity(layer.transform), @"-[ASDisplayNode setFrame:] - self.transform must be identity in order to set the frame property. (From Apple's UIView documentation: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.)"); +//#endif + view.frame = frame; + } else { + 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]; + + if (flags.layoutIfNeeded) + [view layoutIfNeeded]; +} + +// FIXME: Make this more efficient by tracking which properties are set rather than reading everything. ++ (_ASPendingState *)pendingViewStateFromLayer:(CALayer *)layer +{ + if (!layer) { + return nil; + } + _ASPendingState *pendingState = [[_ASPendingState alloc] init]; + pendingState.anchorPoint = layer.anchorPoint; + pendingState.position = layer.position; + pendingState.zPosition = layer.zPosition; + pendingState.bounds = layer.bounds; + pendingState.transform = layer.transform; + pendingState.sublayerTransform = layer.sublayerTransform; + pendingState.contents = layer.contents; + pendingState.contentsGravity = layer.contentsGravity; + pendingState.contentsRect = layer.contentsRect; + pendingState.contentsCenter = layer.contentsCenter; + pendingState.contentsScale = layer.contentsScale; + pendingState.rasterizationScale = layer.rasterizationScale; + pendingState.clipsToBounds = layer.masksToBounds; + pendingState.backgroundColor = layer.backgroundColor; + pendingState.opaque = layer.opaque; + pendingState.hidden = layer.hidden; + pendingState.alpha = layer.opacity; + pendingState.cornerRadius = layer.cornerRadius; + pendingState.contentMode = ASDisplayNodeUIContentModeFromCAContentsGravity(layer.contentsGravity); + pendingState.shadowColor = layer.shadowColor; + pendingState.shadowOpacity = layer.shadowOpacity; + pendingState.shadowOffset = layer.shadowOffset; + pendingState.shadowRadius = layer.shadowRadius; + pendingState.borderWidth = layer.borderWidth; + pendingState.borderColor = layer.borderColor; + pendingState.needsDisplayOnBoundsChange = layer.needsDisplayOnBoundsChange; + pendingState.allowsGroupOpacity = layer.allowsGroupOpacity; + pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; + pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; + return pendingState; +} + +// FIXME: Make this more efficient by tracking which properties are set rather than reading everything. ++ (_ASPendingState *)pendingViewStateFromView:(UIView *)view +{ + if (!view) { + return nil; + } + _ASPendingState *pendingState = [[_ASPendingState alloc] init]; + + CALayer *layer = view.layer; + pendingState.anchorPoint = layer.anchorPoint; + pendingState.position = layer.position; + pendingState.zPosition = layer.zPosition; + pendingState.bounds = view.bounds; + pendingState.transform = layer.transform; + pendingState.sublayerTransform = layer.sublayerTransform; + pendingState.contents = layer.contents; + pendingState.contentsGravity = layer.contentsGravity; + pendingState.contentsRect = layer.contentsRect; + pendingState.contentsCenter = layer.contentsCenter; + pendingState.contentsScale = layer.contentsScale; + pendingState.rasterizationScale = layer.rasterizationScale; + pendingState.clipsToBounds = view.clipsToBounds; + pendingState.backgroundColor = layer.backgroundColor; + pendingState.tintColor = view.tintColor; + pendingState.opaque = layer.opaque; + pendingState.hidden = view.hidden; + pendingState.alpha = view.alpha; + pendingState.cornerRadius = layer.cornerRadius; + pendingState.contentMode = view.contentMode; + pendingState.userInteractionEnabled = view.userInteractionEnabled; +#if TARGET_OS_IOS + pendingState.exclusiveTouch = view.exclusiveTouch; +#endif + pendingState.shadowColor = layer.shadowColor; + pendingState.shadowOpacity = layer.shadowOpacity; + pendingState.shadowOffset = layer.shadowOffset; + pendingState.shadowRadius = layer.shadowRadius; + pendingState.borderWidth = layer.borderWidth; + pendingState.borderColor = layer.borderColor; + pendingState.autoresizingMask = view.autoresizingMask; + pendingState.autoresizesSubviews = view.autoresizesSubviews; + pendingState.needsDisplayOnBoundsChange = layer.needsDisplayOnBoundsChange; + pendingState.allowsGroupOpacity = layer.allowsGroupOpacity; + pendingState.allowsEdgeAntialiasing = layer.allowsEdgeAntialiasing; + pendingState.edgeAntialiasingMask = layer.edgeAntialiasingMask; + pendingState.semanticContentAttribute = view.semanticContentAttribute; + pendingState.layoutMargins = view.layoutMargins; + pendingState.preservesSuperviewLayoutMargins = view.preservesSuperviewLayoutMargins; + if (AS_AVAILABLE_IOS(11)) { + pendingState.insetsLayoutMarginsFromSafeArea = view.insetsLayoutMarginsFromSafeArea; + } + pendingState.isAccessibilityElement = view.isAccessibilityElement; + pendingState.accessibilityLabel = view.accessibilityLabel; + pendingState.accessibilityHint = view.accessibilityHint; + pendingState.accessibilityValue = view.accessibilityValue; +#if __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_11_0 + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + pendingState.accessibilityAttributedLabel = view.accessibilityAttributedLabel; + pendingState.accessibilityAttributedHint = view.accessibilityAttributedHint; + pendingState.accessibilityAttributedValue = view.accessibilityAttributedValue; + } +#endif + pendingState.accessibilityTraits = view.accessibilityTraits; + pendingState.accessibilityFrame = view.accessibilityFrame; + pendingState.accessibilityLanguage = view.accessibilityLanguage; + pendingState.accessibilityElementsHidden = view.accessibilityElementsHidden; + pendingState.accessibilityViewIsModal = view.accessibilityViewIsModal; + pendingState.shouldGroupAccessibilityChildren = view.shouldGroupAccessibilityChildren; + pendingState.accessibilityIdentifier = view.accessibilityIdentifier; + pendingState.accessibilityNavigationStyle = view.accessibilityNavigationStyle; +#if TARGET_OS_TV + pendingState.accessibilityHeaderElements = view.accessibilityHeaderElements; +#endif + pendingState.accessibilityActivationPoint = view.accessibilityActivationPoint; + pendingState.accessibilityPath = view.accessibilityPath; + return pendingState; +} + +- (void)clearChanges +{ + _flags = (ASPendingStateFlags){ 0 }; +} + +- (BOOL)hasSetNeedsLayout +{ + return _flags.needsLayout; +} + +- (BOOL)hasSetNeedsDisplay +{ + return _flags.needsDisplay; +} + +- (BOOL)hasChanges +{ + ASPendingStateFlags flags = _flags; + + return (flags.setAnchorPoint + || flags.setPosition + || flags.setZPosition + || flags.setFrame + || flags.setBounds + || flags.setPosition + || flags.setTransform + || flags.setSublayerTransform + || flags.setContents + || flags.setContentsGravity + || flags.setContentsRect + || flags.setContentsCenter + || flags.setContentsScale + || flags.setRasterizationScale + || flags.setClipsToBounds + || flags.setBackgroundColor + || flags.setTintColor + || flags.setHidden + || flags.setAlpha + || flags.setCornerRadius + || flags.setContentMode + || flags.setUserInteractionEnabled + || flags.setExclusiveTouch + || flags.setShadowOpacity + || flags.setShadowOffset + || flags.setShadowRadius + || flags.setShadowColor + || flags.setBorderWidth + || flags.setBorderColor + || flags.setAutoresizingMask + || flags.setAutoresizesSubviews + || flags.setNeedsDisplayOnBoundsChange + || flags.setAllowsGroupOpacity + || flags.setAllowsEdgeAntialiasing + || flags.setEdgeAntialiasingMask + || flags.needsDisplay + || flags.needsLayout + || flags.setAsyncTransactionContainer + || flags.setOpaque + || flags.setSemanticContentAttribute + || flags.setLayoutMargins + || flags.setPreservesSuperviewLayoutMargins + || flags.setInsetsLayoutMarginsFromSafeArea + || flags.setIsAccessibilityElement + || flags.setAccessibilityLabel + || flags.setAccessibilityAttributedLabel + || flags.setAccessibilityHint + || flags.setAccessibilityAttributedHint + || flags.setAccessibilityValue + || flags.setAccessibilityAttributedValue + || flags.setAccessibilityTraits + || flags.setAccessibilityFrame + || flags.setAccessibilityLanguage + || flags.setAccessibilityElementsHidden + || flags.setAccessibilityViewIsModal + || flags.setShouldGroupAccessibilityChildren + || flags.setAccessibilityIdentifier + || flags.setAccessibilityNavigationStyle + || flags.setAccessibilityHeaderElements + || flags.setAccessibilityActivationPoint + || flags.setAccessibilityPath); +} + +- (void)dealloc +{ + CGColorRelease(backgroundColor); + + if (shadowColor != blackColorRef) { + CGColorRelease(shadowColor); + } + + if (borderColor != blackColorRef) { + CGColorRelease(borderColor); + } +} + +@end diff --git a/submodules/AsyncDisplayKit/Source/_ASScopeTimer.h b/submodules/AsyncDisplayKit/Source/_ASScopeTimer.h new file mode 100644 index 0000000000..523599dd0a --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASScopeTimer.h @@ -0,0 +1,56 @@ +// +// _ASScopeTimer.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 +// + +#pragma once + +/** + Must compile as c++ for this to work. + + Usage: + // Can be an ivar or local variable + NSTimeInterval placeToStoreTiming; + + { + // some scope + AS::ScopeTimer t(placeToStoreTiming); + DoPotentiallySlowWork(); + MorePotentiallySlowWork(); + } + + */ + +namespace AS { + struct ScopeTimer { + NSTimeInterval begin; + NSTimeInterval &outT; + ScopeTimer(NSTimeInterval &outRef) : outT(outRef) { + begin = CACurrentMediaTime(); + } + ~ScopeTimer() { + outT = CACurrentMediaTime() - begin; + } + }; + + // variant where repeated calls are summed + struct SumScopeTimer { + NSTimeInterval begin; + NSTimeInterval &outT; + BOOL enable; + SumScopeTimer(NSTimeInterval &outRef, BOOL enable = YES) : outT(outRef), enable(enable) { + if (enable) { + begin = CACurrentMediaTime(); + } + } + ~SumScopeTimer() { + if (enable) { + outT += CACurrentMediaTime() - begin; + } + } + }; +} diff --git a/submodules/AsyncDisplayKit/Source/_ASTransitionContext.mm b/submodules/AsyncDisplayKit/Source/_ASTransitionContext.mm new file mode 100644 index 0000000000..40a3573c15 --- /dev/null +++ b/submodules/AsyncDisplayKit/Source/_ASTransitionContext.mm @@ -0,0 +1,104 @@ +// +// _ASTransitionContext.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 + + +NSString * const ASTransitionContextFromLayoutKey = @"org.asyncdisplaykit.ASTransitionContextFromLayoutKey"; +NSString * const ASTransitionContextToLayoutKey = @"org.asyncdisplaykit.ASTransitionContextToLayoutKey"; + +@interface _ASTransitionContext () + +@property (weak, nonatomic) id<_ASTransitionContextLayoutDelegate> layoutDelegate; +@property (weak, nonatomic) id<_ASTransitionContextCompletionDelegate> completionDelegate; + +@end + +@implementation _ASTransitionContext + +- (instancetype)initWithAnimation:(BOOL)animated + layoutDelegate:(id<_ASTransitionContextLayoutDelegate>)layoutDelegate + completionDelegate:(id<_ASTransitionContextCompletionDelegate>)completionDelegate +{ + self = [super init]; + if (self) { + _animated = animated; + _layoutDelegate = layoutDelegate; + _completionDelegate = completionDelegate; + } + return self; +} + +#pragma mark - ASContextTransitioning Protocol Implementation + +- (ASLayout *)layoutForKey:(NSString *)key +{ + return [_layoutDelegate transitionContext:self layoutForKey:key]; +} + +- (ASSizeRange)constrainedSizeForKey:(NSString *)key +{ + return [_layoutDelegate transitionContext:self constrainedSizeForKey:key]; +} + +- (CGRect)initialFrameForNode:(ASDisplayNode *)node +{ + return [[self layoutForKey:ASTransitionContextFromLayoutKey] frameForElement:node]; +} + +- (CGRect)finalFrameForNode:(ASDisplayNode *)node +{ + return [[self layoutForKey:ASTransitionContextToLayoutKey] frameForElement:node]; +} + +- (NSArray *)subnodesForKey:(NSString *)key +{ + NSMutableArray *subnodes = [[NSMutableArray alloc] init]; + for (ASLayout *sublayout in [self layoutForKey:key].sublayouts) { + [subnodes addObject:(ASDisplayNode *)sublayout.layoutElement]; + } + return subnodes; +} + +- (NSArray *)insertedSubnodes +{ + return [_layoutDelegate insertedSubnodesWithTransitionContext:self]; +} + +- (NSArray *)removedSubnodes +{ + return [_layoutDelegate removedSubnodesWithTransitionContext:self]; +} + +- (void)completeTransition:(BOOL)didComplete +{ + [_completionDelegate transitionContext:self didComplete:didComplete]; +} + +@end + + +@interface _ASAnimatedTransitionContext () +@property (nonatomic) ASDisplayNode *node; +@property (nonatomic) CGFloat alpha; +@end + +@implementation _ASAnimatedTransitionContext + ++ (instancetype)contextForNode:(ASDisplayNode *)node alpha:(CGFloat)alpha NS_RETURNS_RETAINED +{ + _ASAnimatedTransitionContext *context = [[_ASAnimatedTransitionContext alloc] init]; + context.node = node; + context.alpha = alpha; + return context; +} + +@end diff --git a/submodules/Display/Source/Nodes/ButtonNode.swift b/submodules/Display/Source/Nodes/ButtonNode.swift index a7d6c8035c..290b4c8671 100644 --- a/submodules/Display/Source/Nodes/ButtonNode.swift +++ b/submodules/Display/Source/Nodes/ButtonNode.swift @@ -281,5 +281,6 @@ open class ASButtonNode: ASControlNode { } func f2() { + } }