Merge commit '7621e2f8dec938cf48181c8b10afc9b01f444e68' into beta

This commit is contained in:
Ilya Laktyushin
2025-12-06 02:17:48 +04:00
commit 8344b97e03
28070 changed files with 7995182 additions and 0 deletions
+25
View File
@@ -0,0 +1,25 @@
fastlane/README.md
fastlane/report.xml
fastlane/test_output/*
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.xcscmblueprint
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
.DS_Store
*.dSYM
*.dSYM.zip
*.ipa
*/xcuserdata/*
AsyncDisplayKit.xcodeproj/*
+42
View File
@@ -0,0 +1,42 @@
public_headers = glob([
"Source/PublicHeaders/AsyncDisplayKit/*.h",
])
private_headers = glob([
"Source/*.h",
], allow_empty=True)
objc_library(
name = "AsyncDisplayKit",
enable_modules = True,
module_name = "AsyncDisplayKit",
srcs = glob([
"Source/**/*.m",
"Source/**/*.mm",
], allow_empty=True) + private_headers,
copts = [
"-Werror",
],
cxxopts = [
"-Werror",
"-std=c++17",
],
hdrs = public_headers,
defines = [
"MINIMAL_ASDK",
],
includes = [
"Source/PublicHeaders",
],
sdk_frameworks = [
"Foundation",
"UIKit",
"QuartzCore",
"CoreMedia",
"CoreText",
"CoreGraphics",
],
visibility = [
"//visibility:public",
],
)
+181
View File
@@ -0,0 +1,181 @@
The Texture project was created by Pinterest as a continuation, under a different
name and license, of the AsyncDisplayKit codebase originally developed by Facebook.
All code in Texture is covered by the Apache License, Version 2.0.
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
@@ -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 <vector>
@interface ASAbstractLayoutController (FrameworkPrivate)
+ (std::vector<std::vector<ASRangeTuningParameters>>)defaultTuningParameters;
@end
@@ -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 <AsyncDisplayKit/ASAsciiArtBoxCreator.h>
#import <CoreGraphics/CoreGraphics.h>
#import <cmath>
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
@@ -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 <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASAvailability.h>
#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 <dispatch/once.h>
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
@@ -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 <AsyncDisplayKit/ASCGImageBuffer.h>
#import <sys/mman.h>
#import <mach/mach_init.h>
#import <mach/vm_map.h>
#import <mach/vm_statistics.h>
/**
* 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 = false;//(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 = malloc(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
@@ -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 <AsyncDisplayKit/ASCollections.h>
/**
* 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
@@ -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 <AsyncDisplayKit/ASConfiguration.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
/// 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
@@ -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 <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASConfiguration.h>
#import <AsyncDisplayKit/ASConfigurationDelegate.h>
#import <stdatomic.h>
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<ASConfigurationDelegate> 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];
}
@@ -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 <AsyncDisplayKit/ASControlNode.h>
@interface ASControlNode (Private)
#if TARGET_OS_TV
- (void)_pressDown;
#endif
@end
@@ -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 <AsyncDisplayKit/ASControlNode.h>
#import "ASControlNode+Private.h"
#import <AsyncDisplayKit/ASControlNode+Subclasses.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASControlTargetAction.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASThread.h>
// 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<id<NSCopying>, NSMutableArray<ASControlTargetAction *> *> *_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<NSCopying> _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<NSCopying> 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<NSCopying> eventKey = _ASControlNodeEventKeyForControlEvent(controlEvent);
NSMutableArray *eventTargetActionArray = _controlEventDispatchTable[eventKey];
if (!eventTargetActionArray) {
return;
}
NSPredicate *filterPredicate = [NSPredicate predicateWithBlock:^BOOL(ASControlTargetAction *_Nullable evaluatedObject, NSDictionary<NSString *,id> * _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<ASControlTargetAction *> 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<NSCopying> _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
@@ -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 <AsyncDisplayKit/ASControlTargetAction.h>
@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
@@ -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 <AsyncDisplayKit/ASDimension.h>
#import <AsyncDisplayKit/CoreGraphics+ASConvenience.h>
#import <AsyncDisplayKit/ASAssert.h>
#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
@@ -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 <AsyncDisplayKit/ASDimensionInternal.h>
#pragma mark - ASLayoutElementSize
NSString *NSStringFromASLayoutElementSize(ASLayoutElementSize size)
{
return [NSString stringWithFormat:
@"<ASLayoutElementSize: exact=%@, min=%@, max=%@>",
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};
}
@@ -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 <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
/**
* 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));
@@ -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 <AsyncDisplayKit/ASConfigurationInternal.h>
// Prefer C atomics in this file because ObjC blocks can't capture C++ atomics well.
#import <stdatomic.h>
/**
* 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);
}
});
}
};
@@ -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 <AsyncDisplayKit/ASDisplayNode+Ancestry.h>
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
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<NSFastEnumeration>)supernodes
{
NSEnumerator *result = [[ASNodeAncestryEnumerator alloc] initWithNode:self];
[result nextObject]; // discard first object (self)
return result;
}
- (id<NSFastEnumeration>)supernodesIncludingSelf
{
return [[ASNodeAncestryEnumerator alloc] initWithNode:self];
}
- (nullable __kindof ASDisplayNode *)supernodeOfClass:(Class)supernodeClass includingSelf:(BOOL)includeSelf
{
id<NSFastEnumeration> 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
@@ -0,0 +1,443 @@
//
// 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 <AsyncDisplayKit/_ASCoreAnimationExtras.h>
#import <AsyncDisplayKit/_ASAsyncTransaction.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASAssert.h>
#import "ASDisplayNodeInternal.h"
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASGraphicsContext.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import "ASSignpost.h"
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
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();
UIImage *image = ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, ^{
for (dispatch_block_t block in displayBlocks) {
if (isCancelledBlock()) return;
block();
}
});
ASDN_DELAY_FOR_DISPLAY();
return image;
};
} else {
displayBlock = ^id{
CHECK_CANCELLED_AND_RETURN_NIL();
__block UIImage *image = nil;
void (^workWithContext)() = ^{
CGContextRef currentContext = UIGraphicsGetCurrentContext();
if (shouldCreateGraphicsContext && !currentContext) {
ASDisplayNodeAssert(NO, @"Failed to create a CGContext (size: %@)", NSStringFromCGSize(bounds.size));
return;
}
// 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];
ASDN_DELAY_FOR_DISPLAY();
};
if (shouldCreateGraphicsContext) {
return ASGraphicsCreateImage(self.primitiveTraitCollection, bounds.size, opaque, contentsScaleForDisplay, nil, isCancelledBlock, workWithContext);
} else {
workWithContext();
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();
ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext;
__instanceLock__.unlock();
if (context != NULL) {
if (didDisplayNodeContentWithRenderingContext) {
didDisplayNodeContentWithRenderingContext(context, drawParameters);
}
}
}
- (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<NSObject> 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
@@ -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 <AsyncDisplayKit/ASDisplayNode+Convenience.h>
#import <UIKit/UIViewController.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#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
@@ -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 <AsyncDisplayKit/ASDisplayNode.h>
@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
File diff suppressed because it is too large Load Diff
@@ -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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import "_ASScopeTimer.h"
#import "ASDisplayNodeInternal.h"
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASLayout.h>
#import "ASLayoutSpec+Subclasses.h"
#import "ASLayoutSpecPrivate.h"
#import <AsyncDisplayKit/ASThread.h>
@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<ASLayoutElement> 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<ASLayoutElement>)_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
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -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 <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import "ASDisplayNodeInternal.h"
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Ancestry.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <queue>
#import <AsyncDisplayKit/ASRunLoopQueue.h>
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<ASDisplayNode *> 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<ASDisplayNode *> *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<ASDisplayNode *> *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];
}
@@ -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 <atomic>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASLayoutElement.h>
#import "ASLayoutTransition.h"
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/_ASTransitionContext.h>
#import <AsyncDisplayKit/ASWeakSet.h>
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<ASDisplayNodeAtomicFlags> _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<ASDisplayNode *> *_subnodes;
ASNodeController *_strongNodeController;
__weak ASNodeController *_weakNodeController;
// Set this to nil whenever you modify _subnodes
NSArray<ASDisplayNode *> *_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<ASPrimitiveTraitCollection> _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 <ASLayoutElement>, with a few areas requiring updates.
NSMutableArray<ASDisplayNode *> *_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<int32_t> _transitionID;
std::atomic<int32_t> _pendingTransitionID;
ASLayoutTransition *_pendingLayoutTransition;
ASDisplayNodeLayout _calculatedDisplayNodeLayout;
ASDisplayNodeLayout _pendingDisplayNodeLayout;
/// Sentinel for layout data. Incremented when we get -setNeedsLayout / -invalidateCalculatedLayout.
/// Starts at 1.
std::atomic<NSUInteger> _layoutVersion;
// Layout Spec performance measurement
ASDisplayNodePerformanceMeasurementOptions _measurementOptions;
NSTimeInterval _layoutSpecTotalTime;
NSInteger _layoutSpecNumberOfPasses;
NSTimeInterval _layoutComputationTotalTime;
NSInteger _layoutComputationNumberOfPasses;
// View Loading
ASDisplayNodeViewBlock _viewBlock;
ASDisplayNodeLayerBlock _layerBlock;
NSMutableArray<ASDisplayNodeDidLoadBlock> *_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<ASInterfaceStateDelegate> _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<ASInterfaceStateDelegate> 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<ASLayoutElement>)_locked_layoutElementThatFits:(ASSizeRange)constrainedSize;
@end
NS_ASSUME_NONNULL_END
@@ -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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDimension.h>
@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);
}
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
//
// ASExperimentalFeatures.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASExperimentalFeatures.h>
#import <AsyncDisplayKit/ASCollections.h>
NSArray<NSString *> *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;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic push
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180500
#pragma clang diagnostic ignored "-Wvla-cxx-extension"
#endif
#endif
return ASArrayByFlatMapping(allNames, NSString *name, ({
(flags & (1 << i++)) ? name : nil;
}));
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic pop
#endif
}
// O(N^2) but with counts this small, it's probably faster
// than hashing the strings.
ASExperimentalFeatures ASExperimentalFeaturesFromArray(NSArray<NSString *> *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;
}
@@ -0,0 +1,130 @@
//
// ASGraphicsContext.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASGraphicsContext.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASAvailability.h>
#if AS_AT_LEAST_IOS13
#define ASPerformBlockWithTraitCollection(work, traitCollection) \
if (@available(iOS 13.0, tvOS 13.0, *)) { \
UITraitCollection *uiTraitCollection = ASPrimitiveTraitCollectionToUITraitCollection(traitCollection); \
[uiTraitCollection performAsCurrentTraitCollection:^{ \
work(); \
}];\
} else { \
work(); \
}
#else
#define ASPerformBlockWithTraitCollection(work, traitCollection) work();
#endif
NS_AVAILABLE_IOS(10)
NS_INLINE void ASConfigureExtendedRange(UIGraphicsImageRendererFormat *format)
{
}
UIImage *ASGraphicsCreateImageWithOptions(CGSize size, BOOL opaque, CGFloat scale, UIImage *sourceImage,
asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled,
void (^NS_NOESCAPE work)())
{
return ASGraphicsCreateImage(ASPrimitiveTraitCollectionMakeDefault(), size, opaque, scale, sourceImage, isCancelled, work);
}
UIImage *ASGraphicsCreateImage(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * sourceImage, asdisplaynode_iscancelled_block_t NS_NOESCAPE isCancelled, void (NS_NOESCAPE ^work)()) {
if (@available(iOS 10.0, *)) {
if (true /*ASActivateExperimentalFeature(ASExperimentalDrawingGlobal)*/) {
// If they used default scale, reuse one of two preferred formats.
static UIGraphicsImageRendererFormat *defaultFormat;
static UIGraphicsImageRendererFormat *opaqueFormat;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
defaultFormat = [UIGraphicsImageRendererFormat preferredFormat];
opaqueFormat = [UIGraphicsImageRendererFormat preferredFormat];
} else {
defaultFormat = [UIGraphicsImageRendererFormat defaultFormat];
opaqueFormat = [UIGraphicsImageRendererFormat defaultFormat];
}
opaqueFormat.opaque = YES;
ASConfigureExtendedRange(defaultFormat);
ASConfigureExtendedRange(opaqueFormat);
});
UIGraphicsImageRendererFormat *format;
if (sourceImage) {
if (sourceImage.renderingMode == UIImageRenderingModeAlwaysTemplate) {
// Template images will be black and transparent, so if we use
// sourceImage.imageRenderFormat it will assume a grayscale color space.
// This is not good because a template image should be able to tint to any color,
// so we'll just use the default here.
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
format = [UIGraphicsImageRendererFormat preferredFormat];
} else {
format = [UIGraphicsImageRendererFormat defaultFormat];
}
} else {
format = sourceImage.imageRendererFormat;
}
// We only want the private bits (color space and bits per component) from the image.
// We have our own ideas about opacity and scale.
format.opaque = opaque;
format.scale = scale;
} else if (scale == 0 || scale == ASScreenScale()) {
format = opaque ? opaqueFormat : defaultFormat;
} else {
if (AS_AVAILABLE_IOS_TVOS(11, 11)) {
format = [UIGraphicsImageRendererFormat preferredFormat];
} else {
format = [UIGraphicsImageRendererFormat defaultFormat];
}
if (opaque) format.opaque = YES;
format.scale = scale;
ASConfigureExtendedRange(format);
}
// Avoid using the imageWithActions: method because it does not support cancellation at the
// last moment i.e. before actually creating the resulting image.
__block UIImage *image;
NSError *error;
[[[UIGraphicsImageRenderer alloc] initWithSize:size format:format]
runDrawingActions:^(UIGraphicsImageRendererContext *rendererContext) {
ASDisplayNodeCAssert(UIGraphicsGetCurrentContext(), @"Should have a context!");
ASPerformBlockWithTraitCollection(work, traitCollection);
}
completionActions:^(UIGraphicsImageRendererContext *rendererContext) {
if (isCancelled == nil || !isCancelled()) {
image = rendererContext.currentImage;
}
}
error:&error];
if (error) {
NSCAssert(NO, @"Error drawing: %@", error);
}
return image;
}
}
// Bad OS or experiment flag. Use UIGraphics* API.
UIGraphicsBeginImageContextWithOptions(size, opaque, scale);
ASPerformBlockWithTraitCollection(work, traitCollection)
UIImage *image = nil;
if (isCancelled == nil || !isCancelled()) {
image = UIGraphicsGetImageFromCurrentImageContext();
}
UIGraphicsEndImageContext();
return image;
}
UIImage *ASGraphicsCreateImageWithTraitCollectionAndOptions(ASPrimitiveTraitCollection traitCollection, CGSize size, BOOL opaque, CGFloat scale, UIImage * sourceImage, void (NS_NOESCAPE ^work)()) {
return ASGraphicsCreateImage(traitCollection, size, opaque, scale, sourceImage, nil, work);
}
@@ -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 <AsyncDisplayKit/ASHashing.h>
#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
@@ -0,0 +1,242 @@
//
// 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 <AsyncDisplayKit/ASInternalHelpers.h>
#import <UIKit/UIKit.h>
#import <objc/runtime.h>
#import <cmath>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASRunLoopQueue.h>
#import <AsyncDisplayKit/ASThread.h>
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;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic push
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180500
#pragma clang diagnostic ignored "-Wvla-cxx-extension"
#endif
#endif
char className[resultLength + 1];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic pop
#endif
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
@@ -0,0 +1,387 @@
//
// 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 <AsyncDisplayKit/ASLayout.h>
#import <atomic>
#import <queue>
#import <AsyncDisplayKit/ASCollections.h>
#import <AsyncDisplayKit/ASDimension.h>
#import "ASLayoutSpecUtilities.h"
#import "ASLayoutSpec+Subclasses.h"
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
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 () <ASDescriptionProvider>
{
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<ASLayoutElement>)layoutElement
size:(CGSize)size
position:(CGPoint)position
sublayouts:(nullable NSArray<ASLayout *> *)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<ASLayoutElement>)layoutElement
size:(CGSize)size
position:(CGPoint)position
sublayouts:(nullable NSArray<ASLayout *> *)sublayouts NS_RETURNS_RETAINED
{
return [[self alloc] initWithLayoutElement:layoutElement
size:size
position:position
sublayouts:sublayouts];
}
+ (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)layoutElement
size:(CGSize)size
sublayouts:(nullable NSArray<ASLayout *> *)sublayouts NS_RETURNS_RETAINED
{
return [self layoutWithLayoutElement:layoutElement
size:size
position:ASPointNull
sublayouts:sublayouts];
}
+ (instancetype)layoutWithLayoutElement:(id<ASLayoutElement>)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<Context> queue;
for (ASLayout *sublayout in _sublayouts) {
queue.push_back({sublayout, sublayout.position});
}
std::vector<ASLayout *> 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.
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic push
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180500
#pragma clang diagnostic ignored "-Wvla-cxx-extension"
#endif
#endif
unowned ASLayout *rawSublayouts[sublayoutsCount];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic pop
#endif
[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<ASLayoutElement>)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 <NSDictionary *> *)propertiesForDescription
{
NSMutableArray *result = [NSMutableArray array];
[result addObject:@{ @"size" : [NSValue valueWithCGSize:self.size] }];
if (id<ASLayoutElement> 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<ASLayoutElement> layoutElement, const ASSizeRange sizeRange, const CGSize parentSize)
{
NSCParameterAssert(layoutElement != nil);
return [layoutElement layoutThatFits:sizeRange parentSize:parentSize];
}
ASLayout *ASCalculateRootLayout(id<ASLayoutElement> rootLayoutElement, const ASSizeRange sizeRange)
{
ASLayout *layout = ASCalculateLayout(rootLayoutElement, sizeRange, sizeRange.max);
// Here could specific verfication happen
return layout;
}
@@ -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 <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/ASLayoutElement.h>
#import <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <atomic>
#include <pthread.h>
using AS::MutexLocker;
#if YOGA
#import YOGA_HEADER_PATH
#import <AsyncDisplayKit/ASYogaUtilities.h>
#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<ASLayoutElementSize> _size;
std::atomic<CGFloat> _spacingBefore;
std::atomic<CGFloat> _spacingAfter;
std::atomic<CGFloat> _flexGrow;
std::atomic<CGFloat> _flexShrink;
std::atomic<ASDimension> _flexBasis;
std::atomic<ASStackLayoutAlignSelf> _alignSelf;
std::atomic<CGFloat> _ascender;
std::atomic<CGFloat> _descender;
std::atomic<CGPoint> _layoutPosition;
#if YOGA
YGNodeRef _yogaNode;
std::atomic<YGWrap> _flexWrap;
std::atomic<ASStackLayoutDirection> _flexDirection;
std::atomic<YGDirection> _direction;
std::atomic<ASStackLayoutJustifyContent> _justifyContent;
std::atomic<ASStackLayoutAlignItems> _alignItems;
std::atomic<YGPositionType> _positionType;
std::atomic<ASEdgeInsets> _position;
std::atomic<ASEdgeInsets> _margin;
std::atomic<ASEdgeInsets> _padding;
std::atomic<ASEdgeInsets> _border;
std::atomic<CGFloat> _aspectRatio;
ASStackLayoutAlignItems _parentAlignStyle;
#endif
}
@dynamic width, height, minWidth, maxWidth, minHeight, maxHeight;
@dynamic preferredSize, minSize, maxSize, preferredLayoutSize, minLayoutSize, maxLayoutSize;
#pragma mark - Lifecycle
- (instancetype)initWithDelegate:(id<ASLayoutElementStyleDelegate>)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<NSDictionary *> *)propertiesForDescription
{
NSMutableArray<NSDictionary *> *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
@@ -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 <AsyncDisplayKit/ASLayoutElement.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
@interface ASLayoutElementStyle () <ASDescriptionProvider>
/**
* @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<ASLayoutElementStyleDelegate> delegate;
/**
* @abstract A size constraint that should apply to this ASLayoutElement.
*/
@property (nonatomic, readonly) ASLayoutElementSize size;
@property (nonatomic, assign) ASStackLayoutAlignItems parentAlignStyle;
@end
@@ -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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
AS_SUBCLASSING_RESTRICTED
@interface ASLayoutManager : NSLayoutManager
@end
@@ -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
@@ -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 <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASLayoutSpec.h>
#import <AsyncDisplayKit/ASLayout.h>
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<ASLayoutElement>)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<ASLayoutElement>)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
@@ -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 <AsyncDisplayKit/ASLayoutSpec.h>
#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<ASLayoutElement>)child atIndex:(NSUInteger)index
{
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
id<ASLayoutElement> 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<ASLayoutElement>)childAtIndex:(NSUInteger)index
{
id<ASLayoutElement> 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
@@ -0,0 +1,356 @@
//
// 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 <AsyncDisplayKit/ASLayoutSpec.h>
#import "ASLayoutSpecPrivate.h"
#import "ASLayoutSpec+Subclasses.h"
#import <AsyncDisplayKit/ASCollections.h>
#import "ASLayoutElementStylePrivate.h"
#import <AsyncDisplayKit/ASTraitCollection.h>
#import <AsyncDisplayKit/ASEqualityHelpers.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <objc/runtime.h>
#import <map>
#import <vector>
@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<ASLayoutElement>)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<ASLayoutElement>)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<id<ASLayoutElement>> *)children
{
ASDisplayNodeAssert(self.isMutable, @"Cannot set properties when layout spec is not mutable");
#if ASDISPLAYNODE_ASSERTIONS_ENABLED
for (id<ASLayoutElement> child in children) {
ASDisplayNodeAssert([child conformsToProtocol:NSProtocolFromString(@"ASLayoutElement")], @"Child %@ of spec %@ is not an ASLayoutElement!", child, self);
}
#endif
[_childrenArray setArray:children];
}
- (nullable NSArray<id<ASLayoutElement>> *)children
{
return [_childrenArray copy];
}
- (NSArray<id<ASLayoutElement>> *)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<NSDictionary *> *)propertiesForDescription
{
const auto result = [NSMutableArray<NSDictionary *> array];
if (NSArray *children = self.children) {
// Use tiny descriptions because these trees can get nested very deep.
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic push
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180500
#pragma clang diagnostic ignored "-Wvla-cxx-extension"
#endif
#endif
const auto tinyDescriptions = ASArrayByFlatMapping(children, id object, ASObjectDescriptionMakeTiny(object));
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic pop
#endif
[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<id<ASLayoutElement>> *)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<id<ASLayoutElement>> *)workingSet workingCount:(NSUInteger *)workingCount result:(NSHashTable<id<ASLayoutElement>> * _Nullable *)result
{
Class layoutSpecClass = [ASLayoutSpec class];
for (id<ASLayoutElement> 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<ASLayoutElement>)layoutElement NS_RETURNS_RETAINED
{
return [[self alloc] initWithLayoutElement:layoutElement];
}
- (instancetype)initWithLayoutElement:(id<ASLayoutElement>)layoutElement
{
self = [super init];
if (self) {
self.child = layoutElement;
}
return self;
}
+ (instancetype)wrapperWithLayoutElements:(NSArray<id<ASLayoutElement>> *)layoutElements NS_RETURNS_RETAINED
{
return [[self alloc] initWithLayoutElements:layoutElements];
}
- (instancetype)initWithLayoutElements:(NSArray<id<ASLayoutElement>> *)layoutElements
{
self = [super init];
if (self) {
self.children = layoutElements;
}
return self;
}
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
{
NSArray *children = self.children;
const auto count = children.count;
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic push
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180500
#pragma clang diagnostic ignored "-Wvla-cxx-extension"
#endif
#endif
ASLayout *rawSublayouts[count];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic pop
#endif
int i = 0;
CGSize size = constrainedSize.min;
for (id<ASLayoutElement> 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<ASLayout *> 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<ASLayoutElementAsciiArtProtocol> 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
@@ -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 <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASThread.h>
#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 <ASPrimitiveTraitCollection> _primitiveTraitCollection;
ASLayoutElementStyle *_style;
NSMutableArray *_childrenArray;
}
#if AS_DEDUPE_LAYOUT_SPEC_TREE
/**
* Recursively search the subtree for elements that occur more than once.
*/
- (nullable NSHashTable<id<ASLayoutElement>> *)findDuplicatedElementsInSubtree;
#endif
@end
NS_ASSUME_NONNULL_END
@@ -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 <CoreGraphics/CoreGraphics.h>
#import <algorithm>
#import <functional>
#import <type_traits>
#import <vector>
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 <typename T, typename Func>
auto map(const T &iterable, Func &&func) -> std::vector<decltype(func(std::declval<typename T::value_type>()))>
{
// Some convenience type definitions
typedef decltype(func(std::declval<typename T::value_type>())) value_type;
typedef std::vector<value_type> 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>(func)
);
return res;
}
template<typename Func>
auto map(id<NSFastEnumeration> collection, Func &&func) -> std::vector<decltype(func(std::declval<id>()))>
{
std::vector<decltype(func(std::declval<id>()))> to;
for (id obj in collection) {
to.push_back(func(obj));
}
return to;
}
template <typename T, typename Func>
auto filter(const T &iterable, Func &&func) -> std::vector<typename T::value_type>
{
std::vector<typename T::value_type> 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 };
}
@@ -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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDimension.h>
#import <AsyncDisplayKit/_ASTransitionContext.h>
#import "ASDisplayNodeLayout.h"
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASLayoutSpec.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark - ASLayoutElementTransition
/**
* Objects conform to this project returns if it's possible to layout asynchronous
*/
@protocol ASLayoutElementTransition <NSObject>
/**
* @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 () <ASLayoutElementTransition>
@end
@interface ASLayoutSpec () <ASLayoutElementTransition>
@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
@@ -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 <AsyncDisplayKit/NSArray+Diffing.h>
#import <AsyncDisplayKit/ASLayout.h>
#import "ASDisplayNodeInternal.h" // Required for _insertSubnode... / _removeFromSupernode.
#import <queue>
#if AS_IG_LIST_KIT
#import <IGListKit/IGListKit.h>
#import <AsyncDisplayKit/ASLayout+IGListKit.h>
#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<ASLayout *> 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<ASLayoutElementTransition>)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<AS::RecursiveMutex> __instanceLock__;
BOOL _calculatedSubnodeOperations;
NSArray<ASDisplayNode *> *_insertedSubnodes;
NSArray<ASDisplayNode *> *_removedSubnodes;
std::vector<NSUInteger> _insertedSubnodePositions;
std::vector<std::pair<ASDisplayNode *, NSUInteger>> _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<AS::RecursiveMutex>();
_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<id<ASLayoutElement>, NSUInteger> a,
std::pair<ASDisplayNode *, NSUInteger> b) {
return a.second < b.second;
});
#else
NSIndexSet *insertions, *deletions;
NSArray<NSIndexPath *> *moves;
NSArray<ASDisplayNode *> *previousNodes = [previousLayout.sublayouts valueForKey:@"layoutElement"];
NSArray<ASDisplayNode *> *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<ASDisplayNode *> *)currentSubnodesWithTransitionContext:(_ASTransitionContext *)context
{
MutexLocker l(*__instanceLock__);
return _node.subnodes;
}
- (NSArray<ASDisplayNode *> *)insertedSubnodesWithTransitionContext:(_ASTransitionContext *)context
{
MutexLocker l(*__instanceLock__);
[self calculateSubnodeOperationsIfNeeded];
return _insertedSubnodes;
}
- (NSArray<ASDisplayNode *> *)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<NSUInteger> findNodesInLayoutAtIndexes(ASLayout *layout,
NSIndexSet *indexes,
NSArray<ASDisplayNode *> * __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<NSUInteger> findNodesInLayoutAtIndexesWithFilteredNodes(ASLayout *layout,
NSIndexSet *indexes,
NSArray<ASDisplayNode *> *filteredNodes,
NSArray<ASDisplayNode *> * __strong *storedNodes)
{
NSMutableArray<ASDisplayNode *> *nodes = [NSMutableArray arrayWithCapacity:indexes.count];
std::vector<NSUInteger> positions = std::vector<NSUInteger>();
// 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
@@ -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 <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
AS_SUBCLASSING_RESTRICTED
@interface ASMainSerialQueue : NSObject
@property (nonatomic, readonly) NSUInteger numberOfScheduledBlocks;
- (void)performBlockOnMainThread:(dispatch_block_t)block;
@end
@@ -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 <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
@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
@@ -0,0 +1,209 @@
//
// ASMainThreadDeallocation.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASMainThreadDeallocation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASInternalHelpers.h>
#import <AsyncDisplayKit/ASThread.h>
#import <objc/runtime.h>
#import <UIKit/UIKit.h>
@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);
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic push
#if defined(__IPHONE_OS_VERSION_MAX_ALLOWED) && __IPHONE_OS_VERSION_MAX_ALLOWED >= 180500
#pragma clang diagnostic ignored "-Wvla-cxx-extension"
#endif
#endif
Ivar ivars[count];
#if __IPHONE_OS_VERSION_MAX_ALLOWED >= 180400
#pragma clang diagnostic pop
#endif
[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<Class, NSValue *> *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
@@ -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 <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <UIKit/UIGeometry.h>
#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. "[ <MYObject: 0x00000000> <MYObject: 0xFFFFFFFF> ]"
return [NSString stringWithFormat:@"[ %@ ]", [object componentsJoinedByString:@" "]];
}
return [object description];
}
NSString *_ASObjectDescriptionMakePropertyList(NSArray<NSDictionary *> * _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<NSDictionary *> * _Nullable propertyGroups)
{
return [NSString stringWithFormat:@"{ %@ }", _ASObjectDescriptionMakePropertyList(propertyGroups)];
}
NSString *ASObjectDescriptionMake(__autoreleasing id object, NSArray<NSDictionary *> *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;
}
}
@@ -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 <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
@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
@@ -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 <AsyncDisplayKit/ASThread.h>
#import <AsyncDisplayKit/ASWeakSet.h>
#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<ASDisplayNode *> *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
@@ -0,0 +1,114 @@
//
// ASRecursiveUnfairLock.mm
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASRecursiveUnfairLock.h>
#import <stdatomic.h>
/**
* 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.
#if AS_USE_OS_LOCK
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);
}
#else
const pthread_t s = pthread_self();
if (OSSpinLockTry(&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.
OSSpinLockLock(&l->_lock);
rul_set_thread(l, s);
}
#endif
l->_count++;
}
BOOL ASRecursiveUnfairLockTryLock(ASRecursiveUnfairLock *l)
{
// Same as Lock above. See comments there.
#if AS_USE_OS_LOCK
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;
}
#else
const pthread_t s = pthread_self();
if (OSSpinLockTry(&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;
}
#endif
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);
#if AS_USE_OS_LOCK
os_unfair_lock_unlock(&l->_lock);
#else
OSSpinLockUnlock(&l->_lock);
#endif
}
}
@@ -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 <UIKit/UIResponder.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
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
@@ -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 <AsyncDisplayKit/ASAssert.h>
@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
@@ -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 <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASConfigurationInternal.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASRunLoopQueue.h>
#import <AsyncDisplayKit/ASThread.h>
#import "ASSignpost.h"
#import <QuartzCore/QuartzCore.h>
#import <cstdlib>
#import <deque>
#import <vector>
#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<CFTypeRef> _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<id> 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<id<ASCATransactionQueueObserving>> _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<id<ASCATransactionQueueObserving>> _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<ASCATransactionQueueObserving> &value : _batchBuffer) {
[value prepareForCATransactionCommit];
}
_batchBuffer.clear();
ASSignpostEnd(ASSignpostRunLoopQueueBatch);
}
- (void)enqueue:(id<ASCATransactionQueueObserving>)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
@@ -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 <AsyncDisplayKit/ASScrollDirection.h>
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;
}
@@ -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 <AsyncDisplayKit/ASScrollNode.h>
#import <AsyncDisplayKit/ASDisplayNodeExtras.h>
#import <AsyncDisplayKit/ASDisplayNode+FrameworkPrivate.h>
#import <AsyncDisplayKit/ASDisplayNode+Beta.h>
#import <AsyncDisplayKit/ASDisplayNode+Subclasses.h>
#import <AsyncDisplayKit/ASLayout.h>
#import <AsyncDisplayKit/_ASDisplayLayer.h>
#import <AsyncDisplayKit/ASThread.h>
@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
@@ -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(<sys/kdebug_signpost.h>)
#define AS_KDEBUG_ENABLE 1
#else
#define AS_KDEBUG_ENABLE 0
#endif
#if AS_KDEBUG_ENABLE
#import <sys/kdebug_signpost.h>
// 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
@@ -0,0 +1,268 @@
//
// 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 <AsyncDisplayKit/ASTextKitComponents.h>
#import "ASTextKitContext.h"
#import <AsyncDisplayKit/ASAssert.h>
#import <AsyncDisplayKit/ASMainThreadDeallocation.h>
#import <tgmath.h>
@implementation ASCustomTextContainer
- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage {
self = [super initWithSize:size];
if (self != nil) {
}
return self;
}
- (BOOL)isSimpleRectangularTextContainer {
return false;
}
- (CGRect)lineFragmentRectForProposedRect:(CGRect)proposedRect atIndex:(NSUInteger)characterIndex writingDirection:(NSWritingDirection)baseWritingDirection remainingRect:(nullable CGRect *)remainingRect {
CGRect result = [super lineFragmentRectForProposedRect:proposedRect atIndex:characterIndex writingDirection:baseWritingDirection remainingRect:remainingRect];
NSTextStorage *textStorage = self.layoutManager.textStorage;
if (textStorage != nil) {
NSString *string = textStorage.string;
int index = (int)characterIndex;
if (index >= 0 && index < string.length) {
NSDictionary *attributes = [textStorage attributesAtIndex:index effectiveRange:nil];
NSObject *blockQuote = attributes[@"Attribute__Blockquote"];
if (blockQuote != nil) {
bool isFirstLine = false;
if (index == 0) {
isFirstLine = true;
} else {
NSDictionary *previousAttributes = [textStorage attributesAtIndex:index - 1 effectiveRange:nil];
NSObject *previousBlockQuote = previousAttributes[@"Attribute__Blockquote"];
if (previousBlockQuote == nil) {
isFirstLine = true;
} else if (![blockQuote isEqual:previousBlockQuote]) {
isFirstLine = true;
}
}
if (isFirstLine) {
result.size.width -= 100.0f;
}
}
}
}
return result;
}
@end
@interface ASCustomLayoutManager : NSLayoutManager
@end
@implementation ASCustomLayoutManager
- (void)showCGGlyphs:(const CGGlyph *)glyphs positions:(const CGPoint *)positions count:(NSUInteger)glyphCount font:(UIFont *)font matrix:(CGAffineTransform)textMatrix attributes:(NSDictionary<NSAttributedStringKey,id> *)attributes inContext:(CGContextRef)graphicsContext {
for (NSUInteger i = 0; i < glyphCount; i++) {
if (attributes[@"Attribute__CustomEmoji"] != nil) {
continue;
}
[super showCGGlyphs:&glyphs[i] positions:&positions[i] count:1 font:font matrix:textMatrix attributes:attributes inContext:graphicsContext];
}
}
- (void)drawGlyphsForGlyphRange:(NSRange)glyphsToShow atPoint:(CGPoint)origin {
[super drawGlyphsForGlyphRange:glyphsToShow atPoint:origin];
}
@end
@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:[[ASCustomLayoutManager 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 = [[ASCustomTextContainer alloc] initWithSize:textContainerSize textStorage:textStorage];
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`.
UIEdgeInsets additionalInsets = UIEdgeInsetsZero;
if (CGRectGetWidth(components.textView.threadSafeBounds) != constrainedWidth) {
additionalInsets = self.textView.textContainerInset;
components = [ASTextKitComponents componentsWithAttributedSeedString:components.textStorage textContainerSize:CGSizeMake(constrainedWidth - additionalInsets.left - additionalInsets.right, 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
@@ -0,0 +1,59 @@
//
// 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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASAvailability.h>
#if AS_ENABLE_TEXTNODE
#import <AsyncDisplayKit/ASBaseDefines.h>
/**
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
@interface ASCustomTextContainer : NSTextContainer
- (instancetype)initWithSize:(CGSize)size textStorage:(NSTextStorage *)textStorage;
@end
#endif
@@ -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 <AsyncDisplayKit/ASThread.h>
#include <memory>
@implementation ASTextKitContext
{
// All TextKit operations (even non-mutative ones) must be executed serially.
std::shared_ptr<AS::Mutex> __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<AS::Mutex>();
// 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 = [[ASCustomTextContainer alloc] initWithSize:constrainedSize textStorage:nil];
// 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
@@ -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 <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASAvailability.h>
#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
};
@@ -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 <Foundation/Foundation.h>
#import <UIKit/NSLayoutManager.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
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 <NSLayoutManagerDelegate>
/**
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
@@ -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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASTextNodeTypes.h>
@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
@@ -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 <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASHashing.h>
#import <AsyncDisplayKit/ASTraitCollection.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASLayoutElement.h>
#pragma mark - ASPrimitiveTraitCollection
void ASTraitCollectionPropagateDown(id<ASLayoutElement> element, ASPrimitiveTraitCollection traitCollection) {
if (element) {
element.primitiveTraitCollection = traitCollection;
}
for (id<ASLayoutElement> 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<NSDictionary *> *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
@@ -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 <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
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<Value> : 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<Value> *)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<Value> *)setObject:(Value)value forKey:(Key)key AS_WARN_UNUSED_RESULT;
@end
NS_ASSUME_NONNULL_END
@@ -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<id, ASWeakMapEntry *> *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
@@ -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 <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
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
@@ -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 <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
#import <AsyncDisplayKit/ASAssert.h>
@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
@@ -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 <AsyncDisplayKit/ASWeakSet.h>
@interface ASWeakSet<__covariant ObjectType> ()
@property (nonatomic, readonly) NSHashTable<ObjectType> *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
@@ -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 <AsyncDisplayKit/NSArray+Diffing.h>
#import <UIKit/NSIndexPath+UIKitAdditions.h>
#import <AsyncDisplayKit/ASAssert.h>
#import <unordered_map>
@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<NSIndexPath *> **)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<NSIndexPath *> **)moves compareBlock:(compareBlock)comparison
{
struct NSObjectHash
{
std::size_t operator()(id <NSObject> k) const { return (std::size_t) [k hash]; };
};
struct NSObjectCompare
{
bool operator()(id <NSObject> lhs, id <NSObject> rhs) const { return (bool) [lhs isEqual:rhs]; };
};
std::unordered_multimap<unowned id, NSUInteger, NSObjectHash, NSObjectCompare> potentialMoves;
NSAssert(comparison != nil, @"Comparison block is required");
NSAssert(moves == nil || comparison == [NSArray defaultCompareBlock], @"move detection requires isEqual: and hash (no custom compare)");
NSMutableArray<NSIndexPath *> *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<id, NSUInteger>(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
@@ -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 <Foundation/Foundation.h>
@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<NSIndexPath *> *)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<NSIndexPath *> *)indexPaths;
@end
@@ -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 <UIKit/UIKit.h>
#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<NSIndexPath *> *)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<NSIndexPath *> *)indexPaths
{
NSMutableIndexSet *result = [[NSMutableIndexSet alloc] init];
for (NSIndexPath *indexPath in indexPaths) {
[result addIndex:indexPath.section];
}
return result;
}
@end
@@ -0,0 +1,27 @@
//
// ASAbsoluteLayoutElement.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 <AsyncDisplayKit/ASBaseDefines.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Layout options that can be defined for an ASLayoutElement being added to a ASAbsoluteLayoutSpec.
*/
@protocol ASAbsoluteLayoutElement
/**
* @abstract The position of this object within its parent spec.
*/
@property (nonatomic) CGPoint layoutPosition;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,61 @@
//
// ASAsciiArtBoxCreator.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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ASLayoutElementAsciiArtProtocol <NSObject>
/**
* Returns an ascii-art representation of this object and its children.
* For example, an ASInsetSpec may return something like this:
*
* --ASInsetLayoutSpec--
* | ASTextNode |
* ---------------------
*/
- (NSString *)asciiArtString;
/**
* returns the name of this object that will display in the ascii art. Usually this can
* simply be NSStringFromClass([self class]).
*/
- (NSString *)asciiArtName;
@end
/**
* A that takes a parent and its children and renders as ascii art box.
*/
@interface ASAsciiArtBoxCreator : NSObject
/**
* Renders an ascii art box with the children aligned horizontally
* Example:
* ------------ASStackLayoutSpec-----------
* | ASTextNode ASTextNode ASTextNode |
* ----------------------------------------
*/
+ (NSString *)horizontalBoxStringForChildren:(NSArray<NSString *> *)children parent:(NSString *)parent;
/**
* Renders an ascii art box with the children aligned vertically.
* Example:
* --ASStackLayoutSpec--
* | ASTextNode |
* | ASTextNode |
* | ASTextNode |
* ---------------------
*/
+ (NSString *)verticalBoxStringForChildren:(NSArray<NSString *> *)children parent:(NSString *)parent;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,102 @@
//
// ASAssert.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 <UIKit/UIKit.h>
#import <Foundation/NSException.h>
#import <pthread.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#if !defined(NS_BLOCK_ASSERTIONS)
#define ASDISPLAYNODE_ASSERTIONS_ENABLED 1
#else
#define ASDISPLAYNODE_ASSERTIONS_ENABLED 0
#endif
/**
* Note: In some cases it would be sufficient to do e.g.:
* ASDisplayNodeAssert(...) NSAssert(__VA_ARGS__)
* but we prefer not to, because we want to match the autocomplete behavior of NSAssert.
* The construction listed above does not show the user what arguments are required and what are optional.
*/
#define ASDisplayNodeAssert(condition, desc, ...) NSAssert(condition, desc, ##__VA_ARGS__)
#define ASDisplayNodeCAssert(condition, desc, ...) NSCAssert(condition, desc, ##__VA_ARGS__)
#define ASDisplayNodeAssertNil(condition, desc, ...) ASDisplayNodeAssert((condition) == nil, desc, ##__VA_ARGS__)
#define ASDisplayNodeCAssertNil(condition, desc, ...) ASDisplayNodeCAssert((condition) == nil, desc, ##__VA_ARGS__)
#define ASDisplayNodeAssertNotNil(condition, desc, ...) ASDisplayNodeAssert((condition) != nil, desc, ##__VA_ARGS__)
#define ASDisplayNodeCAssertNotNil(condition, desc, ...) ASDisplayNodeCAssert((condition) != nil, desc, ##__VA_ARGS__)
#define ASDisplayNodeAssertImplementedBySubclass() ASDisplayNodeAssert(NO, @"This method must be implemented by subclass %@", [self class]);
#define ASDisplayNodeAssertNotInstantiable() ASDisplayNodeAssert(NO, nil, @"This class is not instantiable.");
#define ASDisplayNodeAssertNotSupported() ASDisplayNodeAssert(NO, nil, @"This method is not supported by class %@", [self class]);
#define ASDisplayNodeAssertMainThread() ASDisplayNodeAssert(ASMainThreadAssertionsAreDisabled() || 0 != pthread_main_np(), @"This method must be called on the main thread")
#define ASDisplayNodeCAssertMainThread() ASDisplayNodeCAssert(ASMainThreadAssertionsAreDisabled() || 0 != pthread_main_np(), @"This function must be called on the main thread")
#define ASDisplayNodeAssertNotMainThread() ASDisplayNodeAssert(0 == pthread_main_np(), @"This method must be called off the main thread")
#define ASDisplayNodeCAssertNotMainThread() ASDisplayNodeCAssert(0 == pthread_main_np(), @"This function must be called off the main thread")
#define ASDisplayNodeAssertFlag(X, desc, ...) ASDisplayNodeAssert((1 == __builtin_popcount(X)), desc, ##__VA_ARGS__)
#define ASDisplayNodeCAssertFlag(X, desc, ...) ASDisplayNodeCAssert((1 == __builtin_popcount(X)), desc, ##__VA_ARGS__)
#define ASDisplayNodeAssertTrue(condition) ASDisplayNodeAssert((condition), @"Expected %s to be true.", #condition)
#define ASDisplayNodeCAssertTrue(condition) ASDisplayNodeCAssert((condition), @"Expected %s to be true.", #condition)
#define ASDisplayNodeAssertFalse(condition) ASDisplayNodeAssert(!(condition), @"Expected %s to be false.", #condition)
#define ASDisplayNodeCAssertFalse(condition) ASDisplayNodeCAssert(!(condition), @"Expected %s to be false.", #condition)
#define ASDisplayNodeFailAssert(desc, ...) ASDisplayNodeAssert(NO, desc, ##__VA_ARGS__)
#define ASDisplayNodeCFailAssert(desc, ...) ASDisplayNodeCAssert(NO, desc, ##__VA_ARGS__)
#define ASDisplayNodeConditionalAssert(shouldTestCondition, condition, desc, ...) ASDisplayNodeAssert((!(shouldTestCondition) || (condition)), desc, ##__VA_ARGS__)
#define ASDisplayNodeConditionalCAssert(shouldTestCondition, condition, desc, ...) ASDisplayNodeCAssert((!(shouldTestCondition) || (condition)), desc, ##__VA_ARGS__)
#define ASDisplayNodeCAssertPositiveReal(description, num) ASDisplayNodeCAssert(num >= 0 && num <= CGFLOAT_MAX, @"%@ must be a real positive integer: %f.", description, (CGFloat)num)
#define ASDisplayNodeCAssertInfOrPositiveReal(description, num) ASDisplayNodeCAssert(isinf(num) || (num >= 0 && num <= CGFLOAT_MAX), @"%@ must be infinite or a real positive integer: %f.", description, (CGFloat)num)
#define ASDisplayNodeCAssertPermanent(object) ASDisplayNodeCAssert(CFGetRetainCount((__bridge CFTypeRef)(object)) == CFGetRetainCount(kCFNull), @"Expected %s to be a permanent object.", #object)
#define ASDisplayNodeErrorDomain @"ASDisplayNodeErrorDomain"
#define ASDisplayNodeNonFatalErrorCode 1
/**
* In debug methods, it can be useful to disable main thread assertions to get valuable information,
* even if it means violating threading requirements. These functions are used in -debugDescription and let
* threads decide to suppress/re-enable main thread assertions.
*/
#pragma mark - Main Thread Assertions Disabling
AS_EXTERN BOOL ASMainThreadAssertionsAreDisabled(void);
AS_EXTERN void ASPushMainThreadAssertionsDisabled(void);
AS_EXTERN void ASPopMainThreadAssertionsDisabled(void);
#pragma mark - Non-Fatal Assertions
/// Returns YES if assertion passed, NO otherwise.
#define ASDisplayNodeAssertNonFatal(condition, desc, ...) ({ \
BOOL __evaluated = condition; \
if (__evaluated == NO) { \
ASDisplayNodeFailAssert(desc, ##__VA_ARGS__); \
ASDisplayNodeNonFatalErrorBlock block = [ASDisplayNode nonFatalErrorBlock]; \
if (block != nil) { \
NSDictionary *userInfo = nil; \
if (desc.length > 0) { \
userInfo = @{ NSLocalizedDescriptionKey : desc }; \
} \
NSError *error = [NSError errorWithDomain:ASDisplayNodeErrorDomain code:ASDisplayNodeNonFatalErrorCode userInfo:userInfo]; \
block(error); \
} \
} \
__evaluated; \
}) \
@@ -0,0 +1,85 @@
//
// ASAvailability.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 <CoreFoundation/CFBase.h>
#pragma once
#define AS_TLS_AVAILABLE 1
#ifndef AS_ENABLE_TEXTNODE
#define AS_ENABLE_TEXTNODE 1 // Enable old TextNode by default
#endif
// This needs to stay in sync with Weaver
#ifndef AS_USE_VIDEO
#define AS_USE_VIDEO 0
#endif
#ifndef AS_USE_PHOTOS
#define AS_USE_PHOTOS 0
#endif
#ifndef AS_USE_MAPKIT
#define AS_USE_MAPKIT 0
#endif
#ifndef AS_USE_ASSETS_LIBRARY
#define AS_USE_ASSETS_LIBRARY 0
#endif
#ifndef kCFCoreFoundationVersionNumber_iOS_10_0
#define kCFCoreFoundationVersionNumber_iOS_10_0 1348.00
#endif
#ifndef kCFCoreFoundationVersionNumber_iOS_11_0
#define kCFCoreFoundationVersionNumber_iOS_11_0 1438.10
#endif
#ifndef __IPHONE_11_0
#define __IPHONE_11_0 110000
#endif
#define AS_AT_LEAST_IOS10 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_10_0)
#define AS_AT_LEAST_IOS11 (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_11_0)
// Use __builtin_available if we're on Xcode >= 9, AS_AT_LEAST otherwise.
#if __has_builtin(__builtin_available)
#define AS_AVAILABLE_IOS(ver) __builtin_available(iOS ver, *)
#define AS_AVAILABLE_TVOS(ver) __builtin_available(tvOS ver, *)
#define AS_AVAILABLE_IOS_TVOS(ver1, ver2) __builtin_available(iOS ver1, tvOS ver2, *)
#else
#define AS_AVAILABLE_IOS(ver) (TARGET_OS_IOS && AS_AT_LEAST_IOS##ver)
#define AS_AVAILABLE_TVOS(ver) (TARGET_OS_TV && AS_AT_LEAST_IOS##ver)
#define AS_AVAILABLE_IOS_TVOS(ver1, ver2) (AS_AVAILABLE_IOS(ver1) || AS_AVAILABLE_TVOS(ver2))
#endif
// If Yoga is available, make it available anywhere we use ASAvailability.
// This reduces Yoga-specific code in other files.
// NOTE: Yoga integration is experimental and not fully tested. Use with caution and test layouts carefully.
#ifndef YOGA_HEADER_PATH
#define YOGA_HEADER_PATH <yoga/Yoga.h>
#endif
#ifndef YOGA
#define YOGA __has_include(YOGA_HEADER_PATH)
#endif
#ifdef ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE
#error "ASTEXTNODE_EXPERIMENT_GLOBAL_ENABLE is unavailable. See ASConfiguration.h."
#endif
#define AS_PIN_REMOTE_IMAGE __has_include(<PINRemoteImage/PINRemoteImage.h>)
#define AS_IG_LIST_KIT __has_include(<IGListKit/IGListKit.h>)
/**
* For IGListKit versions < 3.0, you have to use IGListCollectionView.
* For 3.0 and later, that class is removed and you use UICollectionView.
*/
#define IG_LIST_COLLECTION_VIEW __has_include(<IGListKit/IGListCollectionView.h>)
@@ -0,0 +1,259 @@
//
// ASBaseDefines.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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#define AS_EXTERN FOUNDATION_EXTERN
#define unowned __unsafe_unretained
/**
* Hack to support building for iOS with Xcode 9. UIUserInterfaceStyle was previously tvOS-only,
* and it was added to iOS 12. Xcode 9 (iOS 11 SDK) will flat-out refuse to build anything that
* references this enum targeting iOS, even if it's guarded with the right availability macros,
* because it thinks the entire platform isn't compatible with the enum.
*/
#if TARGET_OS_TV || (defined(__IPHONE_12_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_12_0)
#define AS_BUILD_UIUSERINTERFACESTYLE 1
#else
#define AS_BUILD_UIUSERINTERFACESTYLE 0
#endif
/**
* Decorates methods that clients can implement in categories on our base class. These methods
* will be stubbed with an empty implementation if no implementation is provided.
*/
#define AS_CATEGORY_IMPLEMENTABLE
#ifdef __GNUC__
# define ASDISPLAYNODE_GNUC(major, minor) \
(__GNUC__ > (major) || (__GNUC__ == (major) && __GNUC_MINOR__ >= (minor)))
#else
# define ASDISPLAYNODE_GNUC(major, minor) 0
#endif
#ifndef ASDISPLAYNODE_INLINE
# if defined (__STDC_VERSION__) && __STDC_VERSION__ >= 199901L
# define ASDISPLAYNODE_INLINE static inline
# elif defined (__MWERKS__) || defined (__cplusplus)
# define ASDISPLAYNODE_INLINE static inline
# elif ASDISPLAYNODE_GNUC (3, 0)
# define ASDISPLAYNODE_INLINE static __inline__ __attribute__ ((always_inline))
# else
# define ASDISPLAYNODE_INLINE static
# endif
#endif
#ifndef ASDISPLAYNODE_WARN_DEPRECATED
# define ASDISPLAYNODE_WARN_DEPRECATED 1
#endif
#ifndef ASDISPLAYNODE_DEPRECATED
# if ASDISPLAYNODE_GNUC (3, 0) && ASDISPLAYNODE_WARN_DEPRECATED
# define ASDISPLAYNODE_DEPRECATED __attribute__ ((deprecated))
# else
# define ASDISPLAYNODE_DEPRECATED
# endif
#endif
#ifndef ASDISPLAYNODE_DEPRECATED_MSG
# if ASDISPLAYNODE_GNUC (3, 0) && ASDISPLAYNODE_WARN_DEPRECATED
# define ASDISPLAYNODE_DEPRECATED_MSG(msg) __deprecated_msg(msg)
# else
# define ASDISPLAYNODE_DEPRECATED_MSG(msg)
# endif
#endif
#ifndef AS_ENABLE_TIPS
#define AS_ENABLE_TIPS 0
#endif
/**
* The event backtraces take a static 2KB of memory
* and retain all objects present in all the registers
* of the stack frames. The memory consumption impact
* is too significant even to be enabled during general
* development.
*/
#ifndef AS_SAVE_EVENT_BACKTRACES
# define AS_SAVE_EVENT_BACKTRACES 0
#endif
#ifndef __has_feature // Optional.
#define __has_feature(x) 0 // Compatibility with non-clang compilers.
#endif
#ifndef __has_attribute // Optional.
#define __has_attribute(x) 0 // Compatibility with non-clang compilers.
#endif
#ifndef NS_RETURNS_RETAINED
#if __has_feature(attribute_ns_returns_retained)
#define NS_RETURNS_RETAINED __attribute__((ns_returns_retained))
#else
#define NS_RETURNS_RETAINED
#endif
#endif
#ifndef CF_RETURNS_RETAINED
#if __has_feature(attribute_cf_returns_retained)
#define CF_RETURNS_RETAINED __attribute__((cf_returns_retained))
#else
#define CF_RETURNS_RETAINED
#endif
#endif
#ifndef ASDISPLAYNODE_REQUIRES_SUPER
#if __has_attribute(objc_requires_super)
#define ASDISPLAYNODE_REQUIRES_SUPER __attribute__((objc_requires_super))
#else
#define ASDISPLAYNODE_REQUIRES_SUPER
#endif
#endif
#ifndef AS_UNAVAILABLE
#if __has_attribute(unavailable)
#define AS_UNAVAILABLE(message) __attribute__((unavailable(message)))
#else
#define AS_UNAVAILABLE(message)
#endif
#endif
#ifndef AS_WARN_UNUSED_RESULT
#if __has_attribute(warn_unused_result)
#define AS_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
#else
#define AS_WARN_UNUSED_RESULT
#endif
#endif
#define ASOVERLOADABLE __attribute__((overloadable))
#if __has_attribute(noescape)
#define AS_NOESCAPE __attribute__((noescape))
#else
#define AS_NOESCAPE
#endif
#if __has_attribute(objc_subclassing_restricted)
#define AS_SUBCLASSING_RESTRICTED __attribute__((objc_subclassing_restricted))
#else
#define AS_SUBCLASSING_RESTRICTED
#endif
#define AS_ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0]))
#define ASCreateOnce(expr) ({ \
static dispatch_once_t onceToken; \
static __typeof__(expr) staticVar; \
dispatch_once(&onceToken, ^{ \
staticVar = expr; \
}); \
staticVar; \
})
/// Ensure that class is of certain kind
#define ASDynamicCast(x, c) ({ \
id __val = x;\
((c *) ([__val isKindOfClass:[c class]] ? __val : nil));\
})
/// Ensure that class is of certain kind, assuming it is subclass restricted
#define ASDynamicCastStrict(x, c) ({ \
id __val = x;\
((c *) ([__val class] == [c class] ? __val : nil));\
})
// Compare two primitives, assign if different. Returns whether the assignment happened.
#define ASCompareAssign(lvalue, newValue) ({ \
BOOL result = (lvalue != newValue); \
if (result) { lvalue = newValue; } \
result; \
})
#define ASCompareAssignObjects(lvalue, newValue) \
ASCompareAssignCustom(lvalue, newValue, ASObjectIsEqual)
// e.g. ASCompareAssignCustom(_myInsets, insets, UIEdgeInsetsEqualToEdgeInsets)
#define ASCompareAssignCustom(lvalue, newValue, isequal) ({ \
BOOL result = !(isequal(lvalue, newValue)); \
if (result) { lvalue = newValue; } \
result; \
})
#define ASCompareAssignCopy(lvalue, newValue) ({ \
BOOL result = !ASObjectIsEqual(lvalue, newValue); \
if (result) { lvalue = [newValue copyWithZone:NULL]; } \
result; \
})
/**
* Create a new set by mapping `collection` over `work`, ignoring nil.
*/
#define ASSetByFlatMapping(collection, decl, work) ({ \
NSMutableSet *s = [[NSMutableSet alloc] init]; \
for (decl in collection) {\
id result = work; \
if (result != nil) { \
[s addObject:result]; \
} \
} \
s; \
})
/**
* Create a new ObjectPointerPersonality NSHashTable by mapping `collection` over `work`, ignoring nil.
*
* capacity: 0 is taken from +hashTableWithOptions.
*/
#define ASPointerTableByFlatMapping(collection, decl, work) ({ \
NSHashTable *t = [[NSHashTable alloc] initWithOptions:NSHashTableObjectPointerPersonality capacity:0]; \
for (decl in collection) {\
id result = work; \
if (result != nil) { \
[t addObject:result]; \
} \
} \
t; \
})
/**
* Create a new array by mapping `collection` over `work`, ignoring nil.
*/
#define ASArrayByFlatMapping(collectionArg, decl, work) ({ \
id __collection = collectionArg; \
NSArray *__result; \
if (__collection) { \
id __buf[[__collection count]]; \
NSUInteger __i = 0; \
for (decl in __collection) {\
if ((__buf[__i] = work)) { \
__i++; \
} \
} \
__result = [NSArray arrayByTransferring:__buf count:__i]; \
} \
__result; \
})
/**
* Capture-and-clear a strong reference without the intervening retain/release pair.
*
* E.g. const auto localVar = ASTransferStrong(_myIvar);
* Post-condition: localVar has the strong value from _myIvar and _myIvar is nil.
* No retain/release is emitted when the optimizer is on.
*/
#define ASTransferStrong(lvalue) ({ \
CFTypeRef *__rawPtr = (CFTypeRef *)(void *)(&(lvalue)); \
CFTypeRef __cfValue = *__rawPtr; \
*__rawPtr = NULL; \
__typeof(lvalue) __result = (__bridge_transfer __typeof(lvalue))__cfValue; \
__result; \
})
@@ -0,0 +1,21 @@
//
// ASBlockTypes.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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
@class ASCellNode;
/**
* ASCellNode creation block. Used to lazily create the ASCellNode instance for a specified indexPath.
*/
typedef ASCellNode * _Nonnull(^ASCellNodeBlock)(void);
// Type for the cancellation checker block passed into the async display blocks. YES means the operation has been cancelled, NO means continue.
typedef BOOL(^asdisplaynode_iscancelled_block_t)(void);
@@ -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 <Foundation/Foundation.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <CoreGraphics/CGDataProvider.h>
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
@@ -0,0 +1,39 @@
//
// ASCollections.h
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@interface NSArray<__covariant ObjectType> (ASCollections)
/**
* Create an immutable NSArray from a C-array of strong pointers.
*
* Note: The memory for the array you pass in will be zero'd (to prevent ARC from releasing
* the references when the array goes out of scope.)
*
* Can be combined with vector like:
* vector<NSString *> vec;
* vec.push_back(@"foo");
* vec.push_back(@"bar");
* NSArray *arr = [NSArray arrayTransferring:vec.data() count:vec.size()]
* ** vec is now { nil, nil } **
*
* Unfortunately making a convenience method to do this is currently impossible because
* vector<NSString *> can't be converted to vector<id> by the compiler (silly).
*
* See the private __CFArrayCreateTransfer function.
*/
+ (NSArray<ObjectType> *)arrayByTransferring:(ObjectType _Nonnull __strong * _Nonnull)pointers
count:(NSUInteger)count NS_RETURNS_RETAINED;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,59 @@
//
// ASConfiguration.h
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <AsyncDisplayKit/ASExperimentalFeatures.h>
@protocol ASConfigurationDelegate;
NS_ASSUME_NONNULL_BEGIN
static NSInteger const ASConfigurationSchemaCurrentVersion = 1;
AS_SUBCLASSING_RESTRICTED
@interface ASConfiguration : NSObject <NSCopying>
/**
* Initialize this configuration with the provided dictionary,
* or nil to create an empty configuration.
*
* The schema is located in `schemas/configuration.json`.
*/
- (instancetype)initWithDictionary:(nullable NSDictionary *)dictionary;
/**
* The delegate for configuration-related events.
* Delegate methods are called from a serial queue.
*/
@property (nonatomic, nullable) id<ASConfigurationDelegate> delegate;
/**
* The experimental features to enable in Texture.
* See ASExperimentalFeatures for functions to convert to/from a string array.
*/
@property (nonatomic) ASExperimentalFeatures experimentalFeatures;
@end
/**
* Implement this method in a category to make your
* configuration available to Texture. It will be read
* only once and copied.
*
* NOTE: To specify your configuration at compile-time, you can
* define AS_FIXED_CONFIG_JSON as a C-string of JSON. This method
* will then be implemented to parse that string and generate
* a configuration.
*/
@interface ASConfiguration (UserProvided)
+ (ASConfiguration *)textureConfiguration NS_RETURNS_RETAINED;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,38 @@
//
// ASConfigurationDelegate.h
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASConfiguration.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Used to communicate configuration-related events to the client.
*/
@protocol ASConfigurationDelegate <NSObject>
/**
* Texture performed its first behavior related to the feature(s).
* This can be useful for tracking the impact of the behavior (A/B testing).
*/
- (void)textureDidActivateExperimentalFeatures:(ASExperimentalFeatures)features;
@optional
/**
* Texture framework initialized. This method is called synchronously
* on the main thread from ASInitializeFrameworkMainThread if you defined
* AS_INITIALIZE_FRAMEWORK_MANUALLY or from the default initialization point
* (currently +load) otherwise.
*/
- (void)textureDidInitialize;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,58 @@
//
// ASConfigurationInternal.h
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
/// Note this has to be public because it's imported by public header ASThread.h =/
/// It will be private again after exp_unfair_lock ends.
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASConfiguration.h>
NS_ASSUME_NONNULL_BEGIN
/**
* Quickly check if an experiment is enabled and notify the delegate
* that it's been activated.
*
* The delegate will be notified asynchronously.
*/
#if DEBUG
#define ASActivateExperimentalFeature(opt) _ASActivateExperimentalFeature(opt)
#else
#define ASActivateExperimentalFeature(opt) ({\
static BOOL result;\
static dispatch_once_t onceToken;\
dispatch_once(&onceToken, ^{ result = _ASActivateExperimentalFeature(opt); });\
result;\
})
#endif
/**
* Internal function. Use the macro without the underbar.
*/
AS_EXTERN BOOL _ASActivateExperimentalFeature(ASExperimentalFeatures option);
/**
* Notify the configuration delegate that the framework initialized, if needed.
*/
AS_EXTERN void ASNotifyInitialized(void);
AS_SUBCLASSING_RESTRICTED
@interface ASConfigurationManager : NSObject
/**
* No API for now.
* Just use ASActivateExperimentalFeature to access this efficiently.
*/
/* Exposed for testing purposes only */
+ (void)test_resetWithConfiguration:(nullable ASConfiguration *)configuration;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,73 @@
//
// ASContextTransitioning.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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDimension.h>
@class ASDisplayNode;
@class ASLayout;
NS_ASSUME_NONNULL_BEGIN
AS_EXTERN NSString * const ASTransitionContextFromLayoutKey;
AS_EXTERN NSString * const ASTransitionContextToLayoutKey;
@protocol ASContextTransitioning <NSObject>
/**
@abstract Defines if the given transition is animated
*/
- (BOOL)isAnimated;
/**
* @abstract Retrieve either the "from" or "to" layout
*/
- (nullable ASLayout *)layoutForKey:(NSString *)key;
/**
* @abstract Retrieve either the "from" or "to" constrainedSize
*/
- (ASSizeRange)constrainedSizeForKey:(NSString *)key;
/**
* @abstract Retrieve the subnodes from either the "from" or "to" layout
*/
- (NSArray<ASDisplayNode *> *)subnodesForKey:(NSString *)key;
/**
* @abstract Subnodes that have been inserted in the layout transition
*/
- (NSArray<ASDisplayNode *> *)insertedSubnodes;
/**
* @abstract Subnodes that will be removed in the layout transition
*/
- (NSArray<ASDisplayNode *> *)removedSubnodes;
/**
@abstract The frame for the given node before the transition began.
@discussion Returns CGRectNull if the node was not in the hierarchy before the transition.
*/
- (CGRect)initialFrameForNode:(ASDisplayNode *)node;
/**
@abstract The frame for the given node when the transition completes.
@discussion Returns CGRectNull if the node is no longer in the hierarchy after the transition.
*/
- (CGRect)finalFrameForNode:(ASDisplayNode *)node;
/**
@abstract Invoke this method when the transition is completed in `animateLayoutTransition:`
@discussion Passing NO to `didComplete` will set the original layout as the new layout.
*/
- (void)completeTransition:(BOOL)didComplete;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,68 @@
//
// ASControlNode+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 <AsyncDisplayKit/ASControlNode.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
/**
* The subclass header _ASControlNode+Subclasses_ defines methods to be
* overridden by custom nodes that subclass ASControlNode.
*
* These methods should never be called directly by other classes.
*/
@interface ASControlNode (Subclassing)
/**
@abstract Sends action messages for the given control events.
@param controlEvents A bitmask whose set flags specify the control events for which action messages are sent. See "Control Events" in ASControlNode.h for bitmask constants.
@param touchEvent An event object encapsulating the information specific to the user event.
@discussion ASControlNode implements this method to send all action messages associated with controlEvents. The list of targets is constructed from prior invocations of addTarget:action:forControlEvents:.
*/
- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(nullable UIEvent *)touchEvent;
/**
@abstract Sent to the control when tracking begins.
@param touch The touch on the receiving control.
@param touchEvent An event object encapsulating the information specific to the user event.
@result YES if the receiver should respond continuously (respond when touch is dragged); NO otherwise.
*/
- (BOOL)beginTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)touchEvent;
/**
@abstract Sent continuously to the control as it tracks a touch within the control's bounds.
@param touch The touch on the receiving control.
@param touchEvent An event object encapsulating the information specific to the user event.
@result YES if touch tracking should continue; NO otherwise.
*/
- (BOOL)continueTrackingWithTouch:(UITouch *)touch withEvent:(nullable UIEvent *)touchEvent;
/**
@abstract Sent to the control when tracking should be cancelled.
@param touchEvent An event object encapsulating the information specific to the user event. This parameter may be nil, indicating that the cancelation was caused by something other than an event, such as the display node being removed from its supernode.
*/
- (void)cancelTrackingWithEvent:(nullable UIEvent *)touchEvent;
/**
@abstract Sent to the control when the last touch completely ends, telling it to stop tracking.
@param touch The touch that ended.
@param touchEvent An event object encapsulating the information specific to the user event.
*/
- (void)endTrackingWithTouch:(nullable UITouch *)touch withEvent:(nullable UIEvent *)touchEvent;
/**
@abstract Settable version of highlighted property.
*/
@property (getter=isHighlighted) BOOL highlighted;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,150 @@
//
// ASControlNode.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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#pragma once
NS_ASSUME_NONNULL_BEGIN
/**
@abstract Kinds of events possible for control nodes.
@discussion These events are identical to their UIControl counterparts.
*/
typedef NS_OPTIONS(NSUInteger, ASControlNodeEvent)
{
/** A touch-down event in the control node. */
ASControlNodeEventTouchDown = 1 << 0,
/** A repeated touch-down event in the control node; for this event the value of the UITouch tapCount method is greater than one. */
ASControlNodeEventTouchDownRepeat = 1 << 1,
/** An event where a finger is dragged inside the bounds of the control node. */
ASControlNodeEventTouchDragInside = 1 << 2,
/** An event where a finger is dragged just outside the bounds of the control. */
ASControlNodeEventTouchDragOutside = 1 << 3,
/** A touch-up event in the control node where the finger is inside the bounds of the node. */
ASControlNodeEventTouchUpInside = 1 << 4,
/** A touch-up event in the control node where the finger is outside the bounds of the node. */
ASControlNodeEventTouchUpOutside = 1 << 5,
/** A system event canceling the current touches for the control node. */
ASControlNodeEventTouchCancel = 1 << 6,
/** A system event triggered when controls like switches, slides, etc change state. */
ASControlNodeEventValueChanged = 1 << 12,
/** A system event when the Play/Pause button on the Apple TV remote is pressed. */
ASControlNodeEventPrimaryActionTriggered = 1 << 13,
/** All events, including system events. */
ASControlNodeEventAllEvents = 0xFFFFFFFF
};
/**
* Compatibility aliases for @c ASControlState enum.
* We previously provided our own enum, but when it was imported
* into Swift, the @c normal (0) option disappeared.
*
* Apple's UIControlState enum gets special treatment here, and
* UIControlStateNormal is available in Swift.
*/
typedef UIControlState ASControlState ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlState.");
static UIControlState const ASControlStateNormal ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlStateNormal.") = UIControlStateNormal;
static UIControlState const ASControlStateDisabled ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlStateDisabled.") = UIControlStateDisabled;
static UIControlState const ASControlStateHighlighted ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlStateHighlighted.") = UIControlStateHighlighted;
static UIControlState const ASControlStateSelected ASDISPLAYNODE_DEPRECATED_MSG("Use UIControlStateSelected.") = UIControlStateSelected;
/**
@abstract ASControlNode is the base class for control nodes (such as buttons), or nodes that track touches to invoke targets with action messages.
@discussion ASControlNode cannot be used directly. It instead defines the common interface and behavior structure for all its subclasses. Subclasses should import "ASControlNode+Subclasses.h" for information on methods intended to be overriden.
*/
@interface ASControlNode : ASDisplayNode
#pragma mark - Control State
/**
@abstract Indicates whether or not the receiver is enabled.
@discussion Specify YES to make the control enabled; otherwise, specify NO to make it disabled. The default value is YES. If the enabled state is NO, the control ignores touch events and subclasses may draw differently.
*/
@property (getter=isEnabled) BOOL enabled;
/**
@abstract Indicates whether or not the receiver is highlighted.
@discussion This is set automatically when the there is a touch inside the control and removed on exit or touch up. This is different from touchInside in that it includes an area around the control, rather than just for touches inside the control.
*/
@property (getter=isHighlighted) BOOL highlighted;
/**
@abstract Indicates whether or not the receiver is highlighted.
@discussion This is set automatically when the receiver is tapped.
*/
@property (getter=isSelected) BOOL selected;
#pragma mark - Tracking Touches
/**
@abstract Indicates whether or not the receiver is currently tracking touches related to an event.
@discussion YES if the receiver is tracking touches; NO otherwise.
*/
@property (readonly, getter=isTracking) BOOL tracking;
/**
@abstract Indicates whether or not a touch is inside the bounds of the receiver.
@discussion YES if a touch is inside the receiver's bounds; NO otherwise.
*/
@property (readonly, getter=isTouchInside) BOOL touchInside;
#pragma mark - Action Messages
/**
@abstract Adds a target-action pair for a particular event (or events).
@param target The object to which the action message is sent. If this is nil, the responder chain is searched for an object willing to respond to the action message. target is not retained.
@param action A selector identifying an action message. May optionally include the sender and the event as parameters, in that order. May not be NULL.
@param controlEvents A bitmask specifying the control events for which the action message is sent. May not be 0. See "Control Events" for bitmask constants.
@discussion You may call this method multiple times, and you may specify multiple target-action pairs for a particular event. Targets are held weakly.
*/
- (void)addTarget:(nullable id)target action:(SEL)action forControlEvents:(ASControlNodeEvent)controlEvents;
/**
@abstract Returns the actions that are associated with a target and a particular control event.
@param target The target object. May not be nil.
@param controlEvent A single constant of type ASControlNodeEvent that specifies a particular user action on the control; for a list of these constants, see "Control Events". May not be 0 or ASControlNodeEventAllEvents.
@result An array of selector names as NSString objects, or nil if there are no action selectors associated with controlEvent.
*/
- (nullable NSArray<NSString *> *)actionsForTarget:(id)target forControlEvent:(ASControlNodeEvent)controlEvent AS_WARN_UNUSED_RESULT;
/**
@abstract Returns all target objects associated with the receiver.
@result A set of all targets for the receiver. The set may include NSNull to indicate at least one nil target (meaning, the responder chain is searched for a target.)
*/
- (NSSet *)allTargets AS_WARN_UNUSED_RESULT;
/**
@abstract Removes a target-action pair for a particular event.
@param target The target object. Pass nil to remove all targets paired with action and the specified control events.
@param action A selector identifying an action message. Pass NULL to remove all action messages paired with target.
@param controlEvents A bitmask specifying the control events associated with target and action. See "Control Events" for bitmask constants. May not be 0.
*/
- (void)removeTarget:(nullable id)target action:(nullable SEL)action forControlEvents:(ASControlNodeEvent)controlEvents;
/**
@abstract Sends the actions for the control events for a particular event.
@param controlEvents A bitmask specifying the control events for which to send actions. See "Control Events" for bitmask constants. May not be 0.
@param event The event which triggered these control actions. May be nil.
*/
- (void)sendActionsForControlEvents:(ASControlNodeEvent)controlEvents withEvent:(nullable UIEvent *)event;
@end
#if TARGET_OS_TV
@interface ASControlNode (tvOS)
/**
@abstract How the node looks when it isn't focused. Exposed here so that subclasses can override.
*/
- (void)setDefaultFocusAppearance;
@end
#endif
NS_ASSUME_NONNULL_END
@@ -0,0 +1,33 @@
//
// ASControlTargetAction.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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
/**
@abstract ASControlTargetAction stores target action pairs registered for specific ASControlNodeEvent values.
*/
@interface ASControlTargetAction : NSObject
/**
The action to be called on the registered target.
*/
@property (nonatomic) SEL action;
/**
Event handler target. The specified action will be called on this object.
*/
@property (nonatomic, weak) id target;
/**
Indicated whether this target was created without a target, so the action should travel up in the responder chain.
*/
@property (nonatomic, readonly) BOOL createdWithNoTarget;
@end
@@ -0,0 +1,317 @@
//
// ASDimension.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 <UIKit/UIKit.h>
#import <UIKit/UIGeometry.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <AsyncDisplayKit/ASAssert.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark -
ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASPointsValidForLayout(CGFloat points)
{
return ((isnormal(points) || points == 0.0) && points >= 0.0 && points < (CGFLOAT_MAX / 2.0));
}
ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASIsCGSizeValidForLayout(CGSize size)
{
return (ASPointsValidForLayout(size.width) && ASPointsValidForLayout(size.height));
}
// Note we want YGUndefined (10E20) to be considered invalid, so we have picked a smaller number than CGFLOAT_MAX/2.0
ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASPointsValidForSize(CGFloat points)
{
return ((isnormal(points) || points == 0.0) && points >= 0.0 && points < 10000000.0);
}
ASDISPLAYNODE_INLINE BOOL AS_WARN_UNUSED_RESULT ASIsCGSizeValidForSize(CGSize size)
{
return (ASPointsValidForSize(size.width) && ASPointsValidForSize(size.height));
}
// Note we want YGUndefined (10E20) to be considered invalid, so we have picked a smaller number than CGFLOAT_MAX/2.0
ASDISPLAYNODE_INLINE BOOL ASIsCGPositionPointsValidForLayout(CGFloat points)
{
return ((isnormal(points) || points == 0.0) && points < 10000000.0);
}
ASDISPLAYNODE_INLINE BOOL ASIsCGPositionValidForLayout(CGPoint point)
{
return (ASIsCGPositionPointsValidForLayout(point.x) && ASIsCGPositionPointsValidForLayout(point.y));
}
ASDISPLAYNODE_INLINE BOOL ASIsCGRectValidForLayout(CGRect rect)
{
return (ASIsCGPositionValidForLayout(rect.origin) && ASIsCGSizeValidForLayout(rect.size));
}
#pragma mark - ASDimension
/**
* A dimension relative to constraints to be provided in the future.
* A ASDimension can be one of three types:
*
* "Auto" - This indicated "I have no opinion" and may be resolved in whatever way makes most sense given the circumstances.
*
* "Points" - Just a number. It will always resolve to exactly this amount.
*
* "Percent" - Multiplied to a provided parent amount to resolve a final amount.
*/
typedef NS_ENUM(NSInteger, ASDimensionUnit) {
/** This indicates "I have no opinion" and may be resolved in whatever way makes most sense given the circumstances. */
ASDimensionUnitAuto,
/** Just a number. It will always resolve to exactly this amount. This is the default type. */
ASDimensionUnitPoints,
/** Multiplied to a provided parent amount to resolve a final amount. */
ASDimensionUnitFraction,
};
typedef struct {
ASDimensionUnit unit;
CGFloat value;
} ASDimension;
/**
* Represents auto as ASDimension
*/
AS_EXTERN ASDimension const ASDimensionAuto;
/**
* Returns a dimension with the specified type and value.
*/
ASOVERLOADABLE ASDISPLAYNODE_INLINE ASDimension ASDimensionMake(ASDimensionUnit unit, CGFloat value)
{
if (unit == ASDimensionUnitAuto ) {
ASDisplayNodeCAssert(value == 0, @"ASDimension auto value must be 0.");
} else if (unit == ASDimensionUnitPoints) {
ASDisplayNodeCAssertPositiveReal(@"Points", value);
} else if (unit == ASDimensionUnitFraction) {
ASDisplayNodeCAssert( 0 <= value && value <= 1.0, @"ASDimension fraction value (%f) must be between 0 and 1.", value);
}
ASDimension dimension;
dimension.unit = unit;
dimension.value = value;
return dimension;
}
/**
* Returns a dimension with the specified points value.
*/
ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASDimension ASDimensionMake(CGFloat points)
{
return ASDimensionMake(ASDimensionUnitPoints, points);
}
/**
* Returns a dimension by parsing the specified dimension string.
* Examples: ASDimensionMake(@"50%") = ASDimensionMake(ASDimensionUnitFraction, 0.5)
* ASDimensionMake(@"0.5pt") = ASDimensionMake(ASDimensionUnitPoints, 0.5)
*/
ASOVERLOADABLE AS_WARN_UNUSED_RESULT AS_EXTERN ASDimension ASDimensionMake(NSString *dimension);
/**
* Returns a dimension with the specified points value.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASDimension ASDimensionMakeWithPoints(CGFloat points)
{
ASDisplayNodeCAssertPositiveReal(@"Points", points);
return ASDimensionMake(ASDimensionUnitPoints, points);
}
/**
* Returns a dimension with the specified fraction value.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASDimension ASDimensionMakeWithFraction(CGFloat fraction)
{
ASDisplayNodeCAssert( 0 <= fraction && fraction <= 1.0, @"ASDimension fraction value (%f) must be between 0 and 1.", fraction);
return ASDimensionMake(ASDimensionUnitFraction, fraction);
}
/**
* Returns whether two dimensions are equal.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASDimensionEqualToDimension(ASDimension lhs, ASDimension rhs)
{
return (lhs.unit == rhs.unit && lhs.value == rhs.value);
}
/**
* Returns a NSString representation of a dimension.
*/
AS_EXTERN AS_WARN_UNUSED_RESULT NSString *NSStringFromASDimension(ASDimension dimension);
/**
* Resolve this dimension to a parent size.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT CGFloat ASDimensionResolve(ASDimension dimension, CGFloat parentSize, CGFloat autoSize)
{
switch (dimension.unit) {
case ASDimensionUnitAuto:
return autoSize;
case ASDimensionUnitPoints:
return dimension.value;
case ASDimensionUnitFraction:
return dimension.value * parentSize;
}
}
#pragma mark - ASLayoutSize
/**
* Expresses a size with relative dimensions. Only used for calculations internally in ASDimension.h
*/
typedef struct {
ASDimension width;
ASDimension height;
} ASLayoutSize;
AS_EXTERN ASLayoutSize const ASLayoutSizeAuto;
/*
* Creates an ASLayoutSize with provided min and max dimensions.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASLayoutSize ASLayoutSizeMake(ASDimension width, ASDimension height)
{
ASLayoutSize size;
size.width = width;
size.height = height;
return size;
}
/**
* Resolve this relative size relative to a parent size.
*/
ASDISPLAYNODE_INLINE CGSize ASLayoutSizeResolveSize(ASLayoutSize layoutSize, CGSize parentSize, CGSize autoSize)
{
return CGSizeMake(ASDimensionResolve(layoutSize.width, parentSize.width, autoSize.width),
ASDimensionResolve(layoutSize.height, parentSize.height, autoSize.height));
}
/*
* Returns a string representation of a relative size.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT NSString *NSStringFromASLayoutSize(ASLayoutSize size)
{
return [NSString stringWithFormat:@"{%@, %@}",
NSStringFromASDimension(size.width),
NSStringFromASDimension(size.height)];
}
#pragma mark - ASSizeRange
/**
* Expresses an inclusive range of sizes. Used to provide a simple constraint to layout.
*/
typedef struct {
CGSize min;
CGSize max;
} ASSizeRange;
/**
* A size range with all dimensions zero.
*/
AS_EXTERN ASSizeRange const ASSizeRangeZero;
/**
* A size range from zero to infinity in both directions.
*/
AS_EXTERN ASSizeRange const ASSizeRangeUnconstrained;
/**
* Returns whether a size range has > 0.1 max width and max height.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASSizeRangeHasSignificantArea(ASSizeRange sizeRange)
{
static CGFloat const limit = 0.1f;
return (sizeRange.max.width > limit && sizeRange.max.height > limit);
}
/**
* Creates an ASSizeRange with provided min and max size.
*/
ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMake(CGSize min, CGSize max)
{
ASDisplayNodeCAssertPositiveReal(@"Range min width", min.width);
ASDisplayNodeCAssertPositiveReal(@"Range min height", min.height);
ASDisplayNodeCAssertInfOrPositiveReal(@"Range max width", max.width);
ASDisplayNodeCAssertInfOrPositiveReal(@"Range max height", max.height);
ASDisplayNodeCAssert(min.width <= max.width,
@"Range min width (%f) must not be larger than max width (%f).", min.width, max.width);
ASDisplayNodeCAssert(min.height <= max.height,
@"Range min height (%f) must not be larger than max height (%f).", min.height, max.height);
ASSizeRange sizeRange;
sizeRange.min = min;
sizeRange.max = max;
return sizeRange;
}
/**
* Creates an ASSizeRange with provided size as both min and max.
*/
ASOVERLOADABLE ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeMake(CGSize exactSize)
{
return ASSizeRangeMake(exactSize, exactSize);
}
/**
* Clamps the provided CGSize between the [min, max] bounds of this ASSizeRange.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT CGSize ASSizeRangeClamp(ASSizeRange sizeRange, CGSize size)
{
return CGSizeMake(MAX(sizeRange.min.width, MIN(sizeRange.max.width, size.width)),
MAX(sizeRange.min.height, MIN(sizeRange.max.height, size.height)));
}
/**
* Intersects another size range. If the other size range does not overlap in either dimension, this size range
* "wins" by returning a single point within its own range that is closest to the non-overlapping range.
*/
AS_EXTERN AS_WARN_UNUSED_RESULT ASSizeRange ASSizeRangeIntersect(ASSizeRange sizeRange, ASSizeRange otherSizeRange);
/**
* Returns whether two size ranges are equal in min and max size.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASSizeRangeEqualToSizeRange(ASSizeRange lhs, ASSizeRange rhs)
{
return CGSizeEqualToSize(lhs.min, rhs.min) && CGSizeEqualToSize(lhs.max, rhs.max);
}
/**
* Returns a string representation of a size range
*/
AS_EXTERN AS_WARN_UNUSED_RESULT NSString *NSStringFromASSizeRange(ASSizeRange sizeRange);
#if YOGA
#pragma mark - ASEdgeInsets
typedef struct {
ASDimension top;
ASDimension left;
ASDimension bottom;
ASDimension right;
ASDimension start;
ASDimension end;
ASDimension horizontal;
ASDimension vertical;
ASDimension all;
} ASEdgeInsets;
AS_EXTERN ASEdgeInsets const ASEdgeInsetsZero;
AS_EXTERN ASEdgeInsets ASEdgeInsetsMake(UIEdgeInsets edgeInsets);
#endif
NS_ASSUME_NONNULL_END
@@ -0,0 +1,102 @@
//
// ASDimensionInternal.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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <AsyncDisplayKit/ASDimension.h>
NS_ASSUME_NONNULL_BEGIN
#pragma mark - ASLayoutElementSize
/**
* A struct specifying a ASLayoutElement's size. Example:
*
* ASLayoutElementSize size = (ASLayoutElementSize){
* .width = ASDimensionMakeWithFraction(0.25),
* .maxWidth = ASDimensionMakeWithPoints(200),
* .minHeight = ASDimensionMakeWithFraction(0.50)
* };
*
* Description: <ASLayoutElementSize: exact={25%, Auto}, min={Auto, 50%}, max={200pt, Auto}>
*
*/
typedef struct {
ASDimension width;
ASDimension height;
ASDimension minWidth;
ASDimension maxWidth;
ASDimension minHeight;
ASDimension maxHeight;
} ASLayoutElementSize;
/**
* Returns an ASLayoutElementSize with default values.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASLayoutElementSize ASLayoutElementSizeMake()
{
return (ASLayoutElementSize){
.width = ASDimensionAuto,
.height = ASDimensionAuto,
.minWidth = ASDimensionAuto,
.maxWidth = ASDimensionAuto,
.minHeight = ASDimensionAuto,
.maxHeight = ASDimensionAuto
};
}
/**
* Returns an ASLayoutElementSize with the specified CGSize values as width and height.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASLayoutElementSize ASLayoutElementSizeMakeFromCGSize(CGSize size)
{
ASLayoutElementSize s = ASLayoutElementSizeMake();
s.width = ASDimensionMakeWithPoints(size.width);
s.height = ASDimensionMakeWithPoints(size.height);
return s;
}
/**
* Returns whether two sizes are equal.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT BOOL ASLayoutElementSizeEqualToLayoutElementSize(ASLayoutElementSize lhs, ASLayoutElementSize rhs)
{
return (ASDimensionEqualToDimension(lhs.width, rhs.width)
&& ASDimensionEqualToDimension(lhs.height, rhs.height)
&& ASDimensionEqualToDimension(lhs.minWidth, rhs.minWidth)
&& ASDimensionEqualToDimension(lhs.maxWidth, rhs.maxWidth)
&& ASDimensionEqualToDimension(lhs.minHeight, rhs.minHeight)
&& ASDimensionEqualToDimension(lhs.maxHeight, rhs.maxHeight));
}
/**
* Returns a string formatted to contain the data from an ASLayoutElementSize.
*/
AS_EXTERN AS_WARN_UNUSED_RESULT NSString *NSStringFromASLayoutElementSize(ASLayoutElementSize size);
/**
* Resolve the given size relative to a parent size and an auto size.
* From the given size uses width, height to resolve the exact size constraint, uses the minHeight and minWidth to
* resolve the min size constraint and the maxHeight and maxWidth to resolve the max size constraint. For every
* dimension with unit ASDimensionUnitAuto the given autoASSizeRange value will be used.
* Based on the calculated exact, min and max size constraints the final size range will be calculated.
*/
AS_EXTERN AS_WARN_UNUSED_RESULT ASSizeRange ASLayoutElementSizeResolveAutoSize(ASLayoutElementSize size, const CGSize parentSize, ASSizeRange autoASSizeRange);
/**
* Resolve the given size to a parent size. Uses internally ASLayoutElementSizeResolveAutoSize with {INFINITY, INFINITY} as
* as autoASSizeRange. For more information look at ASLayoutElementSizeResolveAutoSize.
*/
ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASSizeRange ASLayoutElementSizeResolve(ASLayoutElementSize size, const CGSize parentSize)
{
return ASLayoutElementSizeResolveAutoSize(size, parentSize, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY)));
}
NS_ASSUME_NONNULL_END
@@ -0,0 +1,56 @@
//
// ASDisplayNode+Ancestry.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 <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
NS_ASSUME_NONNULL_BEGIN
@interface ASDisplayNode (Ancestry)
/**
* Returns an object to enumerate the supernode ancestry of this node, starting with its supernode.
*
* For instance, you could write:
* for (ASDisplayNode *node in self.supernodes) {
* if ([node.backgroundColor isEqual:[UIColor blueColor]]) {
* node.hidden = YES;
* }
* }
*
* Note: If this property is read on the main thread, the enumeration will attempt to go up
* the layer hierarchy if it finds a break in the display node hierarchy.
*/
@property (readonly) id<NSFastEnumeration> supernodes;
/**
* Same as `supernodes` but begins the enumeration with self.
*/
@property (readonly) id<NSFastEnumeration> supernodesIncludingSelf;
/**
* Searches the supernodes of this node for one matching the given class.
*
* @param supernodeClass The class of node you're looking for.
* @param includeSelf Whether to include self in the search.
* @return A node of the given class that is an ancestor of this node, or nil.
*
* @note See the documentation on `supernodes` for details about the upward traversal.
*/
- (nullable __kindof ASDisplayNode *)supernodeOfClass:(Class)supernodeClass includingSelf:(BOOL)includeSelf;
/**
* e.g. "(<MYTextNode: 0xFFFF>, <MYTextContainingNode: 0xFFFF>, <MYCellNode: 0xFFFF>)"
*/
@property (copy, readonly) NSString *ancestryDescription;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,175 @@
//
// ASDisplayNode+Beta.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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASAvailability.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
NS_ASSUME_NONNULL_BEGIN
AS_EXTERN void ASPerformBlockOnMainThread(void (^block)(void));
AS_EXTERN void ASPerformBlockOnBackgroundThread(void (^block)(void)); // DISPATCH_QUEUE_PRIORITY_DEFAULT
#if ASEVENTLOG_ENABLE
#define ASDisplayNodeLogEvent(node, ...) [node.eventLog logEventWithBacktrace:(AS_SAVE_EVENT_BACKTRACES ? [NSThread callStackSymbols] : nil) format:__VA_ARGS__]
#else
#define ASDisplayNodeLogEvent(node, ...)
#endif
#if ASEVENTLOG_ENABLE
#define ASDisplayNodeGetEventLog(node) node.eventLog
#else
#define ASDisplayNodeGetEventLog(node) nil
#endif
/**
* Bitmask to indicate what performance measurements the cell should record.
*/
typedef NS_OPTIONS(NSUInteger, ASDisplayNodePerformanceMeasurementOptions) {
ASDisplayNodePerformanceMeasurementOptionLayoutSpec = 1 << 0,
ASDisplayNodePerformanceMeasurementOptionLayoutComputation = 1 << 1
};
typedef struct {
CFTimeInterval layoutSpecTotalTime;
NSInteger layoutSpecNumberOfPasses;
CFTimeInterval layoutComputationTotalTime;
NSInteger layoutComputationNumberOfPasses;
} ASDisplayNodePerformanceMeasurements;
@interface ASDisplayNode (Beta)
/**
* ASTableView and ASCollectionView now throw exceptions on invalid updates
* like their UIKit counterparts. If YES, these classes will log messages
* on invalid updates rather than throwing exceptions.
*
* Note that even if AsyncDisplayKit's exception is suppressed, the app may still crash
* as it proceeds with an invalid update.
*
* This property defaults to NO. It will be removed in a future release.
*/
+ (BOOL)suppressesInvalidCollectionUpdateExceptions AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Collection update exceptions are thrown if assertions are enabled.");
+ (void)setSuppressesInvalidCollectionUpdateExceptions:(BOOL)suppresses;
/**
* @abstract Recursively ensures node and all subnodes are displayed.
* @see Full documentation in ASDisplayNode+FrameworkPrivate.h
*/
- (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously;
/**
* @abstract allow modification of a context before the node's content is drawn
*
* @discussion Set the block to be called after the context has been created and before the node's content is drawn.
* You can override this to modify the context before the content is drawn. You are responsible for saving and
* restoring context if necessary. Restoring can be done in contextDidDisplayNodeContent
* This block can be called from *any* thread and it is unsafe to access any UIKit main thread properties from it.
*/
@property (nullable) ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext;
/**
* @abstract allow modification of a context after the node's content is drawn
*/
@property (nullable) ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext;
/**
* @abstract A bitmask representing which actions (layout spec, layout generation) should be measured.
*/
@property ASDisplayNodePerformanceMeasurementOptions measurementOptions;
/**
* @abstract A simple struct representing performance measurements collected.
*/
@property (readonly) ASDisplayNodePerformanceMeasurements performanceMeasurements;
#if ASEVENTLOG_ENABLE
/*
* @abstract The primitive event tracing object. You shouldn't directly use it to log event. Use the ASDisplayNodeLogEvent macro instead.
*/
@property (nonatomic, readonly) ASEventLog *eventLog;
#endif
/**
* @abstract Whether this node acts as an accessibility container. If set to YES, then this node's accessibility label will represent
* an aggregation of all child nodes' accessibility labels. Nodes in this node's subtree that are also accessibility containers will
* not be included in this aggregation, and will be exposed as separate accessibility elements to UIKit.
*/
@property BOOL isAccessibilityContainer;
/**
* @abstract Returns the default accessibility property values set by Texture on this node. For
* example, the default accessibility label for a text node may be its text content, while most
* other nodes would have nil default labels.
*/
@property (nullable, readonly, copy) NSString *defaultAccessibilityLabel;
@property (nullable, readonly, copy) NSString *defaultAccessibilityHint;
@property (nullable, readonly, copy) NSString *defaultAccessibilityValue;
@property (nullable, readonly, copy) NSString *defaultAccessibilityIdentifier;
@property (readonly) UIAccessibilityTraits defaultAccessibilityTraits;
/**
* @abstract Invoked when a user performs a custom action on an accessible node. Nodes that are children of accessibility containers, have
* an accessibity label and have an interactive UIAccessibilityTrait will automatically receive custom-action handling.
*
* @return Return a boolean value that determine whether to propagate through the responder chain.
* To halt propagation, return YES; otherwise, return NO.
*/
- (BOOL)performAccessibilityCustomAction:(UIAccessibilityCustomAction *)action;
/**
* @abstract Currently used by ASNetworkImageNode and ASMultiplexImageNode to allow their placeholders to stay if they are loading an image from the network.
* Otherwise, a display pass is scheduled and completes, but does not actually draw anything - and ASDisplayNode considers the element finished.
*/
- (BOOL)placeholderShouldPersist AS_WARN_UNUSED_RESULT;
/**
* @abstract Indicates that the receiver and all subnodes have finished displaying. May be called more than once, for example if the receiver has
* a network image node. This is called after the first display pass even if network image nodes have not downloaded anything (text would be done,
* and other nodes that are ready to do their final display). Each render of every progressive jpeg network node would cause this to be called, so
* this hook could be called up to 1 + (pJPEGcount * pJPEGrenderCount) times. The render count depends on how many times the downloader calls the
* progressImage block.
*/
AS_CATEGORY_IMPLEMENTABLE
- (void)hierarchyDisplayDidFinish NS_REQUIRES_SUPER;
/**
* Only called on the root during yoga layout.
*/
AS_CATEGORY_IMPLEMENTABLE
- (void)willCalculateLayout:(ASSizeRange)constrainedSize NS_REQUIRES_SUPER;
/**
* @abstract Whether to draw all descendent nodes' contents into this node's layer's backing store.
*
* @discussion
* When called, causes all descendent nodes' contents to be drawn directly into this node's layer's backing
* store.
*
* If a node's descendants are static (never animated or never change attributes after creation) then that node is a
* good candidate for rasterization. Rasterizing descendants has two main benefits:
* 1) Backing stores for descendant layers are not created. Instead the layers are drawn directly into the rasterized
* container. This can save a great deal of memory.
* 2) Since the entire subtree is drawn into one backing store, compositing and blending are eliminated in that subtree
* which can help improve animation/scrolling/etc performance.
*
* Rasterization does not currently support descendants with transform, sublayerTransform, or alpha. Those properties
* will be ignored when rasterizing descendants.
*
* Note: this has nothing to do with -[CALayer shouldRasterize], which doesn't work with ASDisplayNode's asynchronous
* rendering model.
*
* Note: You cannot add subnodes whose layers/views are already loaded to a rasterized node.
* Note: You cannot call this method after the receiver's layer/view is loaded.
*/
- (void)enableSubtreeRasterization;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,29 @@
//
// ASDisplayNode+Convenience.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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
NS_ASSUME_NONNULL_BEGIN
@class UIViewController;
@interface ASDisplayNode (Convenience)
/**
* @abstract Returns the view controller nearest to this node in the view hierarchy.
*
* @warning This property may only be accessed on the main thread. This property may
* be @c nil until the node's view is actually hosted in the view hierarchy.
*/
@property (nonatomic, nullable, readonly) __kindof UIViewController *closestViewController;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,320 @@
//
// ASDisplayNode+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.
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASObjectDescriptionHelpers.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ASInterfaceStateDelegate;
/**
Hierarchy state is propagated from nodes to all of their children when certain behaviors are required from the subtree.
Examples include rasterization and external driving of the .interfaceState property.
By passing this information explicitly, performance is optimized by avoiding iteration up the supernode chain.
Lastly, this avoidance of supernode traversal protects against the possibility of deadlocks when a supernode is
simultaneously attempting to materialize views / layers for its subtree (as many related methods require property locking)
Note: as the hierarchy deepens, more state properties may be enabled. However, state properties may never be disabled /
cancelled below the point they are enabled. They continue to the leaves of the hierarchy.
*/
typedef NS_OPTIONS(NSUInteger, ASHierarchyState)
{
/** The node may or may not have a supernode, but no supernode has a special hierarchy-influencing option enabled. */
ASHierarchyStateNormal = 0,
/** The node has a supernode with .rasterizesSubtree = YES.
Note: the root node of the rasterized subtree (the one with the property set on it) will NOT have this state set. */
ASHierarchyStateRasterized = 1 << 0,
/** The node or one of its supernodes is managed by a class like ASRangeController. Most commonly, these nodes are
ASCellNode objects or a subnode of one, and are used in ASTableView or ASCollectionView.
These nodes also receive regular updates to the .interfaceState property with more detailed status information. */
ASHierarchyStateRangeManaged = 1 << 1,
/** Down-propagated version of _flags.visibilityNotificationsDisabled. This flag is very rarely set, but by having it
locally available to nodes, they do not have to walk up supernodes at the critical points it is checked. */
ASHierarchyStateTransitioningSupernodes = 1 << 2,
/** One of the supernodes of this node is performing a transition.
Any layout calculated during this state should not be applied immediately, but pending until later. */
ASHierarchyStateLayoutPending = 1 << 3,
};
ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState)
{
return ((hierarchyState & ASHierarchyStateLayoutPending) == ASHierarchyStateLayoutPending);
}
ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRangeManaged(ASHierarchyState hierarchyState)
{
return ((hierarchyState & ASHierarchyStateRangeManaged) == ASHierarchyStateRangeManaged);
}
ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesRasterized(ASHierarchyState hierarchyState)
{
return ((hierarchyState & ASHierarchyStateRasterized) == ASHierarchyStateRasterized);
}
ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesTransitioningSupernodes(ASHierarchyState hierarchyState)
{
return ((hierarchyState & ASHierarchyStateTransitioningSupernodes) == ASHierarchyStateTransitioningSupernodes);
}
__unused static NSString * _Nonnull NSStringFromASHierarchyState(ASHierarchyState hierarchyState)
{
NSMutableArray *states = [NSMutableArray array];
if (hierarchyState == ASHierarchyStateNormal) {
[states addObject:@"Normal"];
}
if (ASHierarchyStateIncludesRangeManaged(hierarchyState)) {
[states addObject:@"RangeManaged"];
}
if (ASHierarchyStateIncludesLayoutPending(hierarchyState)) {
[states addObject:@"LayoutPending"];
}
if (ASHierarchyStateIncludesRasterized(hierarchyState)) {
[states addObject:@"Rasterized"];
}
if (ASHierarchyStateIncludesTransitioningSupernodes(hierarchyState)) {
[states addObject:@"TransitioningSupernodes"];
}
return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]];
}
#define HIERARCHY_STATE_DELTA(Name) ({ \
if ((oldState & ASHierarchyState##Name) != (newState & ASHierarchyState##Name)) { \
[changes appendFormat:@"%c%s ", (newState & ASHierarchyState##Name ? '+' : '-'), #Name]; \
} \
})
__unused static NSString * _Nonnull NSStringFromASHierarchyStateChange(ASHierarchyState oldState, ASHierarchyState newState)
{
if (oldState == newState) {
return @"{ }";
}
NSMutableString *changes = [NSMutableString stringWithString:@"{ "];
HIERARCHY_STATE_DELTA(Rasterized);
HIERARCHY_STATE_DELTA(RangeManaged);
HIERARCHY_STATE_DELTA(TransitioningSupernodes);
HIERARCHY_STATE_DELTA(LayoutPending);
[changes appendString:@"}"];
return changes;
}
#undef HIERARCHY_STATE_DELTA
@interface ASDisplayNode () <ASDescriptionProvider, ASDebugDescriptionProvider>
{
@protected
ASInterfaceState _interfaceState;
ASHierarchyState _hierarchyState;
}
// The view class to use when creating a new display node instance. Defaults to _ASDisplayView.
+ (Class)viewClass;
// Thread safe way to access the bounds of the node
@property (nonatomic) CGRect threadSafeBounds;
// Returns the bounds of the node without reaching the view or layer
- (CGRect)_locked_threadSafeBounds;
// The -pendingInterfaceState holds the value that will be applied to -interfaceState by the
// ASCATransactionQueue. If already applied, it matches -interfaceState. Thread-safe access.
@property (nonatomic, readonly) ASInterfaceState pendingInterfaceState;
// These methods are recursive, and either union or remove the provided interfaceState to all sub-elements.
- (void)enterInterfaceState:(ASInterfaceState)interfaceState;
- (void)exitInterfaceState:(ASInterfaceState)interfaceState;
- (void)recursivelySetInterfaceState:(ASInterfaceState)interfaceState;
// These methods are recursive, and either union or remove the provided hierarchyState to all sub-elements.
- (void)enterHierarchyState:(ASHierarchyState)hierarchyState;
- (void)exitHierarchyState:(ASHierarchyState)hierarchyState;
// Changed before calling willEnterHierarchy / didExitHierarchy.
@property (readonly, getter = isInHierarchy) BOOL inHierarchy;
// Call willEnterHierarchy if necessary and set inHierarchy = YES if visibility notifications are enabled on all of its parents
- (void)__enterHierarchy;
// Call didExitHierarchy if necessary and set inHierarchy = NO if visibility notifications are enabled on all of its parents
- (void)__exitHierarchy;
/**
* @abstract Returns the Hierarchy State of the node.
*
* @return The current ASHierarchyState of the node, indicating whether it is rasterized or managed by a range controller.
*
* @see ASInterfaceState
*/
@property (nonatomic) ASHierarchyState hierarchyState;
/**
* @abstract Return if the node is range managed or not
*
* @discussion Currently only set interface state on nodes in table and collection views. For other nodes, if they are
* in the hierarchy we enable all ASInterfaceState types with `ASInterfaceStateInHierarchy`, otherwise `None`.
*/
- (BOOL)supportsRangeManagedInterfaceState;
- (BOOL)_locked_displaysAsynchronously;
// The two methods below will eventually be exposed, but their names are subject to change.
/**
* @abstract Ensure that all rendering is complete for this node and its descendants.
*
* @discussion Calling this method on the main thread after a node is added to the view hierarchy will ensure that
* placeholder states are never visible to the user. It is used by ASTableView, ASCollectionView, and ASViewController
* to implement their respective ".neverShowPlaceholders" option.
*
* If all nodes have layer.contents set and/or their layer does not have -needsDisplay set, the method will return immediately.
*
* This method is capable of handling a mixed set of nodes, with some not having started display, some in progress on an
* asynchronous display operation, and some already finished.
*
* In order to guarantee against deadlocks, this method should only be called on the main thread.
* It may block on the private queue, [_ASDisplayLayer displayQueue]
*/
- (void)recursivelyEnsureDisplaySynchronously:(BOOL)synchronously;
/**
* @abstract Calls -didExitPreloadState on the receiver and its subnode hierarchy.
*
* @discussion Clears any memory-intensive preloaded content.
* This method is used to notify the node that it should purge any content that is both expensive to fetch and to
* retain in memory.
*
* @see [ASDisplayNode(Subclassing) didExitPreloadState] and [ASDisplayNode(Subclassing) didEnterPreloadState]
*/
- (void)recursivelyClearPreloadedData;
/**
* @abstract Calls -didEnterPreloadState on the receiver and its subnode hierarchy.
*
* @discussion Fetches content from remote sources for the current node and all subnodes.
*
* @see [ASDisplayNode(Subclassing) didEnterPreloadState] and [ASDisplayNode(Subclassing) didExitPreloadState]
*/
- (void)recursivelyPreload;
/**
* @abstract Triggers a recursive call to -didEnterPreloadState when the node has an interfaceState of ASInterfaceStatePreload
*/
- (void)setNeedsPreload;
/**
* @abstract Allows a node to bypass all ensureDisplay passes. Defaults to NO.
*
* @discussion Nodes that are expensive to draw and expected to have placeholder even with
* .neverShowPlaceholders enabled should set this to YES.
*
* ASImageNode uses the default of NO, as it is often used for UI images that are expected to synchronize with ensureDisplay.
*
* ASNetworkImageNode and ASMultiplexImageNode set this to YES, because they load data from a database or server,
* and are expected to support a placeholder state given that display is often blocked on slow data fetching.
*/
@property BOOL shouldBypassEnsureDisplay;
/**
* @abstract Checks whether a node should be scheduled for display, considering its current and new interface states.
*/
- (BOOL)shouldScheduleDisplayWithNewInterfaceState:(ASInterfaceState)newInterfaceState;
/**
* @abstract safeAreaInsets will fallback to this value if the corresponding UIKit property is not available
* (due to an old iOS version).
*
* @discussion This should be set by the owning view controller based on it's layout guides.
* If this is not a view controllet's node the value will be calculated automatically by the parent node.
*/
@property (nonatomic) UIEdgeInsets fallbackSafeAreaInsets;
/**
* @abstract Indicates if this node is a view controller's root node. Defaults to NO.
*
* @discussion Set to YES in -[ASViewController initWithNode:].
*
* YES here only means that this node is used as an ASViewController node. It doesn't mean that this node is a root of
* ASDisplayNode hierarchy, e.g. when its view controller is parented by another ASViewController.
*/
@property (nonatomic, getter=isViewControllerRoot) BOOL viewControllerRoot;
@end
@interface 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;
/**
* @abstract Subclass hook for nodes that are acting as root nodes. This method is called if one of the subnodes
* size is invalidated and may need to result in a different size as the current calculated size.
*/
- (void)_rootNodeDidInvalidateSize;
/**
* 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).
*/
- (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds;
/**
* Layout all of the subnodes based on the sublayouts
*/
- (void)_layoutSublayouts;
@end
@interface ASDisplayNode (ASLayoutTransitionInternal)
/**
* If one or multiple layout transitions are in flight this methods returns if the current layout transition that
* happens in in this particular thread was invalidated through another thread is starting a transition for this node
*/
- (BOOL)_isLayoutTransitionInvalid;
/**
* Same as @c -_isLayoutTransitionInvalid but must be called with the node's instance lock held.
*/
- (BOOL)_locked_isLayoutTransitionInvalid;
/**
* Internal method that can be overriden by subclasses to add specific behavior after the measurement of a layout
* transition did finish.
*/
- (void)_layoutTransitionMeasurementDidFinish;
/**
* Informs the node that the pending layout transition did complete
*/
- (void)_completePendingLayoutTransition;
/**
* Called if the pending layout transition did complete
*/
- (void)_pendingLayoutTransitionDidComplete;
@end
@interface ASDisplayNode (AccessibilityInternal)
- (NSArray *)accessibilityElements;
@end;
NS_ASSUME_NONNULL_END
@@ -0,0 +1,131 @@
//
// ASDisplayNode+InterfaceState.h
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
/**
* Interface state is available on ASDisplayNode and ASViewController, and
* allows checking whether a node is in an interface situation where it is prudent to trigger certain
* actions: measurement, data loading, display, and visibility (the latter for animations or other onscreen-only effects).
*
* The defualt state, ASInterfaceStateNone, means that the element is not predicted to be onscreen soon and
* preloading should not be performed. Swift: use [] for the default behavior.
*/
typedef NS_OPTIONS(NSUInteger, ASInterfaceState)
{
/** The element is not predicted to be onscreen soon and preloading should not be performed */
ASInterfaceStateNone = 0,
/** The element may be added to a view soon that could become visible. Measure the layout, including size calculation. */
ASInterfaceStateMeasureLayout = 1 << 0,
/** The element is likely enough to come onscreen that disk and/or network data required for display should be fetched. */
ASInterfaceStatePreload = 1 << 1,
/** The element is very likely to become visible, and concurrent rendering should be executed for any -setNeedsDisplay. */
ASInterfaceStateDisplay = 1 << 2,
/** The element is physically onscreen by at least 1 pixel.
In practice, all other bit fields should also be set when this flag is set. */
ASInterfaceStateVisible = 1 << 3,
/**
* The node is not contained in a cell but it is in a window.
*
* Currently we only set `interfaceState` to other values for
* nodes contained in table views or collection views.
*/
ASInterfaceStateInHierarchy = ASInterfaceStateMeasureLayout | ASInterfaceStatePreload | ASInterfaceStateDisplay | ASInterfaceStateVisible,
};
@protocol ASInterfaceStateDelegate <NSObject>
/**
* @abstract Called whenever any bit in the ASInterfaceState bitfield is changed.
* @discussion Subclasses may use this to monitor when they become visible, should free cached data, and much more.
* @see ASInterfaceState
*/
- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState;
/**
* @abstract Called whenever the node becomes visible.
* @discussion Subclasses may use this to monitor when they become visible.
* @note This method is guaranteed to be called on main.
*/
- (void)didEnterVisibleState;
/**
* @abstract Called whenever the node is no longer visible.
* @discussion Subclasses may use this to monitor when they are no longer visible.
* @note This method is guaranteed to be called on main.
*/
- (void)didExitVisibleState;
/**
* @abstract Called whenever the the node has entered the display state.
* @discussion Subclasses may use this to monitor when a node should be rendering its content.
* @note This method is guaranteed to be called on main.
*/
- (void)didEnterDisplayState;
/**
* @abstract Called whenever the the node has exited the display state.
* @discussion Subclasses may use this to monitor when a node should no longer be rendering its content.
* @note This method is guaranteed to be called on main.
*/
- (void)didExitDisplayState;
/**
* @abstract Called whenever the the node has entered the preload state.
* @discussion Subclasses may use this to monitor data for a node should be preloaded, either from a local or remote source.
* @note This method is guaranteed to be called on main.
*/
- (void)didEnterPreloadState;
/**
* @abstract Called whenever the the node has exited the preload state.
* @discussion Subclasses may use this to monitor whether preloading data for a node should be canceled.
* @note This method is guaranteed to be called on main.
*/
- (void)didExitPreloadState;
/**
* @abstract Called when the node has completed applying the layout.
* @discussion Can be used for operations that are performed after layout has completed.
* @note This method is guaranteed to be called on main.
*/
- (void)nodeDidLayout;
/**
* @abstract Called when the node loads.
* @discussion Can be used for operations that are performed after the node's view is available.
* @note This method is guaranteed to be called on main.
*/
- (void)nodeDidLoad;
/**
* @abstract Indicates that the receiver and all subnodes have finished displaying.
* @discussion May be called more than once, for example if the receiver has a network image node.
* This is called after the first display pass even if network image nodes have not downloaded anything
* (text would be done, and other nodes that are ready to do their final display). Each render of
* every progressive jpeg network node would cause this to be called, so this hook could be called up to
* 1 + (pJPEGcount * pJPEGrenderCount) times. The render count depends on how many times the downloader calls
* the progressImage block.
* @note This method is guaranteed to be called on main.
*/
- (void)hierarchyDisplayDidFinish;
@optional
/**
* @abstract Called when the node is about to calculate layout. This is only called before
* Yoga-driven layouts.
* @discussion Can be used for operations that are performed after the node's view is available.
* @note This method is guaranteed to be called on main, but implementations should be careful not
* to attempt to ascend the node tree when handling this, as the root node is locked when this is
* called.
*/
- (void)nodeWillCalculateLayout:(ASSizeRange)constrainedSize;
@end
@@ -0,0 +1,67 @@
//
// ASDisplayNode+LayoutSpec.h
// Texture
//
// Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASDimension.h>
@class ASLayout;
NS_ASSUME_NONNULL_BEGIN
@interface ASDisplayNode (ASLayoutSpec)
/**
* @abstract Provides a way to declare a block to provide an ASLayoutSpec without having to subclass ASDisplayNode and
* implement layoutSpecThatFits:
*
* @return A block that takes a constrainedSize ASSizeRange argument, and must return an ASLayoutSpec that includes all
* of the subnodes to position in the layout. This input-output relationship is identical to the subclass override
* method -layoutSpecThatFits:
*
* @warning Subclasses that implement -layoutSpecThatFits: must not also use .layoutSpecBlock. Doing so will trigger
* an exception. A future version of the framework may support using both, calling them serially, with the
* .layoutSpecBlock superseding any values set by the method override.
*
* @code ^ASLayoutSpec *(__kindof ASDisplayNode * _Nonnull node, ASSizeRange constrainedSize) {};
*/
@property (nullable) ASLayoutSpecBlock layoutSpecBlock;
@end
// These methods are intended to be used internally to Texture, and should not be called directly.
@interface ASDisplayNode (ASLayoutSpecPrivate)
/// For internal usage only
- (ASLayout *)calculateLayoutLayoutSpec:(ASSizeRange)constrainedSize;
@end
@interface ASDisplayNode (ASLayoutSpecSubclasses)
/**
* @abstract Return a layout spec that describes the layout of the receiver and its children.
*
* @param constrainedSize The minimum and maximum sizes the receiver should fit in.
*
* @discussion Subclasses that override should expect this method to be called on a non-main thread. The returned layout spec
* is used to calculate an ASLayout and cached by ASDisplayNode for quick access during -layout. Other expensive work that needs to
* be done before display can be performed here, and using ivars to cache any valuable intermediate results is
* encouraged.
*
* @note This method should not be called directly outside of ASDisplayNode; use -layoutThatFits: instead.
*
* @warning Subclasses that implement -layoutSpecThatFits: must not use .layoutSpecBlock. Doing so will trigger an
* exception. A future version of the framework may support using both, calling them serially, with the .layoutSpecBlock
* superseding any values set by the method override.
*/
- (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize;
@end
NS_ASSUME_NONNULL_END
@@ -0,0 +1,493 @@
//
// ASDisplayNode+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 <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASBlockTypes.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <AsyncDisplayKit/ASDisplayNode+LayoutSpec.h>
@class ASLayoutSpec, _ASDisplayLayer;
NS_ASSUME_NONNULL_BEGIN
/**
* The subclass header _ASDisplayNode+Subclasses_ defines the following methods that either must or can be overriden by
* subclasses of ASDisplayNode.
*
* These methods should never be called directly by other classes.
*
* ## Drawing
*
* Implement one of +displayWithParameters:isCancelled: or +drawRect:withParameters:isCancelled: to provide
* drawing for your node.
*
* Use -drawParametersForAsyncLayer: to copy any properties that are involved in drawing into an immutable object for
* use on the display queue. The display and drawRect implementations *MUST* be thread-safe, as they can be called on
* the displayQueue (asynchronously) or the main thread (synchronously/displayImmediately).
*
* Class methods that require passing in copies of the values are used to minimize the need for locking around instance
* variable access, and the possibility of the asynchronous display pass grabbing an inconsistent state across multiple
* variables.
*/
@interface ASDisplayNode (Subclassing) <ASInterfaceStateDelegate>
#pragma mark - Properties
/** @name Properties */
/**
* @abstract Return the calculated layout.
*
* @discussion For node subclasses that implement manual layout (e.g., they have a custom -layout method),
* calculatedLayout may be accessed on subnodes to retrieved cached information about their size.
* This allows -layout to be very fast, saving time on the main thread.
* Note: .calculatedLayout will only be set for nodes that have had -layoutThatFits: called on them.
* For manual layout, make sure you call -layoutThatFits: in your implementation of -calculateSizeThatFits:.
*
* For node subclasses that use automatic layout (e.g., they implement -layoutSpecThatFits:),
* it is typically not necessary to use .calculatedLayout at any point. For these nodes,
* the ASLayoutSpec implementation will automatically call -layoutThatFits: on all of the subnodes,
* and the ASDisplayNode base class implementation of -layout will automatically make use of .calculatedLayout on the subnodes.
*
* @return Layout that wraps calculated size returned by -calculateSizeThatFits: (in manual layout mode),
* or layout already calculated from layout spec returned by -layoutSpecThatFits: (in automatic layout mode).
*
* @warning Subclasses must not override this; it returns the last cached layout and is never expensive.
*/
@property (nullable, readonly) ASLayout *calculatedLayout;
#pragma mark - View Lifecycle
/** @name View Lifecycle */
/**
* @abstract Called on the main thread immediately after self.view is created.
*
* @discussion This is the best time to add gesture recognizers to the view.
*/
AS_CATEGORY_IMPLEMENTABLE
- (void)didLoad ASDISPLAYNODE_REQUIRES_SUPER;
/**
* An empty method that you can implement in a category to add global
* node initialization behavior. This method will be called by [ASDisplayNode init].
*/
AS_CATEGORY_IMPLEMENTABLE
- (void)baseDidInit;
/**
* An empty method that you can implement in a category to add global
* node deallocation behavior. This method will be called by [ASDisplayNode dealloc].
*/
AS_CATEGORY_IMPLEMENTABLE
- (void)baseWillDealloc;
#pragma mark - Layout
/** @name Layout */
/**
* @abstract Called on the main thread by the view's -layoutSubviews.
*
* @discussion Subclasses override this method to layout all subnodes or subviews.
*/
- (void)layout ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Called on the main thread by the view's -layoutSubviews, after -layout.
*
* @discussion Gives a chance for subclasses to perform actions after the subclass and superclass have finished laying
* out.
*/
AS_CATEGORY_IMPLEMENTABLE
- (void)layoutDidFinish ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Called on a background thread if !isNodeLoaded - called on the main thread if isNodeLoaded.
*
* @discussion When the .calculatedLayout property is set to a new ASLayout (directly from -calculateLayoutThatFits: or
* calculated via use of -layoutSpecThatFits:), subclasses may inspect it here.
*/
AS_CATEGORY_IMPLEMENTABLE
- (void)calculatedLayoutDidChange ASDISPLAYNODE_REQUIRES_SUPER;
#pragma mark - Layout calculation
/** @name Layout calculation */
/**
* @abstract Calculate a layout based on given size range.
*
* @param constrainedSize The minimum and maximum sizes the receiver should fit in.
*
* @return An ASLayout instance defining the layout of the receiver (and its children, if the box layout model is used).
*
* @discussion This method is called on a non-main thread. The default implementation calls either -layoutSpecThatFits:
* or -calculateSizeThatFits:, whichever method is overriden. Subclasses rarely need to override this method,
* override -layoutSpecThatFits: or -calculateSizeThatFits: instead.
*
* @note This method should not be called directly outside of ASDisplayNode; use -layoutThatFits: or -calculatedLayout instead.
*/
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize;
/**
* ASDisplayNode's implementation of -layoutThatFits:parentSize: calls this method to resolve the node's size
* against parentSize, intersect it with constrainedSize, and call -calculateLayoutThatFits: with the result.
*
* In certain advanced cases, you may want to customize this logic. Overriding this method allows you to receive all
* three parameters and do the computation yourself.
*
* @warning Overriding this method should be done VERY rarely.
*/
- (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize
restrictedToSize:(ASLayoutElementSize)size
relativeToParentSize:(CGSize)parentSize;
/**
* @abstract Return the calculated size.
*
* @param constrainedSize The maximum size the receiver should fit in.
*
* @discussion Subclasses that override should expect this method to be called on a non-main thread. The returned size
* is wrapped in an ASLayout and cached for quick access during -layout. Other expensive work that needs to
* be done before display can be performed here, and using ivars to cache any valuable intermediate results is
* encouraged.
*
* @note Subclasses that override are committed to manual layout. Therefore, -layout: must be overriden to layout all subnodes or subviews.
*
* @note This method should not be called directly outside of ASDisplayNode; use -layoutThatFits: or layoutThatFits:parentSize: instead.
*/
- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize;
/**
* @abstract Invalidate previously measured and cached layout.
*
* @discussion Subclasses should call this method to invalidate the previously measured and cached layout for the display
* node, when the contents of the node change in such a way as to require measuring it again.
*/
- (void)invalidateCalculatedLayout;
#pragma mark - Observing Node State Changes
/** @name Observing node state changes */
/**
* Declare <ASInterfaceStateDelegate> methods as requiring super calls (this can't be required in the protocol).
* For descriptions, see <ASInterfaceStateDelegate> definition.
*/
AS_CATEGORY_IMPLEMENTABLE
- (void)didEnterVisibleState ASDISPLAYNODE_REQUIRES_SUPER;
AS_CATEGORY_IMPLEMENTABLE
- (void)didExitVisibleState ASDISPLAYNODE_REQUIRES_SUPER;
AS_CATEGORY_IMPLEMENTABLE
- (void)didEnterDisplayState ASDISPLAYNODE_REQUIRES_SUPER;
AS_CATEGORY_IMPLEMENTABLE
- (void)didExitDisplayState ASDISPLAYNODE_REQUIRES_SUPER;
AS_CATEGORY_IMPLEMENTABLE
- (void)didEnterPreloadState ASDISPLAYNODE_REQUIRES_SUPER;
AS_CATEGORY_IMPLEMENTABLE
- (void)didExitPreloadState ASDISPLAYNODE_REQUIRES_SUPER;
AS_CATEGORY_IMPLEMENTABLE
- (void)interfaceStateDidChange:(ASInterfaceState)newState
fromState:(ASInterfaceState)oldState ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Called when the node's ASTraitCollection changes
*
* @discussion Subclasses can override this method to react to a trait collection change.
*/
AS_CATEGORY_IMPLEMENTABLE
- (void)asyncTraitCollectionDidChange;
#pragma mark - Drawing
/** @name Drawing */
/**
* @summary Delegate method to draw layer contents into a CGBitmapContext. The current UIGraphics context will be set
* to an appropriate context.
*
* @param bounds Region to draw in.
* @param parameters An object describing all of the properties you need to draw. Return this from
* -drawParametersForAsyncLayer:
* @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid
* unnecessary work. A return value of YES means cancel drawing and return.
* @param isRasterizing YES if the layer is being rasterized into another layer, in which case drawRect: probably wants
* to avoid doing things like filling its bounds with a zero-alpha color to clear the backing store.
*
* @note Called on the display queue and/or main queue (MUST BE THREAD SAFE)
*/
/*+ (void)drawRect:(CGRect)bounds withParameters:(nullable id)parameters
isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock
isRasterizing:(BOOL)isRasterizing;*/
/**
* @summary Delegate override to provide new layer contents as a UIImage.
*
* @param parameters An object describing all of the properties you need to draw. Return this from
* -drawParametersForAsyncLayer:
* @param isCancelledBlock Execute this block to check whether the current drawing operation has been cancelled to avoid
* unnecessary work. A return value of YES means cancel drawing and return.
*
* @return A UIImage with contents that are ready to display on the main thread. Make sure that the image is already
* decoded before returning it here.
*
* @note Called on the display queue and/or main queue (MUST BE THREAD SAFE)
*/
+ (nullable UIImage *)displayWithParameters:(nullable id)parameters
isCancelled:(AS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock;
/**
* @abstract Delegate override for drawParameters
*
* @param layer The layer that will be drawn into.
*
* @note Called on the main thread only
*/
- (nullable id<NSObject>)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer;
/**
* @abstract Indicates that the receiver is about to display.
*
* @discussion Deprecated in 2.5.
*
* @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) is
* about to begin.
*
* @note Called on the main thread only
*/
- (void)displayWillStart ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use displayWillStartAsynchronously: instead.");
/**
* @abstract Indicates that the receiver is about to display.
*
* @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) is
* about to begin.
*
* @note Called on the main thread only
*/
- (void)displayWillStartAsynchronously:(BOOL)asynchronously ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Indicates that the receiver has finished displaying.
*
* @discussion Subclasses may override this method to be notified when display (asynchronous or synchronous) has
* completed.
*
* @note Called on the main thread only
*/
- (void)displayDidFinish ASDISPLAYNODE_REQUIRES_SUPER;
/**
* Called just before the view is added to a window.
*/
- (void)willEnterHierarchy ASDISPLAYNODE_REQUIRES_SUPER;
/**
* Called after the view is removed from the window.
*/
- (void)didExitHierarchy ASDISPLAYNODE_REQUIRES_SUPER;
/**
* Called just after the view is added to a window.
* Note: this may be called multiple times during view controller transitions. To overcome this: use didEnterVisibleState or its equavalents.
*/
- (void)didEnterHierarchy ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Whether the view or layer of this display node is currently in a window
*/
@property (readonly, getter=isInHierarchy) BOOL inHierarchy;
/**
* Provides an opportunity to clear backing store and other memory-intensive intermediates, such as text layout managers
* on the current node.
*
* @discussion Called by -recursivelyClearContents. Always called on main thread. Base class implements self.contents = nil, clearing any backing
* store, for asynchronous regeneration when needed.
*/
- (void)clearContents ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Indicates that the receiver is about to display its subnodes. This method is not called if there are no
* subnodes present.
*
* @param subnode The subnode of which display is about to begin.
*
* @discussion Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) is
* about to begin.
*/
- (void)subnodeDisplayWillStart:(ASDisplayNode *)subnode ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Indicates that the receiver is finished displaying its subnodes. This method is not called if there are
* no subnodes present.
*
* @param subnode The subnode of which display is about to completed.
*
* @discussion Subclasses may override this method to be notified when subnode display (asynchronous or synchronous) has
* completed.
*/
- (void)subnodeDisplayDidFinish:(ASDisplayNode *)subnode ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Marks the receiver's bounds as needing to be redrawn, with a scale value.
*
* @param contentsScale The scale at which the receiver should be drawn.
*
* @discussion Subclasses should override this if they don't want their contentsScale changed.
*
* @note This changes an internal property.
* -setNeedsDisplay is also available to trigger display without changing contentsScaleForDisplay.
* @see -setNeedsDisplay, contentsScaleForDisplay
*/
- (void)setNeedsDisplayAtScale:(CGFloat)contentsScale;
/**
* @abstract Recursively calls setNeedsDisplayAtScale: on subnodes.
*
* @param contentsScale The scale at which the receiver's subnode hierarchy should be drawn.
*
* @discussion Subclasses may override this if they require modifying the scale set on their child nodes.
*
* @note Only the node tree is walked, not the view or layer trees.
*
* @see setNeedsDisplayAtScale:
* @see contentsScaleForDisplay
*/
- (void)recursivelySetNeedsDisplayAtScale:(CGFloat)contentsScale;
/**
* @abstract The scale factor to apply to the rendering.
*
* @discussion Use setNeedsDisplayAtScale: to set a value and then after display, the display node will set the layer's
* contentsScale. This is to prevent jumps when re-rasterizing at a different contentsScale.
* Read this property if you need to know the future contentsScale of your layer, eg in drawParameters.
*
* @see setNeedsDisplayAtScale:
*/
@property (readonly) CGFloat contentsScaleForDisplay;
#pragma mark - Touch handling
/** @name Touch handling */
/**
* @abstract Tells the node when touches began in its view.
*
* @param touches A set of UITouch instances.
* @param event A UIEvent associated with the touch.
*/
- (void)touchesBegan:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Tells the node when touches moved in its view.
*
* @param touches A set of UITouch instances.
* @param event A UIEvent associated with the touch.
*/
- (void)touchesMoved:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Tells the node when touches ended in its view.
*
* @param touches A set of UITouch instances.
* @param event A UIEvent associated with the touch.
*/
- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER;
/**
* @abstract Tells the node when touches was cancelled in its view.
*
* @param touches A set of UITouch instances.
* @param event A UIEvent associated with the touch.
*/
- (void)touchesCancelled:(nullable NSSet<UITouch *> *)touches withEvent:(nullable UIEvent *)event ASDISPLAYNODE_REQUIRES_SUPER;
#pragma mark - Managing Gesture Recognizers
/** @name Managing Gesture Recognizers */
/**
* @abstract Asks the node if a gesture recognizer should continue tracking touches.
*
* @param gestureRecognizer A gesture recognizer trying to recognize a gesture.
*/
- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer;
#pragma mark - Hit Testing
/** @name Hit Testing */
/**
* @abstract Returns the view that contains the point.
*
* @discussion Override to make this node respond differently to touches: (e.g. hide touches from subviews, send all
* touches to certain subviews (hit area maximizing), etc.)
*
* @param point A point specified in the node's local coordinate system (bounds).
* @param event The event that warranted a call to this method.
*
* @return Returns a UIView, not ASDisplayNode, for two reasons:
* 1) allows sending events to plain UIViews that don't have attached nodes,
* 2) hitTest: is never called before the views are created.
*/
- (nullable UIView *)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;
#pragma mark - Placeholders
/** @name Placeholders */
/**
* @abstract Optionally provide an image to serve as the placeholder for the backing store while the contents are being
* displayed.
*
* @discussion
* Subclasses may override this method and return an image to use as the placeholder. Take caution as there may be a
* time and place where this method is called on a background thread. Note that -[UIImage imageNamed:] is not thread
* safe when using image assets.
*
* To retrieve the CGSize to do any image drawing, use the node's calculatedSize property.
*
* Defaults to nil.
*
* @note Called on the display queue and/or main queue (MUST BE THREAD SAFE)
*/
- (nullable UIImage *)placeholderImage;
#pragma mark - Description
/** @name Description */
/**
* @abstract Return a description of the node
*
* @discussion The function that gets called for each display node in -recursiveDescription
*/
- (NSString *)descriptionForRecursiveDescription;
@end
// Check that at most a layoutSpecBlock or one of the three layout methods is overridden
#define __ASDisplayNodeCheckForLayoutMethodOverrides \
ASDisplayNodeAssert(_layoutSpecBlock != NULL || \
((ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateSizeThatFits:)) ? 1 : 0) \
+ (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(layoutSpecThatFits:)) ? 1 : 0) \
+ (ASDisplayNodeSubclassOverridesSelector(self.class, @selector(calculateLayoutThatFits:)) ? 1 : 0)) <= 1, \
@"Subclass %@ must at least provide a layoutSpecBlock or override at most one of the three layout methods: calculateLayoutThatFits:, layoutSpecThatFits:, or calculateSizeThatFits:", NSStringFromClass(self.class))
#define ASDisplayNodeAssertThreadAffinity(viewNode) ASDisplayNodeAssert(!viewNode || ASMainThreadAssertionsAreDisabled() || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity - this method should not be called off the main thread after the ASDisplayNode's view or layer have been created")
#define ASDisplayNodeCAssertThreadAffinity(viewNode) ASDisplayNodeCAssert(!viewNode || ASMainThreadAssertionsAreDisabled() || ASDisplayNodeThreadIsMain() || !(viewNode).nodeLoaded, @"Incorrect display node thread affinity - this method should not be called off the main thread after the ASDisplayNode's view or layer have been created")
NS_ASSUME_NONNULL_END
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,213 @@
//
// ASDisplayNodeExtras.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 <QuartzCore/QuartzCore.h>
#import <UIKit/UIKit.h>
#import <AsyncDisplayKit/ASBaseDefines.h>
#import <AsyncDisplayKit/ASDisplayNode.h>
/**
* Sets the debugName field for these nodes to the given symbol names, within the domain of "self.class"
* For instance, in `MYButtonNode` if you call `ASSetDebugNames(self.titleNode, _countNode)` the debug names
* for the nodes will be set to `MYButtonNode.titleNode` and `MYButtonNode.countNode`.
*/
#if DEBUG
#define ASSetDebugName(node, format, ...) node.debugName = [NSString stringWithFormat:format, __VA_ARGS__]
#define ASSetDebugNames(...) _ASSetDebugNames(self.class, @"" # __VA_ARGS__, __VA_ARGS__, nil)
#else
#define ASSetDebugName(node, format, ...)
#define ASSetDebugNames(...)
#endif
NS_ASSUME_NONNULL_BEGIN
/// For deallocation of objects on the main thread across multiple run loops.
AS_EXTERN void ASPerformMainThreadDeallocation(id _Nullable __strong * _Nonnull objectPtr);
// Because inline methods can't be extern'd and need to be part of the translation unit of code
// that compiles with them to actually inline, we both declare and define these in the header.
ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesVisible(ASInterfaceState interfaceState)
{
return ((interfaceState & ASInterfaceStateVisible) == ASInterfaceStateVisible);
}
ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesDisplay(ASInterfaceState interfaceState)
{
return ((interfaceState & ASInterfaceStateDisplay) == ASInterfaceStateDisplay);
}
ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesPreload(ASInterfaceState interfaceState)
{
return ((interfaceState & ASInterfaceStatePreload) == ASInterfaceStatePreload);
}
ASDISPLAYNODE_INLINE BOOL ASInterfaceStateIncludesMeasureLayout(ASInterfaceState interfaceState)
{
return ((interfaceState & ASInterfaceStateMeasureLayout) == ASInterfaceStateMeasureLayout);
}
__unused static NSString * NSStringFromASInterfaceState(ASInterfaceState interfaceState)
{
NSMutableArray *states = [NSMutableArray array];
if (interfaceState == ASInterfaceStateNone) {
[states addObject:@"No state"];
}
if (ASInterfaceStateIncludesMeasureLayout(interfaceState)) {
[states addObject:@"MeasureLayout"];
}
if (ASInterfaceStateIncludesPreload(interfaceState)) {
[states addObject:@"Preload"];
}
if (ASInterfaceStateIncludesDisplay(interfaceState)) {
[states addObject:@"Display"];
}
if (ASInterfaceStateIncludesVisible(interfaceState)) {
[states addObject:@"Visible"];
}
return [NSString stringWithFormat:@"{ %@ }", [states componentsJoinedByString:@" | "]];
}
#define INTERFACE_STATE_DELTA(Name) ({ \
if ((oldState & ASInterfaceState##Name) != (newState & ASInterfaceState##Name)) { \
[changes appendFormat:@"%c%s ", (newState & ASInterfaceState##Name ? '+' : '-'), #Name]; \
} \
})
/// e.g. { +Visible, -Preload } (although that should never actually happen.)
/// NOTE: Changes to MeasureLayout state don't really mean anything so we omit them for now.
__unused static NSString *NSStringFromASInterfaceStateChange(ASInterfaceState oldState, ASInterfaceState newState)
{
if (oldState == newState) {
return @"{ }";
}
NSMutableString *changes = [NSMutableString stringWithString:@"{ "];
INTERFACE_STATE_DELTA(Preload);
INTERFACE_STATE_DELTA(Display);
INTERFACE_STATE_DELTA(Visible);
[changes appendString:@"}"];
return changes;
}
#undef INTERFACE_STATE_DELTA
/**
Returns the appropriate interface state for a given ASDisplayNode and window
*/
AS_EXTERN ASInterfaceState ASInterfaceStateForDisplayNode(ASDisplayNode *displayNode, UIWindow *window) AS_WARN_UNUSED_RESULT;
/**
Given a layer, returns the associated display node, if any.
*/
AS_EXTERN ASDisplayNode * _Nullable ASLayerToDisplayNode(CALayer * _Nullable layer) AS_WARN_UNUSED_RESULT;
/**
Given a view, returns the associated display node, if any.
*/
AS_EXTERN ASDisplayNode * _Nullable ASViewToDisplayNode(UIView * _Nullable view) AS_WARN_UNUSED_RESULT;
/**
Given a node, returns the root of the node hierarchy (where supernode == nil)
*/
AS_EXTERN ASDisplayNode *ASDisplayNodeUltimateParentOfNode(ASDisplayNode *node) AS_WARN_UNUSED_RESULT;
/**
If traverseSublayers == YES, this function will walk the layer hierarchy, spanning discontinuous sections of the node hierarchy\
(e.g. the layers of UIKit intermediate views in UIViewControllers, UITableView, UICollectionView).
In the event that a node's backing layer is not created yet, the function will only walk the direct subnodes instead
of forcing the layer hierarchy to be created.
*/
AS_EXTERN void ASDisplayNodePerformBlockOnEveryNode(CALayer * _Nullable layer, ASDisplayNode * _Nullable node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node));
/**
This function will walk the node hierarchy in a breadth first fashion. It does run the block on the node provided
directly to the function call. It does NOT traverse sublayers.
*/
AS_EXTERN void ASDisplayNodePerformBlockOnEveryNodeBFS(ASDisplayNode *node, void(^block)(ASDisplayNode *node));
/**
Identical to ASDisplayNodePerformBlockOnEveryNode, except it does not run the block on the
node provided directly to the function call - only on all descendants.
*/
AS_EXTERN void ASDisplayNodePerformBlockOnEverySubnode(ASDisplayNode *node, BOOL traverseSublayers, void(^block)(ASDisplayNode *node));
/**
Given a display node, traverses up the layer tree hierarchy, returning the first display node that passes block.
*/
AS_EXTERN ASDisplayNode * _Nullable ASDisplayNodeFindFirstSupernode(ASDisplayNode * _Nullable node, BOOL (^block)(ASDisplayNode *node)) AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use the `supernodes` property instead.");
/**
Given a display node, traverses up the layer tree hierarchy, returning the first display node of kind class.
*/
AS_EXTERN __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use the `supernodeOfClass:includingSelf:` method instead.");
/**
* Given a layer, find the window it lives in, if any.
*/
AS_EXTERN UIWindow * _Nullable ASFindWindowOfLayer(CALayer *layer) AS_WARN_UNUSED_RESULT;
/**
* Given a layer, find the closest view it lives in, if any.
*/
AS_EXTERN UIView * _Nullable ASFindClosestViewOfLayer(CALayer *layer) AS_WARN_UNUSED_RESULT;
/**
* Given two nodes, finds their most immediate common parent. Used for geometry conversion methods.
* NOTE: It is an error to try to convert between nodes which do not share a common ancestor. This behavior is
* disallowed in UIKit documentation and the behavior is left undefined. The output does not have a rigorously defined
* failure mode (i.e. returning CGPointZero or returning the point exactly as passed in). Rather than track the internal
* undefined and undocumented behavior of UIKit in ASDisplayNode, this operation is defined to be incorrect in all
* circumstances and must be fixed wherever encountered.
*/
AS_EXTERN ASDisplayNode * _Nullable ASDisplayNodeFindClosestCommonAncestor(ASDisplayNode *node1, ASDisplayNode *node2) AS_WARN_UNUSED_RESULT;
/**
Given a display node, collects all descendants. This is a specialization of ASCollectContainer() that walks the Core Animation layer tree as opposed to the display node tree, thus supporting non-continues display node hierarchies.
*/
AS_EXTERN NSArray<ASDisplayNode *> *ASCollectDisplayNodes(ASDisplayNode *node) AS_WARN_UNUSED_RESULT;
/**
Given a display node, traverses down the node hierarchy, returning all the display nodes that pass the block.
*/
AS_EXTERN NSArray<ASDisplayNode *> *ASDisplayNodeFindAllSubnodes(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)) AS_WARN_UNUSED_RESULT;
/**
Given a display node, traverses down the node hierarchy, returning all the display nodes of kind class.
*/
AS_EXTERN NSArray<__kindof ASDisplayNode *> *ASDisplayNodeFindAllSubnodesOfClass(ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT;
/**
Given a display node, traverses down the node hierarchy, returning the depth-first display node, including the start node that pass the block.
*/
AS_EXTERN __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstNode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)) AS_WARN_UNUSED_RESULT;
/**
Given a display node, traverses down the node hierarchy, returning the depth-first display node, excluding the start node, that pass the block
*/
AS_EXTERN __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSubnode(ASDisplayNode *start, BOOL (^block)(ASDisplayNode *node)) AS_WARN_UNUSED_RESULT;
/**
Given a display node, traverses down the node hierarchy, returning the depth-first display node of kind class.
*/
AS_EXTERN __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSubnodeOfClass(ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT;
AS_EXTERN UIColor *ASDisplayNodeDefaultPlaceholderColor(void) AS_WARN_UNUSED_RESULT;
AS_EXTERN UIColor *ASDisplayNodeDefaultTintColor(void) AS_WARN_UNUSED_RESULT;
/**
Disable willAppear / didAppear / didDisappear notifications for a sub-hierarchy, then re-enable when done. Nested calls are supported.
*/
AS_EXTERN void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node);
AS_EXTERN void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node);
// Not to be called directly.
AS_EXTERN void _ASSetDebugNames(Class owningClass, NSString *names, ASDisplayNode * _Nullable object, ...);
NS_ASSUME_NONNULL_END
@@ -0,0 +1,229 @@
//
// ASEditableTextNode.h
// Texture
//
// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved.
// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved.
// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0
//
#import <AsyncDisplayKit/ASDisplayNode.h>
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
@protocol ASEditableTextNodeDelegate;
@class ASTextKitComponents;
@interface ASEditableTextNodeTargetForAction: NSObject
@property (nonatomic, strong, readonly) id _Nullable target;
- (instancetype)initWithTarget:(id _Nullable)target;
@end
/**
@abstract Implements a node that supports text editing.
@discussion Does not support layer backing.
*/
@interface ASEditableTextNode : ASDisplayNode <UITextInputTraits>
/**
* @abstract Initializes an editable text node using default TextKit components.
*
* @return An initialized ASEditableTextNode.
*/
- (instancetype)init;
/**
* @abstract Initializes an editable text node using the provided TextKit components.
*
* @param textKitComponents The TextKit stack used to render text.
* @param placeholderTextKitComponents The TextKit stack used to render placeholder text.
*
* @return An initialized ASEditableTextNode.
*/
- (instancetype)initWithTextKitComponents:(ASTextKitComponents *)textKitComponents
placeholderTextKitComponents:(ASTextKitComponents *)placeholderTextKitComponents;
//! @abstract The text node's delegate, which must conform to the <ASEditableTextNodeDelegate> protocol.
@property (nullable, weak) id <ASEditableTextNodeDelegate> delegate;
#pragma mark - Configuration
/**
@abstract Enable scrolling on the textView
@default true
*/
@property (nonatomic) BOOL scrollEnabled;
@property (nonatomic, strong) UIFont *baseFont;
/**
@abstract Access to underlying UITextView for more configuration options.
@warning This property should only be used on the main thread and should not be accessed before the editable text node's view is created.
*/
@property (nonatomic, readonly) UITextView *textView;
//! @abstract The attributes to apply to new text being entered by the user.
@property (nullable, nonatomic, copy) NSDictionary<NSString *, id> *typingAttributes;
//! @abstract The range of text currently selected. If length is zero, the range is the cursor location.
@property NSRange selectedRange;
@property (readonly) CGRect selectionRect;
#pragma mark - Placeholder
/**
@abstract Indicates if the receiver is displaying the placeholder text.
@discussion To update the placeholder, see the <attributedPlaceholderText> property.
@result YES if the placeholder is currently displayed; NO otherwise.
*/
- (BOOL)isDisplayingPlaceholder AS_WARN_UNUSED_RESULT;
/**
@abstract The styled placeholder text displayed by the text node while no text is entered
@discussion The placeholder is displayed when the user has not entered any text and the keyboard is not visible.
*/
@property (nullable, nonatomic, copy) NSAttributedString *attributedPlaceholderText;
#pragma mark - Modifying User Text
/**
@abstract The styled text displayed by the receiver.
@discussion When the placeholder is displayed (as indicated by -isDisplayingPlaceholder), this value is nil. Otherwise, this value is the attributed text the user has entered. This value can be modified regardless of whether the receiver is the first responder (and thus, editing) or not. Changing this value from nil to non-nil will result in the placeholder being hidden, and the new value being displayed.
*/
@property (nullable, nonatomic, copy) NSAttributedString *attributedText;
#pragma mark - Managing The Keyboard
//! @abstract The text input mode used by the receiver's keyboard, if it is visible. This value is undefined if the receiver is not the first responder.
@property (nonatomic, readonly) UITextInputMode *textInputMode;
/**
@abstract The textContainerInset of both the placeholder and typed textView. This value defaults to UIEdgeInsetsZero.
*/
@property (nonatomic) UIEdgeInsets textContainerInset;
/**
@abstract The maximum number of lines to display. Additional lines will require scrolling.
@default 0 (No limit)
*/
@property (nonatomic) NSUInteger maximumLinesToDisplay;
/**
@abstract Indicates whether the receiver's text view is the first responder, and thus has the keyboard visible and is prepared for editing by the user.
@result YES if the receiver's text view is the first-responder; NO otherwise.
*/
- (BOOL)isFirstResponder AS_WARN_UNUSED_RESULT;
//! @abstract Makes the receiver's text view the first responder.
- (BOOL)becomeFirstResponder;
//! @abstract Resigns the receiver's text view from first-responder status, if it has it.
- (BOOL)resignFirstResponder;
#pragma mark - Geometry
/**
@abstract Returns the frame of the given range of characters.
@param textRange A range of characters.
@discussion This method raises an exception if `textRange` is not a valid range of characters within the receiver's attributed text.
@result A CGRect that is the bounding box of the glyphs covered by the given range of characters, in the coordinate system of the receiver.
*/
- (CGRect)frameForTextRange:(NSRange)textRange AS_WARN_UNUSED_RESULT;
/**
@abstract <UITextInputTraits> properties.
*/
@property (nonatomic) UITextAutocapitalizationType autocapitalizationType; // default is UITextAutocapitalizationTypeSentences
@property (nonatomic) UITextAutocorrectionType autocorrectionType; // default is UITextAutocorrectionTypeDefault
@property (nonatomic) UITextSpellCheckingType spellCheckingType; // default is UITextSpellCheckingTypeDefault;
@property (nonatomic) UIKeyboardType keyboardType; // default is UIKeyboardTypeDefault
@property (nonatomic) UIKeyboardAppearance keyboardAppearance; // default is UIKeyboardAppearanceDefault
@property (nonatomic) UIReturnKeyType returnKeyType; // default is UIReturnKeyDefault (See note under UIReturnKeyType enum)
@property (nonatomic) BOOL enablesReturnKeyAutomatically; // default is NO (when YES, will automatically disable return key when text widget has zero-length contents, and will automatically enable when text widget has non-zero-length contents)
@property (nonatomic, getter=isSecureTextEntry) BOOL secureTextEntry; // default is NO
@property (nonatomic, strong) NSString * _Nullable initialPrimaryLanguage;
- (void)resetInitialPrimaryLanguage;
- (void)dropAutocorrection;
- (bool)isCurrentlyEmoji;
@end
@interface ASEditableTextNode (Unavailable)
- (instancetype)initWithLayerBlock:(ASDisplayNodeLayerBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE;
- (instancetype)initWithViewBlock:(ASDisplayNodeViewBlock)viewBlock didLoadBlock:(nullable ASDisplayNodeDidLoadBlock)didLoadBlock NS_UNAVAILABLE;
@end
#pragma mark -
/**
* The methods declared by the ASEditableTextNodeDelegate protocol allow the adopting delegate to
* respond to notifications such as began and finished editing, selection changed and text updated;
* and manage whether a specified text should be replaced.
*/
@protocol ASEditableTextNodeDelegate <NSObject>
@optional
/**
@abstract Asks the delegate if editing should begin for the text node.
@param editableTextNode An editable text node.
@discussion YES if editing should begin; NO if editing should not begin -- the default returns YES.
*/
- (BOOL)editableTextNodeShouldBeginEditing:(ASEditableTextNode *)editableTextNode;
/**
@abstract Indicates to the delegate that the text node began editing.
@param editableTextNode An editable text node.
@discussion The invocation of this method coincides with the keyboard animating to become visible.
*/
- (void)editableTextNodeDidBeginEditing:(ASEditableTextNode *)editableTextNode;
/**
@abstract Asks the delegate whether the specified text should be replaced in the editable text node.
@param editableTextNode An editable text node.
@param range The current selection range. If the length of the range is 0, range reflects the current insertion point. If the user presses the Delete key, the length of the range is 1 and an empty string object replaces that single character.
@param text The text to insert.
@discussion YES if the old text should be replaced by the new text; NO if the replacement operation should be aborted.
@result The text node calls this method whenever the user types a new character or deletes an existing character. Implementation of this method is optional -- the default implementation returns YES.
*/
- (BOOL)editableTextNode:(ASEditableTextNode *)editableTextNode shouldChangeTextInRange:(NSRange)range replacementText:(NSString *)text;
/**
@abstract Indicates to the delegate that the text node's selection has changed.
@param editableTextNode An editable text node.
@param fromSelectedRange The previously selected range.
@param toSelectedRange The current selected range. Equivalent to the <selectedRange> property.
@param dueToEditing YES if the selection change was due to editing; NO otherwise.
@discussion You can access the selection of the receiver via <selectedRange>.
*/
- (void)editableTextNodeDidChangeSelection:(ASEditableTextNode *)editableTextNode fromSelectedRange:(NSRange)fromSelectedRange toSelectedRange:(NSRange)toSelectedRange dueToEditing:(BOOL)dueToEditing;
/**
@abstract Indicates to the delegate that the text node's text was updated.
@param editableTextNode An editable text node.
@discussion This method is called each time the user updated the text node's text. It is not called for programmatic changes made to the text via the <attributedText> property.
*/
- (void)editableTextNodeDidUpdateText:(ASEditableTextNode *)editableTextNode;
/**
@abstract Indicates to the delegate that the text node has finished editing.
@param editableTextNode An editable text node.
@discussion The invocation of this method coincides with the keyboard animating to become hidden.
*/
- (void)editableTextNodeDidFinishEditing:(ASEditableTextNode *)editableTextNode;
- (BOOL)editableTextNodeShouldCopy:(ASEditableTextNode *)editableTextNode;
- (BOOL)editableTextNodeShouldPaste:(ASEditableTextNode *)editableTextNode;
- (ASEditableTextNodeTargetForAction * _Nullable)editableTextNodeTargetForAction:(SEL)action;
- (BOOL)editableTextNodeShouldReturn:(ASEditableTextNode *)editableTextNode;
- (void)editableTextNodeBackspaceWhileEmpty:(ASEditableTextNode *)editableTextNode;
- (UIMenu *)editableTextNodeMenu:(ASEditableTextNode *)editableTextNode forTextRange:(NSRange)textRange suggestedActions:(NSArray<UIMenuElement *> *)suggestedActions API_AVAILABLE(ios(16.0));
@end
NS_ASSUME_NONNULL_END

Some files were not shown because too many files have changed in this diff Show More