diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index f542dae64..262595b04 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -17,9 +17,7 @@ 052EE06B1A15A0D8002C6279 /* TestResources in Resources */ = {isa = PBXBuildFile; fileRef = 052EE06A1A15A0D8002C6279 /* TestResources */; }; 057D02C41AC0A66700C7AC3C /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = 057D02C31AC0A66700C7AC3C /* main.mm */; }; 057D02C71AC0A66700C7AC3C /* AppDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 057D02C61AC0A66700C7AC3C /* AppDelegate.mm */; }; - 058D09BE195D04C000B7D73C /* XCTest.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09BD195D04C000B7D73C /* XCTest.framework */; }; 058D09BF195D04C000B7D73C /* Foundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09AF195D04C000B7D73C /* Foundation.framework */; }; - 058D09C1195D04C000B7D73C /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 058D09C0195D04C000B7D73C /* UIKit.framework */; }; 058D09CA195D04C000B7D73C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 058D09C8195D04C000B7D73C /* InfoPlist.strings */; }; 058D0A38195D057000B7D73C /* ASDisplayLayerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A2D195D057000B7D73C /* ASDisplayLayerTests.mm */; }; 058D0A39195D057000B7D73C /* ASDisplayNodeAppearanceTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A2E195D057000B7D73C /* ASDisplayNodeAppearanceTests.mm */; }; @@ -30,6 +28,48 @@ 058D0A40195D057000B7D73C /* ASTextNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A36195D057000B7D73C /* ASTextNodeTests.mm */; }; 058D0A41195D057000B7D73C /* ASTextNodeWordKernerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 058D0A37195D057000B7D73C /* ASTextNodeWordKernerTests.mm */; }; 05EA6FE71AC0966E00E35788 /* ASSnapshotTestCase.mm in Sources */ = {isa = PBXBuildFile; fileRef = 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.mm */; }; + 0F8DC333258AC67B00E6266C /* ASViewController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC329258AC67A00E6266C /* ASViewController.mm */; }; + 0F8DC334258AC67B00E6266C /* ASDisplayNode+Yoga2.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC32A258AC67A00E6266C /* ASDisplayNode+Yoga2.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 0F8DC335258AC67B00E6266C /* ASDisplayNode+Yoga2Logging.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC32B258AC67A00E6266C /* ASDisplayNode+Yoga2Logging.h */; }; + 0F8DC337258AC67B00E6266C /* ASDisplayNode+Yoga2.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC32D258AC67A00E6266C /* ASDisplayNode+Yoga2.mm */; }; + 0F8DC339258AC67B00E6266C /* ASNodeContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC32F258AC67A00E6266C /* ASNodeContext.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0F8DC33A258AC67B00E6266C /* ASNodeContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC330258AC67B00E6266C /* ASNodeContext.mm */; }; + 0F8DC33C258AC67B00E6266C /* ASDisplayNode+Yoga2Logging.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC332258AC67B00E6266C /* ASDisplayNode+Yoga2Logging.mm */; }; + 0F8DC343258AC6F200E6266C /* ASNodeControllerInternal.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC340258AC6F100E6266C /* ASNodeControllerInternal.h */; }; + 0F8DC344258AC6F200E6266C /* ASImageNode+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC341258AC6F200E6266C /* ASImageNode+FrameworkPrivate.h */; }; + 0F8DC345258AC6F200E6266C /* ASNodeContext+Private.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC342258AC6F200E6266C /* ASNodeContext+Private.h */; settings = {ATTRIBUTES = (Private, ); }; }; + 0F8DC351258AC71200E6266C /* ASTextInput.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC349258AC71200E6266C /* ASTextInput.mm */; }; + 0F8DC352258AC71200E6266C /* ASTextInput.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC34A258AC71200E6266C /* ASTextInput.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0F8DC353258AC71200E6266C /* ASTextLine.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC34B258AC71200E6266C /* ASTextLine.h */; }; + 0F8DC354258AC71200E6266C /* ASTextLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC34C258AC71200E6266C /* ASTextLayout.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0F8DC355258AC71200E6266C /* ASTextLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC34D258AC71200E6266C /* ASTextLayout.mm */; }; + 0F8DC356258AC71200E6266C /* ASTextDebugOption.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC34E258AC71200E6266C /* ASTextDebugOption.h */; }; + 0F8DC357258AC71200E6266C /* ASTextLine.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC34F258AC71200E6266C /* ASTextLine.mm */; }; + 0F8DC358258AC71200E6266C /* ASTextDebugOption.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC350258AC71200E6266C /* ASTextDebugOption.mm */; }; + 0F8DC360258AC72300E6266C /* ASTextAttribute.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC35C258AC72300E6266C /* ASTextAttribute.mm */; }; + 0F8DC361258AC72300E6266C /* ASTextAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC35D258AC72300E6266C /* ASTextAttribute.h */; }; + 0F8DC362258AC72300E6266C /* ASTextRunDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC35E258AC72300E6266C /* ASTextRunDelegate.h */; }; + 0F8DC363258AC72300E6266C /* ASTextRunDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC35F258AC72300E6266C /* ASTextRunDelegate.mm */; }; + 0F8DC37D258AC74600E6266C /* ASTextDebugOption.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC369258AC74600E6266C /* ASTextDebugOption.mm */; }; + 0F8DC37E258AC74600E6266C /* ASTextDebugOption.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC36A258AC74600E6266C /* ASTextDebugOption.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0F8DC37F258AC74600E6266C /* ASTextLayout.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC36B258AC74600E6266C /* ASTextLayout.h */; }; + 0F8DC380258AC74600E6266C /* ASTextInput.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC36C258AC74600E6266C /* ASTextInput.h */; }; + 0F8DC381258AC74600E6266C /* ASTextLine.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC36D258AC74600E6266C /* ASTextLine.mm */; }; + 0F8DC382258AC74600E6266C /* ASTextLine.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC36E258AC74600E6266C /* ASTextLine.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0F8DC383258AC74600E6266C /* ASTextLayout.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC36F258AC74600E6266C /* ASTextLayout.mm */; }; + 0F8DC384258AC74600E6266C /* ASTextInput.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC370258AC74600E6266C /* ASTextInput.mm */; }; + 0F8DC385258AC74600E6266C /* ASTextAttribute.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC372258AC74600E6266C /* ASTextAttribute.mm */; }; + 0F8DC386258AC74600E6266C /* ASTextRunDelegate.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC373258AC74600E6266C /* ASTextRunDelegate.mm */; }; + 0F8DC387258AC74600E6266C /* ASTextAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC374258AC74600E6266C /* ASTextAttribute.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 0F8DC388258AC74600E6266C /* ASTextRunDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC375258AC74600E6266C /* ASTextRunDelegate.h */; }; + 0F8DC389258AC74600E6266C /* ASTextUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC377258AC74600E6266C /* ASTextUtilities.h */; }; + 0F8DC38A258AC74600E6266C /* NSParagraphStyle+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC378258AC74600E6266C /* NSParagraphStyle+ASText.h */; }; + 0F8DC38B258AC74600E6266C /* NSAttributedString+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC379258AC74600E6266C /* NSAttributedString+ASText.h */; }; + 0F8DC38C258AC74600E6266C /* NSAttributedString+ASText.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC37A258AC74600E6266C /* NSAttributedString+ASText.mm */; }; + 0F8DC38D258AC74600E6266C /* NSParagraphStyle+ASText.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC37B258AC74600E6266C /* NSParagraphStyle+ASText.mm */; }; + 0F8DC38E258AC74600E6266C /* ASTextUtilities.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC37C258AC74600E6266C /* ASTextUtilities.mm */; }; + 0F8DC397258AC77100E6266C /* ASLayoutElementStyleYoga.mm in Sources */ = {isa = PBXBuildFile; fileRef = 0F8DC393258AC77100E6266C /* ASLayoutElementStyleYoga.mm */; }; + 0F8DC398258AC77100E6266C /* ASLayoutElementStyleYoga.h in Headers */ = {isa = PBXBuildFile; fileRef = 0F8DC394258AC77100E6266C /* ASLayoutElementStyleYoga.h */; settings = {ATTRIBUTES = (Public, ); }; }; 18C2ED7F1B9B7DE800F627B3 /* ASCollectionNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 18C2ED831B9B7DE800F627B3 /* ASCollectionNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */; }; 1A6C000D1FAB4E2100D05926 /* ASCornerLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; @@ -66,6 +106,7 @@ 25E327571C16819500A2170C /* ASPagerNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 25E327541C16819500A2170C /* ASPagerNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 25E327591C16819500A2170C /* ASPagerNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 25E327551C16819500A2170C /* ASPagerNode.mm */; }; 2767E9411BB19BD600EA9B77 /* ASDKViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = ACC945A81BA9E7A0005E1FB8 /* ASDKViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; + 2875326E554776AAA50EF9A8 /* Pods_AsyncDisplayKit_AsyncDisplayKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = C6848AAC6137D25578CADC36 /* Pods_AsyncDisplayKit_AsyncDisplayKitTests.framework */; }; 2911485C1A77147A005D0878 /* ASControlNodeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2911485B1A77147A005D0878 /* ASControlNodeTests.mm */; }; 296A0A351A951ABF005ACEAA /* ASBatchFetchingTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 296A0A341A951ABF005ACEAA /* ASBatchFetchingTests.mm */; }; 29CDC2E21AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 29CDC2E11AAE70D000833CA4 /* ASBasicImageDownloaderContextTests.mm */; }; @@ -98,11 +139,9 @@ 3917EBD41E9C2FC400D04A01 /* _ASCollectionReusableView.h in Headers */ = {isa = PBXBuildFile; fileRef = 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */; settings = {ATTRIBUTES = (Private, ); }; }; 3917EBD51E9C2FC400D04A01 /* _ASCollectionReusableView.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.mm */; }; 3C9C128519E616EF00E942A0 /* ASTableViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */; }; - 407B8BAE2310E2ED00CB979E /* ASLayoutSpecUtilitiesTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 407B8BAD2310E2ED00CB979E /* ASLayoutSpecUtilitiesTests.mm */; }; 4080D66C2350384400CDC199 /* ASPINRemoteImageDownloader.h in Headers */ = {isa = PBXBuildFile; fileRef = 68355B391CB57A5A001D4E68 /* ASPINRemoteImageDownloader.h */; settings = {ATTRIBUTES = (Public, ); }; }; 471D04B1224CB98600649215 /* ASImageNodeBackingSizeTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 471D04B0224CB98600649215 /* ASImageNodeBackingSizeTests.mm */; }; 4E9127691F64157600499623 /* ASRunLoopQueueTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 4E9127681F64157600499623 /* ASRunLoopQueueTests.mm */; }; - 4EE3813FF44E20C399ACB962 /* Pods_AsyncDisplayKitTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5039B74209A895E07057081C /* Pods_AsyncDisplayKitTests.framework */; }; 509E68601B3AED8E009B9150 /* ASScrollDirection.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E111B371BD7007741D0 /* ASScrollDirection.mm */; }; 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */ = {isa = PBXBuildFile; fileRef = 205F0E171B37339C007741D0 /* ASAbstractLayoutController.h */; }; 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */ = {isa = PBXBuildFile; fileRef = 205F0E181B37339C007741D0 /* ASAbstractLayoutController.mm */; }; @@ -168,6 +207,9 @@ 7630FFA81C9E267E007A7C0E /* ASVideoNode.h in Headers */ = {isa = PBXBuildFile; fileRef = AEEC47DF1C20C2DD00EC1693 /* ASVideoNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 764D83D51C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h in Headers */ = {isa = PBXBuildFile; fileRef = 764D83D21C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h */; settings = {ATTRIBUTES = (Public, ); }; }; 767E7F8E1C90191D0066C000 /* AsyncDisplayKit+Debug.mm in Sources */ = {isa = PBXBuildFile; fileRef = 764D83D31C8EA515009B4FB8 /* AsyncDisplayKit+Debug.mm */; }; + 78155F3D25E9B77A00847DE1 /* ASLayout+IGListDiffKit.mm in Sources */ = {isa = PBXBuildFile; fileRef = 78155F3125E9B73C00847DE1 /* ASLayout+IGListDiffKit.mm */; }; + 788B05A3261CF8960012151B /* ASControlNode+Defines.h in Headers */ = {isa = PBXBuildFile; fileRef = 788B05A1261CF8960012151B /* ASControlNode+Defines.h */; }; + 788B05A4261CF8960012151B /* ASControlNode+Defines.mm in Sources */ = {isa = PBXBuildFile; fileRef = 788B05A2261CF8960012151B /* ASControlNode+Defines.mm */; }; 7AB338661C55B3420055FDE8 /* ASRelativeLayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7A06A7381C35F08800FE8DAA /* ASRelativeLayoutSpec.mm */; }; 7AB338671C55B3460055FDE8 /* ASRelativeLayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 7A06A7391C35F08800FE8DAA /* ASRelativeLayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; 7AB338691C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */; }; @@ -182,7 +224,7 @@ 8BBBAB8D1CEBAF1E00107FC6 /* ASDefaultPlaybackButton.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8B0768B21CE752EC002E1453 /* ASDefaultPlaybackButton.mm */; }; 8BDA5FC71CDBDF91007D13B2 /* ASVideoPlayerNode.h in Headers */ = {isa = PBXBuildFile; fileRef = 8BDA5FC31CDBDDE1007D13B2 /* ASVideoPlayerNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = 8BDA5FC41CDBDDE1007D13B2 /* ASVideoPlayerNode.mm */; }; - 9019FBBF1ED8061D00C45F72 /* ASYogaUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 9019FBBB1ED8061D00C45F72 /* ASYogaUtilities.h */; }; + 9019FBBF1ED8061D00C45F72 /* ASYogaUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 9019FBBB1ED8061D00C45F72 /* ASYogaUtilities.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9019FBC01ED8061D00C45F72 /* ASYogaUtilities.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9019FBBC1ED8061D00C45F72 /* ASYogaUtilities.mm */; }; 909C4C751F09C98B00D6B76F /* ASTextNode2.h in Headers */ = {isa = PBXBuildFile; fileRef = 909C4C731F09C98B00D6B76F /* ASTextNode2.h */; settings = {ATTRIBUTES = (Public, ); }; }; 909C4C761F09C98B00D6B76F /* ASTextNode2.mm in Sources */ = {isa = PBXBuildFile; fileRef = 909C4C741F09C98B00D6B76F /* ASTextNode2.mm */; }; @@ -205,10 +247,8 @@ 9C0BA4A62582CE35001C293B /* ASTextAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C0BA4932582CE35001C293B /* ASTextAttribute.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C0BA4A72582CE35001C293B /* ASTextRunDelegate.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C0BA4942582CE35001C293B /* ASTextRunDelegate.h */; }; 9C0BA4A82582CE35001C293B /* ASTextUtilities.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C0BA4962582CE35001C293B /* ASTextUtilities.h */; }; - 9C0BA4A92582CE35001C293B /* NSParagraphStyle+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C0BA4972582CE35001C293B /* NSParagraphStyle+ASText.h */; }; 9C0BA4AA2582CE35001C293B /* NSAttributedString+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C0BA4982582CE35001C293B /* NSAttributedString+ASText.h */; }; 9C0BA4AB2582CE35001C293B /* NSAttributedString+ASText.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C0BA4992582CE35001C293B /* NSAttributedString+ASText.mm */; }; - 9C0BA4AC2582CE35001C293B /* NSParagraphStyle+ASText.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C0BA49A2582CE35001C293B /* NSParagraphStyle+ASText.mm */; }; 9C0BA4AD2582CE35001C293B /* ASTextUtilities.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C0BA49B2582CE35001C293B /* ASTextUtilities.mm */; }; 9C49C3701B853961000B0DD5 /* ASStackLayoutElement.h in Headers */ = {isa = PBXBuildFile; fileRef = 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9C55866B1BD54A1900B50E3A /* ASAsciiArtBoxCreator.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9C5586681BD549CB00B50E3A /* ASAsciiArtBoxCreator.mm */; }; @@ -229,7 +269,7 @@ 9D302F9E2231B373005739C3 /* ASButtonNode+Yoga.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D302F9C2231B373005739C3 /* ASButtonNode+Yoga.h */; }; 9D302F9F2231B373005739C3 /* ASButtonNode+Yoga.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9D302F9D2231B373005739C3 /* ASButtonNode+Yoga.mm */; }; 9D9AA56921E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9D9AA56721E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm */; }; - 9D9AA56B21E254B800172C09 /* ASDisplayNode+Yoga.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D9AA56A21E254B800172C09 /* ASDisplayNode+Yoga.h */; }; + 9D9AA56B21E254B800172C09 /* ASDisplayNode+Yoga.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D9AA56A21E254B800172C09 /* ASDisplayNode+Yoga.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9D9AA56D21E2568500172C09 /* ASDisplayNode+LayoutSpec.h in Headers */ = {isa = PBXBuildFile; fileRef = 9D9AA56C21E2568500172C09 /* ASDisplayNode+LayoutSpec.h */; settings = {ATTRIBUTES = (Public, ); }; }; 9F06E5CD1B4CAF4200F015D8 /* ASCollectionViewTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9F06E5CC1B4CAF4200F015D8 /* ASCollectionViewTests.mm */; }; 9F98C0261DBE29E000476D92 /* ASControlTargetAction.mm in Sources */ = {isa = PBXBuildFile; fileRef = 9F98C0241DBDF2A300476D92 /* ASControlTargetAction.mm */; }; @@ -335,10 +375,18 @@ B350625C1B010F070018CF92 /* ASLog.h in Headers */ = {isa = PBXBuildFile; fileRef = 0516FA3B1A15563400B4EBED /* ASLog.h */; settings = {ATTRIBUTES = (Public, ); }; }; B350625D1B0111740018CF92 /* Photos.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943141A1575670030A7D0 /* Photos.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 051943121A1575630030A7D0 /* AssetsLibrary.framework */; settings = {ATTRIBUTES = (Weak, ); }; }; + B6A33531B97742F7D2FB76A6 /* Pods_AsyncDisplayKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 23BD8B8948B5DB954C01054C /* Pods_AsyncDisplayKit.framework */; }; BB5FC3CE1F9BA689007F191E /* ASNavigationControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3CD1F9BA688007F191E /* ASNavigationControllerTests.mm */; }; BB5FC3D11F9C9389007F191E /* ASTabBarControllerTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = BB5FC3D01F9C9389007F191E /* ASTabBarControllerTests.mm */; }; C018DF21216BF26700181FDA /* ASAbstractLayoutController+FrameworkPrivate.h in Headers */ = {isa = PBXBuildFile; fileRef = C018DF20216BF26600181FDA /* ASAbstractLayoutController+FrameworkPrivate.h */; }; C057D9BD20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = C057D9BC20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm */; }; + C0821DB925F9779100D18030 /* ASLayout+IGListDiffKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C0821DB725F9779100D18030 /* ASLayout+IGListDiffKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C0821DBA25F9779100D18030 /* ASLayout+IGListDiffKit.mm in Sources */ = {isa = PBXBuildFile; fileRef = C0821DB825F9779100D18030 /* ASLayout+IGListDiffKit.mm */; }; + C0821DC025F977F600D18030 /* ASDisplayNode+IGListKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C0821DBE25F977F600D18030 /* ASDisplayNode+IGListKit.h */; }; + C0821DC125F977F600D18030 /* ASDisplayNode+IGListKit.mm in Sources */ = {isa = PBXBuildFile; fileRef = C0821DBF25F977F600D18030 /* ASDisplayNode+IGListKit.mm */; }; + C0821DCC25F9793E00D18030 /* ASLayout+IGListDiffKit.h in Headers */ = {isa = PBXBuildFile; fileRef = C0821DB725F9779100D18030 /* ASLayout+IGListDiffKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; + C0B4751625F836FB00DBFA96 /* NSParagraphStyle+ASText.mm in Sources */ = {isa = PBXBuildFile; fileRef = C0B4751425F836FB00DBFA96 /* NSParagraphStyle+ASText.mm */; }; + C0B4751725F836FB00DBFA96 /* NSParagraphStyle+ASText.h in Headers */ = {isa = PBXBuildFile; fileRef = C0B4751525F836FB00DBFA96 /* NSParagraphStyle+ASText.h */; }; C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */ = {isa = PBXBuildFile; fileRef = B0F880581BEAEC7500D17647 /* ASTableNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC01EB6D23105C2000CDB61A /* TestAsset.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = CC01EB6C23105C2000CDB61A /* TestAsset.xcassets */; }; CC01EB6F23105C7F00CDB61A /* ASImageNodeSnapshotTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = 056D21541ABCEF50001107EF /* ASImageNodeSnapshotTests.mm */; }; @@ -388,7 +436,7 @@ CC55A70E1E529FA200594372 /* UIResponder+AsyncDisplayKit.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC55A70C1E529FA200594372 /* UIResponder+AsyncDisplayKit.mm */; }; CC55A7111E52A0F200594372 /* ASResponderChainEnumerator.h in Headers */ = {isa = PBXBuildFile; fileRef = CC55A70F1E52A0F200594372 /* ASResponderChainEnumerator.h */; }; CC55A7121E52A0F200594372 /* ASResponderChainEnumerator.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC55A7101E52A0F200594372 /* ASResponderChainEnumerator.mm */; }; - CC56013B1F06E9A700DC4FBE /* ASIntegerMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */; }; + CC56013B1F06E9A700DC4FBE /* ASIntegerMap.h in Headers */ = {isa = PBXBuildFile; fileRef = CC5601391F06E9A700DC4FBE /* ASIntegerMap.h */; settings = {ATTRIBUTES = (Public, ); }; }; CC56013C1F06E9A700DC4FBE /* ASIntegerMap.mm in Sources */ = {isa = PBXBuildFile; fileRef = CC56013A1F06E9A700DC4FBE /* ASIntegerMap.mm */; }; CC57EAF71E3939350034C595 /* ASCollectionView+Undeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = CC2E317F1DAC353700EEE891 /* ASCollectionView+Undeprecated.h */; settings = {ATTRIBUTES = (Private, ); }; }; CC57EAF81E3939450034C595 /* ASTableView+Undeprecated.h in Headers */ = {isa = PBXBuildFile; fileRef = CC512B841DAC45C60054848E /* ASTableView+Undeprecated.h */; settings = {ATTRIBUTES = (Private, ); }; }; @@ -474,8 +522,6 @@ DECBD6E81BE56E1900CF4905 /* ASButtonNode.h in Headers */ = {isa = PBXBuildFile; fileRef = DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */; settings = {ATTRIBUTES = (Public, ); }; }; DECBD6EA1BE56E1900CF4905 /* ASButtonNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */; }; DEFAD8131CC48914000527C4 /* ASVideoNode.mm in Sources */ = {isa = PBXBuildFile; fileRef = AEEC47E01C20C2DD00EC1693 /* ASVideoNode.mm */; }; - E517F9C823BF14BC006E40E0 /* ASLayout+IGListDiffKit.mm in Sources */ = {isa = PBXBuildFile; fileRef = E517F9C623BF14BC006E40E0 /* ASLayout+IGListDiffKit.mm */; }; - E517F9C923BF14BC006E40E0 /* ASLayout+IGListDiffKit.h in Headers */ = {isa = PBXBuildFile; fileRef = E517F9C723BF14BC006E40E0 /* ASLayout+IGListDiffKit.h */; settings = {ATTRIBUTES = (Public, ); }; }; E51B78BF1F028ABF00E32604 /* ASLayoutFlatteningTests.mm in Sources */ = {isa = PBXBuildFile; fileRef = E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.mm */; }; E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E00711F1D3828000B30D7 /* ASPagerNode+Beta.h */; settings = {ATTRIBUTES = (Public, ); }; }; E54E81FC1EB357BD00FFE8E1 /* ASPageTable.h in Headers */ = {isa = PBXBuildFile; fileRef = E54E81FA1EB357BD00FFE8E1 /* ASPageTable.h */; }; @@ -564,8 +610,6 @@ 0587F9BC1A7309ED00AFF0BA /* ASEditableTextNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASEditableTextNode.mm; sourceTree = ""; }; 058D09AF195D04C000B7D73C /* Foundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Foundation.framework; path = System/Library/Frameworks/Foundation.framework; sourceTree = SDKROOT; }; 058D09BC195D04C000B7D73C /* AsyncDisplayKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = AsyncDisplayKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 058D09BD195D04C000B7D73C /* XCTest.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = XCTest.framework; path = Library/Frameworks/XCTest.framework; sourceTree = DEVELOPER_DIR; }; - 058D09C0195D04C000B7D73C /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = Library/Frameworks/UIKit.framework; sourceTree = DEVELOPER_DIR; }; 058D09C7195D04C000B7D73C /* AsyncDisplayKitTests-Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = "AsyncDisplayKitTests-Info.plist"; sourceTree = ""; }; 058D09C9195D04C000B7D73C /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/InfoPlist.strings; sourceTree = ""; }; 058D09D5195D050800B7D73C /* ASControlNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASControlNode.h; sourceTree = ""; }; @@ -624,12 +668,24 @@ 058D0A44195D058D00B7D73C /* ASBaseDefines.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASBaseDefines.h; sourceTree = ""; }; 05EA6FE61AC0966E00E35788 /* ASSnapshotTestCase.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASSnapshotTestCase.mm; sourceTree = ""; }; 05F20AA31A15733C00DCA68A /* ASImageProtocols.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageProtocols.h; sourceTree = ""; }; + 0F8DC32A258AC67A00E6266C /* ASDisplayNode+Yoga2.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Yoga2.h"; sourceTree = ""; }; + 0F8DC32B258AC67A00E6266C /* ASDisplayNode+Yoga2Logging.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Yoga2Logging.h"; sourceTree = ""; }; + 0F8DC32D258AC67A00E6266C /* ASDisplayNode+Yoga2.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+Yoga2.mm"; sourceTree = ""; }; + 0F8DC32F258AC67A00E6266C /* ASNodeContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASNodeContext.h; sourceTree = ""; }; + 0F8DC330258AC67B00E6266C /* ASNodeContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASNodeContext.mm; sourceTree = ""; }; + 0F8DC332258AC67B00E6266C /* ASDisplayNode+Yoga2Logging.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+Yoga2Logging.mm"; sourceTree = ""; }; + 0F8DC340258AC6F100E6266C /* ASNodeControllerInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASNodeControllerInternal.h; sourceTree = ""; }; + 0F8DC341258AC6F200E6266C /* ASImageNode+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASImageNode+FrameworkPrivate.h"; sourceTree = ""; }; + 0F8DC342258AC6F200E6266C /* ASNodeContext+Private.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASNodeContext+Private.h"; sourceTree = ""; }; + 0F8DC393258AC77100E6266C /* ASLayoutElementStyleYoga.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElementStyleYoga.mm; sourceTree = ""; }; + 0F8DC394258AC77100E6266C /* ASLayoutElementStyleYoga.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutElementStyleYoga.h; sourceTree = ""; }; 18C2ED7C1B9B7DE800F627B3 /* ASCollectionNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionNode.h; sourceTree = ""; }; 18C2ED7D1B9B7DE800F627B3 /* ASCollectionNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionNode.mm; sourceTree = ""; }; 1950C4481A3BB5C1005C8279 /* ASEqualityHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASEqualityHelpers.h; sourceTree = ""; }; 1A6C000B1FAB4E2000D05926 /* ASCornerLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCornerLayoutSpec.h; sourceTree = ""; }; 1A6C000C1FAB4E2100D05926 /* ASCornerLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCornerLayoutSpec.mm; sourceTree = ""; }; 1A6C000F1FAB4ED400D05926 /* ASCornerLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCornerLayoutSpecSnapshotTests.mm; sourceTree = ""; }; + 1AB64F5EDB37CA6E65E50702 /* Pods-AsyncDisplayKit.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKit.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKit/Pods-AsyncDisplayKit.profile.xcconfig"; sourceTree = ""; }; 205F0E0D1B371875007741D0 /* UICollectionViewLayout+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "UICollectionViewLayout+ASConvenience.h"; sourceTree = ""; }; 205F0E0E1B371875007741D0 /* UICollectionViewLayout+ASConvenience.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "UICollectionViewLayout+ASConvenience.mm"; sourceTree = ""; }; 205F0E111B371BD7007741D0 /* ASScrollDirection.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASScrollDirection.mm; sourceTree = ""; }; @@ -638,6 +694,7 @@ 205F0E1B1B373A2C007741D0 /* ASCollectionViewLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASCollectionViewLayoutController.h; sourceTree = ""; }; 205F0E1C1B373A2C007741D0 /* ASCollectionViewLayoutController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionViewLayoutController.mm; sourceTree = ""; }; 205F0E1F1B376416007741D0 /* CoreGraphics+ASConvenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "CoreGraphics+ASConvenience.h"; sourceTree = ""; }; + 23BD8B8948B5DB954C01054C /* Pods_AsyncDisplayKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AsyncDisplayKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 242995D21B29743C00090100 /* ASBasicImageDownloaderTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASBasicImageDownloaderTests.mm; sourceTree = ""; }; 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASCollectionViewFlowLayoutInspectorTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; 254C6B511BF8FE6D003EC431 /* ASTextKitTruncationTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextKitTruncationTests.mm; sourceTree = ""; }; @@ -679,7 +736,6 @@ 3917EBD21E9C2FC400D04A01 /* _ASCollectionReusableView.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = _ASCollectionReusableView.h; sourceTree = ""; }; 3917EBD31E9C2FC400D04A01 /* _ASCollectionReusableView.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASCollectionReusableView.mm; sourceTree = ""; }; 3C9C128419E616EF00E942A0 /* ASTableViewTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASTableViewTests.mm; sourceTree = ""; xcLanguageSpecificationIdentifier = xcode.lang.objc; }; - 407B8BAD2310E2ED00CB979E /* ASLayoutSpecUtilitiesTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutSpecUtilitiesTests.mm; sourceTree = ""; }; 464052191A3F83C40061C0BA /* ASDataController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; lineEnding = 0; path = ASDataController.h; sourceTree = ""; }; 4640521A1A3F83C40061C0BA /* ASDataController.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; lineEnding = 0; path = ASDataController.mm; sourceTree = ""; }; 4640521B1A3F83C40061C0BA /* ASTableLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTableLayoutController.h; sourceTree = ""; }; @@ -687,7 +743,8 @@ 4640521D1A3F83C40061C0BA /* ASLayoutController.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutController.h; sourceTree = ""; }; 471D04B0224CB98600649215 /* ASImageNodeBackingSizeTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASImageNodeBackingSizeTests.mm; sourceTree = ""; }; 4E9127681F64157600499623 /* ASRunLoopQueueTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRunLoopQueueTests.mm; sourceTree = ""; }; - 5039B74209A895E07057081C /* Pods_AsyncDisplayKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AsyncDisplayKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 5188E1E61FB74BEA34BB6E3E /* Pods-AsyncDisplayKit-AsyncDisplayKitTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKit-AsyncDisplayKitTests.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKit-AsyncDisplayKitTests/Pods-AsyncDisplayKit-AsyncDisplayKitTests.debug.xcconfig"; sourceTree = ""; }; + 53E53CFB3EAFB43A19F71891 /* Pods-AsyncDisplayKit.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKit.debug.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKit/Pods-AsyncDisplayKit.debug.xcconfig"; sourceTree = ""; }; 68355B2E1CB5799E001D4E68 /* ASImageNode+AnimatedImage.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASImageNode+AnimatedImage.mm"; sourceTree = ""; }; 68355B361CB57A5A001D4E68 /* ASPINRemoteImageDownloader.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASPINRemoteImageDownloader.mm; sourceTree = ""; }; 68355B371CB57A5A001D4E68 /* ASImageContainerProtocolCategories.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASImageContainerProtocolCategories.h; sourceTree = ""; }; @@ -742,9 +799,13 @@ 69CB62AA1CB8165900024920 /* _ASDisplayViewAccessiblity.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = _ASDisplayViewAccessiblity.mm; sourceTree = ""; }; 69F10C851C84C35D0026140C /* ASRangeControllerUpdateRangeProtocol+Beta.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASRangeControllerUpdateRangeProtocol+Beta.h"; sourceTree = ""; }; 69FEE53C1D95A9AF0086F066 /* ASLayoutElementStyleTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutElementStyleTests.mm; sourceTree = ""; }; + 6A3A1B1A7956190DBE53C847 /* Pods-AsyncDisplayKit-AsyncDisplayKitTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKit-AsyncDisplayKitTests.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKit-AsyncDisplayKitTests/Pods-AsyncDisplayKit-AsyncDisplayKitTests.release.xcconfig"; sourceTree = ""; }; 6BDC61F51978FEA400E50D21 /* AsyncDisplayKit.h */ = {isa = PBXFileReference; explicitFileType = sourcecode.c.h; path = AsyncDisplayKit.h; sourceTree = ""; }; 764D83D21C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "AsyncDisplayKit+Debug.h"; sourceTree = ""; }; 764D83D31C8EA515009B4FB8 /* AsyncDisplayKit+Debug.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "AsyncDisplayKit+Debug.mm"; sourceTree = ""; }; + 78155F3125E9B73C00847DE1 /* ASLayout+IGListDiffKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASLayout+IGListDiffKit.mm"; sourceTree = ""; }; + 788B05A1261CF8960012151B /* ASControlNode+Defines.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "ASControlNode+Defines.h"; sourceTree = ""; }; + 788B05A2261CF8960012151B /* ASControlNode+Defines.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASControlNode+Defines.mm"; sourceTree = ""; }; 7A06A7381C35F08800FE8DAA /* ASRelativeLayoutSpec.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRelativeLayoutSpec.mm; sourceTree = ""; }; 7A06A7391C35F08800FE8DAA /* ASRelativeLayoutSpec.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRelativeLayoutSpec.h; sourceTree = ""; }; 7AB338681C55B97B0055FDE8 /* ASRelativeLayoutSpecSnapshotTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASRelativeLayoutSpecSnapshotTests.mm; sourceTree = ""; }; @@ -754,6 +815,7 @@ 81EE384D1C8E94F000456208 /* ASRunLoopQueue.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASRunLoopQueue.h; path = ../ASRunLoopQueue.h; sourceTree = ""; }; 81EE384E1C8E94F000456208 /* ASRunLoopQueue.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = ASRunLoopQueue.mm; path = ../ASRunLoopQueue.mm; sourceTree = ""; }; 81FF150622EB5F410039311A /* ASButtonNodeSnapshotTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNodeSnapshotTests.mm; sourceTree = ""; }; + 8383AF842A78759A7A4EF1B4 /* Pods-AsyncDisplayKit-AsyncDisplayKitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKit-AsyncDisplayKitTests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKit-AsyncDisplayKitTests/Pods-AsyncDisplayKit-AsyncDisplayKitTests.profile.xcconfig"; sourceTree = ""; }; 83A7D9581D44542100BF333E /* ASWeakMap.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASWeakMap.h; sourceTree = ""; }; 83A7D9591D44542100BF333E /* ASWeakMap.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASWeakMap.mm; sourceTree = ""; }; 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASWeakMapTests.mm; sourceTree = ""; }; @@ -769,6 +831,7 @@ 92DD2FE11BF4B97E0074C9DD /* ASMapNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASMapNode.h; sourceTree = ""; }; 92DD2FE21BF4B97E0074C9DD /* ASMapNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASMapNode.mm; sourceTree = ""; }; 92DD2FE51BF4D05E0074C9DD /* MapKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = MapKit.framework; path = System/Library/Frameworks/MapKit.framework; sourceTree = SDKROOT; }; + 9613147E8EF4AFA6090F10B5 /* Pods-AsyncDisplayKit.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKit.release.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKit/Pods-AsyncDisplayKit.release.xcconfig"; sourceTree = ""; }; 9644CFDE2193777C00213478 /* ASThrashUtility.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ASThrashUtility.h; sourceTree = ""; }; 9644CFDF2193777C00213478 /* ASThrashUtility.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ASThrashUtility.m; sourceTree = ""; }; 9692B4FE219E12370060C2C3 /* ASCollectionViewThrashTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASCollectionViewThrashTests.mm; sourceTree = ""; }; @@ -785,10 +848,8 @@ 9C0BA4932582CE35001C293B /* ASTextAttribute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextAttribute.h; sourceTree = ""; }; 9C0BA4942582CE35001C293B /* ASTextRunDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextRunDelegate.h; sourceTree = ""; }; 9C0BA4962582CE35001C293B /* ASTextUtilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASTextUtilities.h; sourceTree = ""; }; - 9C0BA4972582CE35001C293B /* NSParagraphStyle+ASText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSParagraphStyle+ASText.h"; sourceTree = ""; }; 9C0BA4982582CE35001C293B /* NSAttributedString+ASText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSAttributedString+ASText.h"; sourceTree = ""; }; 9C0BA4992582CE35001C293B /* NSAttributedString+ASText.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSAttributedString+ASText.mm"; sourceTree = ""; }; - 9C0BA49A2582CE35001C293B /* NSParagraphStyle+ASText.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSParagraphStyle+ASText.mm"; sourceTree = ""; }; 9C0BA49B2582CE35001C293B /* ASTextUtilities.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextUtilities.mm; sourceTree = ""; }; 9C49C36E1B853957000B0DD5 /* ASStackLayoutElement.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASStackLayoutElement.h; sourceTree = ""; }; 9C5586671BD549CB00B50E3A /* ASAsciiArtBoxCreator.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASAsciiArtBoxCreator.h; sourceTree = ""; }; @@ -873,6 +934,13 @@ BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-AsyncDisplayKitTests.profile.xcconfig"; path = "Pods/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests.profile.xcconfig"; sourceTree = ""; }; C018DF20216BF26600181FDA /* ASAbstractLayoutController+FrameworkPrivate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASAbstractLayoutController+FrameworkPrivate.h"; sourceTree = ""; }; C057D9BC20B5453D00FC9112 /* ASTextNode2SnapshotTests.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = ASTextNode2SnapshotTests.mm; sourceTree = ""; }; + C0821DB725F9779100D18030 /* ASLayout+IGListDiffKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASLayout+IGListDiffKit.h"; sourceTree = ""; }; + C0821DB825F9779100D18030 /* ASLayout+IGListDiffKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASLayout+IGListDiffKit.mm"; sourceTree = ""; }; + C0821DBE25F977F600D18030 /* ASDisplayNode+IGListKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+IGListKit.h"; sourceTree = ""; }; + C0821DBF25F977F600D18030 /* ASDisplayNode+IGListKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+IGListKit.mm"; sourceTree = ""; }; + C0B4751425F836FB00DBFA96 /* NSParagraphStyle+ASText.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "NSParagraphStyle+ASText.mm"; sourceTree = ""; }; + C0B4751525F836FB00DBFA96 /* NSParagraphStyle+ASText.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "NSParagraphStyle+ASText.h"; sourceTree = ""; }; + C6848AAC6137D25578CADC36 /* Pods_AsyncDisplayKit_AsyncDisplayKitTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_AsyncDisplayKit_AsyncDisplayKitTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; CC01EB6C23105C2000CDB61A /* TestAsset.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = TestAsset.xcassets; sourceTree = ""; }; CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDisplayNode+Convenience.h"; sourceTree = ""; }; CC034A081E60BEB400626263 /* ASDisplayNode+Convenience.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASDisplayNode+Convenience.mm"; sourceTree = ""; }; @@ -1016,8 +1084,6 @@ DEC146B41C37A16A004A0EE7 /* ASCollectionInternal.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = ASCollectionInternal.h; path = Details/ASCollectionInternal.h; sourceTree = ""; }; DECBD6E51BE56E1900CF4905 /* ASButtonNode.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASButtonNode.h; sourceTree = ""; }; DECBD6E61BE56E1900CF4905 /* ASButtonNode.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASButtonNode.mm; sourceTree = ""; }; - E517F9C623BF14BC006E40E0 /* ASLayout+IGListDiffKit.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = "ASLayout+IGListDiffKit.mm"; sourceTree = ""; }; - E517F9C723BF14BC006E40E0 /* ASLayout+IGListDiffKit.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASLayout+IGListDiffKit.h"; sourceTree = ""; }; E51B78BD1F01A0EE00E32604 /* ASLayoutFlatteningTests.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutFlatteningTests.mm; sourceTree = ""; }; E52405B21C8FEF03004DC8E7 /* ASLayoutTransition.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = ASLayoutTransition.mm; sourceTree = ""; }; E52405B41C8FEF16004DC8E7 /* ASLayoutTransition.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutTransition.h; sourceTree = ""; }; @@ -1081,10 +1147,8 @@ CC36C19F218B894800232F23 /* CoreMedia.framework in Frameworks */, CC36C19E218B894400232F23 /* AVFoundation.framework in Frameworks */, CC90E1F41E383C0400FED591 /* AsyncDisplayKit.framework in Frameworks */, - 058D09BE195D04C000B7D73C /* XCTest.framework in Frameworks */, - 058D09C1195D04C000B7D73C /* UIKit.framework in Frameworks */, 058D09BF195D04C000B7D73C /* Foundation.framework in Frameworks */, - 4EE3813FF44E20C399ACB962 /* Pods_AsyncDisplayKitTests.framework in Frameworks */, + 2875326E554776AAA50EF9A8 /* Pods_AsyncDisplayKit_AsyncDisplayKitTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1103,6 +1167,7 @@ 92DD2FE61BF4D05E0074C9DD /* MapKit.framework in Frameworks */, B350625E1B0111780018CF92 /* AssetsLibrary.framework in Frameworks */, B350625D1B0111740018CF92 /* Photos.framework in Frameworks */, + B6A33531B97742F7D2FB76A6 /* Pods_AsyncDisplayKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -1139,6 +1204,7 @@ 058D09AE195D04C000B7D73C /* Frameworks */, 058D09AD195D04C000B7D73C /* Products */, FD40E2760492F0CAAEAD552D /* Pods */, + C0821DAD25F9754300D18030 /* Recovered References */, ); indentWidth = 2; sourceTree = ""; @@ -1170,9 +1236,8 @@ 051943141A1575670030A7D0 /* Photos.framework */, 051943121A1575630030A7D0 /* AssetsLibrary.framework */, 058D09AF195D04C000B7D73C /* Foundation.framework */, - 058D09BD195D04C000B7D73C /* XCTest.framework */, - 058D09C0195D04C000B7D73C /* UIKit.framework */, - 5039B74209A895E07057081C /* Pods_AsyncDisplayKitTests.framework */, + 23BD8B8948B5DB954C01054C /* Pods_AsyncDisplayKit.framework */, + C6848AAC6137D25578CADC36 /* Pods_AsyncDisplayKit_AsyncDisplayKitTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -1180,6 +1245,12 @@ 058D09B1195D04C000B7D73C /* Source */ = { isa = PBXGroup; children = ( + 0F8DC32A258AC67A00E6266C /* ASDisplayNode+Yoga2.h */, + 0F8DC32D258AC67A00E6266C /* ASDisplayNode+Yoga2.mm */, + 0F8DC32B258AC67A00E6266C /* ASDisplayNode+Yoga2Logging.h */, + 0F8DC332258AC67B00E6266C /* ASDisplayNode+Yoga2Logging.mm */, + 0F8DC32F258AC67A00E6266C /* ASNodeContext.h */, + 0F8DC330258AC67B00E6266C /* ASNodeContext.mm */, CC35CEC120DD7F600006448D /* ASCollections.h */, CC35CEC220DD7F600006448D /* ASCollections.mm */, 058D0A42195D058D00B7D73C /* Base */, @@ -1217,6 +1288,8 @@ 058D09D8195D050800B7D73C /* ASDisplayNode.h */, 058D09D9195D050800B7D73C /* ASDisplayNode.mm */, CC6AA2D81E9F03B900978E87 /* ASDisplayNode+Ancestry.h */, + C0821DBE25F977F600D18030 /* ASDisplayNode+IGListKit.h */, + C0821DBF25F977F600D18030 /* ASDisplayNode+IGListKit.mm */, CC6AA2D91E9F03B900978E87 /* ASDisplayNode+Ancestry.mm */, 68B027791C1A79CC0041016B /* ASDisplayNode+Beta.h */, CC034A071E60BEB400626263 /* ASDisplayNode+Convenience.h */, @@ -1395,7 +1468,6 @@ 83A7D95D1D446A6E00BF333E /* ASWeakMapTests.mm */, CC3B208D1C3F7D0A00798563 /* ASWeakSetTests.mm */, 695BE2541DC1245C008E6EA5 /* ASWrapperSpecSnapshotTests.mm */, - 407B8BAD2310E2ED00CB979E /* ASLayoutSpecUtilitiesTests.mm */, CC698826247855F200487428 /* UIImage+ASConvenienceTests.mm */, 057D02C01AC0A66700C7AC3C /* AsyncDisplayKitTestHost */, CC583ABF1EF9BAB400134156 /* Common */, @@ -1515,6 +1587,9 @@ 058D0A01195D050800B7D73C /* Private */ = { isa = PBXGroup; children = ( + 0F8DC341258AC6F200E6266C /* ASImageNode+FrameworkPrivate.h */, + 0F8DC342258AC6F200E6266C /* ASNodeContext+Private.h */, + 0F8DC340258AC6F100E6266C /* ASNodeControllerInternal.h */, CCE04B2A1E313EDA006AEBBB /* Collection Data Adapter */, E52F8AEE1EAE659600B5A912 /* Collection Layout */, 6947B0BB1E36B4E30007C478 /* Layout */, @@ -1587,6 +1662,8 @@ 0442850C1BAA64EC00D16268 /* ASTwoDimensionalArrayUtils.mm */, 83A7D9581D44542100BF333E /* ASWeakMap.h */, 83A7D9591D44542100BF333E /* ASWeakMap.mm */, + 788B05A1261CF8960012151B /* ASControlNode+Defines.h */, + 788B05A2261CF8960012151B /* ASControlNode+Defines.mm */, ); path = Private; sourceTree = ""; @@ -1721,11 +1798,11 @@ 9C0BA4952582CE35001C293B /* Utility */ = { isa = PBXGroup; children = ( + C0B4751525F836FB00DBFA96 /* NSParagraphStyle+ASText.h */, + C0B4751425F836FB00DBFA96 /* NSParagraphStyle+ASText.mm */, 9C0BA4962582CE35001C293B /* ASTextUtilities.h */, - 9C0BA4972582CE35001C293B /* NSParagraphStyle+ASText.h */, 9C0BA4982582CE35001C293B /* NSAttributedString+ASText.h */, 9C0BA4992582CE35001C293B /* NSAttributedString+ASText.mm */, - 9C0BA49A2582CE35001C293B /* NSParagraphStyle+ASText.mm */, 9C0BA49B2582CE35001C293B /* ASTextUtilities.mm */, ); path = Utility; @@ -1734,6 +1811,10 @@ AC6456051B0A333200CF11B8 /* Layout */ = { isa = PBXGroup; children = ( + C0821DB725F9779100D18030 /* ASLayout+IGListDiffKit.h */, + C0821DB825F9779100D18030 /* ASLayout+IGListDiffKit.mm */, + 0F8DC394258AC77100E6266C /* ASLayoutElementStyleYoga.h */, + 0F8DC393258AC77100E6266C /* ASLayoutElementStyleYoga.mm */, 9C6BB3B01B8CC9C200F13F52 /* ASAbsoluteLayoutElement.h */, ACF6ED181B17843500DA7C62 /* ASAbsoluteLayoutSpec.h */, ACF6ED191B17843500DA7C62 /* ASAbsoluteLayoutSpec.mm */, @@ -1753,8 +1834,6 @@ ACF6ED0A1B17843500DA7C62 /* ASInsetLayoutSpec.mm */, ACF6ED0B1B17843500DA7C62 /* ASLayout.h */, ACF6ED0C1B17843500DA7C62 /* ASLayout.mm */, - E517F9C723BF14BC006E40E0 /* ASLayout+IGListDiffKit.h */, - E517F9C623BF14BC006E40E0 /* ASLayout+IGListDiffKit.mm */, ACF6ED111B17843500DA7C62 /* ASLayoutElement.h */, E55D86311CA8A14000A0C26F /* ASLayoutElement.mm */, 698C8B601CAB49FC0052DC3F /* ASLayoutElementExtensibility.h */, @@ -1779,6 +1858,14 @@ path = Layout; sourceTree = ""; }; + C0821DAD25F9754300D18030 /* Recovered References */ = { + isa = PBXGroup; + children = ( + 78155F3125E9B73C00847DE1 /* ASLayout+IGListDiffKit.mm */, + ); + name = "Recovered References"; + sourceTree = ""; + }; CC224E942066CA6D00BBA57F /* Schemas */ = { isa = PBXGroup; children = ( @@ -1887,6 +1974,12 @@ FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */, D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */, BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */, + 1AB64F5EDB37CA6E65E50702 /* Pods-AsyncDisplayKit.profile.xcconfig */, + 6A3A1B1A7956190DBE53C847 /* Pods-AsyncDisplayKit-AsyncDisplayKitTests.release.xcconfig */, + 8383AF842A78759A7A4EF1B4 /* Pods-AsyncDisplayKit-AsyncDisplayKitTests.profile.xcconfig */, + 53E53CFB3EAFB43A19F71891 /* Pods-AsyncDisplayKit.debug.xcconfig */, + 9613147E8EF4AFA6090F10B5 /* Pods-AsyncDisplayKit.release.xcconfig */, + 5188E1E61FB74BEA34BB6E3E /* Pods-AsyncDisplayKit-AsyncDisplayKitTests.debug.xcconfig */, ); name = Pods; sourceTree = ""; @@ -1894,6 +1987,14 @@ /* End PBXGroup section */ /* Begin PBXHeadersBuildPhase section */ + 78155F4125E9B7C700847DE1 /* Headers */ = { + isa = PBXHeadersBuildPhase; + buildActionMask = 2147483647; + files = ( + C0821DCC25F9793E00D18030 /* ASLayout+IGListDiffKit.h in Headers */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; B35061D71B010EDF0018CF92 /* Headers */ = { isa = PBXHeadersBuildPhase; buildActionMask = 2147483647; @@ -1903,9 +2004,9 @@ 9C0BA4A12582CE35001C293B /* ASTextLine.h in Headers */, 9C0BA49D2582CE35001C293B /* ASTextDebugOption.h in Headers */, 9C0BA49E2582CE35001C293B /* ASTextLayout.h in Headers */, + 0F8DC334258AC67B00E6266C /* ASDisplayNode+Yoga2.h in Headers */, 1A6C000D1FAB4E2100D05926 /* ASCornerLayoutSpec.h in Headers */, E54E00721F1D3828000B30D7 /* ASPagerNode+Beta.h in Headers */, - E517F9C923BF14BC006E40E0 /* ASLayout+IGListDiffKit.h in Headers */, E5B225281F1790D6001E1431 /* ASHashing.h in Headers */, CC034A131E649F1300626263 /* AsyncDisplayKit+IGListKitMethods.h in Headers */, 693A1DCA1ECC944E00D0C9D2 /* IGListAdapter+AsyncDisplayKit.h in Headers */, @@ -1936,6 +2037,8 @@ B350620F1B010EFD0018CF92 /* _ASDisplayLayer.h in Headers */, B35062111B010EFD0018CF92 /* _ASDisplayView.h in Headers */, 9C55866C1BD54A3000B50E3A /* ASAsciiArtBoxCreator.h in Headers */, + 0F8DC345258AC6F200E6266C /* ASNodeContext+Private.h in Headers */, + 0F8DC339258AC67B00E6266C /* ASNodeContext.h in Headers */, 509E68611B3AEDA0009B9150 /* ASAbstractLayoutController.h in Headers */, CCA282B81E9EA8E40037E8B7 /* AsyncDisplayKit+Tips.h in Headers */, B35062571B010F070018CF92 /* ASAssert.h in Headers */, @@ -1948,7 +2051,6 @@ CCB1F95C1EFB6350009C7475 /* ASSignpost.h in Headers */, C018DF21216BF26700181FDA /* ASAbstractLayoutController+FrameworkPrivate.h in Headers */, 34EFC7611B701C9C00AD841F /* ASBackgroundLayoutSpec.h in Headers */, - 9C0BA4A92582CE35001C293B /* NSParagraphStyle+ASText.h in Headers */, B35062591B010F070018CF92 /* ASBaseDefines.h in Headers */, B35062131B010EFD0018CF92 /* ASBasicImageDownloader.h in Headers */, 9C0BA4A82582CE35001C293B /* ASTextUtilities.h in Headers */, @@ -1971,6 +2073,7 @@ 34EFC75B1B701BAF00AD841F /* ASDimension.h in Headers */, 68FC85EA1CE29C7D00EDD713 /* ASVisibilityProtocols.h in Headers */, A37320101C571B740011FC94 /* ASTextNode+Beta.h in Headers */, + 788B05A3261CF8960012151B /* ASControlNode+Defines.h in Headers */, 9C70F2061CDA4F0C007D6C76 /* ASTraitCollection.h in Headers */, CC6AA2DA1E9F03B900978E87 /* ASDisplayNode+Ancestry.h in Headers */, 8021EC1D1D2B00B100799119 /* UIImage+ASConvenience.h in Headers */, @@ -1983,6 +2086,7 @@ 680346941CE4052A0009FEB4 /* ASNavigationController.h in Headers */, B350621B1B010EFD0018CF92 /* ASTableLayoutController.h in Headers */, B350621D1B010EFD0018CF92 /* ASHighlightOverlayLayer.h in Headers */, + C0821DC025F977F600D18030 /* ASDisplayNode+IGListKit.h in Headers */, C78F7E2B1BF7809800CDEAFC /* ASTableNode.h in Headers */, 7AB338671C55B3460055FDE8 /* ASRelativeLayoutSpec.h in Headers */, B35062021B010EFD0018CF92 /* ASImageNode.h in Headers */, @@ -2062,6 +2166,7 @@ 254C6B741BF94DF4003EC431 /* ASTextNodeWordKerner.h in Headers */, 698DFF441E36B6C9002891F1 /* ASStackLayoutSpecUtilities.h in Headers */, CCF18FF41D2575E300DF5895 /* NSIndexSet+ASHelpers.h in Headers */, + 0F8DC398258AC77100E6266C /* ASLayoutElementStyleYoga.h in Headers */, 83A7D95C1D44548100BF333E /* ASWeakMap.h in Headers */, E5711A2C1C840C81009619D4 /* ASCollectionElement.h in Headers */, 6947B0BE1E36B4E30007C478 /* ASStackUnpositionedLayout.h in Headers */, @@ -2074,6 +2179,7 @@ B35062201B010EFD0018CF92 /* ASLayoutController.h in Headers */, B35062211B010EFD0018CF92 /* ASLayoutRangeType.h in Headers */, CC2F65EE1E5FFB1600DA57C9 /* ASMutableElementMap.h in Headers */, + 0F8DC343258AC6F200E6266C /* ASNodeControllerInternal.h in Headers */, 34EFC76A1B701CE600AD841F /* ASLayoutSpec.h in Headers */, CCA282D01E9EBF6C0037E8B7 /* ASTipsWindow.h in Headers */, B350625C1B010F070018CF92 /* ASLog.h in Headers */, @@ -2084,11 +2190,13 @@ B13CA0F81C519EBA00E031AB /* ASCollectionViewLayoutFacilitatorProtocol.h in Headers */, 909C4C751F09C98B00D6B76F /* ASTextNode2.h in Headers */, B35062061B010EFD0018CF92 /* ASNetworkImageNode.h in Headers */, + 0F8DC335258AC67B00E6266C /* ASDisplayNode+Yoga2Logging.h in Headers */, CCA282C81E9EB64B0037E8B7 /* ASDisplayNodeTipState.h in Headers */, 34EFC76C1B701CED00AD841F /* ASOverlayLayoutSpec.h in Headers */, B35062261B010EFD0018CF92 /* ASRangeController.h in Headers */, 34EFC76E1B701CF400AD841F /* ASRatioLayoutSpec.h in Headers */, DB55C2671C641AE4004EDCF5 /* ASContextTransitioning.h in Headers */, + C0B4751725F836FB00DBFA96 /* NSParagraphStyle+ASText.h in Headers */, CCA282C41E9EAE630037E8B7 /* ASLayerBackingTipProvider.h in Headers */, CCEDDDCF200C42A200FFCD0A /* ASConfigurationDelegate.h in Headers */, E5C347B31ECB40AA00EC4BE4 /* ASTableNode+Beta.h in Headers */, @@ -2107,6 +2215,7 @@ CC0F885C1E42807F00576FED /* ASCollectionViewFlowLayoutInspector.h in Headers */, 764D83D51C8EA515009B4FB8 /* AsyncDisplayKit+Debug.h in Headers */, CC7FD9E21BB603FF005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */, + 0F8DC344258AC6F200E6266C /* ASImageNode+FrameworkPrivate.h in Headers */, 254C6B761BF94DF4003EC431 /* ASTextNodeTypes.h in Headers */, CCA282B41E9EA7310037E8B7 /* ASTipsController.h in Headers */, 34EFC7711B701CFF00AD841F /* ASStackLayoutSpec.h in Headers */, @@ -2116,6 +2225,7 @@ 9C6BB3B31B8CC9C200F13F52 /* ASAbsoluteLayoutElement.h in Headers */, 34EFC7731B701D0700AD841F /* ASAbsoluteLayoutSpec.h in Headers */, B350620A1B010EFD0018CF92 /* ASTableView.h in Headers */, + C0821DB925F9779100D18030 /* ASLayout+IGListDiffKit.h in Headers */, B350620C1B010EFD0018CF92 /* ASTableViewProtocols.h in Headers */, B350620D1B010EFD0018CF92 /* ASTextNode.h in Headers */, B35062391B010EFD0018CF92 /* ASThread.h in Headers */, @@ -2154,6 +2264,7 @@ buildConfigurationList = 058D09D2195D04C000B7D73C /* Build configuration list for PBXNativeTarget "AsyncDisplayKitTests" */; buildPhases = ( 2E61B6A0DB0F436A9DDBE86F /* [CP] Check Pods Manifest.lock */, + 78155F4125E9B7C700847DE1 /* Headers */, 058D09B8195D04C000B7D73C /* Sources */, 058D09B9195D04C000B7D73C /* Frameworks */, 058D09BA195D04C000B7D73C /* Resources */, @@ -2173,6 +2284,7 @@ isa = PBXNativeTarget; buildConfigurationList = B35061ED1B010EDF0018CF92 /* Build configuration list for PBXNativeTarget "AsyncDisplayKit" */; buildPhases = ( + 8AADBEAA1CB256CB0B7D2C35 /* [CP] Check Pods Manifest.lock */, B35061D51B010EDF0018CF92 /* Sources */, B35061D61B010EDF0018CF92 /* Frameworks */, B35061D71B010EDF0018CF92 /* Headers */, @@ -2270,7 +2382,7 @@ ); name = "[CP] Check Pods Manifest.lock"; outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-AsyncDisplayKitTests-checkManifestLockResult.txt", + "$(DERIVED_FILE_DIR)/Pods-AsyncDisplayKit-AsyncDisplayKitTests-checkManifestLockResult.txt", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; @@ -2283,18 +2395,46 @@ files = ( ); inputPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-frameworks.sh", + "${PODS_ROOT}/Target Support Files/Pods-AsyncDisplayKit-AsyncDisplayKitTests/Pods-AsyncDisplayKit-AsyncDisplayKitTests-frameworks.sh", + "${BUILT_PRODUCTS_DIR}/IGListKit/IGListKit.framework", + "${BUILT_PRODUCTS_DIR}/Yoga/yoga.framework", + "${BUILT_PRODUCTS_DIR}/JGMethodSwizzler/JGMethodSwizzler.framework", "${BUILT_PRODUCTS_DIR}/OCMock/OCMock.framework", "${BUILT_PRODUCTS_DIR}/iOSSnapshotTestCase/FBSnapshotTestCase.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/IGListKit.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/yoga.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/JGMethodSwizzler.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/OCMock.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/FBSnapshotTestCase.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AsyncDisplayKitTests/Pods-AsyncDisplayKitTests-frameworks.sh\"\n"; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-AsyncDisplayKit-AsyncDisplayKitTests/Pods-AsyncDisplayKit-AsyncDisplayKitTests-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; + 8AADBEAA1CB256CB0B7D2C35 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-AsyncDisplayKit-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -2360,7 +2500,6 @@ ACF6ED601B178DC700DA7C62 /* ASLayoutSpecSnapshotTestsHelper.mm in Sources */, CC7FD9E11BB5F750005CCB2B /* ASPhotosFrameworkImageRequestTests.mm in Sources */, 052EE0661A159FEF002C6279 /* ASMultiplexImageNodeTests.mm in Sources */, - 407B8BAE2310E2ED00CB979E /* ASLayoutSpecUtilitiesTests.mm in Sources */, 058D0A3C195D057000B7D73C /* ASMutableAttributedStringBuilderTests.mm in Sources */, F325E48C21745F9E00AC93A4 /* ASButtonNodeTests.mm in Sources */, 9692B4FF219E12370060C2C3 /* ASCollectionViewThrashTests.mm in Sources */, @@ -2436,6 +2575,7 @@ B350624C1B010EFD0018CF92 /* _ASPendingState.mm in Sources */, 698371DC1E4379CD00437585 /* ASNodeController+Beta.mm in Sources */, CC6AA2DB1E9F03B900978E87 /* ASDisplayNode+Ancestry.mm in Sources */, + 0F8DC397258AC77100E6266C /* ASLayoutElementStyleYoga.mm in Sources */, 509E68621B3AEDA5009B9150 /* ASAbstractLayoutController.mm in Sources */, 254C6B861BF94F8A003EC431 /* ASTextKitContext.mm in Sources */, DBDB83971C6E879900D0098C /* ASPagerFlowLayout.mm in Sources */, @@ -2443,6 +2583,7 @@ 9C8898BC1C738BA800D6B02E /* ASTextKitFontSizeAdjuster.mm in Sources */, 690ED59B1E36D118000627C0 /* ASImageNode+tvOS.mm in Sources */, CCDC9B4E200991D10063C1F8 /* ASGraphicsContext.mm in Sources */, + C0B4751625F836FB00DBFA96 /* NSParagraphStyle+ASText.mm in Sources */, 34EFC7621B701CA400AD841F /* ASBackgroundLayoutSpec.mm in Sources */, 9D9AA56921E23EE200172C09 /* ASDisplayNode+LayoutSpec.mm in Sources */, DE8BEAC41C2DF3FC00D57C12 /* ASDelegateProxy.mm in Sources */, @@ -2462,6 +2603,7 @@ 69CB62AE1CB8165900024920 /* _ASDisplayViewAccessiblity.mm in Sources */, B35061F61B010EFD0018CF92 /* ASCollectionView.mm in Sources */, CCA282C51E9EAE630037E8B7 /* ASLayerBackingTipProvider.mm in Sources */, + C0821DC125F977F600D18030 /* ASDisplayNode+IGListKit.mm in Sources */, 509E68641B3AEDB7009B9150 /* ASCollectionViewLayoutController.mm in Sources */, B35061F91B010EFD0018CF92 /* ASControlNode.mm in Sources */, 8021EC1F1D2B00B100799119 /* UIImage+ASConvenience.mm in Sources */, @@ -2477,10 +2619,12 @@ 9D302F9F2231B373005739C3 /* ASButtonNode+Yoga.mm in Sources */, 34EFC75C1B701BD200AD841F /* ASDimension.mm in Sources */, B350624E1B010EFD0018CF92 /* ASDisplayNode+AsyncDisplay.mm in Sources */, + 0F8DC337258AC67B00E6266C /* ASDisplayNode+Yoga2.mm in Sources */, E5667E8E1F33872700FA6FC0 /* _ASCollectionGalleryLayoutInfo.mm in Sources */, 25E327591C16819500A2170C /* ASPagerNode.mm in Sources */, 636EA1A41C7FF4EC00EE152F /* NSArray+Diffing.mm in Sources */, B35062501B010EFD0018CF92 /* ASDisplayNode+DebugTiming.mm in Sources */, + 788B05A4261CF8960012151B /* ASControlNode+Defines.mm in Sources */, 254C6B891BF94F8A003EC431 /* ASTextKitRenderer+Positioning.mm in Sources */, 68355B341CB579B9001D4E68 /* ASImageNode+AnimatedImage.mm in Sources */, E5711A301C840C96009619D4 /* ASCollectionElement.mm in Sources */, @@ -2514,6 +2658,7 @@ CCEDDDD1200C488000FFCD0A /* ASConfiguration.mm in Sources */, 254C6B841BF94F8A003EC431 /* ASTextNodeWordKerner.mm in Sources */, E5E2D7301EA780DF005C24C6 /* ASCollectionGalleryLayoutDelegate.mm in Sources */, + 0F8DC33A258AC67B00E6266C /* ASNodeContext.mm in Sources */, 34EFC76B1B701CEB00AD841F /* ASLayoutSpec.mm in Sources */, CC3B20861C3F76D600798563 /* ASPendingStateController.mm in Sources */, 254C6B8C1BF94F8A003EC431 /* ASTextKitTailTruncater.mm in Sources */, @@ -2528,6 +2673,7 @@ B35062271B010EFD0018CF92 /* ASRangeController.mm in Sources */, 0442850A1BAA63FE00D16268 /* ASBatchFetching.mm in Sources */, CC35CEC420DD7F600006448D /* ASCollections.mm in Sources */, + 0F8DC33C258AC67B00E6266C /* ASDisplayNode+Yoga2Logging.mm in Sources */, 68FC85E61CE29B9400EDD713 /* ASNavigationController.mm in Sources */, 9C0BA4A42582CE35001C293B /* ASTextAttribute.mm in Sources */, 34EFC76F1B701CF700AD841F /* ASRatioLayoutSpec.mm in Sources */, @@ -2536,7 +2682,6 @@ 90FC784F1E4BFE1B00383C5A /* ASDisplayNode+Yoga.mm in Sources */, CCA282C91E9EB64B0037E8B7 /* ASDisplayNodeTipState.mm in Sources */, 509E68601B3AED8E009B9150 /* ASScrollDirection.mm in Sources */, - E517F9C823BF14BC006E40E0 /* ASLayout+IGListDiffKit.mm in Sources */, B35062091B010EFD0018CF92 /* ASScrollNode.mm in Sources */, 69BCE3D91EC6513B007DCCAD /* ASDisplayNode+Layout.mm in Sources */, 8BDA5FC81CDBDF95007D13B2 /* ASVideoPlayerNode.mm in Sources */, @@ -2556,7 +2701,6 @@ 690C35621E055C5D00069B91 /* ASDimensionInternal.mm in Sources */, 909C4C761F09C98B00D6B76F /* ASTextNode2.mm in Sources */, 68C2155A1DE10D330019C4BC /* ASCollectionViewLayoutInspector.mm in Sources */, - 9C0BA4AC2582CE35001C293B /* NSParagraphStyle+ASText.mm in Sources */, DB78412E1C6BCE1600A9E2B4 /* _ASTransitionContext.mm in Sources */, B350620B1B010EFD0018CF92 /* ASTableView.mm in Sources */, B350620E1B010EFD0018CF92 /* ASTextNode.mm in Sources */, @@ -2691,6 +2835,11 @@ GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; ONLY_ACTIVE_ARCH = YES; + OTHER_CFLAGS = "-Wno-error=deprecated-implementations"; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-Wno-error=gnu-inline-cpp-without-extern", + ); SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -2739,6 +2888,11 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; + OTHER_CFLAGS = "-Wno-error=deprecated-implementations"; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-Wno-error=gnu-inline-cpp-without-extern", + ); SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -2747,7 +2901,7 @@ }; 058D09D3195D04C000B7D73C /* Debug */ = { isa = XCBuildConfiguration; - baseConfigurationReference = FB07EABBCF28656C6297BC2D /* Pods-AsyncDisplayKitTests.debug.xcconfig */; + baseConfigurationReference = 5188E1E61FB74BEA34BB6E3E /* Pods-AsyncDisplayKit-AsyncDisplayKitTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( @@ -2771,7 +2925,7 @@ }; 058D09D4195D04C000B7D73C /* Release */ = { isa = XCBuildConfiguration; - baseConfigurationReference = D3779BCFF841AD3EB56537ED /* Pods-AsyncDisplayKitTests.release.xcconfig */; + baseConfigurationReference = 6A3A1B1A7956190DBE53C847 /* Pods-AsyncDisplayKit-AsyncDisplayKitTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( @@ -2794,8 +2948,9 @@ }; B35061EE1B010EDF0018CF92 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 53E53CFB3EAFB43A19F71891 /* Pods-AsyncDisplayKit.debug.xcconfig */; buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; + APPLICATION_EXTENSION_API_ONLY = NO; CLANG_WARN_UNREACHABLE_CODE = YES; CURRENT_PROJECT_VERSION = 1; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; @@ -2826,8 +2981,9 @@ }; B35061EF1B010EDF0018CF92 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 9613147E8EF4AFA6090F10B5 /* Pods-AsyncDisplayKit.release.xcconfig */; buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; + APPLICATION_EXTENSION_API_ONLY = NO; CLANG_WARN_UNREACHABLE_CODE = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; @@ -2899,6 +3055,11 @@ GCC_WARN_UNUSED_LABEL = YES; GCC_WARN_UNUSED_VARIABLE = YES; IPHONEOS_DEPLOYMENT_TARGET = 9.0; + OTHER_CFLAGS = "-Wno-error=deprecated-implementations"; + OTHER_CPLUSPLUSFLAGS = ( + "$(OTHER_CFLAGS)", + "-Wno-error=gnu-inline-cpp-without-extern", + ); SDKROOT = iphoneos; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; @@ -2907,7 +3068,7 @@ }; DB1020821CBCA2AD00FA6FE1 /* Profile */ = { isa = XCBuildConfiguration; - baseConfigurationReference = BDC2D162BD55A807C1475DA5 /* Pods-AsyncDisplayKitTests.profile.xcconfig */; + baseConfigurationReference = 8383AF842A78759A7A4EF1B4 /* Pods-AsyncDisplayKit-AsyncDisplayKitTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; FRAMEWORK_SEARCH_PATHS = ( @@ -2947,8 +3108,9 @@ }; DB1020841CBCA2AD00FA6FE1 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 1AB64F5EDB37CA6E65E50702 /* Pods-AsyncDisplayKit.profile.xcconfig */; buildSettings = { - APPLICATION_EXTENSION_API_ONLY = YES; + APPLICATION_EXTENSION_API_ONLY = NO; CLANG_WARN_UNREACHABLE_CODE = YES; COPY_PHASE_STRIP = NO; CURRENT_PROJECT_VERSION = 1; diff --git a/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme b/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme index 717ca42d1..3ba46ddf6 100644 --- a/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme +++ b/AsyncDisplayKit.xcodeproj/xcshareddata/xcschemes/AsyncDisplayKit.xcscheme @@ -47,9 +47,6 @@ ReferencedContainer = "container:AsyncDisplayKit.xcodeproj"> - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d4becbdcc..ba57d8a0f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -89,7 +89,6 @@ Copy and paste this to the top of your new file(s): // // Copyright (c) Pinterest, Inc. All rights reserved. // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 -// ``` If you’ve modified an existing file, change the header to: @@ -226,10 +225,7 @@ static void someFunction() { - There is mostly no sense using nullability annotations outside of interface declarations. ```objc // Properties -// Never include: `atomic`, `readwrite`, `strong`, `assign`. -// Only specify nullability if it isn't assumed from NS_ASSUME. -// (nullability, atomicity, storage class, writability, custom getter, custom setter) -@property (nullable, copy) NSNumber *status +@property(nonatomic, strong, nullable) NSNumber *status // Methods - (nullable NSNumber *)doSomethingWithString:(nullable NSString *)str; diff --git a/Podfile b/Podfile index 4d8a38822..1f23eda28 100644 --- a/Podfile +++ b/Podfile @@ -1,8 +1,15 @@ platform :ios, '9.0' -target :'AsyncDisplayKitTests' do +target :'AsyncDisplayKit' do platform :ios, '10.0' use_frameworks! - pod 'OCMock', '~>3.6' - pod 'iOSSnapshotTestCase/Core', '~> 6.2' + + pod 'Yoga' + pod 'IGListKit', '~>3.4' + + target :'AsyncDisplayKitTests' do + pod 'JGMethodSwizzler' + pod 'OCMock', '=3.4.1' + pod 'iOSSnapshotTestCase/Core', '~>6.2' + end end diff --git a/Podfile.lock b/Podfile.lock index b281b28df..d76b57c42 100644 --- a/Podfile.lock +++ b/Podfile.lock @@ -1,20 +1,36 @@ PODS: + - IGListKit (3.4.0): + - IGListKit/Default (= 3.4.0) + - IGListKit/Default (3.4.0): + - IGListKit/Diffing + - IGListKit/Diffing (3.4.0) - iOSSnapshotTestCase/Core (6.2.0) - - OCMock (3.6) + - JGMethodSwizzler (2.0.1) + - OCMock (3.4.1) + - Yoga (1.14.0) DEPENDENCIES: + - IGListKit (~> 3.4) - iOSSnapshotTestCase/Core (~> 6.2) - - OCMock (~> 3.6) + - JGMethodSwizzler + - OCMock (= 3.4.1) + - Yoga SPEC REPOS: trunk: + - IGListKit - iOSSnapshotTestCase + - JGMethodSwizzler - OCMock + - Yoga SPEC CHECKSUMS: + IGListKit: 7a5d788e9fb746bcd402baa8e8b24bc3bd2a5a07 iOSSnapshotTestCase: 9ab44cb5aa62b84d31847f40680112e15ec579a6 - OCMock: 5ea90566be239f179ba766fd9fbae5885040b992 + JGMethodSwizzler: e0ea5c1c6c55304ec4ef3f54e441400c6735c899 + OCMock: 2cd0716969bab32a2283ff3a46fd26a8c8b4c5e3 + Yoga: cff67a400f6b74dc38eb0bad4f156673d9aa980c -PODFILE CHECKSUM: 1b4ea0e8ab7d94a46b1964a2354686c2e599c8c2 +PODFILE CHECKSUM: a94ede4b4176c5971b7727c82520f4a166351fcd -COCOAPODS: 1.10.0 +COCOAPODS: 1.10.1 diff --git a/Source/ASButtonNode+Yoga.mm b/Source/ASButtonNode+Yoga.mm index f4493bc40..bd23eea5f 100644 --- a/Source/ASButtonNode+Yoga.mm +++ b/Source/ASButtonNode+Yoga.mm @@ -33,13 +33,13 @@ @implementation ASButtonNode (Yoga) - (void)updateYogaLayoutIfNeeded { + if (!self.yoga) return; NSMutableArray *children = [[NSMutableArray alloc] initWithCapacity:2]; { ASLockScopeSelf(); // Build up yoga children for button node again unowned ASLayoutElementStyle *style = [self _locked_style]; - [style yogaNodeCreateIfNeeded]; // Setup stack layout values style.flexDirection = _laysOutHorizontally ? ASStackLayoutDirectionHorizontal : ASStackLayoutDirectionVertical; @@ -50,12 +50,10 @@ - (void)updateYogaLayoutIfNeeded // Setup new yoga children if (_imageNode.image != nil) { - [_imageNode.style yogaNodeCreateIfNeeded]; [children addObject:_imageNode]; } if (_titleNode.attributedText.length > 0) { - [_titleNode.style yogaNodeCreateIfNeeded]; if (_imageAlignment == ASButtonNodeImageAlignmentBeginning) { [children addObject:_titleNode]; } else { @@ -80,7 +78,6 @@ - (void)updateYogaLayoutIfNeeded // Add background node if (_backgroundImageNode.image) { - [_backgroundImageNode.style yogaNodeCreateIfNeeded]; [children insertObject:_backgroundImageNode atIndex:0]; _backgroundImageNode.style.positionType = YGPositionTypeAbsolute; diff --git a/Source/ASButtonNode.mm b/Source/ASButtonNode.mm index 66b9cd332..e87e36a3d 100644 --- a/Source/ASButtonNode.mm +++ b/Source/ASButtonNode.mm @@ -9,6 +9,7 @@ #import #import +#import #import #import #import diff --git a/Source/ASCellNode.mm b/Source/ASCellNode.mm index 5c7451198..7477c22b3 100644 --- a/Source/ASCellNode.mm +++ b/Source/ASCellNode.mm @@ -24,6 +24,8 @@ #import #import +using namespace AS; + #pragma mark - #pragma mark ASCellNode @@ -34,6 +36,7 @@ @interface ASCellNode () ASDisplayNode *_viewControllerNode; UIViewController *_viewController; UICollectionViewLayoutAttributes *_layoutAttributes; + __weak id _interactionDelegate; BOOL _suspendInteractionDelegate; BOOL _selected; BOOL _highlighted; @@ -43,7 +46,6 @@ @interface ASCellNode () @end @implementation ASCellNode -@synthesize interactionDelegate = _interactionDelegate; - (instancetype)init { @@ -111,8 +113,8 @@ - (void)layout - (void)_rootNodeDidInvalidateSize { - if (_interactionDelegate != nil) { - [_interactionDelegate nodeDidInvalidateSize:self]; + if (auto interactionDelegate = self.interactionDelegate) { + [interactionDelegate nodeDidInvalidateSize:self]; } else { [super _rootNodeDidInvalidateSize]; } @@ -120,8 +122,8 @@ - (void)_rootNodeDidInvalidateSize - (void)_layoutTransitionMeasurementDidFinish { - if (_interactionDelegate != nil) { - [_interactionDelegate nodeDidInvalidateSize:self]; + if (auto interactionDelegate = self.interactionDelegate) { + [interactionDelegate nodeDidInvalidateSize:self]; } else { [super _layoutTransitionMeasurementDidFinish]; } @@ -205,6 +207,18 @@ - (UIViewController *)viewController return self.collectionElement.owningNode; } +- (id)interactionDelegate +{ + MutexLocker l(__instanceLock__); + return _interactionDelegate; +} + +- (void)setInteractionDelegate:(id)interactionDelegate +{ + MutexLocker l(__instanceLock__); + _interactionDelegate = interactionDelegate; +} + #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wobjc-missing-super-calls" diff --git a/Source/ASCollectionNode+Beta.h b/Source/ASCollectionNode+Beta.h index feb485ba8..c3612497a 100644 --- a/Source/ASCollectionNode+Beta.h +++ b/Source/ASCollectionNode+Beta.h @@ -56,6 +56,21 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, readonly, getter=isSynchronized) BOOL synchronized; +/** + * When bounds change in non-scrollable direction, we remeasure all cells against the new bounds. If + * this property is YES (the default, for historical reasons,) this remeasurement takes place within + * the setBounds: call on the collection view's layer. Otherwise, the remeasurement occurs inside of + * the collection view's layoutSubviews call. + * + * Setting this to NO can avoid duplicated work for example during rotation, where collection view content + * is reloaded or updated in the time between the setBounds: and the layout pass. Having it be YES + * we may remeasure nodes that will be immediately discarded and replaced. + * + * Leaving this as YES will retain historical behavior on which existing application-side collection view + * machinery may depend. + */ +@property (nonatomic) BOOL remeasuresBeforeLayoutPassOnBoundsChange; + /** * Schedules a block to be performed (on the main thread) as soon as the completion block is called * on performBatchUpdates:. @@ -64,6 +79,47 @@ NS_ASSUME_NONNULL_BEGIN */ - (void)onDidFinishSynchronizing:(void (^)(void))didFinishSynchronizing; +/** + * Whether to immediately apply layouts that are generated in the background (if nodes aren't + * loaded). + * + * This feature is considered experimental; please report any issues you encounter. + * + * Defaults to NO. The default may change to YES in the future. + */ +@property(nonatomic) BOOL immediatelyApplyComputedLayouts; + +/** + * The maximum number of elements to insert in each chunk of a collection view update. + * + * 0 means all items will be inserted in one chunk. Default is 0. + */ +@property (nonatomic) NSUInteger updateBatchSize; + +/** + * Whether cell nodes should be temporarily stored in, and pulled from, a global cache + * during updates. The nodeModel will be used as the key. This is useful to reduce the + * cost of operations such as reloading content due to iPad rotation, or moving content + * from one collection node to another, or calling reloadData. Default is NO. + */ +@property (nonatomic) BOOL useNodeCache; + +/** + * A way to override the default ASCellLayoutModeNone behavior of forcing all initial updates to be + * synchronous. Defaults to NO, will eventually flip to YES. + */ +@property(nonatomic) BOOL allowAsyncUpdatesForInitialContent; + +/** + * Whether to defer each layout pass to the next run loop. Useful when, for example, the collection + * node is fully obscured by another view and you want to break up large layout operations such as + * rotations into multiple run loop iterations. + * + * Defaults to NO. + */ +@property (nonatomic) BOOL shouldDelayLayout; + + - (instancetype)initWithFrame:(CGRect)frame collectionViewLayout:(UICollectionViewLayout *)layout layoutFacilitator:(nullable id)layoutFacilitator; - (instancetype)initWithLayoutDelegate:(id)layoutDelegate layoutFacilitator:(nullable id)layoutFacilitator; diff --git a/Source/ASCollectionNode.h b/Source/ASCollectionNode.h index fde5a1302..03cc149f6 100644 --- a/Source/ASCollectionNode.h +++ b/Source/ASCollectionNode.h @@ -774,6 +774,10 @@ NS_ASSUME_NONNULL_BEGIN * @param indexPath The index path of the item. * * @return A constrained size range for layout for the item at this index path. + * + * NOTE: In Yoga2, the min size is ignored and always assumed to be zero. Set the min-width or + * min-height property of your content, or simply choose whatever size you prefer regardless of the + * cell node's measured dimensions. */ - (ASSizeRange)collectionNode:(ASCollectionNode *)collectionNode constrainedSizeForItemAtIndexPath:(NSIndexPath *)indexPath; diff --git a/Source/ASCollectionNode.mm b/Source/ASCollectionNode.mm index 77ac1fa1d..4cf21e733 100644 --- a/Source/ASCollectionNode.mm +++ b/Source/ASCollectionNode.mm @@ -45,6 +45,10 @@ @interface _ASCollectionPendingState : NSObject { unsigned int showsVerticalScrollIndicator:1; unsigned int showsHorizontalScrollIndicator:1; unsigned int pagingEnabled:1; + unsigned int remeasuresBeforeLayoutPass:1; // default is YES + unsigned int immediatelyApplyComputedLayouts:1; + unsigned int allowAsyncUpdatesForInitialContent:1; + unsigned int shouldDelayLayout:1; } _flags; } @property (nonatomic, weak) id delegate; @@ -65,6 +69,10 @@ @interface _ASCollectionPendingState : NSObject { @property (nonatomic) BOOL showsVerticalScrollIndicator; @property (nonatomic) BOOL showsHorizontalScrollIndicator; @property (nonatomic) BOOL pagingEnabled; +@property (nonatomic) BOOL immediatelyApplyComputedLayouts; +@property (nonatomic) BOOL allowAsyncUpdatesForInitialContent; +@property (nonatomic) NSUInteger updateBatchSize; +@property (nonatomic) BOOL useNodeCache; @end @implementation _ASCollectionPendingState @@ -86,6 +94,7 @@ - (instancetype)init _flags.showsVerticalScrollIndicator = YES; _flags.showsHorizontalScrollIndicator = YES; _flags.pagingEnabled = NO; + _flags.remeasuresBeforeLayoutPass = YES; } return self; } @@ -202,6 +211,22 @@ - (void)setPagingEnabled:(BOOL)pagingEnabled _flags.pagingEnabled = pagingEnabled; } +- (BOOL)immediatelyApplyComputedLayouts { + return _flags.immediatelyApplyComputedLayouts; +} + +- (void)setImmediatelyApplyComputedLayouts:(BOOL)immediatelyApply { + _flags.immediatelyApplyComputedLayouts = immediatelyApply; +} + +- (BOOL)allowAsyncUpdatesForInitialContent { + return _flags.allowAsyncUpdatesForInitialContent; +} + +- (void)setAllowAsyncUpdatesForInitialContent:(BOOL)allowAsyncUpdatesForInitialContent { + _flags.allowAsyncUpdatesForInitialContent = allowAsyncUpdatesForInitialContent; +} + #pragma mark Tuning Parameters - (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType @@ -313,16 +338,18 @@ - (void)didLoad if (_pendingState) { _ASCollectionPendingState *pendingState = _pendingState; - self.pendingState = nil; - view.asyncDelegate = pendingState.delegate; - view.asyncDataSource = pendingState.dataSource; - view.inverted = pendingState.inverted; - view.allowsSelection = pendingState.allowsSelection; - view.allowsMultipleSelection = pendingState.allowsMultipleSelection; - view.cellLayoutMode = pendingState.cellLayoutMode; - view.layoutInspector = pendingState.layoutInspector; - view.showsVerticalScrollIndicator = pendingState.showsVerticalScrollIndicator; - view.showsHorizontalScrollIndicator = pendingState.showsHorizontalScrollIndicator; + self.pendingState = nil; + view.asyncDelegate = pendingState.delegate; + view.asyncDataSource = pendingState.dataSource; + view.inverted = pendingState.inverted; + view.allowsSelection = pendingState.allowsSelection; + view.allowsMultipleSelection = pendingState.allowsMultipleSelection; + view.cellLayoutMode = pendingState.cellLayoutMode; + view.layoutInspector = pendingState.layoutInspector; + view.showsVerticalScrollIndicator = pendingState.showsVerticalScrollIndicator; + view.showsHorizontalScrollIndicator = pendingState.showsHorizontalScrollIndicator; + view.remeasuresBeforeLayoutPassOnBoundsChange = pendingState->_flags.remeasuresBeforeLayoutPass; + view.shouldDelayLayout = pendingState->_flags.shouldDelayLayout; #if !TARGET_OS_TV view.pagingEnabled = pendingState.pagingEnabled; #endif @@ -361,7 +388,14 @@ - (void)didLoad if (pendingState.rangeMode != ASLayoutRangeModeUnspecified) { [_rangeController updateCurrentRangeWithMode:pendingState.rangeMode]; } - + + if (pendingState.immediatelyApplyComputedLayouts) { + view.dataController.immediatelyApplyComputedLayouts = YES; + } + view.updateBatchSize = pendingState.updateBatchSize; + view.useNodeCache = pendingState.useNodeCache; + view.allowAsyncUpdatesForInitialContent = pendingState.allowAsyncUpdatesForInitialContent; + // Don't need to set collectionViewLayout to the view as the layout was already used to init the view in view block. } } @@ -679,6 +713,43 @@ - (BOOL)isPagingEnabled } #endif +- (void)setRemeasuresBeforeLayoutPassOnBoundsChange:(BOOL)remeasuresBeforeLayoutPass +{ + if ([self pendingState]) { + _pendingState->_flags.remeasuresBeforeLayoutPass = remeasuresBeforeLayoutPass; + } else { + self.view.remeasuresBeforeLayoutPassOnBoundsChange = remeasuresBeforeLayoutPass; + } +} + +- (BOOL)remeasuresBeforeLayoutPassOnBoundsChange +{ + if ([self pendingState]) { + return _pendingState->_flags.remeasuresBeforeLayoutPass; + } else { + return self.view.remeasuresBeforeLayoutPassOnBoundsChange; + } +} + +- (void)setShouldDelayLayout:(BOOL)shouldDelayLayout +{ + if ([self pendingState]) { + _pendingState->_flags.shouldDelayLayout = shouldDelayLayout; + } else { + self.view.shouldDelayLayout = shouldDelayLayout; + } +} + +- (BOOL)shouldDelayLayout +{ + if ([self pendingState]) { + return _pendingState->_flags.shouldDelayLayout; + } else { + return self.view.shouldDelayLayout; + } +} + + - (void)setCollectionViewLayout:(UICollectionViewLayout *)layout { if ([self pendingState]) { @@ -797,6 +868,78 @@ - (void)setCellLayoutMode:(ASCellLayoutMode)cellLayoutMode } } +- (BOOL)immediatelyApplyComputedLayouts +{ + if ([self pendingState]) { + return _pendingState.immediatelyApplyComputedLayouts; + } else { + return self.dataController.immediatelyApplyComputedLayouts; + } +} + +- (void)setImmediatelyApplyComputedLayouts:(BOOL)immediatelyApplyComputedLayouts +{ + if ([self pendingState]) { + _pendingState.immediatelyApplyComputedLayouts = immediatelyApplyComputedLayouts; + } else { + self.dataController.immediatelyApplyComputedLayouts = immediatelyApplyComputedLayouts; + } +} + +- (NSUInteger)updateBatchSize +{ + if ([self pendingState]) { + return _pendingState.updateBatchSize; + } else { + return self.dataController.updateBatchSize; + } +} + +- (void)setUpdateBatchSize:(NSUInteger)updateBatchSize +{ + if ([self pendingState]) { + _pendingState.updateBatchSize = updateBatchSize; + } else { + self.dataController.updateBatchSize = updateBatchSize; + } +} + +- (BOOL)useNodeCache +{ + if ([self pendingState]) { + return _pendingState.useNodeCache; + } else { + return self.dataController.useNodeCache; + } +} + +- (void)setUseNodeCache:(BOOL)useNodeCache +{ + if ([self pendingState]) { + _pendingState.useNodeCache = useNodeCache; + } else { + self.dataController.useNodeCache = useNodeCache; + } +} + +- (BOOL)allowAsyncUpdatesForInitialContent +{ + if ([self pendingState]) { + return _pendingState.allowAsyncUpdatesForInitialContent; + } else { + return self.view.allowAsyncUpdatesForInitialContent; + } +} + +- (void)setAllowAsyncUpdatesForInitialContent:(BOOL)allowAsyncUpdatesForInitialContent +{ + if ([self pendingState]) { + _pendingState.allowAsyncUpdatesForInitialContent = allowAsyncUpdatesForInitialContent; + } else { + self.view.allowAsyncUpdatesForInitialContent = allowAsyncUpdatesForInitialContent; + } +} + #pragma mark - Range Tuning - (ASRangeTuningParameters)tuningParametersForRangeType:(ASLayoutRangeType)rangeType diff --git a/Source/ASCollectionView.h b/Source/ASCollectionView.h index 1b250a21f..a9baea7ca 100644 --- a/Source/ASCollectionView.h +++ b/Source/ASCollectionView.h @@ -81,6 +81,17 @@ NS_ASSUME_NONNULL_BEGIN */ - (nullable id)contextForSection:(NSInteger)section AS_WARN_UNUSED_RESULT; +/** + * Ignores size changes to the cells. Enable if you are using a custom layout that adjusts cell + * size. + */ +@property(nonatomic) BOOL ignoreCellNodeSizeChanges; + +/** + * @see ASCollectionNode+Beta.h for full documentation. + */ +@property (nonatomic) BOOL shouldDelayLayout; + @end @interface ASCollectionView (Deprecated) diff --git a/Source/ASCollectionView.mm b/Source/ASCollectionView.mm index b81365e8f..5e1dfd153 100644 --- a/Source/ASCollectionView.mm +++ b/Source/ASCollectionView.mm @@ -8,6 +8,7 @@ // #import +#import #import #import #import @@ -80,6 +81,39 @@ typedef NS_ENUM(NSUInteger, ASCollectionViewInvalidationStyle) { /// Used for all cells and supplementaries. UICV keys by supp-kind+reuseID so this is plenty. static NSString * const kReuseIdentifier = @"_ASCollectionReuseIdentifier"; +#if AS_SIGNPOST_ENABLE +static void InstallPrepareLayoutSignposts(Class layoutClass) { + static dispatch_once_t onceToken; + static NSMutableSet *hookedClasses; + static NSMutableSet *activeInstances; + dispatch_once(&onceToken, ^{ + hookedClasses = ASCreatePointerBasedMutableSet(); + activeInstances = ASCreatePointerBasedMutableSet(); + }); + if ([hookedClasses containsObject:layoutClass]) return; + [hookedClasses addObject:layoutClass]; + __block void (*origPrepareLayout)(id, SEL) = (void (*)(id, SEL))ASReplaceMethodWithBlock( + layoutClass, @selector(prepareLayout), (id) ^ (UICollectionViewLayout * layout) { + // We only want one pair of signposts per call, but we may have injected into multiple + // clsses in the same hierarchy. Use set for deduping. + BOOL isRootCall = ![activeInstances containsObject:layout]; + if (isRootCall) { + [activeInstances addObject:layout]; + UICollectionView *cv = layout.collectionView; + CGRect frame = [cv convertRect:cv.bounds toView:nil]; + ASSignpostStart(CollectionPrepareLayout, layout, "%@ %@", ASObjectDescriptionMakeTiny(cv), + NSStringFromCGRect(frame)); + } + // Do the actual work. + origPrepareLayout(layout, @selector(prepareLayout)); + if (isRootCall) { + [activeInstances removeObject:layout]; + ASSignpostEnd(CollectionPrepareLayout, layout, ""); + } + }); +} +#endif + #pragma mark - #pragma mark ASCollectionView. @@ -108,6 +142,9 @@ @interface ASCollectionView () _superIsPendingDataLoad = YES; updates(); [self _superReloadData:nil completion:nil]; os_log_debug(ASCollectionLog(), "Did reloadData %@", self.collectionNode); @@ -2343,8 +2518,17 @@ - (void)nodeHighlightedStateDidChange:(ASCellNode *)node - (void)nodeDidInvalidateSize:(ASCellNode *)node { + ASDisplayNodeAssertMainThread(); + if (self.ignoreCellNodeSizeChanges) { + return; + } + [_cellsForLayoutUpdates addObject:node]; - [self setNeedsLayout]; + if (auto node = self.asyncdisplaykit_node) { + [node setNeedsLayout]; + } else { + [self setNeedsLayout]; + } } - (void)nodesDidRelayout:(NSArray *)nodes @@ -2430,6 +2614,14 @@ - (void)didMoveToWindow } } +#pragma mark - UIView Overrides + +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event +{ + id uikitAction = [super actionForLayer:layer forKey:event]; + return ASDisplayNodeActionForLayer(layer, event, self.collectionNode, uikitAction); +} + - (void)willMoveToSuperview:(UIView *)newSuperview { if (self.superview == nil && newSuperview != nil) { @@ -2446,26 +2638,12 @@ - (void)didMoveToSuperview #pragma mark ASCALayerExtendedDelegate -/** - * TODO: This code was added when we used @c calculatedSize as the size for - * items (e.g. collectionView:layout:sizeForItemAtIndexPath:) and so it - * was critical that we remeasured all nodes at this time. - * - * The assumption was that cv-bounds-size-change -> constrained-size-change, so - * this was the time when we get new constrained sizes for all items and remeasure - * them. However, the constrained sizes for items can be invalidated for many other - * reasons, hence why we never reuse the old constrained size anymore. - * - * UICollectionView inadvertently triggers a -prepareLayout call to its layout object - * between [super setFrame:] and [self layoutSubviews] during size changes. So we need - * to get in there and re-measure our nodes before that -prepareLayout call. - * We can't wait until -layoutSubviews or the end of -setFrame:. - * - * @see @p testThatNodeCalculatedSizesAreUpdatedBeforeFirstPrepareLayoutAfterRotation - */ -- (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds newValue:(CGRect)newBounds +/// If bounds changed in non-scrolling direction since last measurement, remeasure. +- (void)remeasureNodesIfNeededForBoundsChange { - CGSize newSize = newBounds.size; + CGRect bounds = self.bounds; + + CGSize newSize = bounds.size; CGSize lastUsedSize = _lastBoundsSizeUsedForMeasuringNodes; if (CGSizeEqualToSize(lastUsedSize, newSize)) { return; @@ -2494,6 +2672,36 @@ - (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds new } } +/** + * TODO: This code was added when we used @c calculatedSize as the size for + * items (e.g. collectionView:layout:sizeForItemAtIndexPath:) and so it + * was critical that we remeasured all nodes at this time. + * + * The assumption was that cv-bounds-size-change -> constrained-size-change, so + * this was the time when we get new constrained sizes for all items and remeasure + * them. However, the constrained sizes for items can be invalidated for many other + * reasons, hence why we never reuse the old constrained size anymore. + * + * UICollectionView inadvertently triggers a -prepareLayout call to its layout object + * between [super setFrame:] and [self layoutSubviews] during size changes. So we need + * to get in there and re-measure our nodes before that -prepareLayout call. + * We can't wait until -layoutSubviews or the end of -setFrame:. + * + * @see @p testThatNodeCalculatedSizesAreUpdatedBeforeFirstPrepareLayoutAfterRotation + */ +- (void)layer:(CALayer *)layer didChangeBoundsWithOldValue:(CGRect)oldBounds newValue:(CGRect)newBounds +{ + if (_collectionViewFlags.remeasuresBeforeLayoutPass) { + if (_collectionViewFlags.shouldDelayLayout) { + dispatch_async(dispatch_get_main_queue(), ^{ + [self remeasureNodesIfNeededForBoundsChange]; + }); + } else { + [self remeasureNodesIfNeededForBoundsChange]; + } + } +} + #pragma mark - UICollectionView dead-end intercepts - (void)setPrefetchDataSource:(id)prefetchDataSource diff --git a/Source/ASCollectionViewProtocols.h b/Source/ASCollectionViewProtocols.h index ec1b8d577..e5cb8015c 100644 --- a/Source/ASCollectionViewProtocols.h +++ b/Source/ASCollectionViewProtocols.h @@ -23,8 +23,9 @@ typedef NS_OPTIONS(unsigned short, ASCellLayoutMode) { * * Note: Sync & Async flags force the behavior to be always one or the other, regardless of the * items. Default: If neither ASCellLayoutModeAlwaysSync or ASCellLayoutModeAlwaysAsync is set, - * default behavior is synchronous when there are 0 or 1 ASCellNodes in the data source, and - * asynchronous when there are 2 or more. + * default behavior is synchronous when there are 0 or 1 ASCellNodes in the data source, or if + * the current content does not fill the bounds, and asynchronous when there are 2 or more. + * The latter restriction about content size can be disabled using the allowAsyncUpdatesForInitialContent flag. */ ASCellLayoutModeAlwaysSync = 1 << 1, // Default OFF ASCellLayoutModeAlwaysAsync = 1 << 2, // Default OFF diff --git a/Source/ASConfigurationInternal.h b/Source/ASConfigurationInternal.h index fa6949622..d286de999 100644 --- a/Source/ASConfigurationInternal.h +++ b/Source/ASConfigurationInternal.h @@ -31,6 +31,15 @@ NS_ASSUME_NONNULL_BEGIN }) #endif +#define ASAssertExperiment(opt) \ + NSAssert(ASActivateExperimentalFeature(opt), @"%s should be enabled.", #opt) +#define ASAssertNotExperiment(opt) \ + NSAssert(!ASActivateExperimentalFeature(opt), @"%s should be disabled.", #opt) +#define ASCAssertExperiment(opt) \ + NSCAssert(ASActivateExperimentalFeature(opt), @"%s should be enabled.", #opt) +#define ASCAssertNotExperiment(opt) \ + NSCAssert(!ASActivateExperimentalFeature(opt), @"%s should be disabled.", #opt) + /** * Internal function. Use the macro without the underbar. */ diff --git a/Source/ASConfigurationInternal.mm b/Source/ASConfigurationInternal.mm index 4bcc1ffd5..dea09dbee 100644 --- a/Source/ASConfigurationInternal.mm +++ b/Source/ASConfigurationInternal.mm @@ -22,6 +22,7 @@ } @implementation ASConfigurationManager { +@package ASConfiguration *_config; dispatch_queue_t _delegateQueue; BOOL _frameworkInitialized; @@ -31,8 +32,8 @@ @implementation ASConfigurationManager { + (ASConfiguration *)defaultConfiguration NS_RETURNS_RETAINED { ASConfiguration *config = [[ASConfiguration alloc] init]; - // TODO(wsdwsd0829): Fix #788 before enabling it. - // config.experimentalFeatures = ASExperimentalInterfaceStateCoalescing; + // TODO(wsdwsd0829): Fix #788 before enabling it. (Did we?) + config.experimentalFeatures = ASExperimentalInterfaceStateCoalescing; return config; } diff --git a/Source/ASControlNode.mm b/Source/ASControlNode.mm index 4bc0ae604..a59899f1c 100644 --- a/Source/ASControlNode.mm +++ b/Source/ASControlNode.mm @@ -11,7 +11,9 @@ #import #import #import +#import #import +#import #import #import #import @@ -19,6 +21,7 @@ #import #endif + // 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. @@ -108,7 +111,9 @@ - (void)didLoad - (void)setUserInteractionEnabled:(BOOL)userInteractionEnabled { [super setUserInteractionEnabled:userInteractionEnabled]; - self.isAccessibilityElement = userInteractionEnabled; + if ([ASControlNode shouldUserInteractionEnabledSetIsAXElement]) { + self.isAccessibilityElement = userInteractionEnabled; + } } - (void)__exitHierarchy diff --git a/Source/ASDKViewController.mm b/Source/ASDKViewController.mm index da4a19e82..a2401bad3 100644 --- a/Source/ASDKViewController.mm +++ b/Source/ASDKViewController.mm @@ -105,6 +105,14 @@ - (void)_initializeInstance } } +- (void)dealloc +{ + if (ASActivateExperimentalFeature(ASExperimentalOOMBackgroundDeallocDisable)) { + return; + } + ASPerformBackgroundDeallocation(&_node); +} + - (void)loadView { // Apple applies a frame and autoresizing masks we need. Allocating a view is not diff --git a/Source/ASDisplayNode+Beta.h b/Source/ASDisplayNode+Beta.h index 882042a97..20aaf5918 100644 --- a/Source/ASDisplayNode+Beta.h +++ b/Source/ASDisplayNode+Beta.h @@ -132,6 +132,8 @@ AS_CATEGORY_IMPLEMENTABLE AS_CATEGORY_IMPLEMENTABLE - (void)willCalculateLayout:(ASSizeRange)constrainedSize NS_REQUIRES_SUPER; +AS_CATEGORY_IMPLEMENTABLE +- (void)didCalculateLayout:(ASSizeRange)constrainedSize NS_REQUIRES_SUPER; /** * Only ASLayoutRangeModeVisibleOnly or ASLayoutRangeModeLowMemory are recommended. Default is ASLayoutRangeModeVisibleOnly, * because this is the only way to ensure an application will not have blank / flashing views as the user navigates back after @@ -166,6 +168,85 @@ AS_CATEGORY_IMPLEMENTABLE */ - (void)enableSubtreeRasterization; +/** + * Enable the yoga layout engine for this node and its subtree. + * + * Note: You cannot add a yoga node to a non-yoga supernode. The entire + * tree must be one way or the other. + * + * Note: You can use ASNodeContext to enable Yoga on a per-context basis. + */ +- (void)enableYoga; + +/** + * Whether yoga is enabled. + */ +@property (readonly) BOOL yoga; + +/** + * Enable View Flattening support for this node and its subtree + * + * Note: You cannot enable view flattening in yoga1 right now. + */ +- (void)enableViewFlattening; + +/** + * Returns YES if the node will have a custom measure function. This means that, when using the + * Yoga2 layout engine, the node cannot have any children. + */ +- (BOOL)hasCustomMeasure; + +/** + * A dictionary of actions that will be performed when the node is removed from the view hierarchy. + * This is only supported with Yoga2 and allows for nodes to be animated during removal. + */ +@property (nullable) NSDictionary> *disappearanceActions; + +/** + * Set to YES when a node is in the process of having its disappearance animations performed. + * This can be canceled if the node is re-added before its disappearance has been completed. + */ +@property BOOL isDisappearing; + +/** + * @abstract Top, left, bottom, right padding values for the node. + */ +@property (readonly) UIEdgeInsets paddings; + +/** + * @abstract Called when the node's layer is about to enter the hierarchy. + * @discussion May be called more than once if the layer is participating in a higher-level + * animation, such as a UIViewController transition. These animations can cause the layer to get + * re-parented multiple times, and each time will trigger this call. + * @note This method is guaranteed to be called on main. + */ + +AS_CATEGORY_IMPLEMENTABLE +- (void)didEnterHierarchy; + +/** + * @abstract Called after controller sets its childern. + * @discussion By default it will set as Yoga children this controller's node when this node is not + * leaf. + * @note subclass need to access node on controller in order to create subtrees correctly. + */ +- (void)controllerDidSetChildren:(NSArray *)children; + +/** + * @abstract Called after the controller inserts a child controller. + * @discussion By default it will insert child to this controller's node at index when this node is + * not leaf. + * @note subclass need to access node on controller in order to create subtress correctly. + */ +- (void)controllerDidInsertChild:(id)child atIndex:(NSInteger)index; + +/** + * @abastract Called after controller removed a child controller. + * @discussion By default it will remove yoga child from this controller's node when this node is + * not leaf. + */ +- (void)controllerDidRemoveChild:(id)child; + @end NS_ASSUME_NONNULL_END diff --git a/Source/ASDisplayNode+IGListKit.h b/Source/ASDisplayNode+IGListKit.h new file mode 100644 index 000000000..75a6d0647 --- /dev/null +++ b/Source/ASDisplayNode+IGListKit.h @@ -0,0 +1,20 @@ +#import + +#if AS_IG_LIST_KIT + +#import + +#import + +NS_ASSUME_NONNULL_BEGIN + +/** + * A trivial implementation of IGListDiffable for nodes. We currently use IGListDiff to update the + * node tree to match the yoga tree. + */ +@interface ASDisplayNode (IGListDiffable) +@end + +NS_ASSUME_NONNULL_END + +#endif diff --git a/Source/ASDisplayNode+IGListKit.mm b/Source/ASDisplayNode+IGListKit.mm new file mode 100644 index 000000000..2484573ca --- /dev/null +++ b/Source/ASDisplayNode+IGListKit.mm @@ -0,0 +1,19 @@ +#import + +#if AS_IG_LIST_KIT + +@implementation ASDisplayNode (IGListDiffable) + +#pragma mark IGListDiffable + +- (id)diffIdentifier { + return self; +} + +- (BOOL)isEqualToDiffableObject:(id)object { + return self == object; +} + +@end + +#endif diff --git a/Source/ASDisplayNode+Layout.mm b/Source/ASDisplayNode+Layout.mm index ab99d3ae1..932ae06b2 100644 --- a/Source/ASDisplayNode+Layout.mm +++ b/Source/ASDisplayNode+Layout.mm @@ -9,6 +9,10 @@ #import #import +#import +#import +#import +#import #import #import #import @@ -16,9 +20,10 @@ #import #import #import +#import #import -using AS::MutexLocker; +using namespace AS; @interface ASDisplayNode (ASLayoutElementStyleDelegate) @end @@ -28,6 +33,8 @@ @implementation ASDisplayNode (ASLayoutElementStyleDelegate) #pragma mark - (void)style:(ASLayoutElementStyle *)style propertyDidChange:(NSString *)propertyName { + // NOTE: In Yoga we do not use this pathway. Yoga internally propagates dirtiness up. + Yoga2::AssertDisabled(self); [self setNeedsLayout]; } @@ -59,12 +66,18 @@ - (ASLayoutElementStyle *)_locked_style DISABLED_ASAssertLocked(__instanceLock__); if (_style == nil) { #if YOGA - // In Yoga mode we use the delegate to inform the tree if properties changes + if (_flags.yoga) { + ASNodeContextPush(_nodeContext); + _style = (ASLayoutElementStyle *)[[ASLayoutElementStyleYoga alloc] init]; + ASNodeContextPop(); + } else { + _style = [[ASLayoutElementStyle alloc] initWithDelegate:self]; + } +#else // YOGA _style = [[ASLayoutElementStyle alloc] initWithDelegate:self]; -#else - _style = [[ASLayoutElementStyle alloc] init]; -#endif +#endif // YOGA } + return _style; } @@ -80,14 +93,38 @@ - (ASLayoutElementType)layoutElementType #pragma mark Measurement Pass +- (CGSize)measure:(ASSizeRange)sizeRange { + ASSignpostStart(Measure, self, "%@ main: %d width: %d", ASObjectDescriptionMakeTiny(self), + ASDisplayNodeThreadIsMain(), (int)sizeRange.max.width); + CGSize result = CGSizeZero; + if (Yoga2::GetEnabled(self)) { + MutexLocker l(__instanceLock__); + Yoga2::CalculateLayoutAtRoot(self, sizeRange.max); + result = Yoga2::GetCalculatedSize(self); + + // Clamp the resulting size to the min size if necessary + result = CGSizeMake(std::max(result.width, sizeRange.min.width), + std::max(result.height, sizeRange.min.height)); + } else { + result = [self layoutThatFits:sizeRange].size; + } + ASSignpostEnd(Measure, self, "height: %d", (int)result.height); + return result; +} + - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize { + if (Yoga2::GetEnabled(self)) { + MutexLocker l(__instanceLock__); + Yoga2::CalculateLayoutAtRoot(self, constrainedSize.max); + return Yoga2::GetCalculatedLayout(self, constrainedSize); + } return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max]; } - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize parentSize:(CGSize)parentSize { - ASScopedLockSelfOrToRoot(); + ASLockScopeSelf(); // If one or multiple layout transitions are in flight it still can happen that layout information is requested // on other threads. As the pending and calculated layout to be updated in the layout transition in here just a @@ -137,7 +174,12 @@ - (void)setPrimitiveTraitCollection:(ASPrimitiveTraitCollection)traitCollection _primitiveTraitCollection = traitCollection; l.unlock(); + if (ASActivateExperimentalFeature(ASExperimentalTraitCollectionDidChangeWithPreviousCollection)) { [self asyncTraitCollectionDidChangeWithPreviousTraitCollection:previousTraitCollection]; + } else { + [self asyncTraitCollectionDidChange]; + } + } } @@ -164,35 +206,30 @@ - (NSString *)asciiArtName @end -#pragma mark - #pragma mark - ASDisplayNode (ASLayout) @implementation ASDisplayNode (ASLayout) -- (ASLayoutEngineType)layoutEngineType +- (ASLayout *)calculatedLayout { + if (Yoga2::GetEnabled(self)) { #if YOGA - MutexLocker l(__instanceLock__); - YGNodeRef yogaNode = _style.yogaNode; - BOOL hasYogaParent = (_yogaParent != nil); - BOOL hasYogaChildren = (_yogaChildren.count > 0); - if (yogaNode != NULL && (hasYogaParent || hasYogaChildren)) { - return ASLayoutEngineTypeYoga; + AS::LockSet locks = [self lockToRootIfNeededForLayout]; +#endif // YOGA + return Yoga2::GetCalculatedLayout(self, ASSizeRangeZero); } -#endif - - return ASLayoutEngineTypeLayoutSpec; -} - -- (ASLayout *)calculatedLayout -{ - MutexLocker l(__instanceLock__); return _calculatedDisplayNodeLayout.layout; } - (CGSize)calculatedSize { - MutexLocker l(__instanceLock__); + if (Yoga2::GetEnabled(self)) { +#if YOGA + AS::LockSet locks = [self lockToRootIfNeededForLayout]; +#endif // YOGA + return Yoga2::GetCalculatedSize(self); + } + if (_pendingDisplayNodeLayout.isValid(_layoutVersion)) { return _pendingDisplayNodeLayout.layout.size; } @@ -208,6 +245,7 @@ - (ASSizeRange)constrainedSizeForCalculatedLayout - (ASSizeRange)_locked_constrainedSizeForCalculatedLayout { DISABLED_ASAssertLocked(__instanceLock__); + Yoga2::AssertDisabled(self); if (_pendingDisplayNodeLayout.isValid(_layoutVersion)) { return _pendingDisplayNodeLayout.constrainedSize; } @@ -216,7 +254,6 @@ - (ASSizeRange)_locked_constrainedSizeForCalculatedLayout @end -#pragma mark - #pragma mark - ASDisplayNode (ASLayoutElementStylability) @implementation ASDisplayNode (ASLayoutElementStylability) @@ -229,7 +266,6 @@ - (instancetype)styledWithBlock:(AS_NOESCAPE void (^)(__kindof ASLayoutElementSt @end -#pragma mark - #pragma mark - ASDisplayNode (ASLayoutInternal) @implementation ASDisplayNode (ASLayoutInternal) @@ -242,6 +278,7 @@ @implementation ASDisplayNode (ASLayoutInternal) */ - (void)_u_setNeedsLayoutFromAbove { + Yoga2::AssertDisabled(self); ASDisplayNodeAssertThreadAffinity(self); DISABLED_ASAssertUnlocked(__instanceLock__); @@ -269,9 +306,10 @@ - (void)_u_setNeedsLayoutFromAbove // cannot due to locking to root in `_u_measureNodeWithBoundsIfNecessary`. - (void)_rootNodeDidInvalidateSize { + Yoga2::AssertDisabled(self); ASDisplayNodeAssertThreadAffinity(self); __instanceLock__.lock(); - + // We are the root node and need to re-flow the layout; at least one child needs a new size. CGSize boundsSizeForLayout = ASCeilSizeValues(self.bounds.size); @@ -308,6 +346,7 @@ - (void)_rootNodeDidInvalidateSize // causing real issues in cases of resizing nodes. - (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size { + Yoga2::AssertDisabled(self); ASDisplayNodeAssertThreadAffinity(self); // The default implementation of display node changes the size of itself to the new size @@ -331,7 +370,8 @@ - (void)displayNodeDidInvalidateSizeNewSize:(CGSize)size - (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds { DISABLED_ASAssertUnlocked(__instanceLock__); - ASScopedLockSelfOrToRoot(); + ASLockScopeSelf(); + Yoga2::AssertDisabled(self); // Check if we are a subnode in a layout transition. // In this case no measurement is needed as it's part of the layout transition @@ -402,19 +442,9 @@ - (void)_u_measureNodeWithBoundsIfNecessary:(CGRect)bounds // Use the last known constrainedSize passed from a parent during layout (if never, use bounds). NSUInteger version = _layoutVersion; ASSizeRange constrainedSize = [self _locked_constrainedSizeForLayoutPass]; -#if YOGA - // This flag indicates to the Texture+Yoga code that this next layout is intended to be - // displayed (vs. just for measurement). This will cause it to call setNeedsLayout on any nodes - // whose layout changes as a result of the Yoga recalculation. This is necessary because a - // change in one Yoga node can change the layout for any other node in the tree. - self.willApplyNextYogaCalculatedLayout = YES; -#endif ASLayout *layout = [self calculateLayoutThatFits:constrainedSize restrictedToSize:self.style.size relativeToParentSize:boundsSizeForLayout]; -#if YOGA - self.willApplyNextYogaCalculatedLayout = NO; -#endif nextLayout = ASDisplayNodeLayout(layout, constrainedSize, boundsSizeForLayout, version); // Now that the constrained size of pending layout might have been reused, the layout is useless // Release it and any orphaned subnodes it retains @@ -480,6 +510,7 @@ - (ASSizeRange)_constrainedSizeForLayoutPass - (ASSizeRange)_locked_constrainedSizeForLayoutPass { + Yoga2::AssertDisabled(self); // TODO: The logic in -_u_setNeedsLayoutFromAbove seems correct and doesn't use this method. // logic seems correct. For what case does -this method need to do the CGSizeEqual checks? // IF WE CAN REMOVE BOUNDS CHECKS HERE, THEN WE CAN ALSO REMOVE "REQUESTED FROM ABOVE" CHECK @@ -511,6 +542,7 @@ - (void)_layoutSublayouts { ASDisplayNodeAssertThreadAffinity(self); DISABLED_ASAssertUnlocked(__instanceLock__); + Yoga2::AssertDisabled(self); ASLayout *layout; { @@ -535,7 +567,6 @@ - (void)_layoutSublayouts @end -#pragma mark - #pragma mark - ASDisplayNode (ASAutomatic Subnode Management) @implementation ASDisplayNode (ASAutomaticSubnodeManagement) @@ -545,6 +576,9 @@ @implementation ASDisplayNode (ASAutomaticSubnodeManagement) - (BOOL)automaticallyManagesSubnodes { MutexLocker l(__instanceLock__); + if (Yoga2::GetEnabled(self)) { + return YES; + } return _flags.automaticallyManagesSubnodes; } @@ -675,7 +709,7 @@ - (void)transitionLayoutWithSizeRange:(ASSizeRange)constrainedSize NSUInteger newLayoutVersion = self->_layoutVersion; ASLayout *newLayout; { - ASScopedLockSelfOrToRoot(); + ASLockScopeSelf(); ASLayoutElementContext *ctx = [[ASLayoutElementContext alloc] init]; ctx.transitionID = transitionID; @@ -1008,15 +1042,10 @@ - (void)_assertSubnodeState - (void)_pendingLayoutTransitionDidComplete { - // This assertion introduces a breaking behavior for nodes that has ASM enabled but also manually manage some subnodes. - // Let's gate it behind YOGA flag. -#if YOGA - [self _assertSubnodeState]; -#endif - // Subclass hook // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 DISABLED_ASAssertUnlocked(__instanceLock__); + Yoga2::AssertDisabled(self); [self calculatedLayoutDidChange]; // Grab lock after calling out to subclass @@ -1069,30 +1098,3 @@ - (void)_locked_setCalculatedDisplayNodeLayout:(const ASDisplayNodeLayout &)disp } @end - -#pragma mark - -#pragma mark - ASDisplayNode (YogaLayout) - -@implementation ASDisplayNode (YogaLayout) - -- (BOOL)locked_shouldLayoutFromYogaRoot { -#if YOGA - YGNodeRef yogaNode = _style.yogaNode; - BOOL hasYogaParent = (_yogaParent != nil); - BOOL hasYogaChildren = (_yogaChildren.count > 0); - BOOL usesYoga = (yogaNode != NULL && (hasYogaParent || hasYogaChildren)); - if (usesYoga) { - if ([self shouldHaveYogaMeasureFunc] == NO) { - return YES; - } else { - return NO; - } - } else { - return NO; - } -#else - return NO; -#endif -} - -@end diff --git a/Source/ASDisplayNode+Subclasses.h b/Source/ASDisplayNode+Subclasses.h index 9adb9336a..f8b879309 100644 --- a/Source/ASDisplayNode+Subclasses.h +++ b/Source/ASDisplayNode+Subclasses.h @@ -171,6 +171,13 @@ AS_CATEGORY_IMPLEMENTABLE */ - (void)invalidateCalculatedLayout; +/** + * @abstract Implement to support yoga baseline layout calculation. + * + * Default implementation returns the height, but you should not call it. + */ +- (float)yogaBaselineWithSize:(CGSize)size; + #pragma mark - Observing Node State Changes /** @name Observing node state changes */ @@ -201,12 +208,23 @@ 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 ASDISPLAYNODE_REQUIRES_SUPER ASDISPLAYNODE_DEPRECATED_MSG("Use asyncTraitCollectionDidChangeWithPreviousTraitCollection: instead."); + + /** * @abstract Called when the node's ASTraitCollection changes * * @discussion Subclasses can override this method to react to a trait collection change. * * @param previousTraitCollection The ASPrimitiveTraitCollection object before the interface environment changed. + * + * @note Enable `ASExperimentalTraitCollectionDidChangeWithPreviousCollection` experiment to have this method called instead of `asyncTraitCollectionDidChange`. */ AS_CATEGORY_IMPLEMENTABLE - (void)asyncTraitCollectionDidChangeWithPreviousTraitCollection:(ASPrimitiveTraitCollection)previousTraitCollection ASDISPLAYNODE_REQUIRES_SUPER; diff --git a/Source/ASDisplayNode+Yoga.h b/Source/ASDisplayNode+Yoga.h index 000bb90e0..b97a8abc9 100644 --- a/Source/ASDisplayNode+Yoga.h +++ b/Source/ASDisplayNode+Yoga.h @@ -6,65 +6,57 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // +#import + +#import #import #if YOGA NS_ASSUME_NONNULL_BEGIN -@class ASLayout; - -ASDK_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Nullable node, void(^block)(ASDisplayNode *node)); - @interface ASDisplayNode (Yoga) -@property (copy) NSArray *yogaChildren; +@property(copy) NSArray *yogaChildren; +@property(readonly, weak) ASDisplayNode *yogaParent; + +// This is class method for the ease of testing rtl behaviors. ++ (BOOL)isRTLForNode:(ASDisplayNode *)node; - (void)addYogaChild:(ASDisplayNode *)child; - (void)removeYogaChild:(ASDisplayNode *)child; - (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index; - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute; +- (UIUserInterfaceLayoutDirection)yogaLayoutDirection; -@property BOOL yogaLayoutInProgress; -// TODO: Make this atomic (lock). -@property (nullable, nonatomic) ASLayout *yogaCalculatedLayout; -@property (nonatomic) BOOL willApplyNextYogaCalculatedLayout; +// If set, Yoga will not perform custom measurement on this node even if it overrides @c +// calculateSizeThatFits:. +@property(nonatomic) BOOL shouldSuppressYogaCustomMeasure; // Will walk up the Yoga tree and returns the root node - (ASDisplayNode *)yogaRoot; - @end +#ifdef __cplusplus @interface ASDisplayNode (YogaLocking) + /** - * @discussion Attempts(spinning) to lock all node up to root node when yoga is enabled. - * This will lock self when yoga is not enabled; + * @discussion Attempts (yielding on failure) to lock all nodes up to root node when yoga is + * enabled. This will lock self when yoga is not enabled. Returns whether the locking into the given + * LockSet was successful i.e. you can use this inside your while loop for multi-locking. */ -- (ASLockSet)lockToRootIfNeededForLayout; +- (BOOL)lockToRootIfNeededForLayout:(AS::LockSet *)locks; -@end - - -// These methods are intended to be used internally to Texture, and should not be called directly. -@interface ASDisplayNode (YogaInternal) - -/// For internal usage only -- (BOOL)shouldHaveYogaMeasureFunc; -/// For internal usage only -- (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize; -/// For internal usage only -- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize willApply:(BOOL)willApply; -/// For internal usage only -- (void)invalidateCalculatedYogaLayout; /** - * @discussion return true only when yoga enabled and the node is in yoga tree and the node is - * not leaf that implemented measure function. + * Same as above, but returns a new lock set instead of using one that you provide. Prefer the + * above method in new code. */ -- (BOOL)locked_shouldLayoutFromYogaRoot; +- (AS::LockSet)lockToRootIfNeededForLayout; @end +#endif @interface ASDisplayNode (YogaDebugging) @@ -74,15 +66,13 @@ ASDK_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Null @interface ASLayoutElementStyle (Yoga) -- (YGNodeRef)yogaNodeCreateIfNeeded; -- (void)destroyYogaNode; - -@property (readonly) YGNodeRef yogaNode; +@property(readonly) YGNodeRef yogaNode; @property ASStackLayoutDirection flexDirection; @property YGDirection direction; @property ASStackLayoutJustifyContent justifyContent; @property ASStackLayoutAlignItems alignItems; +@property ASStackLayoutAlignItems alignContent; @property YGPositionType positionType; @property ASEdgeInsets position; @property ASEdgeInsets margin; @@ -95,9 +85,4 @@ ASDK_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode * _Null NS_ASSUME_NONNULL_END -// When Yoga is enabled, there are several points where we want to lock the tree to the root but otherwise (without Yoga) -// will want to simply lock self. -#define ASScopedLockSelfOrToRoot() ASScopedLockSet lockSet = [self lockToRootIfNeededForLayout] -#else -#define ASScopedLockSelfOrToRoot() ASLockScopeSelf() #endif diff --git a/Source/ASDisplayNode+Yoga.mm b/Source/ASDisplayNode+Yoga.mm index 619f5a780..d0bc2da76 100644 --- a/Source/ASDisplayNode+Yoga.mm +++ b/Source/ASDisplayNode+Yoga.mm @@ -11,24 +11,33 @@ #if YOGA /* YOGA */ -#import -#import #import -#import #import +#import #import #import +#import #import #import #import +#import #import - +#import +#import +#import #import #define YOGA_LAYOUT_LOGGING 0 +// Access style property directly or use the getter to create one +#define _LOCKED_ACCESS_STYLE() (_style ?: [self _locked_style]) + +AS_ASSUME_NORETAIN_BEGIN + #pragma mark - ASDisplayNode+Yoga +using namespace AS; + @interface ASDisplayNode (YogaPrivate) @property (nonatomic, weak) ASDisplayNode *yogaParent; - (ASSizeRange)_locked_constrainedSizeForLayoutPass; @@ -38,6 +47,7 @@ @implementation ASDisplayNode (Yoga) - (ASDisplayNode *)yogaRoot { + Yoga2::AssertEnabled(self); ASDisplayNode *yogaRoot = self; ASDisplayNode *yogaParent = nil; while ((yogaParent = yogaRoot.yogaParent)) { @@ -48,7 +58,11 @@ - (ASDisplayNode *)yogaRoot - (void)setYogaChildren:(NSArray *)yogaChildren { - ASScopedLockSelfOrToRoot(); + if (ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + Yoga2::SetChildren(self, yogaChildren); + return; + } + LockSet locks = [self lockToRootIfNeededForLayout]; for (ASDisplayNode *child in [_yogaChildren copy]) { // Make sure to un-associate the YGNodeRef tree before replacing _yogaChildren // If this becomes a performance bottleneck, it can be optimized by not doing the NSArray removals here. @@ -62,29 +76,42 @@ - (void)setYogaChildren:(NSArray *)yogaChildren - (NSArray *)yogaChildren { - ASLockScope(self.yogaRoot); + if (ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + return Yoga2::CopyChildren(self); + } + AS::MutexLocker l(__instanceLock__); return [_yogaChildren copy] ?: @[]; } - (void)addYogaChild:(ASDisplayNode *)child { - ASScopedLockSelfOrToRoot(); + if (ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + Yoga2::InsertChild(self, child, -1); + return; + } + LockSet locks = [self lockToRootIfNeededForLayout]; [self _locked_addYogaChild:child]; } - (void)_locked_addYogaChild:(ASDisplayNode *)child { - [self insertYogaChild:child atIndex:_yogaChildren.count]; + ASAssertNotExperiment(ASExperimentalUnifiedYogaTree); + [self _locked_insertYogaChild:child atIndex:_yogaChildren.count]; } - (void)removeYogaChild:(ASDisplayNode *)child { - ASScopedLockSelfOrToRoot(); + if (ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + Yoga2::RemoveChild(self, child); + return; + } + LockSet locks = [self lockToRootIfNeededForLayout]; [self _locked_removeYogaChild:child]; } - (void)_locked_removeYogaChild:(ASDisplayNode *)child { + ASAssertNotExperiment(ASExperimentalUnifiedYogaTree); if (child == nil) { return; } @@ -98,15 +125,22 @@ - (void)_locked_removeYogaChild:(ASDisplayNode *)child - (void)insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index { - ASScopedLockSelfOrToRoot(); + if (ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + Yoga2::InsertChild(self, child, index); + return; + } + LockSet locks = [self lockToRootIfNeededForLayout]; [self _locked_insertYogaChild:child atIndex:index]; } - (void)_locked_insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index { + ASAssertNotExperiment(ASExperimentalUnifiedYogaTree); if (child == nil) { return; } + ASDisplayNodeAssert(_nodeContext == [child nodeContext], + @"Cannot add yoga child from different node context."); if (_yogaChildren == nil) { _yogaChildren = [[NSMutableArray alloc] init]; } @@ -118,27 +152,45 @@ - (void)_locked_insertYogaChild:(ASDisplayNode *)child atIndex:(NSUInteger)index // YGNodeRef insertion is done in setParent: child.yogaParent = self; + if (_flags.yoga) { + [child enableYoga]; + } [self setNeedsLayout]; } #pragma mark - Subclass Hooks ++ (BOOL)isRTLForNode:(ASDisplayNode *)node { + return [node yogaLayoutDirection] == UIUserInterfaceLayoutDirectionRightToLeft; +} + - (void)semanticContentAttributeDidChange:(UISemanticContentAttribute)attribute { + Yoga2::AssertEnabled(self); UIUserInterfaceLayoutDirection layoutDirection = [UIView userInterfaceLayoutDirectionForSemanticContentAttribute:attribute]; - self.style.direction = (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight - ? YGDirectionLTR : YGDirectionRTL); + ASLockScopeSelf(); + _LOCKED_ACCESS_STYLE().direction = (layoutDirection == UIUserInterfaceLayoutDirectionLeftToRight + ? YGDirectionLTR : YGDirectionRTL); +} + +- (UIUserInterfaceLayoutDirection)yogaLayoutDirection +{ + Yoga2::AssertEnabled(self); + return _LOCKED_ACCESS_STYLE().direction == YGDirectionRTL + ? UIUserInterfaceLayoutDirectionRightToLeft + : UIUserInterfaceLayoutDirectionLeftToRight; } - (void)setYogaParent:(ASDisplayNode *)yogaParent { + ASAssertNotExperiment(ASExperimentalUnifiedYogaTree); ASLockScopeSelf(); if (_yogaParent == yogaParent) { return; } - YGNodeRef yogaNode = [self.style yogaNodeCreateIfNeeded]; + YGNodeRef yogaNode = [_LOCKED_ACCESS_STYLE() yogaNode]; YGNodeRef oldParentRef = YGNodeGetParent(yogaNode); if (oldParentRef != NULL) { YGNodeRemoveChild(oldParentRef, yogaNode); @@ -146,7 +198,7 @@ - (void)setYogaParent:(ASDisplayNode *)yogaParent _yogaParent = yogaParent; if (yogaParent) { - YGNodeRef newParentRef = [yogaParent.style yogaNodeCreateIfNeeded]; + YGNodeRef newParentRef = [yogaParent.style yogaNode]; YGNodeInsertChild(newParentRef, yogaNode, YGNodeGetChildCount(newParentRef)); } } @@ -156,282 +208,27 @@ - (ASDisplayNode *)yogaParent return _yogaParent; } -- (void)setYogaCalculatedLayout:(ASLayout *)yogaCalculatedLayout -{ - _yogaCalculatedLayout = yogaCalculatedLayout; +- (BOOL)shouldSuppressYogaCustomMeasure { + MutexLocker l(__instanceLock__); + return _flags.shouldSuppressYogaCustomMeasure; } -- (ASLayout *)yogaCalculatedLayout -{ - return _yogaCalculatedLayout; -} - -- (BOOL)willApplyNextYogaCalculatedLayout { - return _flags.willApplyNextYogaCalculatedLayout; -} - -- (void)setWillApplyNextYogaCalculatedLayout:(BOOL)willApplyNextYogaCalculatedLayout { - _flags.willApplyNextYogaCalculatedLayout = willApplyNextYogaCalculatedLayout; -} - -- (void)setYogaLayoutInProgress:(BOOL)yogaLayoutInProgress -{ - setFlag(YogaLayoutInProgress, yogaLayoutInProgress); - [self updateYogaMeasureFuncIfNeeded]; -} - -- (BOOL)yogaLayoutInProgress -{ - return checkFlag(YogaLayoutInProgress); -} - -- (ASLayout *)layoutForYogaNode -{ - YGNodeRef yogaNode = self.style.yogaNode; - - CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); - CGPoint position = CGPointMake(YGNodeLayoutGetLeft(yogaNode), YGNodeLayoutGetTop(yogaNode)); - - if (!ASIsCGSizeValidForSize(size)) { - size = CGSizeZero; - } - - if (!ASIsCGPositionValidForLayout(position)) { - position = CGPointZero; - } - return [ASLayout layoutWithLayoutElement:self size:size position:position sublayouts:nil]; -} - -- (void)setupYogaCalculatedLayoutAndSetNeedsLayoutForChangedNodes:(BOOL)setNeedsLayoutForChangedNodes -{ - ASScopedLockSelfOrToRoot(); - - YGNodeRef yogaNode = self.style.yogaNode; - uint32_t childCount = YGNodeGetChildCount(yogaNode); - ASDisplayNodeAssert(childCount == _yogaChildren.count, - @"Yoga tree should always be in sync with .yogaNodes array! %@", - _yogaChildren); - - ASLayout *rawSublayouts[childCount]; - int i = 0; - for (ASDisplayNode *subnode in _yogaChildren) { - rawSublayouts[i++] = [subnode layoutForYogaNode]; - } - const auto sublayouts = [NSArray arrayByTransferring:rawSublayouts count:childCount]; - - // The layout for self should have position CGPointNull, but include the calculated size. - CGSize size = CGSizeMake(YGNodeLayoutGetWidth(yogaNode), YGNodeLayoutGetHeight(yogaNode)); - if (!ASIsCGSizeValidForSize(size)) { - size = CGSizeZero; - } - ASLayout *layout = [ASLayout layoutWithLayoutElement:self size:size sublayouts:sublayouts]; - -#if ASDISPLAYNODE_ASSERTIONS_ENABLED - // Assert that the sublayout is already flattened. - for (ASLayout *sublayout in layout.sublayouts) { - if (sublayout.sublayouts.count > 0 || ASDynamicCast(sublayout.layoutElement, ASDisplayNode) == nil) { - ASDisplayNodeAssert(NO, @"Yoga sublayout is not flattened! %@, %@", self, sublayout); +- (void)setShouldSuppressYogaCustomMeasure:(BOOL)shouldSuppressYogaCustomMeasure { + Yoga2::AssertEnabled(self); + BOOL shouldMarkContentDirty = NO; + BOOL yoga2Enabled = NO; + { + MutexLocker l(__instanceLock__); + if (_flags.shouldSuppressYogaCustomMeasure != shouldSuppressYogaCustomMeasure) { + _flags.shouldSuppressYogaCustomMeasure = shouldSuppressYogaCustomMeasure; + Yoga2::UpdateMeasureFunction(self); + shouldMarkContentDirty = YES; + yoga2Enabled = AS::Yoga2::GetEnabled(self); } } -#endif - - // Because this layout won't go through the rest of the logic in calculateLayoutThatFits:, flatten it now. - layout = [layout filteredNodeLayoutTree]; - - if ([self.yogaCalculatedLayout isEqual:layout] == NO) { - if (setNeedsLayoutForChangedNodes && !self.willApplyNextYogaCalculatedLayout) { - // This flag will be set when this layout is intended for immediate display. In this case, we - // want to ensure that we call setNeedsLayout on any other nodes. Note that we skip any nodes - // whose willApplyNextYogaCalculatedLayout flags are set, as those are the nodes that are - // already being laid out. - [self setNeedsLayout]; - } - self.yogaCalculatedLayout = layout; - } else { - layout = self.yogaCalculatedLayout; - ASYogaLog("-setupYogaCalculatedLayout: applying identical ASLayout: %@", layout); - } - - // Setup _pendingDisplayNodeLayout to reference the Yoga-calculated ASLayout, *unless* we are a leaf node. - // Leaf yoga nodes may have their own .sublayouts, if they use a layout spec (such as ASButtonNode). - // Their _pending variable is set after passing the Yoga checks at the start of -calculateLayoutThatFits: - - // For other Yoga nodes, there is no code that will set _pending unless we do it here. Why does it need to be set? - // When CALayer triggers the -[ASDisplayNode __layout] call, we will check if our current _pending layout - // has a size which matches our current bounds size. If it does, that layout will be used without recomputing it. - - // NOTE: Yoga does not make the constrainedSize available to intermediate nodes in the tree (e.g. not root or leaves). - // Although the size range provided here is not accurate, this will only affect caching of calls to layoutThatFits: - // These calls will behave as if they are not cached, starting a new Yoga layout pass, but this will tap into Yoga's - // own internal cache. - - if ([self shouldHaveYogaMeasureFunc] == NO) { - YGNodeRef parentNode = YGNodeGetParent(yogaNode); - CGSize parentSize = CGSizeZero; - if (parentNode) { - parentSize.width = YGNodeLayoutGetWidth(parentNode); - parentSize.height = YGNodeLayoutGetHeight(parentNode); - } - // For the root node in a Yoga tree, make sure to preserve the constrainedSize originally provided. - // This will be used for all relayouts triggered by children, since they escalate to root. - ASSizeRange range = parentNode ? ASSizeRangeUnconstrained : self.constrainedSizeForCalculatedLayout; - _pendingDisplayNodeLayout = ASDisplayNodeLayout(layout, range, parentSize, _layoutVersion); - } -} - -- (BOOL)shouldHaveYogaMeasureFunc -{ - ASLockScopeSelf(); - // Size calculation via calculateSizeThatFits: or layoutSpecThatFits: - // For these nodes, we assume they may need custom Baseline calculation too. - // This will be used for ASTextNode, as well as any other node that has no Yoga children - BOOL isLeafNode = (_yogaChildren.count == 0); - BOOL definesCustomLayout = [self implementsLayoutMethod]; - return (isLeafNode && definesCustomLayout); -} - -- (void)updateYogaMeasureFuncIfNeeded -{ - // We set the measure func only during layout. Otherwise, a cycle is created: - // The YGNodeRef Context will retain the ASDisplayNode, which retains the style, which owns the YGNodeRef. - BOOL shouldHaveMeasureFunc = ([self shouldHaveYogaMeasureFunc] && checkFlag(YogaLayoutInProgress)); - - ASLayoutElementYogaUpdateMeasureFunc(self.style.yogaNode, shouldHaveMeasureFunc ? self : nil); -} - -- (void)invalidateCalculatedYogaLayout -{ - ASLockScopeSelf(); - YGNodeRef yogaNode = self.style.yogaNode; - if (yogaNode && [self shouldHaveYogaMeasureFunc]) { - // Yoga internally asserts that MarkDirty() may only be called on nodes with a measurement function. - BOOL needsTemporaryMeasureFunc = (YGNodeGetMeasureFunc(yogaNode) == NULL); - if (needsTemporaryMeasureFunc) { - ASDisplayNodeAssert(self.yogaLayoutInProgress == NO, - @"shouldHaveYogaMeasureFunc == YES, and inside a layout pass, but no measure func pointer! %@", self); - YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc); - } - YGNodeMarkDirty(yogaNode); - if (needsTemporaryMeasureFunc) { - YGNodeSetMeasureFunc(yogaNode, NULL); - } - } - self.yogaCalculatedLayout = nil; -} - -- (ASLayout *)calculateLayoutYoga:(ASSizeRange)constrainedSize -{ - AS::UniqueLock l(__instanceLock__); - - // There are several cases where Yoga could arrive here: - // - This node is not in a Yoga tree: it has neither a yogaParent nor yogaChildren. - // - This node is a Yoga tree root: it has no yogaParent, but has yogaChildren. - // - This node is a Yoga tree node: it has both a yogaParent and yogaChildren. - // - This node is a Yoga tree leaf: it has a yogaParent, but no yogaChidlren. - if ([self locked_shouldLayoutFromYogaRoot]) { - // If we're a yoga root, tree node, or leaf with no measure func (e.g. spacer), then - // initiate a new Yoga calculation pass from root. - as_activity_create_for_scope("Yoga layout calculation"); - if (self.yogaLayoutInProgress == NO) { - ASYogaLog("Calculating yoga layout from root %@, %@", self, - NSStringFromASSizeRange(constrainedSize)); - [self calculateLayoutFromYogaRoot:constrainedSize willApply:self.willApplyNextYogaCalculatedLayout]; - } else { - ASYogaLog("Reusing existing yoga layout %@", _yogaCalculatedLayout); - } - ASDisplayNodeAssert(_yogaCalculatedLayout, - @"Yoga node should have a non-nil layout at this stage: %@", self); - return _yogaCalculatedLayout; - } else { - // If we're a yoga leaf node with custom measurement function, proceed with normal layout so - // layoutSpecs can run (e.g. ASButtonNode). - ASYogaLog("PROCEEDING past Yoga check to calculate ASLayout for: %@", self); - } - - // Delegate to layout spec layout for nodes that do not support Yoga - return [self calculateLayoutLayoutSpec:constrainedSize]; -} - -- (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize willApply:(BOOL)willApply -{ - ASScopedLockSet lockSet = [self lockToRootIfNeededForLayout]; - ASDisplayNode *yogaRoot = self.yogaRoot; - - if (self != yogaRoot) { - ASYogaLog("ESCALATING to Yoga root: %@", self); - // TODO(appleguy): Consider how to get the constrainedSize for the yogaRoot when escalating manually. - [yogaRoot calculateLayoutFromYogaRoot:ASSizeRangeUnconstrained willApply:willApply]; - return; + if (shouldMarkContentDirty && yoga2Enabled) { + Yoga2::MarkContentMeasurementDirty(self); } - - if (ASSizeRangeEqualToSizeRange(rootConstrainedSize, ASSizeRangeUnconstrained)) { - rootConstrainedSize = [self _locked_constrainedSizeForLayoutPass]; - } - - [self willCalculateLayout:rootConstrainedSize]; - [self enumerateInterfaceStateDelegates:^(id _Nonnull delegate) { - if ([delegate respondsToSelector:@selector(nodeWillCalculateLayout:)]) { - [delegate nodeWillCalculateLayout:rootConstrainedSize]; - } - }]; - - // Prepare all children for the layout pass with the current Yoga tree configuration. - ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode *_Nonnull node) { - node.yogaLayoutInProgress = YES; - ASDisplayNode *yogaParent = node.yogaParent; - if (yogaParent) { - node.style.parentAlignStyle = yogaParent.style.alignItems; - } else { - node.style.parentAlignStyle = ASStackLayoutAlignItemsNotSet; - }; - }); - - ASYogaLog("CALCULATING at Yoga root with constraint = {%@, %@}: %@", - NSStringFromCGSize(rootConstrainedSize.min), NSStringFromCGSize(rootConstrainedSize.max), self); - - YGNodeRef rootYogaNode = self.style.yogaNode; - - // Apply the constrainedSize as a base, known frame of reference. - // If the root node also has style.*Size set, these will be overridden below. - // YGNodeCalculateLayout currently doesn't offer the ability to pass a minimum size (max is passed there). - - // TODO(appleguy): Reconcile the self.style.*Size properties with rootConstrainedSize - YGNodeStyleSetMinWidth (rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.width)); - YGNodeStyleSetMinHeight(rootYogaNode, yogaFloatForCGFloat(rootConstrainedSize.min.height)); - - // It is crucial to use yogaFloat... to convert CGFLOAT_MAX into YGUndefined here. - YGNodeCalculateLayout(rootYogaNode, - yogaFloatForCGFloat(rootConstrainedSize.max.width), - yogaFloatForCGFloat(rootConstrainedSize.max.height), - YGDirectionInherit); - - // Reset accessible elements, since layout may have changed. - ASPerformBlockOnMainThread(^{ - if (self.nodeLoaded && !self.isSynchronous) { - self.view.accessibilityElements = nil; - } - }); - - ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { - [node setupYogaCalculatedLayoutAndSetNeedsLayoutForChangedNodes:willApply]; - node.yogaLayoutInProgress = NO; - }); - -#if YOGA_LAYOUT_LOGGING /* YOGA_LAYOUT_LOGGING */ - // Concurrent layouts will interleave the NSLog messages unless we serialize. - // Use @synchornize rather than trampolining to the main thread so the tree state isn't changed. - @synchronized ([ASDisplayNode class]) { - NSLog(@"****************************************************************************"); - NSLog(@"******************** STARTING YOGA -> ASLAYOUT CREATION ********************"); - NSLog(@"****************************************************************************"); - ASDisplayNodePerformBlockOnEveryYogaChild(self, ^(ASDisplayNode * _Nonnull node) { - NSLog(@"node = %@", node); - YGNodePrint(node.style.yogaNode, (YGPrintOptions)(YGPrintOptionsStyle | YGPrintOptionsLayout)); - NSCAssert(ASIsCGSizeValidForSize(node.yogaCalculatedLayout.size), @"Yoga layout returned an invalid size"); - NSLog(@" "); // Newline - }); - } -#endif /* YOGA_LAYOUT_LOGGING */ } @end @@ -440,32 +237,42 @@ - (void)calculateLayoutFromYogaRoot:(ASSizeRange)rootConstrainedSize willApply:( @implementation ASDisplayNode (YogaLocking) -- (ASLockSet)lockToRootIfNeededForLayout { - ASLockSet lockSet = ASLockSequence(^BOOL(ASAddLockBlock addLock) { - if (!addLock(self)) { - return NO; - } -#if YOGA - if (![self locked_shouldLayoutFromYogaRoot]) { - return YES; - } - if (self.nodeController && !addLock(self.nodeController)) { +- (BOOL)lockToRootIfNeededForLayout:(AS::LockSet *)locks { + // If we have a Texture context, then there is no need to lock to root. Just lock the context. + if (_nodeContext) { + if (!locks->TryAdd(_nodeContext, _nodeContext->_mutex)) return NO; + return YES; + } + + if (!locks->TryAdd(self, __instanceLock__)) return NO; + + // In Yoga we always lock to root. + if (Yoga2::GetEnabled(self)) { + ASNodeController *ctrl = ASDisplayNodeGetController(self); + if (ctrl && !locks->TryAdd(ctrl, ctrl->__instanceLock__)) { return NO; } ASDisplayNode *parent = _supernode; while (parent) { - if (!addLock(parent)) { + if (!locks->TryAdd(parent, parent->__instanceLock__)) { return NO; } - if (parent.nodeController && !addLock(parent.nodeController)) { + ASNodeController *parentCtrl = ASDisplayNodeGetController(parent); + if (parentCtrl && !locks->TryAdd(parentCtrl, parentCtrl->__instanceLock__)) { return NO; } parent = parent->_supernode; } -#endif - return true; - }); - return lockSet; + } + return YES; +} + +- (AS::LockSet)lockToRootIfNeededForLayout { + AS::LockSet locks; + while (locks.empty()) { + if (![self lockToRootIfNeededForLayout:&locks]) continue; + } + return locks; } @end @@ -477,13 +284,15 @@ - (NSString *)yogaTreeDescription { } - (NSString *)_yogaTreeDescription:(NSString *)indent { - auto subtree = [NSMutableString stringWithFormat:@"%@%@\n", indent, self.description]; - for (ASDisplayNode *n in self.yogaChildren) { - [subtree appendString:[n _yogaTreeDescription:[indent stringByAppendingString:@"| "]]]; - } - return subtree; + // TODO: In Yoga v1.16.0, YGNodeToString has become available only #if DEBUG. + // #if DEBUG + // facebook::yoga::YGNodeToString(s, self.style.yogaNode, (YGPrintOptions)(YGPrintOptionsStyle | + // YGPrintOptionsLayout), 0); + // #endif + return [self debugDescription]; // way less useful but temporary } - @end +AS_ASSUME_NORETAIN_END + #endif /* YOGA */ diff --git a/Source/ASDisplayNode+Yoga2.h b/Source/ASDisplayNode+Yoga2.h new file mode 100644 index 000000000..2a32360c9 --- /dev/null +++ b/Source/ASDisplayNode+Yoga2.h @@ -0,0 +1,170 @@ +// +// ASDisplayNode+Yoga2.h +// AsyncDisplayKit +// +// Created by Adlai Holler on 3/8/19. +// Copyright © 2019 Pinterest. All rights reserved. +// + +#if defined(__cplusplus) + +#import +#import +#import + +#if YOGA +#import YOGA_HEADER_PATH +#endif + +NS_ASSUME_NONNULL_BEGIN +AS_ASSUME_NORETAIN_BEGIN + +namespace AS { +namespace Yoga2 { + +/** + * Returns whether Yoga2 is enabled for this node. + */ +bool GetEnabled(ASDisplayNode *node); + +inline void AssertEnabled() { + ASDisplayNodeCAssert(false, @"Expected Yoga2 to be enabled."); +} + +inline void AssertEnabled(ASDisplayNode *node) { + ASDisplayNodeCAssert(GetEnabled(node), @"Expected Yoga2 to be enabled."); +} + +inline void AssertDisabled(ASDisplayNode *node) { + ASDisplayNodeCAssert(!GetEnabled(node), @"Expected Yoga2 to be disabled."); +} + +/** + * Enable Yoga2 for this node. This should be done only once, and before any layout calculations + * have occurred. + */ +void Enable(ASDisplayNode *node); + +/** + * Update or clear measure function according to @c -[ASDisplayNode shouldSuppressMeasureFunction]. + * This should be called with lock held. + */ +void UpdateMeasureFunction(ASDisplayNode *texture); + +/** + * Enable using style yoga node globally for all nodes if Yoga2 is enabled. + * This is a global flag and should be set within initialization. + */ +void SetEnableStyleYogaNode(bool enable); + +/** + * Asserts unlocked. Locks to root. + * Marks the node dirty if it has a custom measure function (e.g. text node). Otherwise does + * nothing. + */ +void MarkContentMeasurementDirty(ASDisplayNode *node); + +/** + * Asserts root. Asserts locked. + * This is analogous to -sizeThatFits:. + * Subsequent calls to GetCalculatedSize and GetCalculatedLayout will return values based on this. + */ +void CalculateLayoutAtRoot(ASDisplayNode *node, CGSize maxSize); + +/** + * Asserts locked. Asserts thread affinity. + * Update layout for all nodes in tree from yoga root based on its bounds. + * This is analogous to -layoutSublayers. If not root, does nothing. + */ +void ApplyLayoutForCurrentBoundsIfRoot(ASDisplayNode *node); + +/** + * Handle a call to -layoutIfNeeded. Asserts thread affinity. Other cases should be handled by + * pending state. + * + * Note: this method _also_ asserts thread affinity for the root yoga node. There are cases where + * people call -layoutIfNeeded on an unloaded node that has a yoga ancestor that is in hierarchy + * i.e. the receiver node is pending addition to the layer tree. This is legal only on main. + */ +void HandleExplicitLayoutIfNeeded(ASDisplayNode *node); + +/** + * The size of the most recently calculated layout. Asserts root, locked. + * Returns CGSizeZero if never measured. + */ +CGSize GetCalculatedSize(ASDisplayNode *node); + +/** + * Returns the most recently calculated layout. Asserts root, locked. + * The size of the returning ASLayout will take in the consideration of the + * ASSizeRange passed in. If ASSizeRangeZero is passed in no clamping will happen. + * Note: The layout will be returned even if the tree is dirty. + */ +ASLayout *_Nullable GetCalculatedLayout(ASDisplayNode *node, ASSizeRange sizeRange); + +/** + * Returns a CGRect corresponding to the position and size of the node's children. This is safe to + * call even during layout or from calculatedLayoutDidChange. + */ +CGRect GetChildrenRect(ASDisplayNode *node); + +/** Return the number of times external measurement methods have been called by Yoga since the last + * root layout began. **/ +int MeasuredNodesForThread(); + +/// This section for functions only available when yoga is linked. +#if YOGA + +/** + * Insert the display node into the yoga tree at the given position. + * @precondition index <= parent.childCount + * @precondition Parent and child are in the same node context. + * If child has a different parent it will be removed. + * Pass -1 as index to indicate you want to append child. + */ +void InsertChild(ASDisplayNode *node, ASDisplayNode *child, int index); + +/** + * Remove the display node from the yoga tree. + */ +void RemoveChild(ASDisplayNode *node, ASDisplayNode *child); + +/** + * Update the yoga children of the given display node. + */ +void SetChildren(ASDisplayNode *node, NSArray *children); + +/** + * Call from dealloc. + */ +void TearDown(AS_NORETAIN_ALWAYS ASDisplayNode *node); + +/** + * Copy the child array, translated into Texture nodes. + */ +NSArray *CopyChildren(ASDisplayNode *node); + +/** + * Visit the yoga children using a std::function. + */ +void VisitChildren(ASDisplayNode *node, + const std::function &visitor); + +/** + * Get the display node corresponding to the given yoga node. + * Macros are ugly but you simply can't return ObjC objects without retain/release under ARC. Even a + * function that is inlined by the compiler will still cause retain/release. + */ +#define GetTexture(yoga) ((__bridge ASDisplayNode *)GetTextureCF(yoga)) + +/** Get display node without ARC traffic. */ +CFTypeRef GetTextureCF(YGNodeRef yoga); +#endif + +} // namespace Yoga2 +} // namespace AS + +AS_ASSUME_NORETAIN_END +NS_ASSUME_NONNULL_END + +#endif // defined(__cplusplus) diff --git a/Source/ASDisplayNode+Yoga2.mm b/Source/ASDisplayNode+Yoga2.mm new file mode 100644 index 000000000..042b9d480 --- /dev/null +++ b/Source/ASDisplayNode+Yoga2.mm @@ -0,0 +1,973 @@ +// +// ASDisplayNode+Yoga2.mm +// AsyncDisplayKit +// +// Created by Adlai Holler on 3/8/19. +// Copyright © 2019 Pinterest. All rights reserved. +// + +#import +#import +#import + +AS_ASSUME_NORETAIN_BEGIN + +#if YOGA + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#import YOGA_HEADER_PATH + +namespace AS { +namespace Yoga2 { + +bool GetEnabled(ASDisplayNode *node) { + if (node) { + MutexLocker l(node->__instanceLock__); + return node->_flags.yoga; + } else { + return false; + } +} + +/** + * Whether to manually round using ASCeilPixelValue instead of Yoga's internal pixel-grid rounding. + * + * When it's critical to have exact layout parity between yoga2 and the other layout systems, this + * flag must be on. + */ +static constexpr bool kUseLegacyRounding = true; + +/** + * Whether to clamp measurement results before returning them into Yoga. This may produce worse + * visual results, but it matches existing mainline Texture layout. See b/129227140. + */ +static constexpr bool kClampMeasurementResults = true; + +/// Texture -> Yoga node. +static inline YGNodeRef GetYoga(ASDisplayNode *node) { return [node.style yogaNode]; } + +CFTypeRef GetTextureCF(YGNodeRef yoga) { + CFTypeRef result = nullptr; + if (yoga) { + result = (CFTypeRef)YGNodeGetContext(yoga); + ASDisplayNodeCAssert(result, @"Failed to get Texture node for yoga node."); + } + return result; +} + +/// Asserts all nodes already locked. +static inline YGNodeRef GetRoot(YGNodeRef yoga) { + DISABLED_ASAssertLocked(GetTexture(yoga)->__instanceLock__); + YGNodeRef next = YGNodeGetOwner(yoga); + while (next) { + yoga = next; + DISABLED_ASAssertLocked(GetTexture(yoga)->__instanceLock__); + next = YGNodeGetOwner(next); + } + return yoga; +} + +static inline bool IsRoot(YGNodeRef yoga) { return !YGNodeGetOwner(yoga); } + +// Read node layout origin, round if legacy, and return as CGPoint. +static inline CGPoint CGOriginOfYogaLayout(YGNodeRef yoga) { + if (kUseLegacyRounding) { + return CGPointMake(ASCeilPixelValue(YGNodeLayoutGetLeft(yoga)), + ASCeilPixelValue(YGNodeLayoutGetTop(yoga))); + } + + return CGPointMake(YGNodeLayoutGetLeft(yoga), + YGNodeLayoutGetTop(yoga)); +} + +// Read YGLayout size, round if legacy, and return as CGSize. +// If layout has undefined values, returns CGSizeZero. +static inline CGSize CGSizeOfYogaLayout(YGNodeRef yoga) { + if (YGFloatIsUndefined(YGNodeLayoutGetHeight(yoga))) { + return CGSizeZero; + } + + if (kUseLegacyRounding) { + return CGSizeMake(ASCeilPixelValue(YGNodeLayoutGetWidth(yoga)), + ASCeilPixelValue(YGNodeLayoutGetHeight(yoga))); + } + + return CGSizeMake(YGNodeLayoutGetWidth(yoga), + YGNodeLayoutGetHeight(yoga)); +} + +static inline CGRect CGRectOfYogaLayout(YGNodeRef yoga) { + return {CGOriginOfYogaLayout(yoga), CGSizeOfYogaLayout(yoga)}; +} + +static inline void AssertRoot(ASDisplayNode *node) { + ASDisplayNodeCAssert(IsRoot(GetYoga(node)), @"Must be called on root: %@", node); +} + +static thread_local int measuredNodes = 0; +/** + * We are root and the tree is dirty. There are few things we need to do. + * **All of the following must be done on the main thread, so the first thing is to get there.** + * - If we are loaded, we need to call [layer setNeedsLayout] on main. + * - If we are range-managed, we need to inform our container that our size is invalid. + * - If we are in hierarchy, we need to call [superlayer setNeedsLayout] so that it knows that we + * (may) need resizing. + */ +void HandleRootDirty(ASDisplayNode *node) { + MutexLocker l(node->__instanceLock__); + unowned CALayer *layer = node->_layer; + const bool isRangeManaged = ASHierarchyStateIncludesRangeManaged(node->_hierarchyState); + + // Not loaded, not range managed, nothing to be done. + if (!layer && !isRangeManaged) { + return; + } + + // Not on main thread. Get on the main thread and come back in. + if (!ASDisplayNodeThreadIsMain()) { + dispatch_async(dispatch_get_main_queue(), ^{ + HandleRootDirty(node); + }); + return; + } + + // On main. If no longer root, nothing to be done. Our new tree was invalidated when we got + // added to it. + YGNodeRef yoga = GetYoga(node); + if (!IsRoot(yoga)) { + return; + } + + // Loaded or range-managed. On main. Root. Locked. + + // Dirty our own layout so that even if our size didn't change we get a layout pass. + [layer setNeedsLayout]; + + // If we are a cell node with an interaction delegate, inform it our size is invalid. + if (id interactionDelegate = + ASDynamicCast(node, ASCellNode).interactionDelegate) { + [interactionDelegate nodeDidInvalidateSize:(ASCellNode *)node]; + } else { + // Otherwise just inform our superlayer (if we have one.) + [layer.superlayer setNeedsLayout]; + } +} + +void YogaDirtied(YGNodeRef yoga) { + // NOTE: We may be locked – if someone set a property directly on us – or we may not – if this + // dirtiness is propagating up from below. + + // If we are not root, ignore. Yoga will propagate the dirt up to root. + if (IsRoot(yoga)) { + ASDisplayNode *node = [GetTexture(yoga) tryRetain]; + if (node) { + HandleRootDirty(node); + } + } +} + +/// If mode is undefined, returns CGFLOAT_MAX. Otherwise asserts valid & converts to CGFloat. +CGFloat CGConstraintFromYoga(float dim, YGMeasureMode mode) { + if (mode == YGMeasureModeUndefined) { + return CGFLOAT_MAX; + } else { + ASDisplayNodeCAssert(!YGFloatIsUndefined(dim), @"Yoga said it gave us a size but it didn't."); + return dim; + } +} + +inline float YogaConstraintFromCG(CGFloat constraint) { + return constraint == CGFLOAT_MAX || isinf(constraint) ? YGUndefined : constraint; +} + +/// Convert a Yoga layout to an ASLayout. +ASLayout *ASLayoutCreateFromYogaNodeLayout(YGNodeRef yoga, ASSizeRange sizeRange) { + // If our layout has no dimensions, return nil now. + if (YGFloatIsUndefined(YGNodeLayoutGetHeight(yoga))) { + return nil; + } + + const uint32_t child_count = YGNodeGetChildCount(yoga); + ASLayout *sublayouts[child_count]; + for (uint32_t i = 0; i < child_count; i++) { + // If any node in the subtree has no layout, then there is no layout. Return nil. + YGNodeRef child = YGNodeGetChild(yoga, i); + if (!(sublayouts[i] = ASLayoutCreateFromYogaNodeLayout(child, sizeRange))) { + return nil; + } + } + auto boxed_sublayouts = [NSArray arrayByTransferring:sublayouts + count:child_count]; + CGSize yogaLayoutSize = CGSizeOfYogaLayout(yoga); + if (!ASSizeRangeEqualToSizeRange(sizeRange, ASSizeRangeZero)) { + yogaLayoutSize = CGSizeMake(std::max(yogaLayoutSize.width, sizeRange.min.width), + std::max(yogaLayoutSize.height, sizeRange.min.height)); + } + + return [[ASLayout alloc] initWithLayoutElement:GetTexture(yoga) + size:yogaLayoutSize + position:CGOriginOfYogaLayout(yoga) + sublayouts:boxed_sublayouts]; +} + +// Only set on nodes that implement calculateSizeThatFits:. +YGSize YogaMeasure(YGNodeRef yoga, float width, YGMeasureMode widthMode, float height, + YGMeasureMode heightMode) { + ASDisplayNode *node = GetTexture(yoga); + + // Go straight to calculateSizeThatFits:, not sizeThatFits:. Caching is handled inside of yoga – + // if we got here, we need to do a calculation so call out to the node subclass. + const CGSize constraint = + CGSizeMake(CGConstraintFromYoga(width, widthMode), CGConstraintFromYoga(height, heightMode)); + CGSize size = [node calculateSizeThatFits:constraint]; + + if (kClampMeasurementResults) { + size.width = MIN(size.width, constraint.width); + size.height = MIN(size.height, constraint.height); + } + + // To match yoga1, we ceil this value (see ASLayoutElementYogaMeasureFunc). + if (kUseLegacyRounding) { + size = ASCeilSizeValues(size); + } + + // Do verbose logging if enabled. + measuredNodes++; +#if ASEnableVerboseLogging + NSString *thread = @""; + if (ASDisplayNodeThreadIsMain()) { + // good place for a breakpoint. + thread = @"main thread "; + } + as_log_verbose(ASLayoutLog(), "did %@leaf measurement for %@ (%g %g) -> (%g %g)", thread, + ASObjectDescriptionMakeTiny(node), constraint.width, constraint.height, size.width, + size.height); +#endif // ASEnableVerboseLogging + + return {(float)size.width, (float)size.height}; +} + +float YogaBaseline(YGNodeRef yoga, float width, float height) { + return [GetTexture(yoga) yogaBaselineWithSize:CGSizeMake(width, height)]; +} + +void Enable(ASDisplayNode *texture) { + NSCParameterAssert(texture != nil); + if (!texture) { + return; + } + MutexLocker l(texture->__instanceLock__); + YGNodeRef yoga = GetYoga(texture); + YGNodeSetContext(yoga, (__bridge void *)texture); + YGNodeSetDirtiedFunc(yoga, &YogaDirtied); + // Note: No print func. See Yoga2Logging.h. + + // Set measure & baseline funcs if needed. + if (texture->_methodOverrides & ASDisplayNodeMethodOverrideYogaBaseline) { + YGNodeSetBaselineFunc(yoga, &YogaBaseline); + } + + UpdateMeasureFunction(texture); +} + +void UpdateMeasureFunction(ASDisplayNode *texture) { + DISABLED_ASAssertLocked(node); + YGNodeRef yoga = GetYoga(texture); + if (texture.shouldSuppressYogaCustomMeasure) { + YGNodeSetMeasureFunc(yoga, NULL); + } else { + if (0 != (texture->_methodOverrides & ASDisplayNodeMethodOverrideCalcSizeThatFits)) { + YGNodeSetMeasureFunc(yoga, &YogaMeasure); + } + } +} + +void MarkContentMeasurementDirty(ASDisplayNode *node) { + DISABLED_ASAssertUnlocked(node); + AS::LockSet locks = [node lockToRootIfNeededForLayout]; + AssertEnabled(node); + YGNodeRef yoga = GetYoga(node); + if (YGNodeHasMeasureFunc(yoga) && !YGNodeIsDirty(yoga)) { + as_log_verbose(ASLayoutLog(), "mark content dirty for %@", ASObjectDescriptionMakeTiny(node)); + YGNodeMarkDirty(yoga); + } +} + +void CalculateLayoutAtRoot(ASDisplayNode *node, CGSize maxSize) { + AssertEnabled(node); + DISABLED_ASAssertLocked(node->__instanceLock__); + AssertRoot(node); + + // Notify. + measuredNodes = 0; + const ASSizeRange sizeRange = ASSizeRangeMake(CGSizeZero, maxSize); + [node willCalculateLayout:sizeRange]; + [node enumerateInterfaceStateDelegates:^(id _Nonnull delegate) { + if ([delegate respondsToSelector:@selector(nodeWillCalculateLayout:)]) { + [delegate nodeWillCalculateLayout:sizeRange]; + } + }]; + + // Log the calculation request. +#if ASEnableVerboseLogging + static std::atomic counter(1); + const long request_id = counter.fetch_add(1); + as_log_verbose(ASLayoutLog(), "enter layout calculation %ld for %@: %@", request_id, + ASObjectDescriptionMakeTiny(node), NSStringFromCGSize(maxSize)); +#endif + + const YGNodeRef yoga = GetYoga(node); + + // Force setting flex shrink to 0 on all children of nodes with YGOverflowScroll. This preserves + // backwards compatibility with Yoga1, but we should consider a breaking change going forward to + // remove this, as it's not great to meddle with flex properties arbitrarily. + // TODO(b/134073740): [Yoga2 Launch] Re-consider this. + YGTraversePreOrder(yoga, [](YGNodeRef yoga) { + if (YGNodeStyleGetOverflow(yoga) == YGOverflowScroll) { + for (uint32_t i = 0, iMax = YGNodeGetChildCount(yoga); i < iMax; ++i) { + YGNodeRef yoga_child = YGNodeGetChild(yoga, i); + YGNodeStyleSetFlexShrink(yoga_child, 0); + } + } + }); + + // Do the calculation. + YGNodeCalculateLayout(yoga, YogaConstraintFromCG(maxSize.width), + YogaConstraintFromCG(maxSize.height), YGDirectionInherit); + node->_yogaCalculatedLayoutMaxSize = maxSize; + + [node didCalculateLayout:sizeRange]; + // Log and return result. +#if ASEnableVerboseLogging + const CGSize result = CGSizeOfYogaLayout(yoga); + as_log_verbose(ASLayoutLog(), "finish layout calculation %ld with %@", request_id, + NSStringFromCGSize(result)); +#endif +} + +/// Collect all flattened children for given node +static void CollectChildrenRecursively(unowned NSMutableArray *children, ASDisplayNode *node) { + if (ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + VisitChildren(node, [&](unowned ASDisplayNode *subnode, int index){ + if (subnode.isFlattenable) { + CollectChildrenRecursively(children, subnode); + } else { + [children addObject:subnode]; + } + }); + return; + } + + for (ASDisplayNode *subnode in node->_yogaChildren) { + if (subnode.isFlattenable) { + CollectChildrenRecursively(children, subnode); + } else { + [children addObject:subnode]; + } + } +} + +void ApplyLayoutForCurrentBoundsIfRoot(ASDisplayNode *node) { + AssertEnabled(node); + ASDisplayNodeCAssertThreadAffinity(node); + DISABLED_ASAssertLocked(node->__instanceLock__); + YGNodeRef yoga_root = GetYoga(node); + + // We always update the entire tree from root and push all invalidations to the top. Nodes that do + // not have a different layout will be ignored using their `YGNodeGetHasNewLayout` flag. + if (!IsRoot(yoga_root)) { + return; + } + + // In some cases, a descendent view will call layoutIfNeeded during a layout application. + // This will escalate up to the root and we re-enter here. If this happens, we do not want to + // interrupt our layout so we instead take note and perform another layout pass immediately after + // finishing the current one. + if (node->_flags.yogaIsApplyingLayout) { + node->_flags.yogaRequestedNestedLayout = 1; + return; + } + node->_flags.yogaIsApplyingLayout = 1; + Cleanup layout_flag_cleanup([&] { node->_flags.yogaIsApplyingLayout = 0; }); + + // Note: We used to short-circuit here when the Yoga root had no children. However, we need to + // ensure that calculatedLayoutDidChange is called, so we need to pass through. The Yoga + // calculation should be cached, so it should be a cheap operation. + +#if ASEnableVerboseLogging + static std::atomic counter(1); + const long layout_id = counter.fetch_add(1); + as_log_verbose(ASLayoutLog(), "enter layout apply %ld for %@", layout_id, + ASObjectDescriptionMakeTiny(node)); +#endif + + // Determine a compatible size range to measure with. Yoga can create slightly different layouts + // if we re-measure with a different constraint than previously (even if it matches the resulting + // size of the previous measurement, due to rounding errors!), so avoid remeasuring if the + // caller is clearly just trying to apply the previous measurement. + + + CGSize sizeForLayout = node.bounds.size; + if (CGSizeEqualToSize(sizeForLayout, GetCalculatedSize(node))) { + sizeForLayout = node->_yogaCalculatedLayoutMaxSize; + } + + // Calculate layout for our bounds. If this size is compatible with a previous calculation + // then the measurement cache will kick in. + CalculateLayoutAtRoot(node, sizeForLayout); + + const bool tree_loaded = _loaded(node); + + // Traverse down the yoga tree, cleaning each node. + YGTraversePreOrder(yoga_root, [&yoga_root, tree_loaded](YGNodeRef yoga) { + unowned ASDisplayNode *node = GetTexture(yoga); + + bool use_layout_flattening = node->_flags.viewFlattening; + + if (!YGNodeGetHasNewLayout(yoga) && + // We can only short circuit if the node is flattenable. Otherwise the child + // frame could have changed and need to be adjusted + (!use_layout_flattening || (use_layout_flattening && node.isFlattenable))) { + if (YGNodeStyleGetOverflow(yoga) == YGOverflowScroll) { + // If a node has YGOverflowScroll, then always call calculatedLayoutDidChange so it can + // respond to any changes in the layout of its children. + [node calculatedLayoutDidChange]; + } + return; + } + YGNodeSetHasNewLayout(yoga, false); + + if (DISPATCH_EXPECT(node == nil, 0)) { + ASDisplayNodeCFailAssert(@"No texture node."); + return; + } + + // If the node is a layoutContainer and therefore will not appear in the view hierarchy + // continue without any flattening process, but only if it's not the root node. + if (use_layout_flattening && node.isFlattenable && yoga != yoga_root) { + // We can short-circuit here, because if this node is flattened, it means that all of its + // Yoga children have already been added to one of this node's ancestors. + + // We will clear the subnodes for nodes that gonna be flattened due to cases like: + // - A container node exists that was previously *not* flattenable and has subnodes + // - In an update it goes from *not* flattenable to flattenable and the _yogaChildren + // are changing due to the update. This container still existing subnodes that are not the + // same which will not be the same as it's `_yogaChildren` though, therefore it will still + // have subnodes although it's flattened away. + // - Subnodes that will eventually move to other nodes will be retained due to the Yoga tree + // Go in reverse order so we don't shift our indexes. + NSInteger count = node->_subnodes.count; + for (NSInteger i = count-1; i >= 0; --i) { + [node->_subnodes[i] removeFromSupernode]; + } + return; + } + + // Lock this node. Note we are already locked at root so if we ever get to the point where trees + // share one lock, this line can go away. + MutexLocker l(node->__instanceLock__); + + // Set node frame unless we ARE root. Root already has its frame. + if (yoga != yoga_root) { + CGRect newFrame = CGRectOfYogaLayout(yoga); + as_log_verbose(ASLayoutLog(), "layout: %@ %@ -> %@", ASObjectDescriptionMakeTiny(node), + NSStringFromCGRect(node.frame), NSStringFromCGRect(newFrame)); + + // Adjust the node's frame if view flattening is happening + if (use_layout_flattening) { + // Walk up the tree until the first non container node and from there add + // all origins to get the adjustment we need for this node's frame. + // At this point all of the frames of the supernodes should be set + + // Go up to first non container node and collect all the layout offset adjustments + CGPoint layoutOffset = CGPointZero; + YGNodeRef firstNonContainerNode = YGNodeGetOwner(yoga); + while (GetTexture(firstNonContainerNode).isFlattenable && firstNonContainerNode != yoga_root) { + CGPoint parentOrigin = CGRectOfYogaLayout(firstNonContainerNode).origin; + layoutOffset = ASPointAddPoint(layoutOffset, parentOrigin); + firstNonContainerNode = YGNodeGetOwner(firstNonContainerNode); + } + + NSCAssert(firstNonContainerNode, @"At least one non container node need to exist in tree."); + + // Adjust the frame for the node to the node that is not flattened via the layoutOffset + newFrame.origin = ASPointAddPoint(newFrame.origin, layoutOffset); + } + + // Set the new Frame + node.frame = newFrame; + } + + // Collect the new subnodes. Apply flattening if enabled, or just the yoga children. + NSArray *newSubnodes; + if (use_layout_flattening || ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + // In the flattening or unified-tree case, we reuse a temporary non-owning buffer. + // In the legacy case, we already have an NSArray we can use. + static dispatch_once_t onceToken; + static pthread_key_t threadKey; + dispatch_once(&onceToken, ^{ + ASInitializeTemporaryObjectStorage(&threadKey); + }); + NSMutableArray *mNewSubnodes; + if (ASActivateExperimentalFeature(ASExperimentalUseNonThreadLocalArrayWhenApplyingLayout)) { + mNewSubnodes = ASCreateNonOwningMutableArray(); + } else { + mNewSubnodes = (__bridge id)ASGetTemporaryNonowningMutableArray(threadKey); + } + if (use_layout_flattening) { + CollectChildrenRecursively(mNewSubnodes, node); + } else { + VisitChildren(node, [&](unowned ASDisplayNode *node, int index) { + [mNewSubnodes addObject:node]; + }); + } + newSubnodes = mNewSubnodes; + } else { + newSubnodes = node->_yogaChildren; + } + + // Cancel disappearing of any of the now-present subnodes. + for (ASDisplayNode *yogaChild in newSubnodes) { + if (yogaChild.isDisappearing) { + [yogaChild.layer removeAllAnimations]; + } + yogaChild.isDisappearing = NO; + } + + // Update the node tree to match the new subnodes. + if (!ASObjectIsEqual(node->_subnodes, newSubnodes)) { + // NOTE: Calculate the diff only if subnodes is non-empty. Otherwise insert + // (0, newSubnodeCount). + NSUInteger newSubnodeCount = newSubnodes.count; + IGListIndexSetResult *diff; + if (node->_subnodes.count > 0) { + diff = IGListDiff(node->_subnodes, newSubnodes, IGListDiffPointerPersonality); + } + + // Log the diff. + as_log_verbose(ASLayoutLog(), "layout: %@ updating children", + ASObjectDescriptionMakeTiny(node)); + + // Apply the diff. + + // If we have moves, we need to apply them at the end but use the original indexes for + // move-from. It would be great to do this in-place but correctness comes first. See + // discussion at https://github.com/Instagram/IGListKit/issues/1006 We use unowned here + // because ownership is established by the _real_ subnodes array, and this is quite a + // performance-sensitive code path. Note also, we could opt to create a vector of ONLY the + // moved subnodes, but I bet this approach is faster since it's just a memcpy rather than + // repeated objectAtIndex: calls with all their retain/release traffic. + std::vector oldSubnodesForMove; + if (diff.moves.count) { + oldSubnodesForMove.resize(node->_subnodes.count); + [node->_subnodes getObjects:oldSubnodesForMove.data() + range:NSMakeRange(0, node->_subnodes.count)]; + } + + // deferredRemovalFixups maintains a mapping for _subnodes insertion index. Because we + // sometimes defer subnode removal, while the indices returned from IGListDiff assume that + // we have actually removed the nodes, we need to maintain this fixup table. The fixup entry + // in the array is added to the IGListDiff index to map to the actual index needed. To avoid + // this somewhat-expensive operation when it's unnecessary, we keep a flag to indicate whether + // any subnodes have their removal deferred and don't allocate the vector contents until + // we know it will be used. + std::vector deferredRemovalFixups; + BOOL needToFixupIndices = NO; + NSUInteger numSubnodes = node->_subnodes.count; + + // Deletes descending so we don't invalidate our own indexes. + NSIndexSet *deletes = diff.deletes; + for (NSInteger i = deletes.lastIndex; deletes != nil && i != NSNotFound; + i = [deletes indexLessThanIndex:i]) { + as_log_verbose(ASLayoutLog(), "removing %@", + ASObjectDescriptionMakeTiny(node->_subnodes[i])); + // If tree isn't loaded, we never do deferred removal. Remove now. + if (!tree_loaded) { + [node->_subnodes[i] removeFromSupernode]; + continue; + } + + if (node->_subnodes[i].isDisappearing) { + // Do not try to disappear a node more than once as it could interfere with the + // animation and cause the node removal to be called twice. + needToFixupIndices = YES; + deferredRemovalFixups.resize(numSubnodes + 1); + for (NSInteger j = i + 1; j < deferredRemovalFixups.size(); j++) { + deferredRemovalFixups[j]++; + } + continue; + } + node->_subnodes[i].isDisappearing = YES; + + // If it is loaded, ask if any node in the subtree wants to defer. + // Unfortunately unconditionally deferring causes unexpected behavior for e.g. unit tests + // that depend on the tree reflecting its new state immediately by default. + [CATransaction begin]; + __block BOOL shouldDefer = NO; + ASDisplayNodePerformBlockOnEveryNode( + nil, node->_subnodes[i], NO, ^(ASDisplayNode *blockNode) { + NSDictionary> *actions = blockNode.disappearanceActions; + if (![actions count]) return; + if (!shouldDefer) { + // We must set the completion block before doing any animations. + ASDisplayNode *subnode = node->_subnodes[i]; + [CATransaction setCompletionBlock:^{ + // The disappearance may have been cancelled if the node was re-added while + // being disappeared. + if (subnode.isDisappearing) { + [subnode removeFromSupernode]; + } + }]; + } + shouldDefer = YES; + + for (NSString *key in actions) { + id action = actions[key]; + [action runActionForKey:key object:blockNode.layer arguments:nil]; + } + }); + if (shouldDefer) { + needToFixupIndices = YES; + deferredRemovalFixups.resize(numSubnodes + 1); + for (NSInteger j = i + 1; j < deferredRemovalFixups.size(); j++) { + deferredRemovalFixups[j]++; + } + } else { + [node->_subnodes[i] removeFromSupernode]; + } + [CATransaction commit]; + } + + // Inserts. Note we need to handle the case where diff is nil, which means we skipped the diff + // and just insert (0, newSubnodeCount). + NSIndexSet *inserts = diff.inserts; + for (NSInteger i = inserts ? inserts.firstIndex : 0; + inserts ? i != NSNotFound : i < newSubnodeCount; + inserts ? i = [inserts indexGreaterThanIndex:i] : ++i) { + NSInteger fixedUpIndex = i; + if (needToFixupIndices) { + fixedUpIndex = i + deferredRemovalFixups[i]; + // Fixup the fixups to account for the new inserted item. + deferredRemovalFixups.insert(deferredRemovalFixups.begin() + i, deferredRemovalFixups[i]); + } + as_log_verbose(ASLayoutLog(), "inserting %@ at %ld", + ASObjectDescriptionMakeTiny(newSubnodes[i]), (long)fixedUpIndex); + [node insertSubnode:newSubnodes[i] atIndex:fixedUpIndex]; + } + + // Moves. Manipulate the arrays directly to avoid extra traffic. + for (IGListMoveIndex *idx in diff.moves) { + auto &subnode = oldSubnodesForMove[idx.from]; + as_log_verbose(ASLayoutLog(), "moving %@ to %ld", ASObjectDescriptionMakeTiny(subnode), + (long)idx.to); + MutexLocker l(subnode->__instanceLock__); + if (needToFixupIndices) { + NSInteger fromIndex = [node->_subnodes indexOfObjectIdenticalTo:subnode]; + deferredRemovalFixups.erase(deferredRemovalFixups.begin() + fromIndex); + } + [node->_subnodes removeObjectIdenticalTo:subnode]; + NSInteger fixedUpIndex = idx.to; + if (needToFixupIndices) { + fixedUpIndex = idx.to + deferredRemovalFixups[idx.to]; + deferredRemovalFixups.insert(deferredRemovalFixups.begin() + idx.to, deferredRemovalFixups[idx.to]); + } + [node->_subnodes insertObject:subnode atIndex:fixedUpIndex]; + // If tree is loaded and subnode isn't rasterized, update view or layer array. + if (tree_loaded && !ASHierarchyStateIncludesRasterized(subnode->_hierarchyState)) { + if (subnode->_flags.layerBacked) { + [node->_layer insertSublayer:subnode->_layer atIndex:(unsigned int)fixedUpIndex]; + } else { + [node->_view insertSubview:subnode->_view atIndex:fixedUpIndex]; + } + } + } + + // Invalidate accessibility if needed due to tree change. + [node invalidateFirstAccessibilityContainerOrNonLayerBackedNode]; + } + }); + + // Traverse again, calling calculatedLayoutDidChange. This is done after updating frames + // so that higher nodes have an accurate picture of lower nodes' current frames. + // Note: "calculated" here really means "applied" – misnomer. + YGTraversePreOrder(yoga_root, [](YGNodeRef yoga) { + [GetTexture(yoga) calculatedLayoutDidChange]; + }); + + // Reset the flag and repeat if needed. + layout_flag_cleanup.release()(); + if (node->_flags.yogaRequestedNestedLayout) { + node->_flags.yogaRequestedNestedLayout = 0; + ApplyLayoutForCurrentBoundsIfRoot(node); + } +} + +void HandleExplicitLayoutIfNeeded(ASDisplayNode *node) { + ASDisplayNodeCAssertThreadAffinity(node); + if (_loaded(node)) { + // We are loaded. Just call this on the layer. It will escalate to the highest dirty layer and + // update downward. Since we're on main, we can access the layer without lock. + [node->_layer layoutIfNeeded]; + return; + } + + // We are not loaded. Our yoga root actually might be loaded and in hierarchy though! + // Lock to root, and repeat the call on the yoga root. + LockSet locks = [node lockToRootIfNeededForLayout]; + YGNodeRef yoga_self = GetYoga(node); + YGNodeRef yoga_root = GetRoot(yoga_self); + // Unfortunately we have to unlock here, or else we trigger an assertion when we call out to the + // subclasses during layout. + locks.clear(); + if (yoga_self != yoga_root) { + // Go back through the layoutIfNeeded code path in case the root node is loaded when we aren't. + ASDisplayNode *rootNode = GetTexture(yoga_root); + [rootNode layoutIfNeeded]; + } else { + // OK we are: yoga root, not loaded. + // That means all we need to do is call __layout and we will apply layout based on current + // bounds. + [node __layout]; + } +} + +CGSize GetCalculatedSize(ASDisplayNode *node) { + AssertEnabled(node); + DISABLED_ASAssertLocked(GetTexture(GetRoot(GetYoga(node)))->__instanceLock__); + + return CGSizeOfYogaLayout(GetYoga(node)); +} + +ASLayout *GetCalculatedLayout(ASDisplayNode *node, ASSizeRange sizeRange) { + AssertEnabled(node); + DISABLED_ASAssertLocked(GetTexture(GetRoot(GetYoga(node)))->__instanceLock__); + + return ASLayoutCreateFromYogaNodeLayout(GetYoga(node), sizeRange); +} + +CGRect GetChildrenRect(ASDisplayNode *node) { + AssertEnabled(node); + DISABLED_ASAssertLocked(GetTexture(GetRoot(GetYoga(node)))->__instanceLock__); + + CGRect childrenRect = CGRectZero; + YGNodeRef yoga_self = GetYoga(node); + for (uint32_t i = 0, iMax = YGNodeGetChildCount(yoga_self); i < iMax; ++i) { + YGNodeRef yoga_child = YGNodeGetChild(yoga_self, i); + CGRect frame = CGRectOfYogaLayout(yoga_child); + childrenRect = CGRectUnion(childrenRect, frame); + } + + return childrenRect; +} + +void InsertChild(ASDisplayNode *node, ASDisplayNode *child, int index) { + ASCAssertExperiment(ASExperimentalUnifiedYogaTree); + if (AS_PREDICT_FALSE(!child || !node)) return; + LockSet locks = [node lockToRootIfNeededForLayout]; + ASDisplayNodeCAssert([node nodeContext] == [child nodeContext], + @"Cannot add yoga child from different node context."); + YGNodeRef yoga = GetYoga(node); + YGNodeRef yogaChild = GetYoga(child); + int childCount = YGNodeGetChildCount(yoga); + if (index > childCount) { + ASDisplayNodeCFailAssert(@"Index out of bounds! %d vs. %d", childCount, (int)index); + index = childCount; + } else if (index == -1) { + index = childCount; + } + YGNodeRef oldOwner = YGNodeGetOwner(yogaChild); + if (oldOwner) { + // Was in tree before. Remove child but "steal" +1 i.e. do not release. + YGNodeRemoveChild(oldOwner, yogaChild); + } else { + // Was not in a tree before. Retain now. + CFRetain((CFTypeRef)child); + } + + YGNodeInsertChild(yoga, yogaChild, index); + child->_yogaParent = node; +} + +void RemoveChild(ASDisplayNode *node, ASDisplayNode *child) { + ASCAssertExperiment(ASExperimentalUnifiedYogaTree); + if (AS_PREDICT_FALSE(!node || !child)) return; + LockSet locks = [node lockToRootIfNeededForLayout]; + YGNodeRef yoga = GetYoga(node); + YGNodeRef childYoga = GetYoga(child); + YGNodeRemoveChild(yoga, childYoga); + child->_yogaParent = nil; + CFRelease((CFTypeRef)child); +} + +void SetChildren(ASDisplayNode *node, NSArray *children) { + ASCAssertExperiment(ASExperimentalUnifiedYogaTree); + if (AS_PREDICT_FALSE(!node)) return; + LockSet locks = [node lockToRootIfNeededForLayout]; + YGNodeRef yoga = GetYoga(node); + int newCount = children.count; + int oldCount = YGNodeGetChildCount(yoga); + + // Fast paths for some special cases. + if (newCount == 0) { + if (oldCount == 0) return; + for (int i = 0; i < oldCount; ++i) { + unowned ASDisplayNode *node = GetTexture(YGNodeGetChild(yoga, i)); + node->_yogaParent = nil; + CFRelease((CFTypeRef)node); + } + YGNodeRemoveAllChildren(yoga); + return; + } + + // Go through new children putting them in a vector, and updating the parent if they aren't + // already. + std::vector rawYogaChildren; + rawYogaChildren.reserve(newCount); + for (ASDisplayNode *child in children) { + YGNodeRef childYoga = GetYoga(child); + rawYogaChildren.emplace_back(childYoga); + if (!YGNodeGetOwner(childYoga)) { + // Not previously in a tree. Retain. + CFRetain((CFTypeRef)child); + } + // Update parent pointer. + child->_yogaParent = node; + } + + // Go through old children, clearing parent & releasing for any that aren't also in the new + // children. + for (int i = 0; i < oldCount; ++i) { + YGNodeRef childYoga = YGNodeGetChild(yoga, i); + auto it = std::find(rawYogaChildren.begin(), rawYogaChildren.end(), childYoga); + // If child is being removed, clear its pointer and release it. + if (it == rawYogaChildren.end()) { + unowned ASDisplayNode *node = GetTexture(childYoga); + node->_yogaParent = nil; + CFRelease((CFTypeRef)node); + } + } + + // Actually update the yoga tree. + YGNodeSetChildren(yoga, rawYogaChildren); +} + +void TearDown(AS_NORETAIN_ALWAYS ASDisplayNode *node) { + ASCAssertExperiment(ASExperimentalUnifiedYogaTree); + if (AS_PREDICT_FALSE(!node)) return; + MutexLocker lock(node->__instanceLock__); + YGNodeRef yoga = GetYoga(node); + uint32_t count = YGNodeGetChildCount(yoga); + for (uint32_t i = 0; i < count; ++i) { + YGNodeRef childYoga = YGNodeGetChild(yoga, i); + if (unowned ASDisplayNode *child = GetTexture(childYoga)) { + child->_yogaParent = nil; + CFRelease((CFTypeRef)child); + } + } + // No need to call YGNodeRemoveAllChildren, that will come from YGNodeFree which is + // run by the style. +} + +NSArray *CopyChildren(ASDisplayNode *node) { + ASCAssertExperiment(ASExperimentalUnifiedYogaTree); + if (AS_PREDICT_FALSE(!node)) return @[]; + MutexLocker lock(node->__instanceLock__); + YGNodeRef yoga = GetYoga(node); + uint32_t count = YGNodeGetChildCount(yoga); + std::vector rawChildren; + rawChildren.reserve(count); + VisitChildren(node, [&](unowned ASDisplayNode *node, int _) { rawChildren.emplace_back(node); }); + return [NSArray arrayByTransferring:rawChildren.data() count:rawChildren.size()]; +} + +void VisitChildren(ASDisplayNode *node, + const std::function &f) { + ASCAssertExperiment(ASExperimentalUnifiedYogaTree); + if (AS_PREDICT_FALSE(!node)) return; + MutexLocker lock(node->__instanceLock__); + YGNodeRef yoga = GetYoga(node); + uint32_t count = YGNodeGetChildCount(yoga); + for (uint32_t i = 0; i < count; ++i) { + YGNodeRef childYoga = YGNodeGetChild(yoga, i); + if (unowned ASDisplayNode *child = GetTexture(childYoga)) { + f(child, i); + } + } +} + +int MeasuredNodesForThread() { + return measuredNodes; +} + +#else // !YOGA + +namespace AS { +namespace Yoga2 { + +void AlertNeedYoga() { + ASDisplayNodeCFailAssert(@"Yoga experiment is enabled but we were compiled without yoga!"); +} + +void CalculateLayoutAtRoot(ASDisplayNode *node, CGSize maxSize) { + AssertEnabled(); + AlertNeedYoga(); +} + +void HandleExplicitLayoutIfNeeded(ASDisplayNode *node) { + AssertEnabled(); + AlertNeedYoga(); +} + +bool GetEnabled(ASDisplayNode *node) { return false; } +void Enable(ASDisplayNode *node) { + AssertEnabled(); + AlertNeedYoga(); +} +void MarkContentMeasurementDirty(ASDisplayNode *node) { + AssertEnabled(); + AlertNeedYoga(); +} +CGSize SizeThatFits(ASDisplayNode *node, CGSize maxSize) { + AssertEnabled(); + AlertNeedYoga(); + return CGSizeZero; +} +void ApplyLayoutForCurrentBoundsIfRoot(ASDisplayNode *node) { + AssertEnabled(); + AlertNeedYoga(); +} +CGSize GetCalculatedSize(ASDisplayNode *node) { + AssertEnabled(); + AlertNeedYoga(); + return CGSizeZero; +} +ASLayout *GetCalculatedLayout(ASDisplayNode *node, ASSizeRange sizeRange) { + AssertEnabled(); + AlertNeedYoga(); + return nil; +} +CGRect GetChildrenRect(ASDisplayNode *node) { + AssertEnabled(); + AlertNeedYoga(); + return CGRectZero; +} +int MeasuredNodesForThread() { + AssertEnabled(); + AlertNeedYoga(); + return 0; +} +#endif // YOGA + +} // namespace Yoga2 +} // namespace AS + +AS_ASSUME_NORETAIN_END diff --git a/Source/ASDisplayNode+Yoga2Logging.h b/Source/ASDisplayNode+Yoga2Logging.h new file mode 100644 index 000000000..5e9878103 --- /dev/null +++ b/Source/ASDisplayNode+Yoga2Logging.h @@ -0,0 +1,35 @@ +#if defined(__cplusplus) + +#import + +#if YOGA + +#include + +#import YOGA_HEADER_PATH + +/** + * Implements the YGConfig logging function. Yoga logging is actually a bit + * tricky in a multithreaded environment, so this gets its own source file. + */ +namespace AS { +namespace Yoga2 { +namespace Logging { +/** + * Note: Don't set a print func on yoga nodes. See details in the implementation + * of Log, or at https://github.com/facebook/yoga/issues/879 + */ + +/** + * The log callback to be provided to Yoga. + */ +int Log(const YGConfigRef config, const YGNodeRef node, YGLogLevel level, + const char *format, va_list args); + +} // namespace Logging +} // namespace Yoga2 +} // namespace AS + +#endif // YOGA + +#endif // defined(__cplusplus) diff --git a/Source/ASDisplayNode+Yoga2Logging.mm b/Source/ASDisplayNode+Yoga2Logging.mm new file mode 100644 index 000000000..a2d142023 --- /dev/null +++ b/Source/ASDisplayNode+Yoga2Logging.mm @@ -0,0 +1,111 @@ +#import + +#if YOGA + +#import + +#import + +#import +#import + +namespace AS { +namespace Yoga2 { +namespace Logging { + +/// The maximum length for a log chunk. If Yoga goes over, we just log an error. +constexpr size_t kChunkCapacity = 128; + +/// The destructor for the pthread_specific key for the preamble. +void PreambleStorageDestructor(void *ptr) { delete reinterpret_cast(ptr); } + +/** + * Get a thread-local std::string buffer for the preamble of a multipart yoga log statement. + * + * Most Yoga log statements (gPrintChanges) actually come in three phases: + * - A preamble, e.g. "{1." + * - A YGNodePrint, which we would turn into "" + * - An ending, e.g. "} d: 100 200" + * + * Especially since we're multithreaded, this does not work for us. It causes interleaving of log + * statements. Additionally it causes extra log metadata to be spat out with each phase and breaks + * up the lines. So when we detect this pattern, we use this thread-local buffer to store the + * prefix, we _do not implement_ YGNodePrint, and then when the ending comes through we combine all + * three pieces. Details at https://github.com/facebook/yoga/issues/879 + */ +std::string *GetPreambleStorage() { + static pthread_key_t key; + static dispatch_once_t once_token; + dispatch_once(&once_token, ^{ + key = pthread_key_create(&key, PreambleStorageDestructor); + }); + + auto str = reinterpret_cast(pthread_getspecific(key)); + if (!str) { + str = new std::string; + pthread_setspecific(key, str); + } + return str; +} + +inline os_log_type_t OSTypeFromYogaLevel(YGLogLevel level) { + switch (level) { + case YGLogLevelInfo: + case YGLogLevelWarn: + return OS_LOG_TYPE_INFO; + case YGLogLevelVerbose: + case YGLogLevelDebug: + return OS_LOG_TYPE_DEBUG; + case YGLogLevelError: + case YGLogLevelFatal: + // Note: yoga will issue abort() after fatal logs. + return OS_LOG_TYPE_ERROR; + } +} + +int Log(const YGConfigRef config, const YGNodeRef node, YGLogLevel level, const char *format, + va_list args) { + if (!ASEnableVerboseLogging && level == YGLogLevelVerbose) { + return 0; + } + os_log_type_t os_type = OSTypeFromYogaLevel(level); + + // If this log type isn't enabled right now, bail. + if (!os_log_type_enabled(ASLayoutLog(), os_type)) { + return 0; + } + + char c[kChunkCapacity]; + int str_size = vsnprintf(c, kChunkCapacity, format, args); + if (str_size < 0 || str_size >= kChunkCapacity) { + ASDisplayNodeCFailAssert(@"Yoga log chunk over capacity!"); + return 0; + } + + bool has_open_brace = (strchr(c, '{') != nullptr); + bool has_close_brace = (strchr(c, '}') != nullptr); + if (has_open_brace && !has_close_brace) { + // This is the preamble. Store it in our TLS buffer and wait for the rest. + std::string *preamble = GetPreambleStorage(); + ASDisplayNodeCAssert(preamble->empty(), @"Two Yoga log preambles in a row."); + preamble->assign(c); + } else if (!has_open_brace && has_close_brace) { + // This is the end. Combine the parts and log them with the node. + std::string preamble; + preamble.swap(*GetPreambleStorage()); + os_log_with_type(ASLayoutLog(), os_type, "%s %@ %s", preamble.c_str(), + ASObjectDescriptionMakeTiny(GetTexture(node)), c); + + } else { + // This is a normal one-shot message. Just log it. + os_log_with_type(ASLayoutLog(), os_type, "%s", c); + } + // Always report that we printed the whole string. + return str_size; +} + +} // namespace Logging +} // namespace Yoga2 +} // namespace AS + +#endif // YOGA diff --git a/Source/ASDisplayNode.h b/Source/ASDisplayNode.h index eff930ccf..d095c698a 100644 --- a/Source/ASDisplayNode.h +++ b/Source/ASDisplayNode.h @@ -28,7 +28,7 @@ NS_ASSUME_NONNULL_BEGIN #define AS_MAX_INTERFACE_STATE_DELEGATES 4 #endif -@class ASDisplayNode; +@class ASDisplayNode, ASNodeContext; @protocol ASContextTransitioning; /** @@ -46,6 +46,11 @@ typedef UIViewController * _Nonnull(^ASDisplayNodeViewControllerBlock)(void); */ typedef CALayer * _Nonnull(^ASDisplayNodeLayerBlock)(void); +/** + * Accessibility elements creation block. Used to specify accessibility elements of the node. + */ +typedef NSArray *_Nullable (^ASDisplayNodeAccessibilityElementsBlock)(void); + /** * ASDisplayNode loaded callback block. This block is called BEFORE the -didLoad method and is always called on the main thread. */ @@ -191,6 +196,11 @@ ASDK_EXTERN NSInteger const ASDefaultDrawingPriority; */ - (void)setLayerBlock:(ASDisplayNodeLayerBlock)layerBlock; +/** + * Get the context for this node, if one was set during creation. + */ +@property (nullable, readonly) ASNodeContext *nodeContext; + /** * @abstract Returns whether the node is synchronous. * @@ -363,6 +373,14 @@ ASDK_EXTERN NSInteger const ASDefaultDrawingPriority; */ - (void)removeFromSupernode; +/** + * @abstract Move the given subnode to a new index. + * + * @discussion This avoids extra traffic that would be involved with removing the subnode and + * inserting it as separate operations. + */ +- (void)moveSubnode:(ASDisplayNode *)node toIndex:(NSInteger)newIndex; + /** * @abstract The receiver's immediate subnodes. */ @@ -560,6 +578,29 @@ ASDK_EXTERN NSInteger const ASDefaultDrawingPriority; */ @property BOOL automaticallyRelayoutOnLayoutMarginsChanges; +/** + * @abstract For subclasses to overwrite to determine if the ASDisplayNode is flattenable and not need to be + * rendered. + * + * @note This is only effective if Yoga is used for layout. + * + * Defaults to NO. + */ +- (BOOL)computeFlattenability; + +/** + * @abstract Invalidate any previous flattenable state computed via computeFlattenability. + * + * @discussion Call this if any state changes of the node that could invalidate a previous computed + * flattenability. + */ +- (void)invalidateIsFlattenable; + +/** + * Hook into Xcode's Quick Look system. Returns the view/layer of the node if loaded. + */ +- (nullable id)debugQuickLookObject; + @end /** @@ -788,6 +829,13 @@ ASDK_EXTERN NSInteger const ASDefaultDrawingPriority; @end +/** + * "AS_FORCE_ACCESSIBILITY_FOR_TESTING" is A command line argument that, if present, forces Texture + * to process accessibility regardless of whether voice over is currently running. + * + * This is useful for test environments that run in release mode. + */ + @interface ASDisplayNode (UIViewBridgeAccessibility) // Accessibility support @@ -817,6 +865,19 @@ ASDK_EXTERN NSInteger const ASDefaultDrawingPriority; @end + +@interface ASDisplayNode (CustomAccessibilityBehavior) + +/** + * Set the block that should be used to determining the accessibility elements of the node. + * When set, the accessibility-related logic (e.g. label aggregation) will not be triggered. + * + * @param block The block that returns the accessibility elements of the node. + */ +- (void)setAccessibilityElementsBlock:(ASDisplayNodeAccessibilityElementsBlock)block; + +@end + @interface ASDisplayNode (ASLayoutElement) /** @@ -824,15 +885,21 @@ ASDK_EXTERN NSInteger const ASDefaultDrawingPriority; * * @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). + * @return An ASLayout instance defining the layout of the receiver (and its children, if the box + * layout model is used). * - * @discussion Though this method does not set the bounds of the view, it does have side effects--caching both the - * constraint and the result. + * @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 caches results from -calculateLayoutThatFits:. Calling this method may - * be expensive if result is not cached. + * @warning Subclasses must not override this; it caches results from -calculateLayoutThatFits:. + * Calling this method may be expensive if result is not cached. * * @see [ASDisplayNode(Subclassing) calculateLayoutThatFits:] + * + * @note In the yoga2 experiment, this method may only be called on yoga root nodes. You can however + * call this on the root node, and then call -calculatedLayout on the intermediary node of interest. + * Note also that in yoga2, the minimum size specified here is ignored – you can manipulate + * style.minWidth and style.minHeight to force a minimum size. */ - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize; @@ -842,18 +909,8 @@ ASDK_EXTERN NSInteger const ASDefaultDrawingPriority; @end -typedef NS_ENUM(NSInteger, ASLayoutEngineType) { - ASLayoutEngineTypeLayoutSpec, - ASLayoutEngineTypeYoga -}; - @interface ASDisplayNode (ASLayout) -/** - * @abstract Returns the current layout type the node uses for layout the subtree. - */ -@property (readonly) ASLayoutEngineType layoutEngineType; - /** * @abstract Return the calculated size. * diff --git a/Source/ASDisplayNode.mm b/Source/ASDisplayNode.mm index b96a77477..cdd0eee29 100644 --- a/Source/ASDisplayNode.mm +++ b/Source/ASDisplayNode.mm @@ -34,6 +34,11 @@ #import #import #import +#import + +using namespace AS; + +AS_ASSUME_NORETAIN_BEGIN // Conditionally time these scopes to our debug ivars (only exist in debug/profile builds) #if TIME_DISPLAYNODE_OPS @@ -41,12 +46,6 @@ #else #define TIME_SCOPED(outVar) #endif -// This is trying to merge non-rangeManaged with rangeManaged, so both range-managed and standalone nodes wait before firing their exit-visibility handlers, as UIViewController transitions now do rehosting at both start & end of animation. -// Enable this will mitigate interface updating state when coalescing disabled. -// TODO(wsdwsd0829): Rework enabling code to ensure that interface state behavior is not altered when ASCATransactionQueue is disabled. -#define ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR 0 - -using AS::MutexLocker; static ASDisplayNodeNonFatalErrorBlock _nonFatalErrorBlock = nil; @@ -80,7 +79,7 @@ BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLaye _ASPendingState *ASDisplayNodeGetPendingState(ASDisplayNode *node) { - ASLockScope(node); + MutexLocker l(node->__instanceLock__); _ASPendingState *result = node->_pendingViewState; if (result == nil) { result = [[_ASPendingState alloc] init]; @@ -90,6 +89,7 @@ BOOL ASDisplayNodeNeedsSpecialPropertiesHandling(BOOL isSynchronous, BOOL isLaye } void StubImplementationWithNoArgs(id receiver, SEL _cmd) {} +BOOL StubBoolImplementationWithNoArgs(id receiver, SEL _cmd) { return NO; } void StubImplementationWithSizeRange(id receiver, SEL _cmd, ASSizeRange sr) {} void StubImplementationWithTwoInterfaceStates(id receiver, SEL _cmd, ASInterfaceState s0, ASInterfaceState s1) {} @@ -102,10 +102,11 @@ void StubImplementationWithTwoInterfaceStates(id receiver, SEL _cmd, ASInterface * * @param c the class, required * @param instance the instance, which may be nil. (If so, the class is inspected instead) - * @remarks The instance value is used only if we suspect the class may be dynamic (because it overloads - * +respondsToSelector: or -respondsToSelector.) In that case we use our "slow path", calling this - * method on each -init and passing the instance value. While this may seem like an unlikely scenario, - * it turns out our own internal tests use a dynamic class, so it's worth capturing this edge case. + * @remarks The instance value is used only if we suspect the class may be dynamic (because + * it overloads +respondsToSelector: or -respondsToSelector.) In that case we use our "slow path", + * calling this method on each -init and passing the instance value. While this may seem like an + * unlikely scenario, it turns out our own internal tests use a dynamic class, so it's worth + * capturing this edge case. * * @return ASDisplayNode flags. */ @@ -170,6 +171,9 @@ static ASDisplayNodeMethodOverrides GetASDisplayNodeMethodOverrides(Class c) if (ASDisplayNodeSubclassOverridesSelector(c, @selector(calculateSizeThatFits:))) { overrides |= ASDisplayNodeMethodOverrideCalcSizeThatFits; } + if (ASDisplayNodeSubclassOverridesSelector(c, @selector(yogaBaselineWithSize:))) { + overrides |= ASDisplayNodeMethodOverrideYogaBaseline; + } return overrides; } @@ -241,11 +245,13 @@ + (void)initialize class_addMethod(self, @selector(didEnterVisibleState), noArgsImp, "v@:"); class_addMethod(self, @selector(didExitVisibleState), noArgsImp, "v@:"); class_addMethod(self, @selector(hierarchyDisplayDidFinish), noArgsImp, "v@:"); + class_addMethod(self, @selector(asyncTraitCollectionDidChange), noArgsImp, "v@:"); class_addMethod(self, @selector(calculatedLayoutDidChange), noArgsImp, "v@:"); - + auto type0 = "v@:" + std::string(@encode(ASSizeRange)); class_addMethod(self, @selector(willCalculateLayout:), (IMP)StubImplementationWithSizeRange, type0.c_str()); - + class_addMethod(self, @selector(didCalculateLayout:), (IMP)StubImplementationWithSizeRange, + type0.c_str()); auto interfaceStateType = std::string(@encode(ASInterfaceState)); auto type1 = "v@:" + interfaceStateType + interfaceStateType; class_addMethod(self, @selector(interfaceStateDidChange:fromState:), (IMP)StubImplementationWithTwoInterfaceStates, type1.c_str()); @@ -281,7 +287,12 @@ - (void)_staticInitialize - (void)_initializeInstance { [self _staticInitialize]; - __instanceLock__.SetDebugNameWithObject(self); + _weakSelf = self; + _nodeContext = ASNodeContextGet(); + __instanceLock__.Configure(_nodeContext ? &_nodeContext->_mutex : nullptr); + if (!_nodeContext) { + __instanceLock__.get().SetDebugNameWithObject(self); + } _viewClass = [self.class viewClass]; _layerClass = [self.class layerClass]; @@ -293,10 +304,10 @@ - (void)_initializeInstance _contentsScaleForDisplay = ASScreenScale(); _drawingPriority = ASDefaultTransactionPriority; _maskedCorners = kASCACornerAllCorners; - + _primitiveTraitCollection = ASPrimitiveTraitCollectionMakeDefault(); - _layoutVersion = 1; + std::atomic_init(&_layoutVersion, (NSUInteger)1); _defaultLayoutTransitionDuration = 0.2; _defaultLayoutTransitionDelay = 0.0; @@ -305,12 +316,11 @@ - (void)_initializeInstance _flags.canClearContentsOfLayer = YES; _flags.canCallSetNeedsDisplayOfLayer = YES; - _fallbackSafeAreaInsets = UIEdgeInsetsZero; _flags.fallbackInsetsLayoutMarginsFromSafeArea = YES; - _flags.isViewControllerRoot = NO; - _flags.automaticallyRelayoutOnSafeAreaChanges = NO; - _flags.automaticallyRelayoutOnLayoutMarginsChanges = NO; + if (ASCheckFlag(_nodeContext.options, ASNodeContextUseYoga)) { + [self enableYoga]; + } [self baseDidInit]; } @@ -426,10 +436,10 @@ - (void)onDidLoad:(ASDisplayNodeDidLoadBlock)body l.unlock(); body(self); return; - } else if (_onDidLoadBlocks == nil) { - _onDidLoadBlocks = [NSMutableArray arrayWithObject:body]; } else { - [_onDidLoadBlocks addObject:body]; + // Initial capacity taken from CFArray. + _onDidLoadBlocks.reserve(4); + _onDidLoadBlocks.emplace_back(body); } } @@ -445,17 +455,18 @@ - (void)asyncTraitCollectionDidChangeWithPreviousTraitCollection:(ASPrimitiveTra __instanceLock__.unlock(); if (primitiveTraitCollection.userInterfaceStyle != previousTraitCollection.userInterfaceStyle) { if (loaded) { - // we need to run that on main thread, cause accessing CALayer properties. - // It seems than in iOS 13 sometimes it causes deadlock. + // CALayer properties must accessed from main thread in Texture. + // See (https://github.com/TextureGroup/Texture/pull/1762) which documents an observed deadlock. ASPerformBlockOnMainThread(^{ self->__instanceLock__.lock(); CGFloat cornerRadius = self->_cornerRadius; ASCornerRoundingType cornerRoundingType = self->_cornerRoundingType; UIColor *backgroundColor = self->_backgroundColor; + ASPrimitiveTraitCollection primitiveTraitCollection = self->_primitiveTraitCollection; self->__instanceLock__.unlock(); - // TODO: we should resolve color using node's trait collection - // but Texture changes it from many places, so we may receive the wrong one. - CGColorRef cgBackgroundColor = backgroundColor.CGColor; + UITraitCollection *traitCollection = ASPrimitiveTraitCollectionToUITraitCollection(primitiveTraitCollection); + UIColor *resolvedBackgroundColor = [backgroundColor resolvedColorWithTraitCollection:traitCollection]; + CGColorRef cgBackgroundColor = resolvedBackgroundColor.CGColor; if (!CGColorEqualToColor(self->_layer.backgroundColor, cgBackgroundColor)) { // Background colors do not dynamically update for layer backed nodes since they utilize CGColorRef // instead of UIColor. Non layer backed node also receive color to the layer (see [_ASPendingState -applyToView:withSpecialPropertiesHandling:]). @@ -468,7 +479,7 @@ - (void)asyncTraitCollectionDidChangeWithPreviousTraitCollection:(ASPrimitiveTra if (cornerRoundingType == ASCornerRoundingTypeClipping && cornerRadius > 0.0f) { [self _updateClipCornerLayerContentsWithRadius:cornerRadius backgroundColor:backgroundColor]; } - + [self setNeedsDisplay]; }); } @@ -478,6 +489,7 @@ - (void)asyncTraitCollectionDidChangeWithPreviousTraitCollection:(ASPrimitiveTra - (void)dealloc { + MutexLocker l(__instanceLock__); _flags.isDeallocating = YES; [self baseWillDealloc]; @@ -496,12 +508,22 @@ - (void)dealloc for (ASDisplayNode *subnode in _subnodes) [subnode _setSupernode:nil]; - [self scheduleIvarsForMainThreadDeallocation]; +#if YOGA + if (_flags.yoga && ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + Yoga2::TearDown(self); + } +#endif // YOGA + + [self handleMainThreadDeallocationIfNeeded]; // TODO: Remove this? If supernode isn't already nil, this method isn't dealloc-safe anyway. [self _setSupernode:nil]; } +- (instancetype)tryRetain { + return _weakSelf; +} + #pragma mark - Loading - (BOOL)_locked_shouldLoadViewOrLayer @@ -575,7 +597,6 @@ - (void)_locked_loadViewOrLayer if (_flags.layerBacked) { TIME_SCOPED(_debugTimeToCreateView); _layer = [self _locked_layerToLoad]; - static int ASLayerDelegateAssociationKey; /** * CALayer's .delegate property is documented to be weak, but the implementation is actually assign. @@ -585,7 +606,7 @@ - (void)_locked_loadViewOrLayer */ ASWeakProxy *instance = [ASWeakProxy weakProxyWithTarget:self]; _layer.delegate = (id)instance; - objc_setAssociatedObject(_layer, &ASLayerDelegateAssociationKey, instance, OBJC_ASSOCIATION_RETAIN_NONATOMIC); + _layer.as_retainedDelegate = (id)instance; } else { TIME_SCOPED(_debugTimeToCreateView); _view = [self _locked_viewToLoad]; @@ -607,10 +628,11 @@ - (void)_didLoad [self didLoad]; __instanceLock__.lock(); - const auto onDidLoadBlocks = ASTransferStrong(_onDidLoadBlocks); + const auto onDidLoadBlocks = std::move(_onDidLoadBlocks); + _onDidLoadBlocks.clear(); __instanceLock__.unlock(); - for (ASDisplayNodeDidLoadBlock block in onDidLoadBlocks) { + for (const auto &block : onDidLoadBlocks) { block(self); } [self enumerateInterfaceStateDelegates:^(id del) { @@ -929,6 +951,62 @@ - (void)__setNodeController:(ASNodeController *)controller } } +- (NSDictionary> *)disappearanceActions { + MutexLocker l(__instanceLock__); + return _disappearanceActions; +} + +- (void)setDisappearanceActions:(NSDictionary> *)disappearanceActions { + MutexLocker l(__instanceLock__); + _disappearanceActions = disappearanceActions; +} + +- (BOOL)isDisappearing { + MutexLocker l(__instanceLock__); + return _flags.isDisappearing; +} + +- (void)setIsDisappearing:(BOOL)isDisappearing { + MutexLocker l(__instanceLock__); + _flags.isDisappearing = isDisappearing; +} + +- (UIEdgeInsets)paddings { + MutexLocker l(__instanceLock__); +#if YOGA + if (_flags.yoga) { + YGNodeRef yogaNode = _style.yogaNode; + CGFloat top = YGNodeLayoutGetPadding(yogaNode, YGEdgeTop); + CGFloat left = YGNodeLayoutGetPadding(yogaNode, YGEdgeLeft); + CGFloat bottom = YGNodeLayoutGetPadding(yogaNode, YGEdgeBottom); + CGFloat right = YGNodeLayoutGetPadding(yogaNode, YGEdgeRight); + return UIEdgeInsetsMake(top, left, bottom, right); + } +#endif // YOGA + return UIEdgeInsetsZero; + +} + +#pragma mark - UIResponder + +#define HANDLE_NODE_RESPONDER_METHOD(__sel) \ + /* All responder methods should be called on the main thread */ \ + ASDisplayNodeAssertMainThread(); \ + if (checkFlag(Synchronous)) { \ + /* If the view is not a _ASDisplayView subclass (Synchronous) just call through to the view as we + expect it's a non _ASDisplayView subclass that will respond */ \ + return [_view __sel]; \ + } else { \ + if (ASSubclassOverridesSelector([_ASDisplayView class], _viewClass, @selector(__sel))) { \ + /* If the subclass overwrites canBecomeFirstResponder just call through + to it as we expect it will handle it */ \ + return [_view __sel]; \ + } else { \ + /* Call through to _ASDisplayView's superclass to get it handled */ \ + return [(_ASDisplayView *)_view __##__sel]; \ + } \ + } \ + - (void)checkResponderCompatibility { #if ASDISPLAYNODE_ASSERTIONS_ENABLED @@ -962,6 +1040,66 @@ - (void)setDebugName:(NSString *)debugName } } +#pragma mark - Flatten Support + +- (void)enableViewFlattening +{ + MutexLocker l(__instanceLock__); + if (_flags.viewFlattening) return; +#if YOGA + if (_yogaParent && !_yogaParent->_flags.viewFlattening) { + ASDisplayNodeFailAssert(@"Cannot enable viewFlattening on a child that is not enabled for viewFlattening"); + return; + } +#endif // YOGA + _flags.viewFlattening = YES; +#if YOGA + if (ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + Yoga2::VisitChildren(self, [](unowned ASDisplayNode *node, int index) { [node enableViewFlattening]; }); + } else { + for (ASDisplayNode *yogaChild in _yogaChildren) { + [yogaChild enableViewFlattening]; + } + } +#endif // YOGA +} + +- (BOOL)computeFlattenability +{ + return NO; +} + +- (BOOL)isFlattenable +{ + MutexLocker l(__instanceLock__); + if (!_flags.viewFlattening) { + return NO; + } + + if (_flags.haveCachedIsFlattenable) { + return _flags.cachedIsFlattenable; + } + + // Compute the flattenability + _flags.haveCachedIsFlattenable = YES; + _flags.cachedIsFlattenable = [self computeFlattenability]; + return _flags.cachedIsFlattenable; +} + +- (void)invalidateIsFlattenable +{ + UniqueLock l(__instanceLock__); + if (!_flags.viewFlattening) { + l.unlock(); + return; + } + _flags.haveCachedIsFlattenable = NO; + _flags.cachedIsFlattenable = NO; + l.unlock(); + + [self setNeedsLayout]; +} + #pragma mark - Layout #pragma mark @@ -975,6 +1113,13 @@ - (BOOL)canLayoutAsynchronous - (void)__setNeedsLayout { + UniqueLock l(__instanceLock__); + if (Yoga2::GetEnabled(self)) { + l.unlock(); + Yoga2::MarkContentMeasurementDirty(self); + return; + } + l.unlock(); [self invalidateCalculatedLayout]; } @@ -985,17 +1130,27 @@ - (void)invalidateCalculatedLayout _layoutVersion++; _unflattenedLayout = nil; - -#if YOGA - [self invalidateCalculatedYogaLayout]; -#endif } - (void)__layout { ASDisplayNodeAssertThreadAffinity(self); DISABLED_ASAssertUnlocked(__instanceLock__); - + UniqueLock l(__instanceLock__); + if (Yoga2::GetEnabled(self)) { + Yoga2::ApplyLayoutForCurrentBoundsIfRoot(self); + + // Per API contract, these are only called if the node is loaded. + if (_loaded(self)) { + l.unlock(); + [self layout]; + [self _layoutClipCornersIfNeeded]; + [self layoutDidFinish]; + } + return; + } + l.unlock(); + BOOL loaded = NO; { AS::UniqueLock l(__instanceLock__); @@ -1055,6 +1210,7 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize restrictedToSize:(ASLayoutElementSize)size relativeToParentSize:(CGSize)parentSize { + Yoga2::AssertDisabled(self); as_activity_scope_verbose(as_activity_create("Calculate node layout", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); as_log_verbose(ASLayoutLog(), "Calculating layout for %@ sizeRange %@", self, NSStringFromASSizeRange(constrainedSize)); @@ -1085,23 +1241,7 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize { __ASDisplayNodeCheckForLayoutMethodOverrides; - switch (self.layoutEngineType) { - case ASLayoutEngineTypeLayoutSpec: - return [self calculateLayoutLayoutSpec:constrainedSize]; -#if YOGA - case ASLayoutEngineTypeYoga: - return [self calculateLayoutYoga:constrainedSize]; -#endif - // If YOGA is not defined but for some reason the layout type engine is Yoga - // we explicitly fallthrough here - default: - break; - } - - // If this case is reached a layout type engine was defined for a node that is currently - // not supported. - ASDisplayNodeAssert(NO, @"No layout type determined"); - return nil; + return [self calculateLayoutLayoutSpec:constrainedSize]; } - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize @@ -1111,6 +1251,11 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize return ASIsCGSizeValidForSize(constrainedSize) ? constrainedSize : CGSizeZero; } +- (float)yogaBaselineWithSize:(CGSize)size +{ + return size.height; +} + - (void)layout { // Hook for subclasses @@ -1229,6 +1374,61 @@ - (void)enableSubtreeRasterization } } +- (void)enableYoga { +#if YOGA + MutexLocker l(__instanceLock__); + if (_flags.yoga) return; + _flags.yoga = YES; + Yoga2::Enable(self); + if (ASActivateExperimentalFeature(ASExperimentalUnifiedYogaTree)) { + Yoga2::VisitChildren(self, [](ASDisplayNode *node, int index) { [node enableYoga]; }); + } else { + for (ASDisplayNode *yogaChild in _yogaChildren) { + [yogaChild enableYoga]; + } + } +#endif // YOGA +} + +- (BOOL)yoga +{ +#if YOGA + MutexLocker l(__instanceLock__); + return _flags.yoga; +#else + return NO; +#endif // YOGA +} + +- (void)controllerDidSetChildren:(NSArray *)children { +#if YOGA + if (![self hasCustomMeasure]) { + [self setYogaChildren:children]; + } +#endif +} + +- (void)controllerDidInsertChild:(ASNodeController *)child atIndex:(NSInteger)index { +#if YOGA + if (![self hasCustomMeasure]) { + [self insertYogaChild:[child node] atIndex:index]; + } +#endif +} + +- (void)controllerDidRemoveChild:(ASNodeController *)child { +#if YOGA + if (![self hasCustomMeasure]) { + [self removeYogaChild:[child node]]; + } +#endif +} + +- (BOOL)hasCustomMeasure { + MutexLocker l(__instanceLock__); + return 0 != (_methodOverrides & ASDisplayNodeMethodOverrideCalcSizeThatFits); +} + - (CGFloat)contentsScaleForDisplay { MutexLocker l(__instanceLock__); @@ -1507,14 +1707,19 @@ - (void)_updateClipCornerLayerContentsWithRadius:(CGFloat)radius backgroundColor UIImage *newContents = ASGraphicsCreateImage(self.primitiveTraitCollection, size, NO, self.contentsScaleForDisplay, nil, nil, ^{ CGContextRef ctx = UIGraphicsGetCurrentContext(); if (isRight == YES) { - CGContextTranslateCTM(ctx, -radius + 1, 0); + CGContextTranslateCTM(ctx, -radius + 1, 0); } if (isTop == NO) { - CGContextTranslateCTM(ctx, 0, -radius + 1); + CGContextTranslateCTM(ctx, 0, -radius + 1); } - UIBezierPath *roundedRect = [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) cornerRadius:radius]; + + UIBezierPath *roundedRect = + [UIBezierPath bezierPathWithRoundedRect:CGRectMake(0, 0, radius * 2, radius * 2) + cornerRadius:radius]; [roundedRect setUsesEvenOddFillRule:YES]; - [roundedRect appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, radius * 2 + 1)]]; + [roundedRect + appendPath:[UIBezierPath bezierPathWithRect:CGRectMake(-1, -1, radius * 2 + 1, + radius * 2 + 1)]]; [backgroundColor setFill]; [roundedRect fill]; }); @@ -1571,7 +1776,7 @@ - (void)updateCornerRoundingWithType:(ASCornerRoundingType)newRoundingType ASPerformBlockOnMainThread(^{ ASDisplayNodeAssertMainThread(); - + if (oldRoundingType != newRoundingType || oldCornerRadius != newCornerRadius || oldMaskedCorners != newMaskedCorners) { if (oldRoundingType == ASCornerRoundingTypeDefaultSlowCALayer) { if (newRoundingType == ASCornerRoundingTypePrecomposited) { @@ -1646,7 +1851,9 @@ - (void)recursivelySetDisplaySuspended:(BOOL)flag } // TODO: Replace this with ASDisplayNodePerformBlockOnEveryNode or a variant with a condition / test block. -static void _recursivelySetDisplaySuspended(ASDisplayNode *node, CALayer *layer, BOOL flag) +// We use __strong instead of __unsafe_retained for the mtuable arguments because otherwise the ARC dance +// on the accessors will fail and the objects will end up in the autorelease pool. +static void _recursivelySetDisplaySuspended(__strong ASDisplayNode *node, __strong CALayer *layer, BOOL flag) { // If there is no layer, but node whose its view is loaded, then we can traverse down its layer hierarchy. Otherwise we must stick to the node hierarchy to avoid loading views prematurely. Note that for nodes that haven't loaded their views, they can't possibly have subviews/sublayers, so we don't need to traverse the layer hierarchy for them. if (!layer && node && node.nodeLoaded) { @@ -1857,12 +2064,10 @@ - (CGPoint)convertPoint:(CGPoint)point fromNode:(ASDisplayNode *)node return point; } } - - // Get root node of the accessible node hierarchy, if node not specified - node = node ? : ASDisplayNodeUltimateParentOfNode(self); // Calculate transform to map points between coordinate spaces - CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); + // Get root node of the accessible node hierarchy, if node not specified + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node ?: ASDisplayNodeUltimateParentOfNode(self), self); CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); @@ -1883,11 +2088,9 @@ - (CGPoint)convertPoint:(CGPoint)point toNode:(ASDisplayNode *)node } } - // Get root node of the accessible node hierarchy, if node not specified - node = node ? : ASDisplayNodeUltimateParentOfNode(self); - // Calculate transform to map points between coordinate spaces - CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + // Get root node of the accessible node hierarchy, if node not specified + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node ?: ASDisplayNodeUltimateParentOfNode(self)); CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); @@ -1907,12 +2110,9 @@ - (CGRect)convertRect:(CGRect)rect fromNode:(ASDisplayNode *)node return rect; } } - - // Get root node of the accessible node hierarchy, if node not specified - node = node ? : ASDisplayNodeUltimateParentOfNode(self); // Calculate transform to map points between coordinate spaces - CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node, self); + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(node ?: ASDisplayNodeUltimateParentOfNode(self), self); CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); @@ -1932,12 +2132,11 @@ - (CGRect)convertRect:(CGRect)rect toNode:(ASDisplayNode *)node return rect; } } - - // Get root node of the accessible node hierarchy, if node not specified - node = node ? : ASDisplayNodeUltimateParentOfNode(self); - // Calculate transform to map points between coordinate spaces - CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget(self, node); + // Calculate transform to map points between coordinate spaces. + // Use root if not specified. + CATransform3D nodeTransform = _calculateTransformFromReferenceToTarget( + self, node ?: ASDisplayNodeUltimateParentOfNode(self)); CGAffineTransform flattenedTransform = CATransform3DGetAffineTransform(nodeTransform); ASDisplayNodeAssertTrue(CATransform3DIsAffine(nodeTransform)); @@ -2059,12 +2258,7 @@ - (void)_setSupernode:(ASDisplayNode *)newSupernode - (NSArray *)subnodes { MutexLocker l(__instanceLock__); - if (_cachedSubnodes == nil) { - _cachedSubnodes = [_subnodes copy]; - } else { - ASDisplayNodeAssert(ASObjectIsEqual(_cachedSubnodes, _subnodes), @"Expected _subnodes and _cachedSubnodes to have the same contents."); - } - return _cachedSubnodes ?: @[]; + return [_subnodes copy]; } /* @@ -2082,7 +2276,7 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnod ASDisplayNodeAssertThreadAffinity(self); // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 DISABLED_ASAssertUnlocked(__instanceLock__); - + as_log_verbose(ASNodeLog(), "Insert subnode %@ at index %zd of %@ and remove subnode %@", subnode, subnodeIndex, self, oldSubnode); if (subnode == nil || subnode == self) { @@ -2106,6 +2300,11 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnod return; } + if (_nodeContext != subnode->_nodeContext) { + ASDisplayNodeFailAssert(@"Cannot mix nodes from different contexts (super: %@, sub: %@)", ASObjectDescriptionMakeTiny(self), ASObjectDescriptionMakeTiny(subnode)); + return; + } + __instanceLock__.lock(); NSUInteger subnodesCount = _subnodes.count; __instanceLock__.unlock(); @@ -2129,7 +2328,6 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnod _subnodes = [[NSMutableArray alloc] init]; } [_subnodes insertObject:subnode atIndex:subnodeIndex]; - _cachedSubnodes = nil; __instanceLock__.unlock(); // This call will apply our .hierarchyState to the new subnode. @@ -2141,10 +2339,16 @@ - (void)_insertSubnode:(ASDisplayNode *)subnode atSubnodeIndex:(NSInteger)subnod if (isRasterized) { if (self.inHierarchy) { [subnode __enterHierarchy]; + if (ASInterfaceStateIncludesVisible(self.interfaceState)) { + [subnode invalidateFirstAccessibilityContainerOrNonLayerBackedNode]; + } } } else if (self.nodeLoaded) { // If not rasterizing, and node is loaded insert the subview/sublayer now. [self _insertSubnodeSubviewOrSublayer:subnode atIndex:sublayerIndex]; + if (ASInterfaceStateIncludesVisible(self.interfaceState)) { + [subnode invalidateFirstAccessibilityContainerOrNonLayerBackedNode]; + } } // Otherwise we will insert subview/sublayer when we get loaded ASDisplayNodeAssert(disableNotifications == shouldDisableNotificationsForMovingBetweenParents(oldParent, self), @"Invariant violated"); @@ -2385,7 +2589,7 @@ - (void)insertSubnode:(ASDisplayNode *)subnode atIndex:(NSInteger)idx ASDisplayNodeAssertThreadAffinity(self); // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 DISABLED_ASAssertUnlocked(__instanceLock__); - + if (subnode == nil) { ASDisplayNodeFailAssert(@"Cannot insert a nil subnode"); return; @@ -2423,7 +2627,7 @@ - (void)_removeSubnode:(ASDisplayNode *)subnode ASDisplayNodeAssertThreadAffinity(self); // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 DISABLED_ASAssertUnlocked(__instanceLock__); - + // Don't call self.supernode here because that will retain/autorelease the supernode. This method -_removeSupernode: is often called while tearing down a node hierarchy, and the supernode in question might be in the middle of its -dealloc. The supernode is never messaged, only compared by value, so this is safe. // The particular issue that triggers this edge case is when a node calls -removeFromSupernode on a subnode from within its own -dealloc method. if (!subnode || subnode.supernode != self) { @@ -2432,7 +2636,6 @@ - (void)_removeSubnode:(ASDisplayNode *)subnode __instanceLock__.lock(); [_subnodes removeObjectIdenticalTo:subnode]; - _cachedSubnodes = nil; __instanceLock__.unlock(); [subnode _setSupernode:nil]; @@ -2443,7 +2646,7 @@ - (void)removeFromSupernode ASDisplayNodeAssertThreadAffinity(self); // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 DISABLED_ASAssertUnlocked(__instanceLock__); - + __instanceLock__.lock(); __weak ASDisplayNode *supernode = _supernode; __weak UIView *view = _view; @@ -2458,7 +2661,7 @@ - (void)_removeFromSupernodeIfEqualTo:(ASDisplayNode *)supernode ASDisplayNodeAssertThreadAffinity(self); // TODO: Disabled due to PR: https://github.com/TextureGroup/Texture/pull/1204 DISABLED_ASAssertUnlocked(__instanceLock__); - + __instanceLock__.lock(); // Only remove if supernode is still the expected supernode @@ -2491,9 +2694,19 @@ - (void)_removeFromSupernode:(ASDisplayNode *)supernode view:(UIView *)view laye [view removeFromSuperview]; } else if (layer != nil) { [layer removeFromSuperlayer]; + if (ASInterfaceStateIncludesVisible(self.interfaceState)) { + [self invalidateFirstAccessibilityContainerOrNonLayerBackedNode]; + } } } +- (void)moveSubnode:(ASDisplayNode *)subnode toIndex:(NSInteger)index { + ASDisplayNodeAssert(subnode->_supernode == self, + @"Request to move subnode that is not in receiver."); + [_subnodes removeObjectIdenticalTo:subnode]; + [_subnodes insertObject:subnode atIndex:index]; +} + #pragma mark - Visibility API - (BOOL)__visibilityNotificationsDisabled @@ -2904,6 +3117,7 @@ - (ASInterfaceState)interfaceState - (void)setInterfaceState:(ASInterfaceState)newState { + ASDisplayNodeAssertMainThread(); if (!ASCATransactionQueueGet().enabled) { [self applyPendingInterfaceState:newState]; } else { @@ -2921,6 +3135,17 @@ - (ASInterfaceState)pendingInterfaceState return _pendingInterfaceState; } ++ (BOOL)shouldCoalesceInterfaceStateDuringTransaction +{ + return ASActivateExperimentalFeature(ASExperimentalCoalesceRootNodeInTransaction) && ASCATransactionQueueGet().enabled; +} + +- (void)recursivelyApplyPendingInterfaceState { + ASDisplayNodePerformBlockOnEveryNode(nil, self, NO, ^(ASDisplayNode *_Nonnull node) { + [node applyPendingInterfaceState:[node pendingInterfaceState]]; + }); +} + - (void)applyPendingInterfaceState:(ASInterfaceState)newPendingState { //This method is currently called on the main thread. The assert has been added here because all of the @@ -3329,6 +3554,25 @@ - (UIEdgeInsets)hitTestSlop return _hitTestSlop; } +- (BOOL)isRTL { + ASDisplayNodeAssertMainThread(); +#if YOGA + ASLockScopeSelf(); + return _style.direction == YGDirectionRTL; +#endif + if (AS_AVAILABLE_IOS_TVOS(10, 10)) { + return _primitiveTraitCollection.layoutDirection == + UITraitEnvironmentLayoutDirectionRightToLeft; + } else { + return [UIView userInterfaceLayoutDirectionForSemanticContentAttribute: + _view.semanticContentAttribute] == UIUserInterfaceLayoutDirectionRightToLeft; + } +} + +- (UIEdgeInsets)adjustedHitTestSlopFor:(UIEdgeInsets)slop { + return [self isRTL] ? UIEdgeInsetsMake(slop.top, slop.right, slop.bottom, slop.left) : slop; +} + - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { ASDisplayNodeAssertMainThread(); @@ -3337,10 +3581,15 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event // Safer to use UIView's -pointInside:withEvent: if we can. return [_view pointInside:point withEvent:event]; } else { - return CGRectContainsPoint(UIEdgeInsetsInsetRect(self.bounds, slop), point); + return CGRectContainsPoint( + UIEdgeInsetsInsetRect(self.bounds, [self adjustedHitTestSlopFor:slop]), point); } } +- (id)debugQuickLookObject +{ + return _view ?: _layer; +} #pragma mark - Pending View State @@ -3393,19 +3642,22 @@ - (void)_locked_applyPendingViewState [_pendingViewState applyToLayer:_layer]; } else { BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), _flags.layerBacked); - [_pendingViewState applyToView:_view withSpecialPropertiesHandling:specialPropertiesHandling]; + [_pendingViewState applyToView:_view + withSpecialPropertiesHandling:specialPropertiesHandling + node:self]; } + // TODO: Root-cause why the range-managed check is not clearing pending states, or just always nil it. // _ASPendingState objects can add up very quickly when adding // many nodes. This is especially an issue in large collection views // and table views. This needs to be weighed against the cost of // reallocing a _ASPendingState. So in range managed nodes we // delete the pending state, otherwise we just clear it. - if (ASHierarchyStateIncludesRangeManaged(_hierarchyState)) { + // if (ASHierarchyStateIncludesRangeManaged(_hierarchyState)) { _pendingViewState = nil; - } else { - [_pendingViewState clearChanges]; - } + // } else { + // [_pendingViewState clearChanges]; + // } } // This method has proved helpful in a few rare scenarios, similar to a category extension on UIView, but assumes knowledge of _ASDisplayView. @@ -3453,7 +3705,7 @@ - (ASDisplayNodePerformanceMeasurementOptions)measurementOptions - (ASDisplayNodePerformanceMeasurements)performanceMeasurements { MutexLocker l(__instanceLock__); - ASDisplayNodePerformanceMeasurements measurements = { .layoutSpecNumberOfPasses = -1, .layoutSpecTotalTime = NAN, .layoutComputationNumberOfPasses = -1, .layoutComputationTotalTime = NAN }; + ASDisplayNodePerformanceMeasurements measurements = { .layoutComputationNumberOfPasses = -1, .layoutComputationTotalTime = NAN, .layoutSpecNumberOfPasses = -1, .layoutSpecTotalTime = NAN }; if (_measurementOptions & ASDisplayNodePerformanceMeasurementOptionLayoutSpec) { measurements.layoutSpecNumberOfPasses = _layoutSpecNumberOfPasses; measurements.layoutSpecTotalTime = _layoutSpecTotalTime; @@ -3555,6 +3807,10 @@ - (UIAccessibilityTraits)defaultAccessibilityTraits } } + if (!UIEdgeInsetsEqualToEdgeInsets(self.hitTestSlop, UIEdgeInsetsZero)) { + [result addObject:@{ @"hitTestSlop" : [NSValue valueWithUIEdgeInsets:self.hitTestSlop] }]; + } + if (_view != nil) { [result addObject:@{ @"alpha" : @(_view.alpha) }]; [result addObject:@{ @"frame" : [NSValue valueWithCGRect:_view.frame] }]; @@ -3735,6 +3991,8 @@ - (ASDisplayNode *)asyncdisplaykit_node @implementation CALayer (ASDisplayNodeInternal) +@dynamic as_retainedDelegate; + - (void)setAsyncdisplaykit_node:(ASDisplayNode *)node { ASWeakProxy *weakProxy = [ASWeakProxy weakProxyWithTarget:node]; @@ -3787,3 +4045,5 @@ - (void)addSubnode:(ASDisplayNode *)subnode } @end + +AS_ASSUME_NORETAIN_END diff --git a/Source/ASDisplayNodeExtras.h b/Source/ASDisplayNodeExtras.h index 03f294043..852a82132 100644 --- a/Source/ASDisplayNodeExtras.h +++ b/Source/ASDisplayNodeExtras.h @@ -146,7 +146,8 @@ ASDK_EXTERN ASDisplayNode * _Nullable ASDisplayNodeFindFirstSupernode(ASDisplayN /** Given a display node, traverses up the layer tree hierarchy, returning the first display node of kind class. */ -ASDK_EXTERN __kindof ASDisplayNode * _Nullable ASDisplayNodeFindFirstSupernodeOfClass(ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT ASDISPLAYNODE_DEPRECATED_MSG("Use the `supernodeOfClass:includingSelf:` method instead."); +ASDK_EXTERN __kindof ASDisplayNode *_Nullable ASDisplayNodeFindFirstSupernodeOfClass( + ASDisplayNode *start, Class c) AS_WARN_UNUSED_RESULT; /** * Given a layer, find the window it lives in, if any. @@ -207,6 +208,11 @@ ASDK_EXTERN UIColor *ASDisplayNodeDefaultTintColor(void) AS_WARN_UNUSED_RESULT; ASDK_EXTERN void ASDisplayNodeDisableHierarchyNotifications(ASDisplayNode *node); ASDK_EXTERN void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node); +/** + Returns the CAAction to apply to this node. + */ +ASDK_EXTERN idASDisplayNodeActionForLayer(CALayer *layer, NSString *event, ASDisplayNode *node, id uikitAction); + // Not to be called directly. ASDK_EXTERN void _ASSetDebugNames(Class owningClass, NSString *names, ASDisplayNode * _Nullable object, ...); diff --git a/Source/ASDisplayNodeExtras.mm b/Source/ASDisplayNodeExtras.mm index caf68ca8e..2e8372049 100644 --- a/Source/ASDisplayNodeExtras.mm +++ b/Source/ASDisplayNodeExtras.mm @@ -335,3 +335,19 @@ void ASDisplayNodeEnableHierarchyNotifications(ASDisplayNode *node) { [node __decrementVisibilityNotificationsDisabled]; } + +#pragma mark - Layer Actions + +idASDisplayNodeActionForLayer(CALayer *layer, NSString *event, ASDisplayNode *node, id uikitAction) +{ + // Even though the UIKit action will take precedence, we still unconditionally forward to the node so that it can + // track events like kCAOnOrderIn. + id nodeAction = [(id) node actionForLayer:layer forKey:event]; + + // If UIKit specifies an action, that takes precedence. That's an animation block so it's explicit. + if (uikitAction && uikitAction != (id)kCFNull) { + return uikitAction; + } + + return nodeAction; +} diff --git a/Source/ASEditableTextNode.mm b/Source/ASEditableTextNode.mm index 60be21f98..6af82ff65 100644 --- a/Source/ASEditableTextNode.mm +++ b/Source/ASEditableTextNode.mm @@ -474,8 +474,10 @@ - (void)_layoutTextView // When we resize to fit (above) the prior layout becomes invalid. For whatever reason, UITextView doesn't invalidate its layout when its frame changes on its own, so we have to do so ourselves. [_textKitComponents.layoutManager invalidateLayoutForCharacterRange:NSMakeRange(0, [_textKitComponents.textStorage length]) actualCharacterRange:NULL]; - // When you type beyond UITextView's bounds it scrolls you down a line. We need to remain at the top. - [_textKitComponents.textView setContentOffset:CGPointZero animated:NO]; + // When text does not fill bounds, pin to content offset 0. UIKit will introduce jitter otherwise. + if (_textKitComponents.textView.contentSize.height <= self.bounds.size.height) { + [_textKitComponents.textView setContentOffset:CGPointZero animated:NO]; + } } #pragma mark - Keyboard diff --git a/Source/ASExperimentalFeatures.h b/Source/ASExperimentalFeatures.h index a8b655ce9..c82dd279e 100644 --- a/Source/ASExperimentalFeatures.h +++ b/Source/ASExperimentalFeatures.h @@ -18,19 +18,37 @@ NS_ASSUME_NONNULL_BEGIN typedef NS_OPTIONS(NSUInteger, ASExperimentalFeatures) { // If AS_ENABLE_TEXTNODE=0 or TextNode2 subspec is used this setting is a no op and ASTextNode2 // will be used in all cases - ASExperimentalTextNode = 1 << 0, // exp_text_node - ASExperimentalInterfaceStateCoalescing = 1 << 1, // exp_interface_state_coalesce - ASExperimentalLayerDefaults = 1 << 2, // exp_infer_layer_defaults - ASExperimentalCollectionTeardown = 1 << 3, // exp_collection_teardown - ASExperimentalFramesetterCache = 1 << 4, // exp_framesetter_cache - ASExperimentalSkipClearData = 1 << 5, // exp_skip_clear_data - ASExperimentalDidEnterPreloadSkipASMLayout = 1 << 6, // exp_did_enter_preload_skip_asm_layout - ASExperimentalDispatchApply = 1 << 7, // exp_dispatch_apply - ASExperimentalDrawingGlobal = 1 << 8, // exp_drawing_global - ASExperimentalOptimizeDataControllerPipeline = 1 << 9, // exp_optimize_data_controller_pipeline - ASExperimentalDisableGlobalTextkitLock = 1 << 10, // exp_disable_global_textkit_lock - ASExperimentalMainThreadOnlyDataController = 1 << 11, // exp_main_thread_only_data_controller - ASExperimentalRangeUpdateOnChangesetUpdate = 1 << 12, // exp_range_update_on_changeset_update + ASExperimentalTextNode = 1 << 0, // exp_text_node + ASExperimentalInterfaceStateCoalescing = 1 << 1, // exp_interface_state_coalesce + ASExperimentalUnfairLock = 1 << 2, // exp_unfair_lock + ASExperimentalLayerDefaults = 1 << 3, // exp_infer_layer_defaults + ASExperimentalCollectionTeardown = 1 << 4, // exp_collection_teardown + ASExperimentalFramesetterCache = 1 << 5, // exp_framesetter_cache + ASExperimentalSkipClearData = 1 << 6, // exp_skip_clear_data + ASExperimentalDidEnterPreloadSkipASMLayout = 1 << 7, // exp_did_enter_preload_skip_asm_layout + ASExperimentalDispatchApply = 1 << 8, // exp_dispatch_apply + ASExperimentalOOMBackgroundDeallocDisable = 1 << 9, // exp_oom_bg_dealloc_disable + ASExperimentalRemoveTextKitInitialisingLock = 1 << 10, // exp_remove_textkit_initialising_lock + ASExperimentalDrawingGlobal = 1 << 11, // exp_drawing_global + ASExperimentalDeferredNodeRelease = 1 << 12, // exp_deferred_node_release + ASExperimentalFasterWebPDecoding = 1 << 13, // exp_faster_webp_decoding + ASExperimentalFasterWebPGraphicsImageRenderer = 1 + << 14, // exp_faster_webp_graphics_image_renderer + ASExperimentalAnimatedWebPNoCache = 1 << 15, // exp_animated_webp_no_cache + ASExperimentalDeallocElementMapOffMain = 1 << 16, // exp_dealloc_element_map_off_main + ASExperimentalUnifiedYogaTree = 1 << 17, // exp_unified_yoga_tree + ASExperimentalCoalesceRootNodeInTransaction = 1 << 18, // exp_coalesce_root_node_in_transaction + ASExperimentalUseNonThreadLocalArrayWhenApplyingLayout = 1 << 19, + // exp_use_non_tls_array + ASExperimentalOptimizeDataControllerPipeline = 1 << 20, // exp_optimize_data_controller_pipeline + ASExperimentalTraitCollectionDidChangeWithPreviousCollection = 1 << 21, // exp_trait_collection_did_change_with_previous_collection + ASExperimentalFillTemplateImagesWithTintColor = 1 << 22, // exp_fill_template_images_with_tint_color + ASExperimentalDoNotCacheAccessibilityElements = 1 << 23, // exp_do_not_cache_accessibility_elements + ASExperimentalDisableGlobalTextkitLock = 1 << 24, // exp_disable_global_textkit_lock + ASExperimentalMainThreadOnlyDataController = 1 << 25, // exp_main_thread_only_data_controller, + ASExperimentalEnableNodeIsHiddenFromAcessibility = 1 << 26, // exp_enable_node_is_hidden_from_accessibility + ASExperimentalEnableAcessibilityElementsReturnNil = 1 << 27, // exp_enable_accessibility_elements_return_nil + ASExperimentalRangeUpdateOnChangesetUpdate = 1 << 28, // exp_range_update_on_changeset_update ASExperimentalFeatureAll = 0xFFFFFFFF }; @@ -40,4 +58,10 @@ ASDK_EXTERN NSArray *ASExperimentalFeaturesGetNames(ASExperimentalFe /// Convert name array -> flags. ASDK_EXTERN ASExperimentalFeatures ASExperimentalFeaturesFromArray(NSArray *array); +// This is trying to merge non-rangeManaged with rangeManaged, so both range-managed and standalone nodes wait before firing their exit-visibility handlers, as UIViewController transitions now do rehosting at both start & end of animation. +// Enable this will mitigate interface updating state when coalescing disabled. +// TODO(wsdwsd0829): Rework enabling code to ensure that interface state behavior is not altered when ASCATransactionQueue is disabled. +// TODO Make this a real experiment flag +#define ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR 1 + NS_ASSUME_NONNULL_END diff --git a/Source/ASExperimentalFeatures.mm b/Source/ASExperimentalFeatures.mm index 6113dc405..119d593ac 100644 --- a/Source/ASExperimentalFeatures.mm +++ b/Source/ASExperimentalFeatures.mm @@ -12,19 +12,37 @@ NSArray *ASExperimentalFeaturesGetNames(ASExperimentalFeatures flags) { - NSArray *allNames = ASCreateOnce((@[@"exp_text_node", - @"exp_interface_state_coalesce", - @"exp_infer_layer_defaults", - @"exp_collection_teardown", - @"exp_framesetter_cache", - @"exp_skip_clear_data", - @"exp_did_enter_preload_skip_asm_layout", - @"exp_dispatch_apply", - @"exp_drawing_global", - @"exp_optimize_data_controller_pipeline", - @"exp_disable_global_textkit_lock", - @"exp_main_thread_only_data_controller", - @"exp_range_update_on_changeset_update"])); + NSArray *allNames = ASCreateOnce((@[ + @"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_dispatch_apply", + @"exp_oom_bg_dealloc_disable", + @"exp_remove_textkit_initialising_lock", + @"exp_drawing_global", + @"exp_deferred_node_release", + @"exp_faster_webp_decoding", + @"exp_faster_webp_graphics_image_renderer", + @"exp_animated_webp_no_cache", + @"exp_dealloc_element_map_off_main", + @"exp_unified_yoga_tree", + @"exp_coalesce_root_node_in_transaction", + @"exp_use_non_tls_array", + @"exp_optimize_data_controller_pipeline", + @"exp_trait_collection_did_change_with_previous_collection", + @"exp_fill_template_images_with_tint_color", + @"exp_do_not_cache_accessibility_elements", + @"exp_disable_global_textkit_lock", + @"exp_main_thread_only_data_controller", + @"exp_enable_node_is_hidden_from_accessibility", + @"exp_enable_accessibility_elements_return_nil", + @"exp_range_update_on_changeset_update", + ])); if (flags == ASExperimentalFeatureAll) { return allNames; } diff --git a/Source/ASImageNode+AnimatedImage.mm b/Source/ASImageNode+AnimatedImage.mm index 9716f8529..03208378c 100644 --- a/Source/ASImageNode+AnimatedImage.mm +++ b/Source/ASImageNode+AnimatedImage.mm @@ -79,6 +79,11 @@ - (void)_locked_setAnimatedImage:(id )animatedImage // not fire e.g. while scrolling down CFRunLoopPerformBlock(CFRunLoopGetCurrent(), kCFRunLoopCommonModes, ^(void) { [self animatedImageSet:animatedImage previousAnimatedImage:previousAnimatedImage]; + + // Animated image can take while to dealloc, do it off the main queue + if (previousAnimatedImage != nil && ASActivateExperimentalFeature(ASExperimentalOOMBackgroundDeallocDisable) == NO) { + ASPerformBackgroundDeallocation(&previousAnimatedImage); + } }); // Don't need to wakeup the runloop as the current is already running // CFRunLoopWakeUp(runLoop); // Should not be necessary @@ -88,7 +93,7 @@ - (void)animatedImageSet:(id )newAnimatedImage previous { // Subclass hook should not be called with the lock held DISABLED_ASAssertUnlocked(__instanceLock__); - + // Subclasses may override } @@ -171,8 +176,14 @@ - (void)setAnimatedImageRunLoopMode:(NSString *)runLoopMode } if (_displayLink != nil) { - [_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode]; - [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode]; + if (ASActivateExperimentalFeature(ASExperimentalAnimatedWebPNoCache)) { + NSAssert(_displayLinkRunloop, @"No Runloop for _displayLink"); + [_displayLink removeFromRunLoop:_displayLinkRunloop forMode:_animatedImageRunLoopMode]; + [_displayLink addToRunLoop:_displayLinkRunloop forMode:runLoopMode]; + } else { + [_displayLink removeFromRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode]; + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:runLoopMode]; + } } _animatedImageRunLoopMode = [runLoopMode copy]; } @@ -226,7 +237,7 @@ - (void)_locked_startAnimating if (!ASInterfaceStateIncludesVisible(self.interfaceState)) { return; } - + if (_imageNodeFlags.animatedImagePaused) { return; } @@ -244,11 +255,39 @@ - (void)_locked_startAnimating _playHead = 0; _displayLink = [CADisplayLink displayLinkWithTarget:[ASWeakProxy weakProxyWithTarget:self] selector:@selector(displayLinkFired:)]; _lastSuccessfulFrameIndex = NSUIntegerMax; - - [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode]; + if (ASActivateExperimentalFeature(ASExperimentalAnimatedWebPNoCache)) { + _displayLinkThread = [[NSThread alloc] initWithTarget:self + selector:@selector(threadHandler:) + object:nil]; + [_displayLinkThread start]; + } else { + [_displayLink addToRunLoop:[NSRunLoop mainRunLoop] forMode:_animatedImageRunLoopMode]; + } } else { _displayLink.paused = NO; } + [self animatedImageTransitionToState:ASAnimatedImageStart]; +} + +- (void)threadHandler:(id)object { + _displayLinkRunloop = [NSRunLoop currentRunLoop]; + [_displayLink addToRunLoop:_displayLinkRunloop forMode:NSDefaultRunLoopMode]; + CFRunLoopRun(); +} + +- (void)animatedImageTransitionToState:(ASAnimatedImageState)toState { + ASLockScopeSelf(); + auto oldState = _animationState; + if (oldState == toState && toState != ASAnimatedImageEndLoop) { + return; + } + _animationState = toState; + { + ASUnlockScope(self) + // We may still be locked due to recursive locks from outside. At the time of this writing + // no call paths have that, and if we ever encounter it we need to revisit this code. + [self animatedImageDidEnterState:toState fromState:oldState]; + } } - (void)stopAnimating @@ -273,6 +312,11 @@ - (void)_locked_stopAnimating self.lastDisplayLinkFire = 0; [_animatedImage clearAnimatedImageCache]; + { + ASUnlockScope(self); + DISABLED_ASAssertUnlocked(__instanceLock__); + [self animatedImageTransitionToState:ASAnimatedImageStopped]; + } } #pragma mark - ASDisplayNode @@ -322,7 +366,9 @@ - (void)didExitDisplayState - (void)displayLinkFired:(CADisplayLink *)displayLink { - ASDisplayNodeAssertMainThread(); + if (!ASActivateExperimentalFeature(ASExperimentalAnimatedWebPNoCache)) { + ASDisplayNodeAssertMainThread(); + } CFTimeInterval timeBetweenLastFire; if (self.lastDisplayLinkFire == 0) { @@ -337,36 +383,46 @@ - (void)displayLinkFired:(CADisplayLink *)displayLink _playHead += timeBetweenLastFire; while (_playHead > self.animatedImage.totalDuration) { - // Set playhead to zero to keep from showing different frames on different playthroughs + // Set playhead to zero to keep from showing different frames on different playthroughs + DISABLED_ASAssertUnlocked(__instanceLock__); + [self animatedImageTransitionToState:ASAnimatedImageEndLoop]; _playHead = 0; _playedLoops++; } if (self.animatedImage.loopCount > 0 && _playedLoops >= self.animatedImage.loopCount) { - [self stopAnimating]; + ASPerformBlockOnMainThread(^{ + [self stopAnimating]; + }); return; } - + NSUInteger frameIndex = [self frameIndexAtPlayHeadPosition:_playHead]; if (frameIndex == _lastSuccessfulFrameIndex) { return; } - CGImageRef frameImage = [self.animatedImage imageAtIndex:frameIndex]; - + + id frameImage = (__bridge id)[self.animatedImage imageAtIndex:frameIndex]; if (frameImage == nil) { //Pause the display link until we get a file ready notification displayLink.paused = YES; self.lastDisplayLinkFire = 0; } else { - self.contents = (__bridge id)frameImage; - _lastSuccessfulFrameIndex = frameIndex; - [self displayDidFinish]; + ASPerformBlockOnMainThread(^{ + [self displayWillStartAsynchronously:NO]; + self.contents = frameImage; + _lastSuccessfulFrameIndex = frameIndex; + [self displayDidFinish]; + }); } } - (NSUInteger)frameIndexAtPlayHeadPosition:(CFTimeInterval)playHead { - ASDisplayNodeAssertMainThread(); + if (!ASActivateExperimentalFeature(ASExperimentalAnimatedWebPNoCache)) { + ASDisplayNodeAssertMainThread(); + } + NSUInteger frameIndex = 0; for (NSUInteger durationIndex = 0; durationIndex < self.animatedImage.frameCount; durationIndex++) { playHead -= [self.animatedImage durationAtIndex:durationIndex]; diff --git a/Source/ASImageNode.h b/Source/ASImageNode.h index dcdb11069..685ed05d8 100644 --- a/Source/ASImageNode.h +++ b/Source/ASImageNode.h @@ -23,11 +23,21 @@ NS_ASSUME_NONNULL_BEGIN */ typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image, ASPrimitiveTraitCollection traitCollection); +typedef NS_ENUM(NSUInteger, ASAnimatedImageState) { + ASAnimatedImageUnknown, + ASAnimatedImageStart, + ASAnimatedImageEndLoop, + ASAnimatedImageStopped +}; + /** * @abstract Draws images. * @discussion Supports cropping, tinting, and arbitrary image modification blocks. */ -@interface ASImageNode : ASControlNode +@interface ASImageNode : ASControlNode { + @package + ASAnimatedImageState _animationState; +} /** * @abstract The image to display. @@ -121,6 +131,17 @@ typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image, */ - (void)setNeedsDisplayWithCompletion:(nullable void (^)(BOOL canceled))displayCompletionBlock; +#if YOGA +/** + * @abstract Whether the image should be reflected across the y-axis when contained within a + * yoga tree using RTL layout direction (https://yogalayout.com/docs/layout-direction/). This + * value is usually set only once at the root based on device locale, not to be confused with + * flex direction (https://yogalayout.com/docs/flex-direction/) which can be flipped from node to + * node based on an author's intent. + */ +@property (assign) BOOL flipsForRightToLeftLayoutDirection; +#endif + #if TARGET_OS_TV /** * A bool to track if the current appearance of the node @@ -175,6 +196,22 @@ typedef UIImage * _Nullable (^asimagenode_modification_block_t)(UIImage *image, */ - (void)animatedImageSet:(nullable id )newAnimatedImage previousAnimatedImage:(nullable id )previousAnimatedImage ASDISPLAYNODE_REQUIRES_SUPER; +/** + * @abstract Method called each time when animated image finish looping. + */ +- (void)animatedImageDidEnterState:(ASAnimatedImageState)state + fromState:(ASAnimatedImageState)fromState; + +/** + * @abstract Method to start the animated image playback. + */ +- (void)startAnimating; + +/** + * @abstract Method to stop the animated image playback. + */ +- (void)stopAnimating; + @end @interface ASImageNode (Unavailable) diff --git a/Source/ASImageNode.mm b/Source/ASImageNode.mm index 95ac20143..b50b04b00 100644 --- a/Source/ASImageNode.mm +++ b/Source/ASImageNode.mm @@ -20,6 +20,8 @@ #import #import #import +#import + #import #import #import @@ -30,10 +32,12 @@ // TODO: It would be nice to remove this dependency; it's the only subclass using more than +FrameworkSubclasses.h #import +static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; + typedef void (^ASImageNodeDrawParametersBlock)(ASWeakMapEntry *entry); -@interface ASImageNodeDrawParameters : NSObject { -@package +@implementation ASImageNodeDrawParameters { + @package UIImage *_image; BOOL _opaque; CGRect _bounds; @@ -51,14 +55,16 @@ @interface ASImageNodeDrawParameters : NSObject { ASDisplayNodeContextModifier _didDisplayNodeContentWithRenderingContext; ASImageNodeDrawParametersBlock _didDrawBlock; ASPrimitiveTraitCollection _traitCollection; + UIUserInterfaceStyle _userInterfaceStyle API_AVAILABLE(tvos(10.0), ios(12.0)); + CGRect _drawRect; + CGRect _adjustedDrawRect; + UIEdgeInsets _paddings; + CGFloat _renderScale; + BOOL _isRTL; } @end -@implementation ASImageNodeDrawParameters - -@end - /** * Contains all data that is needed to generate the content bitmap. */ @@ -140,6 +146,20 @@ - (NSUInteger)hash @end +#if YOGA +static UIImage *_flipImage(UIImage *image) { + UIGraphicsBeginImageContextWithOptions(image.size, NO, image.scale); + CGAffineTransform tx = CGAffineTransformIdentity; + tx = CGAffineTransformTranslate(tx, image.size.width, 0); + tx = CGAffineTransformScale(tx, -1, 1); + CGContextConcatCTM(UIGraphicsGetCurrentContext(), tx); + [image drawAtPoint:CGPointZero blendMode:kCGBlendModeCopy alpha:1.0f]; + UIImage *flipped = UIGraphicsGetImageFromCurrentImageContext(); + UIGraphicsEndImageContext(); + return flipped; +} +#endif + @implementation ASImageNode { @private @@ -156,6 +176,10 @@ @implementation ASImageNode CGSize _forcedSize; //Defaults to CGSizeZero, indicating no forced size. CGRect _cropRect; // Defaults to CGRectMake(0.5, 0.5, 0, 0) CGRect _cropDisplayBounds; // Defaults to CGRectNull +#if YOGA + BOOL _flipsForRightToLeftLayoutDirection; + BOOL _imageFlippedForRightToLeftLayoutDirection; +#endif } @synthesize image = _image; @@ -238,13 +262,30 @@ - (void)setImage:(UIImage *)image [self _locked_setImage:image]; } +#if YOGA +- (void)_locked_setFlipsForRightToLeftLayoutDirection:(BOOL)flipsForRightToLeftLayoutDirection { + _flipsForRightToLeftLayoutDirection = flipsForRightToLeftLayoutDirection; +} +#endif + - (void)_locked_setImage:(UIImage *)image { DISABLED_ASAssertLocked(__instanceLock__); + UIImage *oldImage = _image; +#if YOGA + if (image != oldImage) { + _imageFlippedForRightToLeftLayoutDirection = NO; + } + BOOL flip = _flipsForRightToLeftLayoutDirection && + ([self yogaLayoutDirection] == UIUserInterfaceLayoutDirectionRightToLeft); + if (_imageFlippedForRightToLeftLayoutDirection != flip) { + image = _flipImage(image); + _imageFlippedForRightToLeftLayoutDirection = flip; + } +#endif if (ASObjectIsEqual(_image, image)) { return; } - _image = image; if (image != nil) { @@ -265,6 +306,16 @@ - (void)_locked_setImage:(UIImage *)image } else { self.contents = nil; } + + // Destruction of bigger images on the main thread can be expensive + // and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + CGSize oldImageSize = oldImage.size; + BOOL shouldReleaseImageOnBackgroundThread = oldImageSize.width > kMinReleaseImageOnBackgroundSize.width + || oldImageSize.height > kMinReleaseImageOnBackgroundSize.height; + if (shouldReleaseImageOnBackgroundThread && ASActivateExperimentalFeature(ASExperimentalOOMBackgroundDeallocDisable) == NO) { + ASPerformBackgroundDeallocation(&oldImage); + } } - (UIImage *)image @@ -285,55 +336,81 @@ - (void)setPlaceholderColor:(UIColor *)placeholderColor } } +#if YOGA +- (BOOL)flipsForRightToLeftLayoutDirection { + ASLockScopeSelf(); + return _flipsForRightToLeftLayoutDirection; +} + +- (void)setFlipsForRightToLeftLayoutDirection:(BOOL)flipsForRightToLeftLayoutDirection { + ASLockScopeSelf(); + [self _locked_setFlipsForRightToLeftLayoutDirection:flipsForRightToLeftLayoutDirection]; + [self _locked_setImage:_image]; +} +#endif + #pragma mark - Drawing - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { ASImageNodeDrawParameters *drawParameters = [[ASImageNodeDrawParameters alloc] init]; - - { - ASLockScopeSelf(); - UIImage *drawImage = _image; - if (AS_AVAILABLE_IOS_TVOS(13, 10)) { - if (_imageNodeFlags.regenerateFromImageAsset && drawImage != nil) { - _imageNodeFlags.regenerateFromImageAsset = NO; - UITraitCollection *tc = [UITraitCollection traitCollectionWithUserInterfaceStyle:_primitiveTraitCollection.userInterfaceStyle]; - UIImage *generatedImage = [drawImage.imageAsset imageWithTraitCollection:tc]; - if ( generatedImage != nil ) { - drawImage = generatedImage; + + { + ASLockScopeSelf(); + UIImage *drawImage = _image; + if (AS_AVAILABLE_IOS_TVOS(13, 10)) { + if (_imageNodeFlags.regenerateFromImageAsset && drawImage != nil) { + _imageNodeFlags.regenerateFromImageAsset = NO; + UITraitCollection *tc = [UITraitCollection traitCollectionWithUserInterfaceStyle:_primitiveTraitCollection.userInterfaceStyle]; + UIImage *generatedImage = [drawImage.imageAsset imageWithTraitCollection:tc]; + if ( generatedImage != nil ) { + drawImage = generatedImage; + } } } + + drawParameters->_image = drawImage; + drawParameters->_contentsScale = _contentsScaleForDisplay; + drawParameters->_cropEnabled = _imageNodeFlags.cropEnabled; + drawParameters->_forceUpscaling = _imageNodeFlags.forceUpscaling; + drawParameters->_forcedSize = _forcedSize; + drawParameters->_cropRect = _cropRect; + drawParameters->_cropDisplayBounds = _cropDisplayBounds; + drawParameters->_imageModificationBlock = _imageModificationBlock; + drawParameters->_willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; + drawParameters->_didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; + drawParameters->_traitCollection = _primitiveTraitCollection; + + // Hack for now to retain the weak entry that was created while this drawing happened + drawParameters->_didDrawBlock = ^(ASWeakMapEntry *entry){ + ASLockScopeSelf(); + self->_weakCacheEntry = entry; + }; + } + + // We need to unlock before we access the other accessor. + // Especially tintColor because it needs to walk up the view hierarchy + drawParameters->_bounds = [self threadSafeBounds]; + drawParameters->_opaque = self.opaque; + drawParameters->_backgroundColor = self.backgroundColor; + if (ASActivateExperimentalFeature(ASExperimentalFillTemplateImagesWithTintColor)) { + drawParameters->_tintColor = self.tintColor; + } + drawParameters->_contentMode = self.contentMode; + if (AS_AVAILABLE_IOS_TVOS(12, 10)) { + drawParameters->_userInterfaceStyle = self.primitiveTraitCollection.userInterfaceStyle; } - drawParameters->_image = drawImage; - drawParameters->_contentsScale = _contentsScaleForDisplay; - drawParameters->_cropEnabled = _imageNodeFlags.cropEnabled; - drawParameters->_forceUpscaling = _imageNodeFlags.forceUpscaling; - drawParameters->_forcedSize = _forcedSize; - drawParameters->_cropRect = _cropRect; - drawParameters->_cropDisplayBounds = _cropDisplayBounds; - drawParameters->_imageModificationBlock = _imageModificationBlock; - drawParameters->_willDisplayNodeContentWithRenderingContext = _willDisplayNodeContentWithRenderingContext; - drawParameters->_didDisplayNodeContentWithRenderingContext = _didDisplayNodeContentWithRenderingContext; - drawParameters->_traitCollection = _primitiveTraitCollection; - - // Hack for now to retain the weak entry that was created while this drawing happened - drawParameters->_didDrawBlock = ^(ASWeakMapEntry *entry){ - ASLockScopeSelf(); - self->_weakCacheEntry = entry; - }; - } - - // We need to unlock before we access the other accessor. - // Especially tintColor because it needs to walk up the view hierarchy - drawParameters->_bounds = [self threadSafeBounds]; - drawParameters->_opaque = self.opaque; - drawParameters->_backgroundColor = self.backgroundColor; - drawParameters->_contentMode = self.contentMode; - drawParameters->_tintColor = self.tintColor; + drawParameters->_adjustedDrawRect = CGRectZero; + drawParameters->_paddings = self.paddings; + #if YOGA + if (_flags.yoga) { + drawParameters->_isRTL = [ASDisplayNode isRTLForNode:self]; + } + #endif - return drawParameters; -} + return drawParameters; + } + (UIImage *)displayWithParameters:(id)parameter isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelled { @@ -355,6 +432,7 @@ + (UIImage *)displayWithParameters:(id)parameter isCancelled:(NS_NOESC CGFloat contentsScale = drawParameter->_contentsScale; CGRect cropDisplayBounds = drawParameter->_cropDisplayBounds; CGRect cropRect = drawParameter->_cropRect; + UIEdgeInsets paddings = drawParameter->_paddings; asimagenode_modification_block_t imageModificationBlock = drawParameter->_imageModificationBlock; ASDisplayNodeContextModifier willDisplayNodeContentWithRenderingContext = drawParameter->_willDisplayNodeContentWithRenderingContext; ASDisplayNodeContextModifier didDisplayNodeContentWithRenderingContext = drawParameter->_didDisplayNodeContentWithRenderingContext; @@ -362,6 +440,8 @@ + (UIImage *)displayWithParameters:(id)parameter isCancelled:(NS_NOESC BOOL hasValidCropBounds = cropEnabled && !CGRectIsEmpty(cropDisplayBounds); CGRect bounds = (hasValidCropBounds ? cropDisplayBounds : drawParameterBounds); + // Inset the bound to get the drawing rectangle, then later expand the bound back. + bounds = UIEdgeInsetsInsetRect(bounds, paddings); ASDisplayNodeAssert(contentsScale > 0, @"invalid contentsScale at display time"); @@ -380,7 +460,8 @@ + (UIImage *)displayWithParameters:(id)parameter isCancelled:(NS_NOESC BOOL contentModeSupported = contentMode == UIViewContentModeScaleAspectFill || contentMode == UIViewContentModeScaleAspectFit || - contentMode == UIViewContentModeCenter; + contentMode == UIViewContentModeCenter || + contentMode == UIViewContentModeScaleToFill; CGSize backingSize = CGSizeZero; CGRect imageDrawRect = CGRectZero; @@ -416,6 +497,25 @@ + (UIImage *)displayWithParameters:(id)parameter isCancelled:(NS_NOESC return nil; } + // Scale up/down padding values by paddingScale. + CGFloat paddingScale = backingSize.width / (bounds.size.width + paddings.left + paddings.right); + paddings.top *= paddingScale; + paddings.left *= paddingScale; + paddings.bottom *= paddingScale; + paddings.right *= paddingScale; + + //Expand backingSize back to include paddings. + backingSize.width += paddings.left + paddings.right; + backingSize.height += paddings.top + paddings.bottom; + + // Shift imageDrawRect for padding + imageDrawRect = CGRectOffset(imageDrawRect, paddings.left, paddings.top); + + drawParameter->_paddings = paddings; + drawParameter->_drawRect = imageDrawRect; + drawParameter->_renderScale = backingSize.width / bounds.size.width; + drawParameter->_tintColor = tintColor; + ASImageNodeContentsKey *contentsKey = [[ASImageNodeContentsKey alloc] init]; contentsKey.image = image; contentsKey.backingSize = backingSize; @@ -528,14 +628,39 @@ + (UIImage *)createContentsForkey:(ASImageNodeContentsKey *)key drawParameters:( BOOL canUseCopy = (contextIsClean || ASImageAlphaInfoIsOpaque(CGImageGetAlphaInfo(image.CGImage))); CGBlendMode blendMode = canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal; UIImageRenderingMode renderingMode = [image renderingMode]; - if (renderingMode == UIImageRenderingModeAlwaysTemplate && key.tintColor) { + if (renderingMode == UIImageRenderingModeAlwaysTemplate && key.tintColor && ASActivateExperimentalFeature(ASExperimentalFillTemplateImagesWithTintColor)) { [key.tintColor setFill]; } + BOOL contextIsClipped = false; + + if (ASImageNodeDrawParameters *imageDrawParams = + ASDynamicCast(drawParameters, ASImageNodeDrawParameters)) { + CGRect adjustectRect = imageDrawParams ? imageDrawParams.adjustedDrawRect : CGRectZero; + if (!CGRectEqualToRect(adjustectRect, CGRectZero)) { + key.imageDrawRect = adjustectRect; + } + + // Crop context if needed + if (!UIEdgeInsetsEqualToEdgeInsets(imageDrawParams->_paddings, UIEdgeInsetsZero)) { + CGContextSaveGState(context); + contextIsClipped = true; + CGRect backingRect = {CGPointZero, key.backingSize}; + // Shrink backingRect by padding and draw only inside backingRect + backingRect = UIEdgeInsetsInsetRect(backingRect, imageDrawParams->_paddings); + CGContextClipToRect(context, backingRect); + } + } @synchronized(image) { [image drawInRect:key.imageDrawRect blendMode:blendMode alpha:1]; } + // Reset context clipping + if (contextIsClipped) { + CGContextRestoreGState(context); + contextIsClipped = false; + } + if (context && key.didDisplayNodeContentWithRenderingContext) { key.didDisplayNodeContentWithRenderingContext(context, drawParameters); } @@ -629,8 +754,9 @@ - (void)_setNeedsDisplayOnTemplatedImages - (void)tintColorDidChange { [super tintColorDidChange]; - - [self _setNeedsDisplayOnTemplatedImages]; + if (ASActivateExperimentalFeature(ASExperimentalFillTemplateImagesWithTintColor)) { + [self _setNeedsDisplayOnTemplatedImages]; + } } #pragma mark Interface State @@ -754,6 +880,13 @@ - (void)setImageModificationBlock:(asimagenode_modification_block_t)imageModific _imageModificationBlock = imageModificationBlock; } +#pragma mark - Animated Image + +- (void)animatedImageDidEnterState:(ASAnimatedImageState)state + fromState:(ASAnimatedImageState)fromState { + // Subclass hook. +} + #pragma mark - Debug - (void)layout diff --git a/Source/ASLocking.h b/Source/ASLocking.h index 3e284dc26..8f4bf781e 100644 --- a/Source/ASLocking.h +++ b/Source/ASLocking.h @@ -7,14 +7,9 @@ // #import -#import - -#import NS_ASSUME_NONNULL_BEGIN -#define kLockSetCapacity 32 - /** * An extension of NSLocking that supports -tryLock. */ @@ -26,133 +21,111 @@ NS_ASSUME_NONNULL_BEGIN @end /** - * A set of locks acquired during ASLockSequence. + * These Foundation classes already implement -tryLock. */ -typedef struct { - unsigned count; - CFTypeRef _Nullable locks[kLockSetCapacity]; -} ASLockSet; +@interface NSLock (ASLocking) +@end -/** - * Declare a lock set that is automatically unlocked at the end of scope. - * - * We use this instead of a scope-locking macro because we want to be able - * to step through the lock sequence block in the debugger. - */ -#define ASScopedLockSet __unused ASLockSet __attribute__((cleanup(ASUnlockSet))) +@interface NSRecursiveLock (ASLocking) +@end -/** - * A block that attempts to add a lock to a lock sequence. - * Such a block is provided to the caller of ASLockSequence. - * - * Returns whether the lock was added. You should return - * NO from your lock sequence body if it returns NO. - * - * For instance, you might write `return addLock(l1) && addLock(l2)`. - * - * @param lock The lock to attempt to add. - * @return YES if the lock was added, NO otherwise. - */ -typedef BOOL(^ASAddLockBlock)(id lock); +@interface NSConditionLock (ASLocking) +@end -/** - * A block that attempts to lock multiple locks in sequence. - * Such a block is provided by the caller of ASLockSequence. - * - * The block may be run multiple times, if not all locks are immediately - * available. Therefore the block should be idempotent. - * - * The block should attempt to invoke addLock multiple times with - * different locks. It should return NO as soon as any addLock - * operation fails. - * - * For instance, you might write `return addLock(l1) && addLock(l2)`. - * - * @param addLock A block you can call to attempt to add a lock. - * @return YES if all locks were added, NO otherwise. - */ -typedef BOOL(^ASLockSequenceBlock)(NS_NOESCAPE ASAddLockBlock addLock); +NS_ASSUME_NONNULL_END -/** - * Unlock and release all of the locks in this lock set. - */ -NS_INLINE void ASUnlockSet(ASLockSet *lockSet) { - for (unsigned i = 0; i < lockSet->count; i++) { - CFTypeRef lock = lockSet->locks[i]; - [(__bridge id)lock unlock]; - CFRelease(lock); - } -} +#ifdef __cplusplus + +#include +#include +#include +#include +#include + +#import + +NS_ASSUME_NONNULL_BEGIN + +namespace AS { /** - * Take multiple locks "simultaneously," avoiding deadlocks - * caused by lock ordering. - * - * The block you provide should attempt to take a series of locks, - * using the provided `addLock` block. As soon as any addLock fails, - * you should return NO. + * A helper class for locking multiple mutexes safely in sequence. Usage is like this: * - * For example: - * ASLockSequence(^(ASAddLockBlock addLock) ^{ - * return addLock(l0) && addLock(l1); - * }); - * - * Note: This function doesn't protect from lock ordering deadlocks if - * one of the locks is already locked (recursive.) Only locks taken - * inside this function are guaranteed not to cause a deadlock. + * LockSet locks; + * while (locks.empty()) { + * if (!locks.TryAdd(my_object_1, my_object_1->__instanceLock__)) continue; + * // my_object_1 is now locked. If it failed to add, `locks` is now empty. + * // Additionally, my_object_1 is retained by the set. + * if (!locks.TryAdd(my_object_2, my_object_2->__instanceLock__)) continue; + * // my_object_2 is now also locked and retained. + * } + * // Once we're here, `locks` contains locks and retains on both objects. Either + * // wait for the object to go out of scope, or explicitly call Clear() to release + * // everything. */ -NS_INLINE ASLockSet ASLockSequence(NS_NOESCAPE ASLockSequenceBlock body) -{ - __block ASLockSet locks = (ASLockSet){0, {}}; - BOOL (^addLock)(id) = ^(id obj) { - - // nil lock = ignore. - if (!obj) { - return YES; - } - - // If they go over capacity, assert and return YES. - // If we return NO, they will enter an infinite loop. - if (locks.count == kLockSetCapacity) { - ASDisplayNodeCFailAssert(@"Locking more than %d locks at once is not supported.", kLockSetCapacity); - return YES; - } - - if ([obj tryLock]) { - locks.locks[locks.count++] = (__bridge_retained CFTypeRef)obj; - return YES; - } - return NO; - }; - +class LockSet { +public: + // Note that destruction order matters here. The UniqueLock must be destroyed before the owner is released. + typedef std::pair OwnedLock; + + LockSet() = default; + ~LockSet() = default; + + // Move is allowed. + LockSet(LockSet &&locks) = default; + LockSet &operator=(LockSet &&locks) = default; + + bool empty() const { return inline_locks_count_ == 0; } + /** - * Repeatedly try running their block, passing in our `addLock` - * until it succeeds. If it fails, unlock all and yield the thread - * to reduce spinning. + * Attempt to add a lock on the given mutex to the set. Returns whether + * a lock was successfully added. If this function returns false, the lock set + * is reset. Your while loop should `continue` if this function returns false. + * + * On success, the owner will also be retained by the lock set, to avoid issues with + * objects being deallocated while locked. + * + * Linter note: We suppress linting the function signature because it takes a + * non-const reference which is unusual, but in keeping with the pattern from + * the constructor for std::unique_lock, which is analogous to this function. */ - while (true) { - if (body(addLock)) { - // Success - return locks; + // NOLINTNEXTLINE + bool TryAdd(__unsafe_unretained id owner, AS::Mutex &mutex) { + std::unique_lock l(mutex, std::try_to_lock); + if (!l.owns_lock()) { + clear(); + std::this_thread::yield(); + return false; + } + + if (inline_locks_count_ < inline_locks_.size()) { + inline_locks_[inline_locks_count_++] = OwnedLock(owner, std::move(l)); } else { - ASUnlockSet(&locks); - locks.count = 0; - sched_yield(); + overflow_locks_.push_back(OwnedLock(owner, std::move(l))); } + return true; } -} -/** - * These Foundation classes already implement -tryLock. - */ - -@interface NSLock (ASLocking) -@end + /** + * Unlock all locks in the set, release their owners. After this call the set will be empty. + */ + void clear() { + for (auto it = inline_locks_.begin(), end = it + inline_locks_count_; it != end; ++it) { + *it = OwnedLock(); + } + inline_locks_count_ = 0; + overflow_locks_.clear(); + } -@interface NSRecursiveLock (ASLocking) -@end +private: + static constexpr size_t kInlineLocksCapacity = 16; + size_t inline_locks_count_ = 0; + std::array inline_locks_; + std::vector overflow_locks_; +}; -@interface NSConditionLock (ASLocking) -@end +} // namespace AS NS_ASSUME_NONNULL_END + +#endif // __cplusplus diff --git a/Source/ASMainThreadDeallocation.h b/Source/ASMainThreadDeallocation.h index 391b6bb69..3d8d0da48 100644 --- a/Source/ASMainThreadDeallocation.h +++ b/Source/ASMainThreadDeallocation.h @@ -10,16 +10,23 @@ NS_ASSUME_NONNULL_BEGIN -@interface NSObject (ASMainThreadIvarTeardown) +@interface NSObject (ASMainThreadDeallocation) /** - * Call this from -dealloc to schedule this instance's - * ivars for main thread deallocation as needed. + * Use this method to indicate that an object associated with an instance + * will need to be deallocated on the main thread at some point in the future. + * Call handleMainThreadDeallocationIfNeeded from the instance's dealloc to + * initiate this future deallocation. * - * This method includes a check for whether it's on the main thread, - * and it will do nothing in that case. + * @param obj The associated object that requires main thread deallocation + */ +- (void)scheduleMainThreadDeallocationForObject:(id)obj; + +/** + * Call this from -dealloc to schedule this instance's + * ivars and other objects for main thread deallocation if needed. */ -- (void)scheduleIvarsForMainThreadDeallocation; +- (void)handleMainThreadDeallocationIfNeeded; @end diff --git a/Source/ASMainThreadDeallocation.mm b/Source/ASMainThreadDeallocation.mm index 8b732a5c9..8264ad42b 100644 --- a/Source/ASMainThreadDeallocation.mm +++ b/Source/ASMainThreadDeallocation.mm @@ -15,14 +15,52 @@ @implementation NSObject (ASMainThreadIvarTeardown) -- (void)scheduleIvarsForMainThreadDeallocation +- (void)scheduleMainThreadDeallocationForObject:(id)obj +{ + NSPointerArray *objsForMainThreadDeallocation = + objc_getAssociatedObject(self, @selector(scheduleMainThreadDeallocationForObject:)); + + if (!objsForMainThreadDeallocation) { + objsForMainThreadDeallocation = + [[NSPointerArray alloc] initWithOptions:NSPointerFunctionsStrongMemory]; + } + + [objsForMainThreadDeallocation addPointer:(void *)obj]; + objc_setAssociatedObject(self, @selector(scheduleMainThreadDeallocationForObject:), + objsForMainThreadDeallocation, OBJC_ASSOCIATION_RETAIN); +} + +- (void)handleMainThreadDeallocationIfNeeded +{ + [self deallocateIvarsOnMainThreadIfNeeded]; + [self deallocateScheduledObjectsOnMainThreadIfNeeded]; +} + +- (void)deallocateScheduledObjectsOnMainThreadIfNeeded { if (ASDisplayNodeThreadIsMain()) { return; } - + + id obj = objc_getAssociatedObject(self, @selector(scheduleMainThreadDeallocationForObject:)); + + if (!obj) { + return; + } + + objc_setAssociatedObject(self, @selector(scheduleMainThreadDeallocationForObject:), nil, + OBJC_ASSOCIATION_RETAIN); + ASPerformMainThreadDeallocation(&obj); +} + +- (void)deallocateIvarsOnMainThreadIfNeeded +{ + if (ASDisplayNodeThreadIsMain()) { + return; + } + NSValue *ivarsObj = [[self class] _ivarsThatMayNeedMainDeallocation]; - + // Unwrap the ivar array unsigned int count = 0; // Will be unused if assertions are disabled. diff --git a/Source/ASMultiplexImageNode.mm b/Source/ASMultiplexImageNode.mm index abacdfacb..ba66cef2c 100644 --- a/Source/ASMultiplexImageNode.mm +++ b/Source/ASMultiplexImageNode.mm @@ -39,6 +39,8 @@ static NSString *const kAssetsLibraryURLScheme = @"assets-library"; #endif +static const CGSize kMinReleaseImageOnBackgroundSize = {20.0, 20.0}; + /** @abstract Signature for the block to be performed after an image has loaded. @param image The image that was loaded, or nil if no image was loaded. @@ -532,7 +534,17 @@ - (void)_updateProgressImageBlockOnDownloaderIfNeeded - (void)_clearImage { + // Destruction of bigger images on the main thread can be expensive + // and can take some time, so we dispatch onto a bg queue to + // actually dealloc. + UIImage *image = self.image; + CGSize imageSize = image.size; + BOOL shouldReleaseImageOnBackgroundThread = imageSize.width > kMinReleaseImageOnBackgroundSize.width || + imageSize.height > kMinReleaseImageOnBackgroundSize.height; [self _setImage:nil]; + if (shouldReleaseImageOnBackgroundThread && ASActivateExperimentalFeature(ASExperimentalOOMBackgroundDeallocDisable) == NO) { + ASPerformBackgroundDeallocation(&image); + } } #pragma mark - diff --git a/Source/ASNetworkImageNode.h b/Source/ASNetworkImageNode.h index bc931cbee..57ce323e5 100644 --- a/Source/ASNetworkImageNode.h +++ b/Source/ASNetworkImageNode.h @@ -14,6 +14,17 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASNetworkImageNodeDelegate, ASImageCacheProtocol, ASImageDownloaderProtocol; @class ASNetworkImageLoadInfo; +/** + * Returns whether an image should be rendered upon completing download even + * without knowing its expected downloadIdentifier. + */ +BOOL ASGetEnableImageDownloadSynchronization(void); + +/** + * If set to YES, an image may be rendered upon completing download even without + * knowing its expected downloadIdentifier. + */ +void ASSetEnableImageDownloadSynchronization(BOOL enable); /** * ASNetworkImageNode is a simple image node that can download and display an image from the network, with support for a diff --git a/Source/ASNetworkImageNode.mm b/Source/ASNetworkImageNode.mm index 8509ce39b..702ce9f80 100644 --- a/Source/ASNetworkImageNode.mm +++ b/Source/ASNetworkImageNode.mm @@ -19,11 +19,20 @@ #import #import #import +#import #if AS_PIN_REMOTE_IMAGE #import #endif +static BOOL kEnableImageDownloadSynchronization = NO; +BOOL ASGetEnableImageDownloadSynchronization(void) { + return kEnableImageDownloadSynchronization; +} +void ASSetEnableImageDownloadSynchronization(BOOL enable) { + kEnableImageDownloadSynchronization = enable; +} + @interface ASNetworkImageNode () { // Only access any of these while locked. @@ -338,6 +347,14 @@ - (void)setDelegate:(id)delegate return _delegate; } +- (void)setFlipsForRightToLeftLayoutDirection:(BOOL)flipsForRightToLeftLayoutDirection { + ASLockScopeSelf(); +#if YOGA + [self _locked_setFlipsForRightToLeftLayoutDirection:flipsForRightToLeftLayoutDirection]; +#endif // YOGA + [self _locked__setImage:self.image]; +} + - (void)setShouldRenderProgressImages:(BOOL)shouldRenderProgressImages { if (ASLockedSelfCompareAssign(_networkImageNodeFlags.shouldRenderProgressImages, shouldRenderProgressImages)) { @@ -620,6 +637,7 @@ - (void)_downloadImageWithCompletion:(void (^)(id ima { ASPerformBlockOnBackgroundThread(^{ NSURL *url; + NSInteger cacheSentinel; id downloadIdentifier; BOOL cancelAndReattempt = NO; ASInterfaceState interfaceState; @@ -630,6 +648,7 @@ - (void)_downloadImageWithCompletion:(void (^)(id ima { ASLockScopeSelf(); url = self->_URL; + cacheSentinel = self->_cacheSentinel; interfaceState = self->_interfaceState; } @@ -682,6 +701,10 @@ - (void)_downloadImageWithCompletion:(void (^)(id ima { ASLockScopeSelf(); + if (ASGetEnableImageDownloadSynchronization() && cacheSentinel != _cacheSentinel) { + // The original request has been cancelled/fulfilled already or a new request has started. + return; + } if (ASObjectIsEqual(self->_URL, url)) { // The download we kicked off is correct, no need to do any more work. self->_downloadIdentifier = downloadIdentifier; @@ -800,9 +823,19 @@ - (void)_lazilyLoadImageIfNecessary // Grab the lock for the rest of the block ASLockScope(strongSelf); - //Getting a result back for a different download identifier, download must not have been successfully canceled - if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && downloadIdentifier != nil) { - return; + // Getting a result back for a different download identifier, download must not have been + // successfully canceled + if (ASObjectIsEqual(strongSelf->_downloadIdentifier, downloadIdentifier) == NO && + downloadIdentifier != nil) { + // Note that in some rare cases it's possible that _downloadIdentifier has not yet been + // set because of a race condition between the completion block being called and + // _downloadIdentifier being assigned. If flag is set, only return early for + // "unsuccessful cancellations" where a new download has commenced, hence + // _downloadIdentifier having a different non-nil value + if (!ASGetEnableImageDownloadSynchronization() || + strongSelf->_downloadIdentifier != nil) { + return; + } } //No longer in preload range, no point in setting the results (they won't be cleared in exit preload range) diff --git a/Source/ASNodeContext.h b/Source/ASNodeContext.h new file mode 100644 index 000000000..5f59590d9 --- /dev/null +++ b/Source/ASNodeContext.h @@ -0,0 +1,65 @@ +// +// ASNodeContext.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 + +#import + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@class ASNodeContext; + +/** + * Push the given context, which will apply to any nodes initialized until the corresponding `pop`. + * + * Generally each cell in a collection or table, and the root of an ASViewController, will be a context. + */ +ASDK_EXTERN void ASNodeContextPush(ASNodeContext *context); + +/** + * Get the current top context, if there is one. + */ +ASDK_EXTERN ASNodeContext *_Nullable ASNodeContextGet(void); + +/** + * Pop the current context, matching a previous call to ASNodeContextPush. + */ +ASDK_EXTERN void ASNodeContextPop(void); + +typedef NS_OPTIONS(unsigned char, ASNodeContextOptions) { + ASNodeContextNone = 0, + ASNodeContextUseYoga = 1 << 0, +}; + +/** + * A node context is an object that is shared by, and uniquely identifies, an "embedding" of nodes. For example, + * each cell in a collection view has its own context. Each ASViewController's node has its own context. You can + * also explicitly establish a context for a node tree in another context. + * + * Node contexts store the mutex that is shared by all member nodes for synchronization. Operations such as addSubnode: + * will lock the context's mutex for the duration of the work. + * + * Nodes may not be moved from one context to another. For instance, you may not detach a subnode of a cell node, + * and reattach it to a subtree of another cell node in the same or another collection view. + */ +AS_SUBCLASSING_RESTRICTED +@interface ASNodeContext : NSObject + +- (instancetype)initWithOptions:(ASNodeContextOptions)options NS_DESIGNATED_INITIALIZER; + +/** Default value is None. */ +@property(readonly) ASNodeContextOptions options; + +@end + +#ifdef __cplusplus +/** Get the mutex for the context. Ensure that the context does not die while you are using it. */ +AS::RecursiveMutex *ASNodeContextGetMutex(ASNodeContext *ctx); +#endif + +NS_ASSUME_NONNULL_END diff --git a/Source/ASNodeContext.mm b/Source/ASNodeContext.mm new file mode 100644 index 000000000..f4176f265 --- /dev/null +++ b/Source/ASNodeContext.mm @@ -0,0 +1,96 @@ +// +// ASNodeContext.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 + +#import + +#import + +#import + +#if AS_TLS_AVAILABLE + +AS_ASSUME_NORETAIN_BEGIN + +static thread_local std::stack gContexts; + +void ASNodeContextPush(ASNodeContext *context) { + gContexts.push(context); +} + +ASNodeContext *ASNodeContextGet() { + return gContexts.empty() ? nil : gContexts.top(); +} + +void ASNodeContextPop() { + if (AS_PREDICT_FALSE(gContexts.empty())) { + ASDisplayNodeCFailAssert(@"Attempt to pop empty context stack."); + return; + } + gContexts.pop(); +} + +#else // !AS_TLS_AVAILABLE + +// Only on 32-bit simulator. Performance expendable. + +// Points to a NSMutableArray. +static constexpr NSString *ASNodeContextStackKey = @"org.TextureGroup.Texture.nodeContexts"; + +void ASNodeContextPush(unowned ASNodeContext *context) { + unowned NSMutableDictionary *td = NSThread.currentThread.threadDictionary; + unowned NSMutableArray *stack = td[ASNodeContextStackKey]; + if (!stack) { + td[ASNodeContextStackKey] = [[NSMutableArray alloc] initWithObjects:context, nil]; + } else { + [stack addObject:context]; + } +} + +ASNodeContext *ASNodeContextGet() { + return [NSThread.currentThread.threadDictionary[ASNodeContextStackKey] lastObject]; +} + +void ASNodeContextPop() { + if (ASActivateExperimentalFeature(ASExperimentalNodeContext)) { + [NSThread.currentThread.threadDictionary[ASNodeContextStackKey] removeLastObject]; + } +} + +#endif // !AS_TLS_AVAILABLE + +@implementation ASNodeContext { + ASNodeContextOptions _options; +} + +- (instancetype)initWithOptions:(ASNodeContextOptions)options { + if (self = [super init]) { + _mutex.SetDebugNameWithObject(self); + _options = options; + } + return self; +} + +- (instancetype)init { + return [self initWithOptions:ASNodeContextNone]; +} + +- (ASNodeContextOptions)options { + return _options; +} + +@end + +AS::RecursiveMutex *ASNodeContextGetMutex(ASNodeContext *ctx) { + if (AS_PREDICT_FALSE(!ctx)) { + ASDisplayNodeCFailAssert(@"Passing nil context not allowed!"); + static auto dummy_mutex = new AS::RecursiveMutex; + return dummy_mutex; + } + return &ctx->_mutex; +} + +AS_ASSUME_NORETAIN_END diff --git a/Source/ASNodeController+Beta.h b/Source/ASNodeController+Beta.h index 6501a114a..132c3870b 100644 --- a/Source/ASNodeController+Beta.h +++ b/Source/ASNodeController+Beta.h @@ -10,6 +10,8 @@ #import #import // for ASInterfaceState protocol +@class ASNodeContext; + /* ASNodeController is currently beta and open to change in the future */ @interface ASNodeController<__covariant DisplayNodeType : ASDisplayNode *> : NSObject @@ -19,6 +21,8 @@ // Until an ASNodeController can be provided in place of an ASCellNode, some apps may prefer to have // nodes keep their controllers alive (and a weak reference from controller to node) +@property (readonly) ASNodeContext *nodeContext; + @property (nonatomic) BOOL shouldInvertStrongReference; - (void)loadNode; @@ -47,11 +51,18 @@ - (void)didEnterHierarchy ASDISPLAYNODE_REQUIRES_SUPER; - (void)didExitHierarchy ASDISPLAYNODE_REQUIRES_SUPER; +#ifdef __cplusplus /** - * @discussion Attempts (via ASLockSequence, a backing-off spinlock similar to + * @discussion Attempts (via AS::LockSet, a backing-off multi-lock similar to * std::lock()) to lock both the node and its ASNodeController, if one exists. */ -- (ASLockSet)lockPair; +- (AS::LockSet)lockPair; +#endif + +/** + * Hook into Xcode's Quick Look feature. Returns the view/layer if loaded. + */ +- (id)debugQuickLookObject; @end diff --git a/Source/ASNodeController+Beta.mm b/Source/ASNodeController+Beta.mm index 0245a7d4d..9875cfb8f 100644 --- a/Source/ASNodeController+Beta.mm +++ b/Source/ASNodeController+Beta.mm @@ -9,14 +9,21 @@ #import #import +#import #define _node (_shouldInvertStrongReference ? _weakNode : _strongNode) +AS_ASSUME_NORETAIN_BEGIN + @implementation ASNodeController + +- (instancetype)init { - ASDisplayNode *_strongNode; - __weak ASDisplayNode *_weakNode; - AS::RecursiveMutex __instanceLock__; + if (self = [super init]) { + _nodeContext = ASNodeContextGet(); + __instanceLock__.Configure(_nodeContext ? &_nodeContext->_mutex : nullptr) ; + } + return self; } - (void)loadNode @@ -29,7 +36,12 @@ - (ASDisplayNode *)node { ASLockScopeSelf(); if (_node == nil) { + ASNodeContextPush(_nodeContext); [self loadNode]; + ASNodeContextPop(); + ASDisplayNodeAssert(_node == nil || _nodeContext == [_node nodeContext], + @"Controller and node must share context.\n%@\nvs\n%@", _nodeContext, + [_node nodeContext]); } return _node; } @@ -64,6 +76,13 @@ - (void)setShouldInvertStrongReference:(BOOL)shouldInvertStrongReference { ASLockScopeSelf(); if (_shouldInvertStrongReference != shouldInvertStrongReference) { + // At this point the node needs to be loaded and a strong reference needs to be captured + // if shouldInvertStrongReference will be set to YES, otherwise the node will be deallocated + // immediately in loadNode that is called within the -[ASNodeController node] acccessor. + ASDisplayNodeAssert(!shouldInvertStrongReference || (shouldInvertStrongReference && _node != nil), + @"Node needs to be loaded and captured outside before setting " + @"shouldInvertStrongReference to YES"); + // Because the BOOL controls which ivar we access, get the node before toggling. ASDisplayNode *node = _node; _shouldInvertStrongReference = shouldInvertStrongReference; @@ -93,18 +112,24 @@ - (void)hierarchyDisplayDidFinish {} - (void)didEnterHierarchy {} - (void)didExitHierarchy {} -- (ASLockSet)lockPair { - ASLockSet lockSet = ASLockSequence(^BOOL(ASAddLockBlock addLock) { - if (!addLock(_node)) { - return NO; - } - if (!addLock(self)) { - return NO; +- (AS::LockSet)lockPair { + AS::LockSet locks; + while (locks.empty()) { + // If we have a node context, we just need to lock it. Nothing else. + if (_nodeContext) { + if (!locks.TryAdd(_nodeContext, _nodeContext->_mutex)) continue; + break; } - return YES; - }); + if (_node && !locks.TryAdd(_node, _node->__instanceLock__)) continue; + if (!locks.TryAdd(self, __instanceLock__)) continue; + } - return lockSet; + return locks; +} + +- (id)debugQuickLookObject +{ + return [_node debugQuickLookObject]; } #pragma mark NSLocking @@ -134,3 +159,5 @@ - (ASNodeController *)nodeController } @end + +AS_ASSUME_NORETAIN_END diff --git a/Source/ASRunLoopQueue.h b/Source/ASRunLoopQueue.h index c66a5dbfb..4545b7f8e 100644 --- a/Source/ASRunLoopQueue.h +++ b/Source/ASRunLoopQueue.h @@ -15,6 +15,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASCATransactionQueueObserving - (void)prepareForCATransactionCommit; +- (BOOL)shouldCoalesceInterfaceStateDuringTransaction; @end @interface ASAbstractRunLoopQueue : NSObject @@ -32,7 +33,8 @@ AS_SUBCLASSING_RESTRICTED * * @discussion You may pass @c nil for the handler if you simply want the objects to * be retained at enqueue time, and released during the run loop step. This is useful - * for creating a "main deallocation queue". + * for creating a "main deallocation queue", as @c ASDeallocQueue creates its own + * worker thread with its own run loop. */ - (instancetype)initWithRunLoop:(CFRunLoopRef)runloop retainObjects:(BOOL)retainsObjects @@ -58,6 +60,8 @@ AS_SUBCLASSING_RESTRICTED */ AS_SUBCLASSING_RESTRICTED @interface ASCATransactionQueue : ASAbstractRunLoopQueue +@property (class, readonly) ASCATransactionQueue *sharedQueue; ++ (ASCATransactionQueue *)sharedQueue NS_RETURNS_RETAINED; @property (readonly) BOOL isEmpty; @@ -65,6 +69,9 @@ AS_SUBCLASSING_RESTRICTED - (void)enqueue:(id)object; +// Return YES if within a CATransation commit. ++ (BOOL)inTransactionCommit; + @end extern ASCATransactionQueue *_ASSharedCATransactionQueue; @@ -77,4 +84,14 @@ NS_INLINE ASCATransactionQueue *ASCATransactionQueueGet(void) { return _ASSharedCATransactionQueue; } +@interface ASDeallocQueue : NSObject + ++ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED; + +- (void)drain; + +- (void)releaseObjectInBackground:(id __strong _Nullable * _Nonnull)objectPtr; + +@end + NS_ASSUME_NONNULL_END diff --git a/Source/ASRunLoopQueue.mm b/Source/ASRunLoopQueue.mm index 8c917b07a..658fa5c31 100644 --- a/Source/ASRunLoopQueue.mm +++ b/Source/ASRunLoopQueue.mm @@ -27,6 +27,67 @@ static void runLoopSourceCallback(void *info) { #endif } +#pragma mark - ASDeallocQueue + +@implementation ASDeallocQueue { + std::vector _queue; + AS::Mutex _lock; +} + ++ (ASDeallocQueue *)sharedDeallocationQueue NS_RETURNS_RETAINED +{ + static ASDeallocQueue *deallocQueue = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + deallocQueue = [[ASDeallocQueue alloc] init]; + }); + return deallocQueue; +} + +- (void)dealloc +{ + ASDisplayNodeFailAssert(@"Singleton should not dealloc."); +} + +- (void)releaseObjectInBackground:(id _Nullable __strong *)objectPtr +{ + NSParameterAssert(objectPtr != NULL); + + // Cast to CFType so we can manipulate retain count manually. + const auto cfPtr = (CFTypeRef *)(void *)objectPtr; + if (!cfPtr || !*cfPtr) { + return; + } + + _lock.lock(); + const auto isFirstEntry = _queue.empty(); + // Push the pointer into our queue and clear their pointer. + // This "steals" the +1 from ARC and nils their pointer so they can't + // access or release the object. + _queue.push_back(*cfPtr); + *cfPtr = NULL; + _lock.unlock(); + + if (isFirstEntry) { + dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.100 * NSEC_PER_SEC)), dispatch_get_global_queue(QOS_CLASS_DEFAULT, 0), ^{ + [self drain]; + }); + } +} + +- (void)drain +{ + _lock.lock(); + const auto q = std::move(_queue); + _lock.unlock(); + for (CFTypeRef ref : q) { + // NOTE: Could check that retain count is 1 and retry later if not. + CFRelease(ref); + } +} + +@end + @implementation ASAbstractRunLoopQueue - (instancetype)init @@ -134,10 +195,8 @@ - (void)checkRunLoop - (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. + // This vector is populated regardless of whether we have an execution block, + // because it's important that any objects we release are released while unlocked. std::vector itemsToProcess; BOOL isQueueDrained = NO; @@ -156,31 +215,22 @@ - (void)processQueue 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. + * For each item in the next batch, if it's non-nil then dequeue it nil it out of source array. */ - 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. - */ - unowned id ptr = (__bridge id)[_internalQueue pointerAtIndex:i]; - if (ptr != nil) { - foundItemCount++; - if (hasExecutionBlock) { - itemsToProcess.push_back(ptr); - } + itemsToProcess.reserve(maxCountToProcess); + for (NSInteger i = 0; i < internalQueueCount && itemsToProcess.size() < maxCountToProcess; i++) { + // Note: If this is a weak NSPointerArray, the object will end up in the autorelease pool. + // There is no way around this – it is fate. + if (id o = (__bridge id)[_internalQueue pointerAtIndex:i]) { + // std::move avoids retain/release. + itemsToProcess.push_back(std::move(o)); [_internalQueue replacePointerAtIndex:i withPointer:NULL]; } } - if (foundItemCount == 0) { + if (itemsToProcess.empty()) { // 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. + // is never marked as needsCompaction, 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]; @@ -190,16 +240,15 @@ - (void)processQueue if (_internalQueue.count == 0) { isQueueDrained = YES; } - } + } // end of lock - // itemsToProcess will be empty if _queueConsumer == nil so no need to check again. const auto count = itemsToProcess.size(); - if (count > 0) { + if (_queueConsumer && count > 0) { as_activity_scope_verbose(as_activity_create("Process run loop queue batch", _rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); - const auto itemsEnd = itemsToProcess.cend(); - for (auto iterator = itemsToProcess.begin(); iterator < itemsEnd; iterator++) { - unowned id value = *iterator; - _queueConsumer(value, isQueueDrained && iterator == itemsEnd - 1); + // Use const-ref because this is a __strong id. + for (const auto &value : itemsToProcess) { + bool isLast = isQueueDrained && &value == &itemsToProcess.back(); + _queueConsumer(value, isLast); as_log_verbose(ASDisplayLog(), "processed %@", value); } if (count > 1) { @@ -213,6 +262,8 @@ - (void)processQueue CFRunLoopWakeUp(_runLoop); } + // Clear before ending signpost so that the releases are part of the interval. + itemsToProcess.clear(); ASSignpostEnd(RunLoopQueueBatch, self, "count: %d", (int)count); } @@ -260,6 +311,7 @@ - (BOOL)isEmpty @interface ASCATransactionQueue () { CFRunLoopSourceRef _runLoopSource; CFRunLoopObserverRef _preTransactionObserver; + CFRunLoopObserverRef _postTransactionObserver; // Current buffer for new entries, only accessed from within its mutex. std::vector> _internalQueue; @@ -279,6 +331,9 @@ @interface ASCATransactionQueue () { #if ASRunLoopQueueLoggingEnabled NSTimer *_runloopQueueLoggingTimer; #endif + // We must handle re-entrant transactions. It is perfectly legal, and it does occur, for the + // run loop to be drained from inside of the transaction, causing another observer pair to fire. + int _transactionDepth; } @end @@ -287,7 +342,11 @@ @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; +static int const kASCATransactionQueuePreOrder = 1000000; + +// CoreAnimation commit order is 2000000, the goal of this is to process immediately after +// but after the run loop sleeps. +static int const kASCATransactionQueuePostOrder = 2000001; ASCATransactionQueue *_ASSharedCATransactionQueue; dispatch_once_t _ASSharedCATransactionQueueOnceToken; @@ -315,13 +374,21 @@ - (instancetype)init // Self is guaranteed to outlive the observer. Without the high cost of a weak pointer, // unowned(__unsafe_unretained) allows us to avoid flagging the memory cycle detector. unowned __typeof__(self) weakSelf = self; - _preTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASASCATransactionQueueOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + _preTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASCATransactionQueuePreOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + weakSelf->_transactionDepth++; while (!weakSelf->_internalQueue.empty()) { [weakSelf processQueue]; } }); - CFRunLoopAddObserver(CFRunLoopGetMain(), _preTransactionObserver, kCFRunLoopCommonModes); + _postTransactionObserver = CFRunLoopObserverCreateWithHandler(NULL, kCFRunLoopBeforeWaiting, true, kASCATransactionQueuePostOrder, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) { + ASDisplayNodeCAssert(weakSelf->_transactionDepth > 0, @"Expected to have been in transaction."); + weakSelf->_transactionDepth--; + while (!weakSelf->_internalQueue.empty()) { + [weakSelf processQueue]; + } + }); + CFRunLoopAddObserver(CFRunLoopGetMain(), _postTransactionObserver, 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 @@ -344,17 +411,14 @@ - (instancetype)init - (void)dealloc { ASDisplayNodeAssertMainThread(); + CFRunLoopRemoveSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes); + CFRunLoopObserverInvalidate(_preTransactionObserver); + CFRunLoopObserverInvalidate(_postTransactionObserver); CFRelease(_internalQueueHashSet); - CFRunLoopRemoveSource(CFRunLoopGetMain(), _runLoopSource, kCFRunLoopCommonModes); CFRelease(_runLoopSource); - _runLoopSource = nil; - - if (CFRunLoopObserverIsValid(_preTransactionObserver)) { - CFRunLoopObserverInvalidate(_preTransactionObserver); - } CFRelease(_preTransactionObserver); - _preTransactionObserver = nil; + CFRelease(_postTransactionObserver); } #if ASRunLoopQueueLoggingEnabled @@ -393,13 +457,25 @@ - (void)processQueue ASSignpostEnd(RunLoopQueueBatch, self, "count: %d", (int)count); } ++ (BOOL)inTransactionCommit { + // Note that although the _transactionDepth++ happens within runloop observer of + // kASCATransactionQueuePreOrder, it's unlikely anything would happen after this runloop observer + // and before the CATransaction commit. + return [ASCATransactionQueue sharedQueue]->_transactionDepth > 0; +} + - (void)enqueue:(id)object { + ASDisplayNodeAssertMainThread(); if (!object) { return; } - if (!self.enabled) { + // If we are already in the transaction (say, in a layout method) we need to update now so that + // any changes join the transaction. + if (!self.enabled || + (_transactionDepth > 0 && + !ASActivateExperimentalFeature(ASExperimentalCoalesceRootNodeInTransaction))) { [object prepareForCATransactionCommit]; return; } @@ -427,4 +503,8 @@ - (BOOL)isEnabled return ASActivateExperimentalFeature(ASExperimentalInterfaceStateCoalescing); } ++ (ASCATransactionQueue *)sharedQueue +{ + return ASCATransactionQueueGet(); +} @end diff --git a/Source/ASScrollNode.h b/Source/ASScrollNode.h index 1137e89ed..87037c1c0 100644 --- a/Source/ASScrollNode.h +++ b/Source/ASScrollNode.h @@ -42,6 +42,10 @@ NS_ASSUME_NONNULL_BEGIN * Horizontal: The constrainedSize is interpreted as having unbounded .width (CGFLOAT_MAX), ... * Vertical & Horizontal: the constrainedSize is interpreted as unbounded in both directions. * @default ASScrollDirectionVerticalDirections + * + * @note This property is not consulted in Yoga2. The overflow mode for ASScrollNode defaults to + * "scroll" and you can use min-width=100% and max-width=100% to force the node to match its + * parent's width for example. */ @property ASScrollDirection scrollableDirections; diff --git a/Source/ASScrollNode.mm b/Source/ASScrollNode.mm index bb9c826d8..3e2d80380 100644 --- a/Source/ASScrollNode.mm +++ b/Source/ASScrollNode.mm @@ -59,6 +59,11 @@ - (NSArray *)accessibilityElements return [self.asyncdisplaykit_node accessibilityElements]; } +- (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { + id uikitAction = [super actionForLayer:layer forKey:event]; + return ASDisplayNodeActionForLayer(layer, event, self.scrollNode, uikitAction); +} + @end @implementation ASScrollNode @@ -81,7 +86,7 @@ - (ASLayout *)calculateLayoutThatFits:(ASSizeRange)constrainedSize restrictedToSize:(ASLayoutElementSize)size relativeToParentSize:(CGSize)parentSize { - ASScopedLockSelfOrToRoot(); + ASLockScopeSelf(); ASSizeRange contentConstrainedSize = constrainedSize; if (ASScrollDirectionContainsVerticalDirection(_scrollableDirections)) { @@ -150,6 +155,13 @@ - (void)layout } } +#if YOGA +- (void)enableYoga { + [super enableYoga]; + self.style.overflow = YGOverflowScroll; +} +#endif + - (BOOL)automaticallyManagesContentSize { ASLockScopeSelf(); diff --git a/Source/ASTableView.mm b/Source/ASTableView.mm index 5434f3b99..f940833fb 100644 --- a/Source/ASTableView.mm +++ b/Source/ASTableView.mm @@ -374,6 +374,12 @@ - (void)dealloc [self setAsyncDelegate:nil]; [self setAsyncDataSource:nil]; } + + // Data controller & range controller may own a ton of nodes, let's deallocate those off-main + if (ASActivateExperimentalFeature(ASExperimentalOOMBackgroundDeallocDisable) == NO) { + ASPerformBackgroundDeallocation(&_dataController); + ASPerformBackgroundDeallocation(&_rangeController); + } } #pragma mark - @@ -934,9 +940,9 @@ - (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPa if (element != nil) { ASCellNode *node = element.node; ASDisplayNodeAssertNotNil(node, @"Node must not be nil!"); - height = [node layoutThatFits:element.constrainedSize].size.height; + height = [node measure:element.constrainedSize].height; } - + #if TARGET_OS_IOS /** * Weirdly enough, Apple expects the return value here to _include_ the height @@ -1262,15 +1268,18 @@ - (void)scrollViewDidScroll:(UIScrollView *)scrollView [super scrollViewDidScroll:scrollView]; return; } + ASInterfaceState interfaceState = [self interfaceStateForRangeController:_rangeController]; if (ASInterfaceStateIncludesVisible(interfaceState)) { [self _checkForBatchFetching]; - } + } + for (_ASTableViewCell *tableCell in _cellsForVisibilityUpdates) { [[tableCell node] cellNodeVisibilityEvent:ASCellNodeVisibilityEventVisibleRectChanged inScrollView:scrollView withCellFrame:tableCell.frame]; } + if (_asyncDelegateFlags.scrollViewDidScroll) { [_asyncDelegate scrollViewDidScroll:scrollView]; } @@ -1536,6 +1545,10 @@ - (NSString *)nameForRangeControllerDataSource return self.asyncDataSource ? NSStringFromClass([self.asyncDataSource class]) : NSStringFromClass([self class]); } +- (CALayer *)layerForRangeController:(ASRangeController *)controller { + return self.layer; +} + #pragma mark - ASRangeControllerDelegate - (BOOL)rangeControllerShouldUpdateRanges:(ASRangeController *)rangeController @@ -1721,11 +1734,6 @@ - (BOOL)dataController:(ASDataController *)dataController shouldSynchronouslyPro return NO; } -- (void)dataControllerDidFinishWaiting:(ASDataController *)dataController -{ - // ASCellLayoutMode is not currently supported on ASTableView (see ASCollectionView for details). -} - - (id)dataController:(ASDataController *)dataController nodeModelForItemAtIndexPath:(NSIndexPath *)indexPath { // Not currently supported for tables. Will be added when the collection API stabilizes. @@ -1871,6 +1879,10 @@ - (BOOL)dataController:(ASDataController *)dataController presentedSizeForElemen return (fabs(rect.size.height - size.height) < FLT_EPSILON); } +- (CGRect)dataControllerFrameForDebugging:(ASDataController *)dataController { + return [self convertRect:self.bounds toView:nil]; +} + #pragma mark - _ASTableViewCellDelegate - (void)didLayoutSubviewsOfTableViewCell:(_ASTableViewCell *)tableViewCell diff --git a/Source/ASTextNode+Beta.h b/Source/ASTextNode+Beta.h index ad897c5f0..6768fc796 100644 --- a/Source/ASTextNode+Beta.h +++ b/Source/ASTextNode+Beta.h @@ -40,6 +40,13 @@ NS_ASSUME_NONNULL_BEGIN */ - (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize; +/** + * @abstract Like performAccessibilityCustomAction:, exposed for custom actions that are + * representing a link. This API and the implementation details within ASTextNode are not ready + * for public use and will likely change in the future. + */ +- (BOOL)performAccessibilityCustomActionLink:(UIAccessibilityCustomAction *)action; + @end NS_ASSUME_NONNULL_END diff --git a/Source/ASTextNode.mm b/Source/ASTextNode.mm index 8df442667..fc7c25165 100644 --- a/Source/ASTextNode.mm +++ b/Source/ASTextNode.mm @@ -407,6 +407,12 @@ - (UIAccessibilityTraits)defaultAccessibilityTraits return UIAccessibilityTraitStaticText; } +- (BOOL)performAccessibilityCustomActionLink:(UIAccessibilityCustomAction *)action +{ + AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); + return NO; +} + #pragma mark - Layout and Sizing - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset @@ -435,7 +441,7 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize ASTextKitRenderer *renderer = [self _locked_rendererWithBounds:{.size = constrainedSize}]; CGSize size = renderer.size; - if (_attributedText.length > 0) { + if (_attributedText.length > 0 && !self.yoga) { self.style.ascender = [[self class] ascenderWithAttributedString:_attributedText]; self.style.descender = [[_attributedText attribute:NSFontAttributeName atIndex:_attributedText.length - 1 effectiveRange:NULL] descender]; if (renderer.currentScaleFactor > 0 && renderer.currentScaleFactor < 1.0) { @@ -453,6 +459,12 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize std::fmin(size.height, originalConstrainedSize.height)); } +#if YOGA +- (float)yogaBaselineWithSize:(CGSize)size { + return ASTextGetBaseline(size.height, self.yogaParent, self.attributedText); +} +#endif + #pragma mark - Modifying User Text // Returns the ascender of the first character in attributedString by also including the line height if specified in paragraph style. @@ -510,6 +522,9 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Update attributed text with cleaned attributed string _attributedText = cleanedAttributedString; + if (self.isNodeLoaded) { + [self invalidateFirstAccessibilityContainerOrNonLayerBackedNode]; + } } // Tell the display node superclasses that the cached layout is incorrect now @@ -1492,7 +1507,8 @@ + (void)_registerAttributedText:(NSAttributedString *)str } #endif -// All direct descendants of ASTextNode get their superclass replaced by ASTextNode2. +// If ASExperimentalTextNode flag is set, all direct descendants of ASTextNode get their superclass +// replaced by ASTextNode2. + (void)initialize { // Texture requires that node subclasses call [super initialize] diff --git a/Source/ASTextNode2.h b/Source/ASTextNode2.h index 5848adc37..86ad7b5ea 100644 --- a/Source/ASTextNode2.h +++ b/Source/ASTextNode2.h @@ -14,6 +14,20 @@ NS_ASSUME_NONNULL_BEGIN +/** + * Get and Set ASTextNode to use: + * a) Intrinsic size fix for NSAtrributedStrings with no paragraph styles and + * b) Yoga direction to determine alignment of NSTextAlignmentNatural text nodes. + */ +BOOL ASGetEnableTextNode2ImprovedRTL(void); +void ASSetEnableTextNode2ImprovedRTL(BOOL enable); + +/** + * Get and Set ASTextLayout and ASTextNode2 to enable to calculation of visible text range. + */ +BOOL ASGetEnableTextTruncationVisibleRange(void); +void ASSetEnableTextTruncationVisibleRange(BOOL enable); + /** @abstract Draws interactive rich text. @discussion Backed by the code in TextExperiment folder, on top of CoreText. @@ -171,7 +185,7 @@ NS_ASSUME_NONNULL_BEGIN @param point The point, in the receiver's coordinate system. @param attributeNameOut The name of the attribute at the point. Can be NULL. @param rangeOut The ultimate range of the found text. Can be NULL. - @result YES if an entity exists at `point`; NO otherwise. + @result The entity if it exists at `point`; nil otherwise. */ - (nullable id)linkAttributeValueAtPoint:(CGPoint)point attributeName:(out NSString * _Nullable * _Nullable)attributeNameOut range:(out NSRange * _Nullable)rangeOut AS_WARN_UNUSED_RESULT; @@ -212,7 +226,14 @@ NS_ASSUME_NONNULL_BEGIN @discussion If you still want to handle tap truncation action when passthroughNonlinkTouches is YES, you should set the alwaysHandleTruncationTokenTap to YES. */ -@property (nonatomic) BOOL passthroughNonlinkTouches; +@property BOOL passthroughNonlinkTouches; + +/** + @abstract Whether additionalTruncationMessage is interactive. + @discussion This affects whether touches on additionalTruncationMessage will be intercepted when + passthroughNonlinkTouches is YES. + */ +@property BOOL additionalTruncationMessageIsInteractive; /** @abstract Always handle tap truncationAction, even the passthroughNonlinkTouches is YES. Default is NO. diff --git a/Source/ASTextNode2.mm b/Source/ASTextNode2.mm index e268eca2d..6c10784c0 100644 --- a/Source/ASTextNode2.mm +++ b/Source/ASTextNode2.mm @@ -12,18 +12,89 @@ #import #import -#import -#import #import #import -#import +#import #import +#import +#import +#import +#import #import #import +#import +#import #import +using namespace AS; + +typedef void (^TextAttachmentUpdateBlock)(ASImageNode *imageNode); +void UpdateTextAttachmentForText(NSAttributedString *attributedString, + TextAttachmentUpdateBlock updateBlock); + +BOOL kTextNode2ImprovedRTL = false; +BOOL ASGetEnableTextNode2ImprovedRTL(void) { return kTextNode2ImprovedRTL; } +void ASSetEnableTextNode2ImprovedRTL(BOOL enable) { kTextNode2ImprovedRTL = enable; } + +BOOL kTextTruncationVisibleRange = false; +BOOL ASGetEnableTextTruncationVisibleRange(void) { return kTextTruncationVisibleRange; } +void ASSetEnableTextTruncationVisibleRange(BOOL enable) { kTextTruncationVisibleRange = enable; } + +// Provide a way for an ASAccessibilityElement to dispatch to the ASTextNode for its +// accessibilityFrame +@interface ASTextNode2 () + +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement; + +@end + +/** + * Calculates the accessibility frame for a given ASAccessibilityElement, ASTextLayout and + * ASDisplayNode in the screen cooordinates space. This can be used for setting or + * providing an accessibility frame for the given ASAccessibilityElement. + */ +static CGRect ASTextNodeAccessiblityElementFrame(ASAccessibilityElement *element, + ASTextLayout *layout, + ASDisplayNode *containerNode) { + // This needs to be in the first non layer nodes coordinates space + containerNode = + containerNode ?: ASFindClosestViewOfLayer(element.node.layer).asyncdisplaykit_node; + NSCAssert(containerNode != nil, @"No container node found"); + CGRect textLayoutFrame = CGRectZero; + NSRange accessibilityRange = element.accessibilityRange; + if (accessibilityRange.location == NSNotFound) { + // If no accessibilityRange was specified (as is done for the text element), just use the + // label's range and clamp to the visible range otherwise the returned rect would be invalid. + NSRange range = NSMakeRange(0, element.accessibilityLabel.length); + range = NSIntersectionRange(range, layout.visibleRange); + textLayoutFrame = [layout rectForRange:[ASTextRange rangeWithRange:range]]; + } else { + textLayoutFrame = [layout rectForRange:[ASTextRange rangeWithRange:accessibilityRange]]; + } + CGRect accessibilityFrame = [element.node convertRect:textLayoutFrame toNode:containerNode]; + return UIAccessibilityConvertFrameToScreenCoordinates(accessibilityFrame, containerNode.view); +} + +@interface ASTextNodeFrameProvider : NSObject +@end + +@implementation ASTextNodeFrameProvider + +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement { + ASTextNode2 *textNode = ASDynamicCast(accessibilityElement.node, ASTextNode2); + if (textNode == nil) { + NSCAssert(NO, @"Only accessibility elements from ASTextNode are allowed."); + return CGRectZero; + } + + // Ask the passed in text node for the accessibilityFrame + return [textNode accessibilityFrameForAccessibilityElement:accessibilityElement]; +} + +@end + @interface ASTextCacheValue : NSObject { @package AS::Mutex _m; @@ -43,7 +114,8 @@ @implementation ASTextCacheValue #define AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS 0 /** - * If it can't find a compatible layout, this method creates one. + * Wraps a cache around a call to [ASTextLayout layoutWithContainer:text:]. + * [ASTextLayout layoutWithContainer:text:] creates a layout if it was not found in the cache. * * NOTE: Be careful to copy `text` if needed. */ @@ -59,11 +131,16 @@ @implementation ASTextCacheValue layoutCacheLock->lock(); ASTextCacheValue *cacheValue = [textLayoutCache objectForKey:text]; + + // Disable the cache if the text has attachments, since caching attachment content is expensive. + BOOL shouldCacheLayout = YES; if (cacheValue == nil) { + shouldCacheLayout = ![text as_hasAttribute:ASTextAttachmentAttributeName]; cacheValue = [[ASTextCacheValue alloc] init]; - [textLayoutCache setObject:cacheValue forKey:[text copy]]; + if (shouldCacheLayout) { + [textLayoutCache setObject:cacheValue forKey:[text copy]]; + } } - // Lock the cache item for the rest of the method. Only after acquiring can we release the NSCache. AS::MutexLocker lock(cacheValue->_m); layoutCacheLock->unlock(); @@ -116,15 +193,19 @@ @implementation ASTextCacheValue } // Cache Miss. Compute the text layout. + ASSignpostStart(MeasureText, cacheValue, "%@", [text.string substringToIndex:MIN(text.length, 10)]); ASTextLayout *layout = [ASTextLayout layoutWithContainer:container text:text]; + ASSignpostEnd(MeasureText, cacheValue, ""); // Store the result in the cache. { - // This is a critical section. However we also must hold the lock until this point, in case - // another thread requests this cache item while a layout is being calculated, so they don't race. - cacheValue->_layouts.push_front(std::make_tuple(container.size, layout)); - if (cacheValue->_layouts.size() > 3) { - cacheValue->_layouts.pop_back(); + if (shouldCacheLayout) { + // This is a critical section. However we also must hold the lock until this point, in case + // another thread requests this cache item while a layout is being calculated, so they don't race. + cacheValue->_layouts.push_front(std::make_tuple(container.size, layout)); + if (cacheValue->_layouts.size() > 3) { + cacheValue->_layouts.pop_back(); + } } } @@ -171,7 +252,9 @@ @implementation AS_TN2_CLASSNAME { ASTextNodeHighlightStyle _highlightStyle; BOOL _longPressCancelsTouches; BOOL _passthroughNonlinkTouches; - BOOL _alwaysHandleTruncationTokenTap; + BOOL _additionalTruncationMessageIsInteractive; + + NSMutableDictionary *_drawParameters; } @dynamic placeholderEnabled; @@ -197,7 +280,8 @@ - (instancetype)init // Disable user interaction for text node by default. self.userInteractionEnabled = NO; self.needsDisplayOnBoundsChange = YES; - + + _truncationMode = NSLineBreakByTruncatingTail; _textContainer.truncationType = ASTextTruncationTypeEnd; // The common case is for a text node to be non-opaque and blended over some background. @@ -207,7 +291,7 @@ - (instancetype)init self.linkAttributeNames = DefaultLinkAttributeNames(); // Accessibility - self.isAccessibilityElement = YES; + self.isAccessibilityElement = NO; self.accessibilityTraits = self.defaultAccessibilityTraits; // Placeholders @@ -223,6 +307,14 @@ - (instancetype)init - (void)dealloc { CGColorRelease(_shadowColor); + if (!ASDisplayNodeThreadIsMain()) { + if ([_attributedText as_hasAttribute:ASTextAttachmentAttributeName]) { + ASPerformMainThreadDeallocation(&_attributedText); + } + if ([_truncationAttributedText as_hasAttribute:ASTextAttachmentAttributeName]) { + ASPerformMainThreadDeallocation(&_truncationAttributedText); + } + } } #pragma mark - Description @@ -279,7 +371,7 @@ - (BOOL)supportsLayerBacking return NO; } - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); // If the text contains any links, return NO. NSAttributedString *attributedText = _attributedText; NSRange range = NSMakeRange(0, attributedText.length); @@ -301,7 +393,7 @@ - (BOOL)supportsLayerBacking - (NSString *)defaultAccessibilityLabel { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _attributedText.string; } @@ -310,11 +402,123 @@ - (UIAccessibilityTraits)defaultAccessibilityTraits return UIAccessibilityTraitStaticText; } +- (BOOL)isAccessibilityElement +{ + // If the ASTextNode2 should act as an UIAccessibilityContainer it has to return + // NO for isAccessibilityElement + return NO; +} + +- (NSInteger)accessibilityElementCount +{ + return self.accessibilityElements.count; +} + +// Returns the default ASTextNodeFrameProvider to be used as frame provider of text node's +// accessibility elements. +static ASTextNodeFrameProvider *ASTextNode2ASTextNodeFrameProviderDefault() { + static ASTextNodeFrameProvider *frameProvider = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + frameProvider = [[ASTextNodeFrameProvider alloc] init]; + }); + return frameProvider; +} + +- (NSArray *)accessibilityElements +{ + NSInteger attributedTextLength = _attributedText.length; + if (attributedTextLength == 0) { + return @[]; + } + + NSMutableArray *accessibilityElements = [[NSMutableArray alloc] init]; + + // Search the first node that is not layer backed + ASDisplayNode *containerNode = ASFindClosestViewOfLayer(self.layer).asyncdisplaykit_node; + NSCAssert(containerNode != nil, @"No container node found"); + + // Create an accessibility element to represent the label's text. It's not necessary to specify + // a accessibilityRange here, as the entirety of the text is being represented. + ASAccessibilityElement *accessibilityElement = + [[ASAccessibilityElement alloc] initWithAccessibilityContainer:containerNode.view]; + accessibilityElement.node = self; + accessibilityElement.accessibilityRange = NSMakeRange(NSNotFound, 0); + accessibilityElement.accessibilityIdentifier = self.accessibilityIdentifier; + accessibilityElement.accessibilityLabel = self.accessibilityLabel; + accessibilityElement.accessibilityValue = self.accessibilityValue; + accessibilityElement.accessibilityTraits = self.accessibilityTraits; + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + accessibilityElement.accessibilityAttributedLabel = self.accessibilityAttributedLabel; + accessibilityElement.accessibilityAttributedHint = self.accessibilityAttributedHint; + accessibilityElement.accessibilityAttributedValue = self.accessibilityAttributedValue; + } + accessibilityElement.frameProvider = ASTextNode2ASTextNodeFrameProviderDefault(); + [accessibilityElements addObject:accessibilityElement]; + + // Collect all links as accessiblity items + for (NSString *linkAttributeName in _linkAttributeNames) { + [_attributedText enumerateAttribute:linkAttributeName inRange:NSMakeRange(0, attributedTextLength) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + if (value == nil) { + return; + } + ASAccessibilityElement *accessibilityElement = + [[ASAccessibilityElement alloc] initWithAccessibilityContainer:self]; + accessibilityElement.node = self; + accessibilityElement.accessibilityTraits = UIAccessibilityTraitLink; + accessibilityElement.accessibilityLabel = [_attributedText.string substringWithRange:range]; + accessibilityElement.accessibilityRange = range; + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + accessibilityElement.accessibilityAttributedLabel = + [_attributedText attributedSubstringFromRange:range]; + } + accessibilityElement.frameProvider = ASTextNode2ASTextNodeFrameProviderDefault(); + [accessibilityElements addObject:accessibilityElement]; + }]; + } + return accessibilityElements; +} + +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement { + // Go up the tree to the top container node that contains the ASTextNode + // this is especially necessary if the text node is layer backed + ASDisplayNode *containerNode = ASFindClosestViewOfLayer(_layer).asyncdisplaykit_node; + NSCAssert(containerNode != nil, @"No container node found"); + ASTextLayout *layout = + ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, _attributedText); + return ASTextNodeAccessiblityElementFrame(accessibilityElement, layout, containerNode); +} + +- (BOOL)performAccessibilityCustomActionLink:(UIAccessibilityCustomAction *)action +{ + NSCAssert(0 != (action.accessibilityTraits & UIAccessibilityTraitLink), @"Action needs to have UIAccessibilityTraitLink trait set"); + NSCAssert([action isKindOfClass:[ASAccessibilityCustomAction class]], @"Action needs to be of kind ASAccessibilityCustomAction"); + ASAccessibilityCustomAction *customAction = (ASAccessibilityCustomAction *)action; + + // In TextNode2 forward the link custom action to textNode:tappedLinkAttribute:value:atPoint:textRange: + // the default method that is available for link handling within ASTextNodeDelegate + if ([self.delegate respondsToSelector:@selector(textNode:tappedLinkAttribute:value:atPoint:textRange:)]) { + // Convert from screen coordinates to the node coordinate space + CGPoint centerAccessibilityFrame = CGPointMake(CGRectGetMidX(customAction.accessibilityFrame), CGRectGetMidY(customAction.accessibilityFrame)); + CGPoint center = [self.supernode convertPoint:centerAccessibilityFrame fromNode:nil]; + [self.delegate textNode:(ASTextNode *)self tappedLinkAttribute:NSLinkAttributeName value:customAction.value atPoint:center textRange:customAction.textRange]; + return YES; + } + return NO; +} + #pragma mark - Layout and Sizing +- (void)layoutDidFinish { + [super layoutDidFinish]; + if (CGRectIsEmpty(self.bounds) && self.layer.needsDisplay) { + self.layer.contents = nil; + } +} + - (void)setTextContainerInset:(UIEdgeInsets)textContainerInset { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCustom(_textContainer.insets, textContainerInset, UIEdgeInsetsEqualToEdgeInsets)) { [self setNeedsLayout]; } @@ -333,7 +537,7 @@ - (void)setTextContainerLinePositionModifier:(id)mod - (id)textContainerLinePositionModifier { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _textContainer.linePositionModifier; } @@ -342,16 +546,25 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize ASDisplayNodeAssert(constrainedSize.width >= 0, @"Constrained width for text (%f) is too narrow", constrainedSize.width); ASDisplayNodeAssert(constrainedSize.height >= 0, @"Constrained height for text (%f) is too short", constrainedSize.height); - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); _textContainer.size = constrainedSize; [self _ensureTruncationText]; - + BOOL isCalculatingIntrinsicSize = NO; +#if YOGA + // In Yoga nodes, we cannot count on constrainedSize being infinite as we make AT_MOST measurements which receive fixed at-most values. + // However note the effect of this setting is to always left-align the text and report its actual used-width instead of the + // position of the far edge relative to the left: Which is behavior we always want under Yoga anyway. + if (_flags.yoga) { + isCalculatingIntrinsicSize = YES; + } +#endif // If the constrained size has a max/inf value on the text's forward direction, the text node is calculating its intrinsic size. // Need to consider both width and height when determining if it is calculating instrinsic size. Even the constrained width is provided, the height can be inf // it may provide a text that is longer than the width and require a wordWrapping line break mode and looking for the height to be calculated. - BOOL isCalculatingIntrinsicSize = (_textContainer.size.width >= ASTextContainerMaxSize.width) || (_textContainer.size.height >= ASTextContainerMaxSize.height); - + if (!isCalculatingIntrinsicSize) { + isCalculatingIntrinsicSize = (_textContainer.size.width >= ASTextContainerMaxSize.width) || (_textContainer.size.height >= ASTextContainerMaxSize.height); + } NSMutableAttributedString *mutableText = [_attributedText mutableCopy]; [self prepareAttributedString:mutableText isForIntrinsicSize:isCalculatingIntrinsicSize]; ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, mutableText); @@ -362,6 +575,12 @@ - (CGSize)calculateSizeThatFits:(CGSize)constrainedSize return layout.textBoundingSize; } +#if YOGA +- (float)yogaBaselineWithSize:(CGSize)size { + return ASTextGetBaseline(size.height, self.yogaParent, self.attributedText); +} +#endif + #pragma mark - Modifying User Text // Returns the ascender of the first character in attributedString by also including the line height if specified in paragraph style. @@ -381,19 +600,20 @@ + (CGFloat)ascenderWithAttributedString:(NSAttributedString *)attributedString - (NSAttributedString *)attributedText { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _attributedText; } - (void)setAttributedText:(NSAttributedString *)attributedText { - if (attributedText == nil) { - attributedText = [[NSAttributedString alloc] initWithString:@"" attributes:nil]; + // Avoid copy / create for nil or zero-length arg. Treat them both as singleton zero. + if (attributedText.length == 0) { + attributedText = ASGetZeroAttributedString(); } // Many accessors in this method will acquire the lock (including ASDisplayNode methods). // Holding it for the duration of the method is more efficient in this case. - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); NSAttributedString *oldAttributedText = _attributedText; if (!ASCompareAssignCopy(_attributedText, attributedText)) { @@ -404,7 +624,7 @@ - (void)setAttributedText:(NSAttributedString *)attributedText [self _locked_invalidateTruncationText]; NSUInteger length = attributedText.length; - if (length > 0) { + if (length > 0 && !self.yoga) { ASLayoutElementStyle *style = [self _locked_style]; style.ascender = [[self class] ascenderWithAttributedString:attributedText]; style.descender = [[attributedText attribute:NSFontAttributeName atIndex:attributedText.length - 1 effectiveRange:NULL] descender]; @@ -419,22 +639,23 @@ - (void)setAttributedText:(NSAttributedString *)attributedText // Accessiblity self.accessibilityLabel = self.defaultAccessibilityLabel; - // We update the isAccessibilityElement setting if this node is not switching between strings. - if (oldAttributedText.length == 0 || length == 0) { - // We're an accessibility element by default if there is a string. - self.isAccessibilityElement = (length != 0); - } - #if AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS [ASTextNode _registerAttributedText:_attributedText]; #endif + + if (self.isNodeLoaded) { + // Invalidate the accessibility elements for self as well as for the first accessibility + // container to requery the accessibility items for it + [self invalidateAccessibilityElements]; + [self invalidateFirstAccessibilityContainerOrNonLayerBackedNode]; + } } #pragma mark - Text Layout - (void)setExclusionPaths:(NSArray *)exclusionPaths { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); _textContainer.exclusionPaths = exclusionPaths; [self setNeedsLayout]; @@ -443,14 +664,18 @@ - (void)setExclusionPaths:(NSArray *)exclusionPaths - (NSArray *)exclusionPaths { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _textContainer.exclusionPaths; } - (void)prepareAttributedString:(NSMutableAttributedString *)attributedString isForIntrinsicSize:(BOOL)isForIntrinsicSize { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); NSLineBreakMode innerMode; + // `innerMode` is the truncation mode used by CoreText, within ASTextLayout. Since we are + // supplying our own truncation logic, we replace any actually-truncating modes with + // `NSLineBreakByWordWrapping` which just breaks down the lines for us. We still have the original + // truncationMode available to us from the ASTextContainer when we need it. switch (_truncationMode) { case NSLineBreakByWordWrapping: case NSLineBreakByCharWrapping: @@ -461,38 +686,75 @@ - (void)prepareAttributedString:(NSMutableAttributedString *)attributedString is innerMode = NSLineBreakByWordWrapping; } - // Apply/Fix paragraph style if needed - [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:kNilOptions usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { - - BOOL applyTruncationMode = YES; - NSMutableParagraphStyle *paragraphStyle = nil; - // Only "left" and "justified" alignments are supported while calculating intrinsic size. - // Other alignments like "right", "center" and "natural" cause the size to be bigger than needed and thus should be ignored/overridden. - const BOOL forceLeftAlignment = (style != nil - && isForIntrinsicSize - && style.alignment != NSTextAlignmentLeft - && style.alignment != NSTextAlignmentJustified); - if (style != nil) { - if (innerMode == style.lineBreakMode) { - applyTruncationMode = NO; - } - paragraphStyle = [style mutableCopy]; + // NOTE if there are no attributes, we still get called once with a `nil` style! + [attributedString enumerateAttribute:NSParagraphStyleAttributeName inRange:NSMakeRange(0, attributedString.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(NSParagraphStyle *style, NSRange range, BOOL * _Nonnull stop) { + const NSLineBreakMode previousMode = style ? style.lineBreakMode : NSLineBreakByWordWrapping; + const BOOL applyTruncationMode = innerMode != previousMode; + const BOOL useNaturalAlignment = (!style || style.alignment == NSTextAlignmentNatural); + BOOL forceLeftAlignment = NO; + // This experiment launched so long ago but relies on semanticContentAttribute + if (kTextNode2ImprovedRTL) { + // To calculate intrinsic size, we generally always use NSTextAlignmentLeft (perhaps NSTextAlignmentJustified). + forceLeftAlignment = (isForIntrinsicSize + && (!style + || (style.alignment != NSTextAlignmentLeft + && style.alignment != NSTextAlignmentJustified))); } else { - if (innerMode == NSLineBreakByWordWrapping) { - applyTruncationMode = NO; - } - paragraphStyle = [NSMutableParagraphStyle new]; + forceLeftAlignment = (style != nil + && isForIntrinsicSize + && style.alignment != NSTextAlignmentLeft + && style.alignment != NSTextAlignmentJustified); } - if (!applyTruncationMode && !forceLeftAlignment) { + + if (!applyTruncationMode && !forceLeftAlignment && !useNaturalAlignment) { return; } + NSMutableParagraphStyle *paragraphStyle = + [style mutableCopy] ?: [[NSMutableParagraphStyle alloc] init]; paragraphStyle.lineBreakMode = innerMode; - if (applyTruncationMode) { - paragraphStyle.lineBreakMode = _truncationMode; - } if (forceLeftAlignment) { paragraphStyle.alignment = NSTextAlignmentLeft; + } else if (useNaturalAlignment) { +#if YOGA + if (!kTextNode2ImprovedRTL) { +#endif + if (AS_AVAILABLE_IOS(10)) { + switch (self.primitiveTraitCollection.layoutDirection) { + case UITraitEnvironmentLayoutDirectionLeftToRight: + paragraphStyle.alignment = NSTextAlignmentLeft; + break; + case UITraitEnvironmentLayoutDirectionRightToLeft: + paragraphStyle.alignment = NSTextAlignmentRight; + break; + case UITraitEnvironmentLayoutDirectionUnspecified: + break; + } + } else { + NSNumber *layoutDirection = ASApplicationUserInterfaceLayoutDirection(); + if (layoutDirection) { + switch (static_cast([layoutDirection integerValue])) { + case UIUserInterfaceLayoutDirectionLeftToRight: + paragraphStyle.alignment = NSTextAlignmentLeft; + break; + case UIUserInterfaceLayoutDirectionRightToLeft: + paragraphStyle.alignment = NSTextAlignmentRight; + break; + } + } + } +#if YOGA + } else { + switch ([self yogaLayoutDirection]) { + case UIUserInterfaceLayoutDirectionLeftToRight: + paragraphStyle.alignment = NSTextAlignmentLeft; + break; + case UIUserInterfaceLayoutDirectionRightToLeft: + paragraphStyle.alignment = NSTextAlignmentRight; + break; + } + } +#endif } [attributedString addAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; }]; @@ -517,34 +779,28 @@ - (void)prepareAttributedString:(NSMutableAttributedString *)attributedString is - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer { - ASTextContainer *copiedContainer; - NSMutableAttributedString *mutableText; - BOOL needsTintColor; - id bgColor; - { - // Wrapping all the other access here, because we can't lock while accessing tintColor. - ASLockScopeSelf(); - [self _ensureTruncationText]; + MutexLocker l(__instanceLock__); + [self _ensureTruncationText]; - // Unlike layout, here we must copy the container since drawing is asynchronous. - copiedContainer = [_textContainer copy]; - copiedContainer.size = self.bounds.size; - [copiedContainer makeImmutable]; - mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; + // Unlike layout, here we must copy the container since drawing is asynchronous. + ASTextContainer *copiedContainer = [_textContainer copy]; - [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; - needsTintColor = self.textColorFollowsTintColor && mutableText.length > 0; - bgColor = self.backgroundColor ?: [NSNull null]; + // Some unit tests set insets directly, so don't override them with zero padding + if (!UIEdgeInsetsEqualToEdgeInsets(self.paddings, UIEdgeInsetsZero)) { + copiedContainer.insets = self.paddings; } - + copiedContainer.size = self.bounds.size; + [copiedContainer makeImmutable]; + NSMutableAttributedString *mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; + + [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; + // After all other attributes are set, apply tint color if needed and foreground color is not already specified - if (needsTintColor) { + if (self.textColorFollowsTintColor && mutableText.length > 0) { // Apply tint color if specified and if foreground color is undefined for attributedString NSRange limit = NSMakeRange(0, mutableText.length); // Look for previous attributes that define foreground color UIColor *attributeValue = (UIColor *)[mutableText attribute:NSForegroundColorAttributeName atIndex:limit.location effectiveRange:NULL]; - - // we need to unlock before accessing tintColor UIColor *tintColor = self.tintColor; if (attributeValue == nil && tintColor) { // None are found, apply tint color if available. Fallback to "black" text color @@ -552,11 +808,16 @@ - (NSObject *)drawParametersForAsyncLayer:(_ASDisplayLayer *)layer } } - return @{ - @"container": copiedContainer, - @"text": mutableText, - @"bgColor": bgColor - }; + ASTextLayout *layout = + ASTextNodeCompatibleLayoutWithContainerAndText(copiedContainer, mutableText); + if (!_drawParameters) { + _drawParameters = [[NSMutableDictionary alloc] init]; + } + _drawParameters[@"container"] = copiedContainer; + _drawParameters[@"text"] = mutableText; + _drawParameters[@"bgColor"] = self.backgroundColor ?: [NSNull null]; + _drawParameters[@"layout"] = layout ?: [NSNull null]; + return [_drawParameters copy]; } + (void)drawRect:(CGRect)bounds withParameters:(NSDictionary *)layoutDict isCancelled:(NS_NOESCAPE asdisplaynode_iscancelled_block_t)isCancelledBlock isRasterizing:(BOOL)isRasterizing @@ -639,14 +900,19 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point inAdditionalTruncationMessage:(out BOOL *)inAdditionalTruncationMessageOut forHighlighting:(BOOL)highlighting { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); // TODO: The copy and application of size shouldn't be required, but it is currently. // See discussion in https://github.com/TextureGroup/Texture/pull/396 ASTextContainer *containerCopy = [_textContainer copy]; containerCopy.size = self.calculatedSize; - ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(containerCopy, _attributedText); + NSMutableAttributedString *mutableText = [_attributedText mutableCopy] ?: [[NSMutableAttributedString alloc] init]; + [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; + ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(containerCopy, mutableText); + // Start by checking whether there is any "direct" hit of additionalTruncationMessage. This + // ensures that additionalTruncationMessage still receives the touch if any link's expanded touch + // area overlaps it. if ([self _locked_pointInsideAdditionalTruncationMessage:point withLayout:layout]) { if (inAdditionalTruncationMessageOut != NULL) { *inAdditionalTruncationMessageOut = YES; @@ -655,28 +921,60 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point } NSRange visibleRange = layout.visibleRange; + BOOL enableImprovedTextTruncationVisibleRange = ASGetEnableImprovedTextTruncationVisibleRange(); + BOOL enableTextTruncationVisibleRange = ASGetEnableTextTruncationVisibleRange(); + + if (_truncationAttributedText != nil && [self isTruncated]) { + // Make sure the touch area doesn't include the area of the truncated tokens when the text + // is truncated. + if (enableImprovedTextTruncationVisibleRange) { + // The link attribute should only be fetched if the point is inside the uncovered part of + // the text. This is to make sure tapping on the truncation token will not fire any link + // attributes. + if (layout.truncatedLineBeforeTruncationToken && + CGRectContainsPoint(layout.truncatedLine.bounds, point) && + !CGRectContainsPoint(layout.truncatedLineBeforeTruncationToken.bounds, point)) { + return nil; + } + if (layout.truncatedLineBeforeTruncationTokenRange.location != NSNotFound) { + // The index before the truncated tokens. + NSUInteger truncatedLineBeforeTruncationTokenEnd = + layout.truncatedLineBeforeTruncationTokenRange.location + + layout.truncatedLineBeforeTruncationTokenRange.length; + visibleRange = NSMakeRange(visibleRange.location, + truncatedLineBeforeTruncationTokenEnd - visibleRange.location); + } + } else if (enableTextTruncationVisibleRange) { + visibleRange.length -= _truncationAttributedText.length; + } + } NSRange clampedRange = NSIntersectionRange(visibleRange, NSMakeRange(0, _attributedText.length)); - // Search the 9 points of a 44x44 square around the touch until we find a link. + // Search the 17 points of a 44x44 square around the touch until we find a link. // Start from center, then do sides, then do top/bottom, then do corners. - static constexpr CGSize kRectOffsets[9] = { + static constexpr CGSize kRectOffsets[] = { { 0, 0 }, + { -11, 0 }, { 11, 0 }, { -22, 0 }, { 22, 0 }, + { 0, -11 }, { 0, 11 }, { 0, -22 }, { 0, 22 }, + { -11, -11 }, { -11, 11 }, { -22, -22 }, { -22, 22 }, + { 11, -11 }, { 11, 11 }, { 22, -22 }, { 22, 22 } }; for (const CGSize &offset : kRectOffsets) { const CGPoint testPoint = CGPointMake(point.x + offset.width, point.y + offset.height); - ASTextPosition *pos = [layout closestPositionToPoint:testPoint]; - if (!pos || !NSLocationInRange(pos.offset, clampedRange)) { + ASTextRange *range = [layout textRangeAtPoint:testPoint]; + if (!range || !NSLocationInRange(range.start.offset, clampedRange)) { continue; } + for (NSString *attributeName in _linkAttributeNames) { NSRange effectiveRange = NSMakeRange(0, 0); - id value = [_attributedText attribute:attributeName atIndex:pos.offset + id value = [_attributedText attribute:attributeName atIndex:range.start.offset longestEffectiveRange:&effectiveRange inRange:clampedRange]; if (value == nil) { // Didn't find any links specified with this attribute. @@ -691,7 +989,9 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point continue; } - *rangeOut = NSIntersectionRange(visibleRange, effectiveRange); + if (rangeOut != NULL) { + *rangeOut = NSIntersectionRange(visibleRange, effectiveRange); + } if (attributeNameOut != NULL) { *attributeNameOut = attributeName; @@ -701,6 +1001,20 @@ - (id)_linkAttributeValueAtPoint:(CGPoint)point } } + // If there is no link, we can safely check whether the touch lands in the expanded touch + // area of additionalTruncationMessage. + if (_additionalTruncationMessage) { + for (const CGSize &offset : kRectOffsets) { + const CGPoint testPoint = CGPointMake(point.x + offset.width, point.y + offset.height); + if ([self _locked_pointInsideAdditionalTruncationMessage:testPoint withLayout:layout]) { + if (inAdditionalTruncationMessageOut != NULL) { + *inAdditionalTruncationMessageOut = YES; + } + return nil; + } + } + } + return nil; } @@ -710,21 +1024,22 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout BOOL inAdditionalTruncationMessage = NO; CTLineRef truncatedCTLine = layout.truncatedLine.CTLine; - if (truncatedCTLine != NULL && _additionalTruncationMessage != nil) { + if (truncatedCTLine != NULL && _additionalTruncationMessage != nil && + CGRectContainsPoint(layout.truncatedLine.bounds, point)) { CFIndex stringIndexForPosition = CTLineGetStringIndexForPosition(truncatedCTLine, point); if (stringIndexForPosition != kCFNotFound) { CFIndex truncatedCTLineGlyphCount = CTLineGetGlyphCount(truncatedCTLine); CTLineRef truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_truncationAttributedText); CFIndex truncationTokenLineGlyphCount = truncationTokenLine ? CTLineGetGlyphCount(truncationTokenLine) : 0; - + if (truncationTokenLine) { CFRelease(truncationTokenLine); } CTLineRef additionalTruncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)_additionalTruncationMessage); CFIndex additionalTruncationTokenLineGlyphCount = additionalTruncationTokenLine ? CTLineGetGlyphCount(additionalTruncationTokenLine) : 0; - + if (additionalTruncationTokenLine) { CFRelease(additionalTruncationTokenLine); } @@ -744,14 +1059,14 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout if ((firstTruncatedTokenIndex + truncationTokenLineGlyphCount) < stringIndexForPosition && stringIndexForPosition < (firstTruncatedTokenIndex + composedTruncationTextLineGlyphCount)) { inAdditionalTruncationMessage = YES; - } + } break; } case ASTextTruncationTypeEnd: { if (stringIndexForPosition > (truncatedCTLineGlyphCount - additionalTruncationTokenLineGlyphCount)) { inAdditionalTruncationMessage = YES; } - break; + break; } default: // For now, assume that a tap inside this text, but outside the text range is a tap on the @@ -763,7 +1078,7 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout } } } - + return inAdditionalTruncationMessage; } @@ -772,7 +1087,7 @@ - (BOOL)_locked_pointInsideAdditionalTruncationMessage:(CGPoint)point withLayout - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { ASDisplayNodeAssertMainThread(); - ASLockScopeSelf(); // Protect usage of _highlight* ivars. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. if (gestureRecognizer == _longPressGestureRecognizer) { // Don't allow long press on truncation message @@ -806,21 +1121,21 @@ - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer - (ASTextNodeHighlightStyle)highlightStyle { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _highlightStyle; } - (void)setHighlightStyle:(ASTextNodeHighlightStyle)highlightStyle { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); _highlightStyle = highlightStyle; } - (NSRange)highlightRange { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return _highlightRange; } @@ -838,7 +1153,7 @@ - (void)setHighlightRange:(NSRange)highlightRange animated:(BOOL)animated - (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *)highlightedAttributeName value:(id)highlightedAttributeValue animated:(BOOL)animated { ASDisplayNodeAssertMainThread(); - ASLockScopeSelf(); // Protect usage of _highlight* ivars. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. // Set these so that link tapping works. _highlightedLinkAttributeName = highlightedAttributeName; @@ -906,7 +1221,7 @@ - (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *) ASTextLayout *layout = ASTextNodeCompatibleLayoutWithContainerAndText(textContainerCopy, _attributedText); NSArray *highlightRects = [layout selectionRectsWithoutStartAndEndForRange:[ASTextRange rangeWithRange:highlightRange]]; - NSMutableArray *converted = [NSMutableArray arrayWithCapacity:highlightRects.count]; + NSMutableArray *converted = [[NSMutableArray alloc] initWithCapacity:highlightRects.count]; CALayer *layer = self.layer; UIEdgeInsets shadowPadding = self.shadowPadding; @@ -924,7 +1239,10 @@ - (void)_setHighlightRange:(NSRange)highlightRange forAttributeName:(NSString *) ASHighlightOverlayLayer *overlayLayer = [[ASHighlightOverlayLayer alloc] initWithRects:converted]; overlayLayer.highlightColor = [[self class] _highlightColorForStyle:self.highlightStyle]; - overlayLayer.frame = highlightTargetLayer.bounds; + CGRect frame = highlightTargetLayer.bounds; + frame.origin.x += self.paddings.left; + frame.origin.y += self.paddings.top; + overlayLayer.frame = frame; overlayLayer.masksToBounds = NO; overlayLayer.opacity = [[self class] _highlightOpacityForStyle:self.highlightStyle]; [highlightTargetLayer addSublayer:overlayLayer]; @@ -1007,7 +1325,7 @@ - (UIColor *)placeholderColor - (void)setPlaceholderColor:(UIColor *)placeholderColor { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCopy(_placeholderColor, placeholderColor)) { self.placeholderEnabled = CGColorGetAlpha(placeholderColor.CGColor) > 0; } @@ -1024,15 +1342,11 @@ - (UIImage *)placeholderImage - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event { ASDisplayNodeAssertMainThread(); - ASLockScopeSelf(); // Protect usage of _passthroughNonlinkTouches and _alwaysHandleTruncationTokenTap ivars. + MutexLocker l(__instanceLock__); // Protect usage of ivars. if (!_passthroughNonlinkTouches) { return [super pointInside:point withEvent:event]; } - - if (_alwaysHandleTruncationTokenTap) { - return YES; - } NSRange range = NSMakeRange(0, 0); NSString *linkAttributeName = nil; @@ -1043,7 +1357,10 @@ - (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event range:&range inAdditionalTruncationMessage:&inAdditionalTruncationMessage forHighlighting:YES]; - + if (_additionalTruncationMessageIsInteractive && inAdditionalTruncationMessage) { + return YES; + } + NSUInteger lastCharIndex = NSIntegerMax; BOOL linkCrossesVisibleRange = (lastCharIndex > range.location) && (lastCharIndex < NSMaxRange(range) - 1); @@ -1078,7 +1395,7 @@ - (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event if (inAdditionalTruncationMessage) { NSRange visibleRange = NSMakeRange(0, 0); { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); // TODO: The copy and application of size shouldn't be required, but it is currently. // See discussion in https://github.com/TextureGroup/Texture/pull/396 ASTextContainer *containerCopy = [_textContainer copy]; @@ -1109,7 +1426,7 @@ - (void)touchesEnded:(NSSet *)touches withEvent:(UIEvent *)event ASDisplayNodeAssertMainThread(); [super touchesEnded:touches withEvent:event]; - ASLockScopeSelf(); // Protect usage of _highlight* ivars. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. id delegate = self.delegate; if ([self _pendingLinkTap] && [delegate respondsToSelector:@selector(textNode:tappedLinkAttribute:value:atPoint:textRange:)]) { CGPoint point = [[touches anyObject] locationInView:self.view]; @@ -1130,7 +1447,7 @@ - (void)touchesMoved:(NSSet *)touches withEvent:(UIEvent *)event ASDisplayNodeAssertMainThread(); [super touchesMoved:touches withEvent:event]; - ASLockScopeSelf(); // Protect usage of _highlight* ivars. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. UITouch *touch = [touches anyObject]; CGPoint locationInView = [touch locationInView:self.view]; // on 3D Touch enabled phones, this gets fired with changes in force, and usually will get fired immediately after touchesBegan:withEvent: @@ -1160,7 +1477,7 @@ - (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer if (longPressRecognizer.state == UIGestureRecognizerStateBegan) { id delegate = self.delegate; if ([delegate respondsToSelector:@selector(textNode:longPressedLinkAttribute:value:atPoint:textRange:)]) { - ASLockScopeSelf(); // Protect usage of _highlight* ivars. + MutexLocker l(__instanceLock__); // Protect usage of _highlight* ivars. CGPoint touchPoint = [_longPressGestureRecognizer locationInView:self.view]; [delegate textNode:(ASTextNode *)self longPressedLinkAttribute:_highlightedLinkAttributeName value:_highlightedLinkAttributeValue atPoint:touchPoint textRange:_highlightRange]; } @@ -1169,7 +1486,7 @@ - (void)_handleLongPress:(UILongPressGestureRecognizer *)longPressRecognizer - (BOOL)_pendingLinkTap { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); return (_highlightedLinkAttributeValue != nil && ![self _pendingTruncationTap]) && self.delegate != nil; } @@ -1179,18 +1496,26 @@ - (BOOL)_pendingTruncationTap return [ASLockedSelf(_highlightedLinkAttributeName) isEqualToString:ASTextNodeTruncationTokenAttributeName]; } -- (BOOL)alwaysHandleTruncationTokenTap -{ - ASLockScopeSelf(); - return _alwaysHandleTruncationTokenTap; +- (BOOL)passthroughNonlinkTouches { + MutexLocker l(__instanceLock__); + return _passthroughNonlinkTouches; } -- (void)setAlwaysHandleTruncationTokenTap:(BOOL)alwaysHandleTruncationTokenTap -{ - ASLockScopeSelf(); - _alwaysHandleTruncationTokenTap = alwaysHandleTruncationTokenTap; +- (void)setPassthroughNonlinkTouches:(BOOL)passthroughNonlinkTouches { + MutexLocker l(__instanceLock__); + _passthroughNonlinkTouches = passthroughNonlinkTouches; } - + +- (BOOL)additionalTruncationMessageIsInteractive { + MutexLocker l(__instanceLock__); + return _additionalTruncationMessageIsInteractive; +} + +- (void)setAdditionalTruncationMessageIsInteractive:(BOOL)additionalTruncationMessageIsInteractive { + MutexLocker l(__instanceLock__); + _additionalTruncationMessageIsInteractive = additionalTruncationMessageIsInteractive; +} + #pragma mark - Shadow Properties /** @@ -1206,7 +1531,7 @@ - (CGColorRef)shadowColor - (void)setShadowColor:(CGColorRef)shadowColor { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (_shadowColor != shadowColor && CGColorEqualToColor(shadowColor, _shadowColor) == NO) { CGColorRelease(_shadowColor); _shadowColor = CGColorRetain(shadowColor); @@ -1221,7 +1546,7 @@ - (CGSize)shadowOffset - (void)setShadowOffset:(CGSize)shadowOffset { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCustom(_shadowOffset, shadowOffset, CGSizeEqualToSize)) { [self setNeedsDisplay]; } @@ -1234,7 +1559,7 @@ - (CGFloat)shadowOpacity - (void)setShadowOpacity:(CGFloat)shadowOpacity { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssign(_shadowOpacity, shadowOpacity)) { [self setNeedsDisplay]; } @@ -1247,7 +1572,7 @@ - (CGFloat)shadowRadius - (void)setShadowRadius:(CGFloat)shadowRadius { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssign(_shadowRadius, shadowRadius)) { [self setNeedsDisplay]; } @@ -1262,7 +1587,7 @@ - (UIEdgeInsets)shadowPadding - (void)setPointSizeScaleFactors:(NSArray *)scaleFactors { AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCopy(_pointSizeScaleFactors, scaleFactors)) { [self setNeedsLayout]; } @@ -1275,19 +1600,9 @@ - (void)setPointSizeScaleFactors:(NSArray *)scaleFactors #pragma mark - Truncation Message -static NSAttributedString *DefaultTruncationAttributedString() -{ - static NSAttributedString *defaultTruncationAttributedString; - static dispatch_once_t onceToken; - dispatch_once(&onceToken, ^{ - defaultTruncationAttributedString = [[NSAttributedString alloc] initWithString:NSLocalizedString(@"\u2026", @"Default truncation string")]; - }); - return defaultTruncationAttributedString; -} - - (void)_ensureTruncationText { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (_textContainer.truncationToken == nil) { _textContainer.truncationToken = [self _locked_composedTruncationText]; } @@ -1300,7 +1615,7 @@ - (NSAttributedString *)truncationAttributedText - (void)setTruncationAttributedText:(NSAttributedString *)truncationAttributedText { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCopy(_truncationAttributedText, truncationAttributedText)) { [self _invalidateTruncationText]; } @@ -1313,7 +1628,7 @@ - (NSAttributedString *)additionalTruncationMessage - (void)setAdditionalTruncationMessage:(NSAttributedString *)additionalTruncationMessage { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssignCopy(_additionalTruncationMessage, additionalTruncationMessage)) { [self _invalidateTruncationText]; } @@ -1326,7 +1641,7 @@ - (NSLineBreakMode)truncationMode - (void)setTruncationMode:(NSLineBreakMode)truncationMode { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssign(_truncationMode, truncationMode)) { ASTextTruncationType truncationType; switch (truncationMode) { @@ -1351,19 +1666,31 @@ - (void)setTruncationMode:(NSLineBreakMode)truncationMode - (BOOL)isTruncated { - return ASLockedSelf([self locked_textLayoutForSize:[self _locked_threadSafeBounds].size].truncatedLine != nil); + AS::MutexLocker l(__instanceLock__); + ASTextLayout *layout = [self locked_textLayoutForSize:[self _locked_threadSafeBounds].size]; + return !NSEqualRanges(layout.visibleRange, layout.range); } - (BOOL)shouldTruncateForConstrainedSize:(ASSizeRange)constrainedSize { - return ASLockedSelf([self locked_textLayoutForSize:constrainedSize.max].truncatedLine != nil); + AS::MutexLocker l(__instanceLock__); + ASTextLayout *layout = [self locked_textLayoutForSize:constrainedSize.max]; + return !NSEqualRanges(layout.visibleRange, layout.range); } - (ASTextLayout *)locked_textLayoutForSize:(CGSize)size { - ASTextContainer *container = [_textContainer copy]; - container.size = size; - return ASTextNodeCompatibleLayoutWithContainerAndText(container, _attributedText); + ASTextContainer *container; + if (!CGSizeEqualToSize(_textContainer.size, size)) { + container = [_textContainer copy]; + container.size = size; + [container makeImmutable]; + } else { + container = _textContainer; + } + NSMutableAttributedString *mutableText = [_attributedText mutableCopy]; + [self prepareAttributedString:mutableText isForIntrinsicSize:NO]; + return ASTextNodeCompatibleLayoutWithContainerAndText(container, mutableText); } - (NSUInteger)maximumNumberOfLines @@ -1374,7 +1701,7 @@ - (NSUInteger)maximumNumberOfLines - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); if (ASCompareAssign(_textContainer.maximumNumberOfRows, maximumNumberOfLines)) { [self setNeedsDisplay]; } @@ -1382,16 +1709,15 @@ - (void)setMaximumNumberOfLines:(NSUInteger)maximumNumberOfLines - (NSUInteger)lineCount { - ASLockScopeSelf(); - AS_TEXT_ALERT_UNIMPLEMENTED_FEATURE(); - return 0; + MutexLocker l(__instanceLock__); + return ASTextNodeCompatibleLayoutWithContainerAndText(_textContainer, _attributedText).rowCount; } #pragma mark - Truncation Message - (void)_invalidateTruncationText { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); [self _locked_invalidateTruncationText]; [self setNeedsDisplay]; } @@ -1407,7 +1733,7 @@ - (void)_locked_invalidateTruncationText */ - (NSRange)_additionalTruncationMessageRangeWithVisibleRange:(NSRange)visibleRange { - ASLockScopeSelf(); + MutexLocker l(__instanceLock__); // Check if we even have an additional truncation message. if (!_additionalTruncationMessage) { @@ -1441,36 +1767,8 @@ - (NSAttributedString *)_locked_composedTruncationText composedTruncationText = _truncationAttributedText; } else if (_additionalTruncationMessage != nil) { composedTruncationText = _additionalTruncationMessage; - } else { - composedTruncationText = DefaultTruncationAttributedString(); } - return [self _locked_prepareTruncationStringForDrawing:composedTruncationText]; -} - -/** - * - cleanses it of core text attributes so TextKit doesn't crash - * - Adds whole-string attributes so the truncation message matches the styling - * of the body text - */ -- (NSAttributedString *)_locked_prepareTruncationStringForDrawing:(NSAttributedString *)truncationString -{ - DISABLED_ASAssertLocked(__instanceLock__); - NSMutableAttributedString *truncationMutableString = [truncationString mutableCopy]; - // Grab the attributes from the full string - if (_attributedText.length > 0) { - NSAttributedString *originalString = _attributedText; - NSInteger originalStringLength = _attributedText.length; - // Add any of the original string's attributes to the truncation string, - // but don't overwrite any of the truncation string's attributes - NSDictionary *originalStringAttributes = [originalString attributesAtIndex:originalStringLength-1 effectiveRange:NULL]; - [truncationString enumerateAttributesInRange:NSMakeRange(0, truncationString.length) options:0 usingBlock: - ^(NSDictionary *attributes, NSRange range, BOOL *stop) { - NSMutableDictionary *futureTruncationAttributes = [originalStringAttributes mutableCopy]; - [futureTruncationAttributes addEntriesFromDictionary:attributes]; - [truncationMutableString setAttributes:futureTruncationAttributes range:range]; - }]; - } - return truncationMutableString; + return composedTruncationText; } #if AS_TEXTNODE2_RECORD_ATTRIBUTED_STRINGS @@ -1509,4 +1807,26 @@ - (BOOL)usingExperiment return YES; } +- (void)interfaceStateDidChange:(ASInterfaceState)newState fromState:(ASInterfaceState)oldState { + UpdateTextAttachmentForText(self.attributedText, ^(ASImageNode *imageNode) { + [imageNode exitInterfaceState:oldState]; + [imageNode enterInterfaceState:newState]; + }); + [super interfaceStateDidChange:newState fromState:oldState]; +} + @end + +void UpdateTextAttachmentForText(NSAttributedString *attributedString, + TextAttachmentUpdateBlock updateBlock) { + [attributedString + enumerateAttribute:ASTextAttachmentAttributeName + inRange:NSMakeRange(0, attributedString.length) + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired + usingBlock:^(ASTextAttachment *asTextAttachment, NSRange range, BOOL *_Nonnull stop) { + if (ASImageNode *node = ASDynamicCast(asTextAttachment.content, ASImageNode)) { + updateBlock(node); + } + }]; +} + diff --git a/Source/AsyncDisplayKit.h b/Source/AsyncDisplayKit.h index ebe0c1a97..c8c664678 100644 --- a/Source/AsyncDisplayKit.h +++ b/Source/AsyncDisplayKit.h @@ -30,6 +30,8 @@ #import #import +#import +#import #import #import #import @@ -75,6 +77,7 @@ #import #import #import +#import #import #import #import @@ -108,6 +111,7 @@ #import #import #import +#import #import #import #import @@ -130,5 +134,9 @@ #import #import + +#import +#import +#import #import #import diff --git a/Source/Base/ASAssert.h b/Source/Base/ASAssert.h index 4445b2443..92c00476d 100644 --- a/Source/Base/ASAssert.h +++ b/Source/Base/ASAssert.h @@ -7,11 +7,14 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#pragma once +#import -#import +#import #import -#import +#import + +AS_ASSUME_NORETAIN_BEGIN +NS_ASSUME_NONNULL_BEGIN #if !defined(NS_BLOCK_ASSERTIONS) #define ASDISPLAYNODE_ASSERTIONS_ENABLED 1 @@ -19,6 +22,23 @@ #define ASDISPLAYNODE_ASSERTIONS_ENABLED 0 #endif +/** + * Check a precondition in production and return the given value if it fails. + * Note that this hides an early return! So always use cleanups or RAII objects + * rather than manual cleanup. + */ +#define AS_PRECONDITION(expr, __return_value, ...) \ + if (!AS_EXPECT(expr, 1)) { \ + ASDisplayNodeFailAssert(__VA_ARGS__); \ + return __return_value; \ + } + +#define AS_C_PRECONDITION(expr, __return_value, ...) \ + if (!AS_EXPECT(expr, 1)) { \ + ASDisplayNodeCFailAssert(__VA_ARGS__); \ + return __return_value; \ + } + /** * Note: In some cases it would be sufficient to do e.g.: * ASDisplayNodeAssert(...) NSAssert(__VA_ARGS__) @@ -67,6 +87,14 @@ #define ASDisplayNodeErrorDomain @"ASDisplayNodeErrorDomain" #define ASDisplayNodeNonFatalErrorCode 1 +NS_INLINE void ASAssertOnQueueIfIOS10(dispatch_queue_t q) { +#ifndef NS_BLOCK_ASSERTIONS + if (@available(iOS 10, tvOS 10, *)) { + dispatch_assert_queue(q); + } +#endif +} + /** * 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 @@ -99,3 +127,6 @@ ASDK_EXTERN void ASPopMainThreadAssertionsDisabled(void); } \ __evaluated; \ }) \ + +NS_ASSUME_NONNULL_END +AS_ASSUME_NORETAIN_END diff --git a/Source/Base/ASAssert.mm b/Source/Base/ASAssert.mm index 6a78b06ea..4c3a55a4e 100644 --- a/Source/Base/ASAssert.mm +++ b/Source/Base/ASAssert.mm @@ -7,22 +7,23 @@ // #import -#import #if AS_TLS_AVAILABLE -static _Thread_local int tls_mainThreadAssertionsDisabledCount; +static thread_local int gTlsCount; + BOOL ASMainThreadAssertionsAreDisabled() { - return tls_mainThreadAssertionsDisabledCount > 0; + return gTlsCount > 0; } void ASPushMainThreadAssertionsDisabled() { - tls_mainThreadAssertionsDisabledCount += 1; + gTlsCount += 1; } void ASPopMainThreadAssertionsDisabled() { - tls_mainThreadAssertionsDisabledCount -= 1; - ASDisplayNodeCAssert(tls_mainThreadAssertionsDisabledCount >= 0, @"Attempt to pop thread assertion-disabling without corresponding push."); + gTlsCount -= 1; + ASDisplayNodeCAssert(gTlsCount >= 0, + @"Attempt to pop thread assertion-disabling without corresponding push."); } #else diff --git a/Source/Base/ASAvailability.h b/Source/Base/ASAvailability.h index b210397fc..6fd5680bb 100644 --- a/Source/Base/ASAvailability.h +++ b/Source/Base/ASAvailability.h @@ -80,10 +80,6 @@ #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() #define AS_IG_LIST_KIT __has_include() #define AS_IG_LIST_DIFF_KIT __has_include() diff --git a/Source/Base/ASBaseDefines.h b/Source/Base/ASBaseDefines.h index 53ea66a03..fa2c49bb4 100644 --- a/Source/Base/ASBaseDefines.h +++ b/Source/Base/ASBaseDefines.h @@ -18,6 +18,29 @@ */ #define AS_CATEGORY_IMPLEMENTABLE +#define AS_EXPECT(x, v) __builtin_expect((x), (v)) + +/** + * AS_NORETAIN means nothing if the externally_retained attribute is not available. + * Individual arguments that MUST not be retained (e.g. calling from -dealloc) + * should be marked with AS_NORETAIN_ALWAYS to guarantee no retain. + * + * @note This annotation affects function *definitions*, not declarations, so + * put them in your .mm files for non-inline functions. + */ +#if __has_attribute(objc_externally_retained) +#define AS_NORETAIN __attribute__((objc_externally_retained)) +#define AS_ASSUME_NORETAIN_BEGIN \ + _Pragma("clang attribute push (__attribute__((objc_externally_retained)), apply_to=any(function,objc_method))") +#define AS_ASSUME_NORETAIN_END _Pragma("clang attribute pop") +#define AS_NORETAIN_ALWAYS +#else +#define AS_ASSUME_NORETAIN_BEGIN +#define AS_ASSUME_NORETAIN_END +#define AS_NORETAIN +#define AS_NORETAIN_ALWAYS unowned +#endif + #ifdef __GNUC__ # define ASDISPLAYNODE_GNUC(major, minor) \ (__GNUC__ > (major) || (__GNUC__ == (major) && __GNUC_MINOR__ >= (minor))) @@ -128,6 +151,11 @@ #define AS_ARRAY_SIZE(x) (sizeof(x) / sizeof(x[0])) +// https://github.com/abseil/abseil-cpp/blob/d659fe54b35ab9b8e35c72e50a4b8814167d5a84/absl/base/optimization.h#L174 +#define AS_PREDICT_FALSE(x) (__builtin_expect(x, 0)) +#define AS_PREDICT_TRUE(x) (__builtin_expect(false || (x), true)) + +#define ASCheckFlag(x, v) ((x & v) != 0) #define ASCreateOnce(expr) ({ \ static dispatch_once_t onceToken; \ static __typeof__(expr) staticVar; \ diff --git a/Source/Base/ASDisplayNode+Ancestry.h b/Source/Base/ASDisplayNode+Ancestry.h index 2c61cee00..6f5b729f9 100644 --- a/Source/Base/ASDisplayNode+Ancestry.h +++ b/Source/Base/ASDisplayNode+Ancestry.h @@ -50,6 +50,8 @@ NS_ASSUME_NONNULL_BEGIN */ @property (copy, readonly) NSString *ancestryDescription; +- (ASDisplayNode *)firstNonLayerNode; + @end NS_ASSUME_NONNULL_END diff --git a/Source/Base/ASDisplayNode+Ancestry.mm b/Source/Base/ASDisplayNode+Ancestry.mm index 7003064c7..2576c23f7 100644 --- a/Source/Base/ASDisplayNode+Ancestry.mm +++ b/Source/Base/ASDisplayNode+Ancestry.mm @@ -87,4 +87,13 @@ - (NSString *)ancestryDescription return strings.description; } +- (ASDisplayNode *)firstNonLayerNode { + for (ASDisplayNode *node in self.supernodesIncludingSelf) { + if (!node.isLayerBacked) { + return node; + } + } + return nil; +} + @end diff --git a/Source/Base/ASEqualityHelpers.h b/Source/Base/ASEqualityHelpers.h index 602459547..a34c68b6d 100644 --- a/Source/Base/ASEqualityHelpers.h +++ b/Source/Base/ASEqualityHelpers.h @@ -15,7 +15,6 @@ @param otherObj The second object in the comparison. Can be nil. @result YES if the objects are equal, including cases where both object are nil. */ -ASDISPLAYNODE_INLINE BOOL ASObjectIsEqual(id obj, id otherObj) -{ +ASDISPLAYNODE_INLINE BOOL ASObjectIsEqual(id obj, id otherObj) { return obj == otherObj || [obj isEqual:otherObj]; } diff --git a/Source/Base/ASSignpost.h b/Source/Base/ASSignpost.h index 27a44afbb..7c299afe7 100644 --- a/Source/Base/ASSignpost.h +++ b/Source/Base/ASSignpost.h @@ -12,13 +12,18 @@ typedef NS_ENUM(uint32_t, ASSignpostName) { // Collection/Table ASSignpostDataControllerBatch = 300, // Alloc/layout nodes before collection update. ASSignpostRangeControllerUpdate, // Ranges update pass. - + ASSignpostDataControllerUpdate, // Data controller update. + ASSignpostCollectionPrepareLayout, // CollectionViewLayout -prepareLayout + // Rendering ASSignpostLayerDisplay = 325, // Client display callout. ASSignpostRunLoopQueueBatch, // One batch of ASRunLoopQueue. // Layout ASSignpostCalculateLayout = 350, // Start of calculateLayoutThatFits to end. Max 1 per thread. + ASSignpostMeasure, // Start of measure to end. + ASSignpostMeasureText, // Text layout cache miss. + ASSignpostRemeasureCollection, // Rotation/bounds change remeasure batch. // Misc ASSignpostDeallocQueueDrain = 375, // One chunk of dealloc queue work. arg0 is count. diff --git a/Source/Details/ASCollectionElement.h b/Source/Details/ASCollectionElement.h index b128dd66f..819105ebf 100644 --- a/Source/Details/ASCollectionElement.h +++ b/Source/Details/ASCollectionElement.h @@ -15,8 +15,9 @@ NS_ASSUME_NONNULL_BEGIN +/// Copy returns self. Only here for dictionary use. AS_SUBCLASSING_RESTRICTED -@interface ASCollectionElement : NSObject +@interface ASCollectionElement : NSObject @property (nullable, nonatomic, copy, readonly) NSString *supplementaryElementKind; @property (nonatomic) ASSizeRange constrainedSize; diff --git a/Source/Details/ASCollectionElement.mm b/Source/Details/ASCollectionElement.mm index 9f9a05c5c..2d6bfc2d5 100644 --- a/Source/Details/ASCollectionElement.mm +++ b/Source/Details/ASCollectionElement.mm @@ -84,4 +84,9 @@ - (void)setTraitCollection:(ASPrimitiveTraitCollection)traitCollection } } +- (id)copyWithZone:(NSZone *)zone +{ + return self; +} + @end diff --git a/Source/Details/ASCollectionInternal.h b/Source/Details/ASCollectionInternal.h index 42f651ae6..410776493 100644 --- a/Source/Details/ASCollectionInternal.h +++ b/Source/Details/ASCollectionInternal.h @@ -28,11 +28,36 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic, nullable, readonly) _ASHierarchyChangeSet *changeSet; +/** + * @see ASCollectionNode+Beta.h for full documentation. + */ +@property (nonatomic) BOOL remeasuresBeforeLayoutPassOnBoundsChange; + /** * @see ASCollectionNode+Beta.h for full documentation. */ @property (nonatomic) ASCellLayoutMode cellLayoutMode; +/** + * @see ASCollectionNode+Beta.h for full documentation. + */ +@property(nonatomic) BOOL immediatelyApplyComputedLayouts; + +/** + * @see ASCollectionNode+Beta.h for full documentation. + */ +@property(nonatomic) NSUInteger updateBatchSize; + +/** + * @see ASCollectionNode+Beta.h for full documentation. + */ +@property (nonatomic) BOOL useNodeCache; + +/** + * @see ASCollectionNode+Beta.h for full documentation. + */ +@property(nonatomic) BOOL allowAsyncUpdatesForInitialContent; + /** * Attempt to get the view-layer index path for the item with the given index path. * diff --git a/Source/Details/ASCollectionViewLayoutInspector.mm b/Source/Details/ASCollectionViewLayoutInspector.mm index 1747c4642..c556d45a4 100644 --- a/Source/Details/ASCollectionViewLayoutInspector.mm +++ b/Source/Details/ASCollectionViewLayoutInspector.mm @@ -23,8 +23,10 @@ ASSizeRange NodeConstrainedSizeForScrollDirection(ASCollectionView *collectionVi if (ASScrollDirectionContainsHorizontalDirection(collectionView.scrollableDirections)) { maxSize.width = CGFLOAT_MAX; maxSize.height -= (contentInset.top + contentInset.bottom); + maxSize.height = MAX(maxSize.height, 0); } else { maxSize.width -= (contentInset.left + contentInset.right); + maxSize.width = MAX(maxSize.width, 0); maxSize.height = CGFLOAT_MAX; } return ASSizeRangeMake(CGSizeZero, maxSize); diff --git a/Source/Details/ASDataController.h b/Source/Details/ASDataController.h index 88150cba0..59bb75a9c 100644 --- a/Source/Details/ASDataController.h +++ b/Source/Details/ASDataController.h @@ -18,6 +18,22 @@ NS_ASSUME_NONNULL_BEGIN +/** + * Get and Set ASDataController to use: + * a) GCD dispatch queues' own autorelease pools and + * b) explicit autorelease pool on Texture background queue + */ +BOOL ASGetEnableAutoreleasePoolInQueues(void); +void ASSetEnableAutoreleasePoolInQueues(BOOL enable); + +/** + * Get and Set ASDataController to avoid priority inversion: + * a) use dispatch_block_wait on empty block in serial edit queue. + * b) use dispatch_group_wait for edit dispatch_group + */ +BOOL ASGetRemovePriorityInversion(void); +void ASSetRemovePriorityInversion(BOOL enable); + @class ASCellNode; @class ASCollectionElement; @class ASCollectionLayoutContext; @@ -77,6 +93,8 @@ ASDK_EXTERN NSString * const ASCollectionInvalidUpdateException; - (BOOL)dataController:(ASDataController *)dataController shouldEagerlyLayoutNode:(ASCellNode *)node; - (BOOL)dataControllerShouldSerializeNodeCreation:(ASDataController *)dataController; +- (CGRect)dataControllerFrameForDebugging:(ASDataController *)dataController; + @optional /** @@ -201,6 +219,21 @@ ASDK_EXTERN NSString * const ASCollectionInvalidUpdateException; */ @property (nonatomic, weak) id layoutDelegate; +/** + * See ASCollectionNode+Beta.h for full documentation. + */ +@property (nonatomic) BOOL immediatelyApplyComputedLayouts; + +/** + * See ASCollectionNode+Beta.h for full documentation. + */ +@property (nonatomic) NSUInteger updateBatchSize; + +/** + * See ASCollectionNode+Beta.h for full documentation. + */ +@property (nonatomic) BOOL useNodeCache; + #ifdef __cplusplus /** * Returns the most recently gathered item counts from the data source. If the counts diff --git a/Source/Details/ASDataController.mm b/Source/Details/ASDataController.mm index 10442da62..d9b7a3e13 100644 --- a/Source/Details/ASDataController.mm +++ b/Source/Details/ASDataController.mm @@ -16,7 +16,9 @@ #import #import #import +#import #import +#import #import #import #import @@ -35,6 +37,7 @@ const static char * kASDataControllerEditingQueueKey = "kASDataControllerEditingQueueKey"; const static char * kASDataControllerEditingQueueContext = "kASDataControllerEditingQueueContext"; +using namespace AS; NSString * const ASDataControllerRowNodeKind = @"_ASDataControllerRowNodeKind"; NSString * const ASCollectionInvalidUpdateException = @"ASCollectionInvalidUpdateException"; @@ -43,6 +46,41 @@ typedef void (^ASDataControllerSynchronizationBlock)(); +BOOL gEnableAutoreleasePoolInQueues = NO; +BOOL ASGetEnableAutoreleasePoolInQueues(void) { return gEnableAutoreleasePoolInQueues; } +void ASSetEnableAutoreleasePoolInQueues(BOOL enable) { gEnableAutoreleasePoolInQueues = enable; } + +BOOL gRemovePriorityInversion = NO; +BOOL ASGetRemovePriorityInversion(void) { return gRemovePriorityInversion; } +void ASSetRemovePriorityInversion(BOOL enable) { gRemovePriorityInversion = enable; } + +static NSCache *NodeCache() +{ + ASDisplayNodeCAssertMainThread(); + static constexpr NSTimeInterval kNodeCacheFlushDelay = 3.0; + static constexpr NSTimeInterval kNodeCacheFlushLeeway = 1.0; + + static NSCache *nodeCache; + static dispatch_source_t flushTimer; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + nodeCache = [[NSCache alloc] init]; + nodeCache.name = @"org.TextureGroup.Texture.nodeCache"; + flushTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, + dispatch_get_global_queue(QOS_CLASS_BACKGROUND, 0)); + dispatch_source_set_event_handler(flushTimer, ^{ + [nodeCache removeAllObjects]; + }); + }); + + // On each access, we delay the flush. + dispatch_source_set_timer(flushTimer, + dispatch_time(DISPATCH_TIME_NOW, kNodeCacheFlushDelay * NSEC_PER_SEC), + DISPATCH_TIME_FOREVER, kNodeCacheFlushLeeway * NSEC_PER_SEC); + dispatch_activate(flushTimer); + return nodeCache; +} + @interface ASDataController () { id _layoutDelegate; @@ -89,14 +127,14 @@ - (instancetype)initWithDataSource:(id)dataSource node:( _node = node; _dataSource = dataSource; - _dataSourceFlags.supplementaryNodeKindsInSections = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeKindsInSections:)]; - _dataSourceFlags.supplementaryNodesOfKindInSection = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodesOfKind:inSection:)]; - _dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:supplementaryNodeBlockOfKind:atIndexPath:shouldAsyncLayout:)]; - _dataSourceFlags.constrainedSizeForNodeAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForNodeAtIndexPath:)]; - _dataSourceFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath = [_dataSource respondsToSelector:@selector(dataController:constrainedSizeForSupplementaryNodeOfKind:atIndexPath:)]; - _dataSourceFlags.contextForSection = [_dataSource respondsToSelector:@selector(dataController:contextForSection:)]; - - self.visibleMap = self.pendingMap = [[ASElementMap alloc] init]; + _dataSourceFlags.supplementaryNodeKindsInSections = [dataSource respondsToSelector:@selector(dataController:supplementaryNodeKindsInSections:)]; + _dataSourceFlags.supplementaryNodesOfKindInSection = [dataSource respondsToSelector:@selector(dataController:supplementaryNodesOfKind:inSection:)]; + _dataSourceFlags.supplementaryNodeBlockOfKindAtIndexPath = [dataSource respondsToSelector:@selector(dataController:supplementaryNodeBlockOfKind:atIndexPath:shouldAsyncLayout:)]; + _dataSourceFlags.constrainedSizeForNodeAtIndexPath = [dataSource respondsToSelector:@selector(dataController:constrainedSizeForNodeAtIndexPath:)]; + _dataSourceFlags.constrainedSizeForSupplementaryNodeOfKindAtIndexPath = [dataSource respondsToSelector:@selector(dataController:constrainedSizeForSupplementaryNodeOfKind:atIndexPath:)]; + _dataSourceFlags.contextForSection = [dataSource respondsToSelector:@selector(dataController:contextForSection:)]; + + _visibleMap = _pendingMap = [[ASElementMap alloc] init]; _nextSectionID = 0; @@ -104,10 +142,15 @@ - (instancetype)initWithDataSource:(id)dataSource node:( _synchronized = YES; _onDidFinishSynchronizingBlocks = [[NSMutableSet alloc] init]; - - const char *queueName = [[NSString stringWithFormat:@"org.AsyncDisplayKit.ASDataController.editingTransactionQueue:%p", self] cStringUsingEncoding:NSASCIIStringEncoding]; - _editingTransactionQueue = dispatch_queue_create(queueName, DISPATCH_QUEUE_SERIAL); - dispatch_queue_set_specific(_editingTransactionQueue, &kASDataControllerEditingQueueKey, &kASDataControllerEditingQueueContext, NULL); + + dispatch_queue_attr_t queueAttributes = DISPATCH_QUEUE_SERIAL; + if (AS_AVAILABLE_IOS_TVOS(10, 10)) { + if (gEnableAutoreleasePoolInQueues) { + queueAttributes = DISPATCH_QUEUE_SERIAL_WITH_AUTORELEASE_POOL; + } + } + _editingTransactionQueue = + dispatch_queue_create("org.TextureGroup.editingTransactionQueue", queueAttributes); _editingTransactionGroup = dispatch_group_create(); return self; @@ -140,6 +183,8 @@ - (void)setLayoutDelegate:(id)layoutDelegate - (void)_allocateNodesFromElements:(NSArray *)elements strictlyOnCurrentThread:(BOOL)strictlyOnCurrentThread { + ASAssertOnQueueIfIOS10(_editingTransactionQueue); + NSUInteger nodeCount = elements.count; __weak id weakDataSource = _dataSource; if (nodeCount == 0 || weakDataSource == nil) { @@ -150,7 +195,7 @@ - (void)_allocateNodesFromElements:(NSArray *)elements { as_activity_create_for_scope("Data controller batch"); - + BOOL immediatelyApplyComputedLayouts = _immediatelyApplyComputedLayouts; void(^work)(size_t) = ^(size_t i) { __strong id strongDataSource = weakDataSource; if (strongDataSource == nil) { @@ -168,7 +213,9 @@ - (void)_allocateNodesFromElements:(NSArray *)elements // Layout the node if the size range is valid. ASSizeRange sizeRange = element.constrainedSize; if (ASSizeRangeHasSignificantArea(sizeRange)) { - [self _layoutNode:node withConstrainedSize:sizeRange]; + [self _layoutNode:node + withConstrainedSize:sizeRange + immediatelyApply:immediatelyApplyComputedLayouts]; } }; @@ -192,8 +239,10 @@ - (void)_allocateNodesFromElements:(NSArray *)elements /** * Measure and layout the given node with the constrained size range. */ -- (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrainedSize -{ +- (void)_layoutNode:(ASCellNode *)node + withConstrainedSize:(ASSizeRange)constrainedSize + immediatelyApply:(BOOL)immediatelyApply { + // Note: Method may be called on main or background. if (![_dataSource dataController:self shouldEagerlyLayoutNode:node]) { return; } @@ -201,8 +250,26 @@ - (void)_layoutNode:(ASCellNode *)node withConstrainedSize:(ASSizeRange)constrai ASDisplayNodeAssert(ASSizeRangeHasSignificantArea(constrainedSize), @"Attempt to layout cell node with invalid size range %@", NSStringFromASSizeRange(constrainedSize)); CGRect frame = CGRectZero; - frame.size = [node layoutThatFits:constrainedSize].size; + frame.size = [node measure:constrainedSize]; node.frame = frame; + + /** + * We need to hold the lock between checking if loaded and laying out. Unfortunately, __layout + * expects to be called WITHOUT the lock held and in fact does not hold the lock during layout + * i.e. it locks and then unlocks before calling deeper down to do its work. So there is an + * unavoidable race condition here in theory, but in practice it's still worth experimenting with + * because: + * - If this is the first layout (after allocation) then the only way the view could get loaded + * out from under us is if they, inside their node -init, dispatch_async to main and load the + * view, which is bizarre. + * - If this is a subsequent layout (say, rotation,) then we are being run synchronously + * concurrently _from_ the main thread so the node can't be loaded out from under us. + */ + if (immediatelyApply) { + if (!node.nodeLoaded) { + [node __layout]; + } + } } #pragma mark - Data Source Access (Calling _dataSource) @@ -257,7 +324,6 @@ - (void)_repopulateSupplementaryNodesIntoMap:(ASMutableElementMap *)map traitCollection:(ASPrimitiveTraitCollection)traitCollection indexPathsAreNew:(BOOL)indexPathsAreNew shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges - previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); @@ -279,7 +345,7 @@ - (void)_repopulateSupplementaryNodesIntoMap:(ASMutableElementMap *)map } for (NSString *kind in [self supplementaryKindsInSections:newSections]) { - [self _insertElementsIntoMap:map kind:kind forSections:newSections traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + [self _insertElementsIntoMap:map kind:kind forSections:newSections traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet]; } } @@ -293,7 +359,6 @@ - (void)_repopulateSupplementaryNodesIntoMap:(ASMutableElementMap *)map - (void)_updateSupplementaryNodesIntoMap:(ASMutableElementMap *)map traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges - previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); if (self.layoutDelegate != nil) { @@ -311,7 +376,7 @@ - (void)_updateSupplementaryNodesIntoMap:(ASMutableElementMap *)map // If supplementary node does exist and size is now zero, remove it. // If supplementary node doesn't exist and size is now non-zero, insert one. for (NSIndexPath *indexPath in indexPaths) { - ASCollectionElement *previousElement = [previousMap supplementaryElementOfKind:kind atIndexPath:indexPath]; + ASCollectionElement *previousElement = [_pendingMap supplementaryElementOfKind:kind atIndexPath:indexPath]; newSizeRange = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPath]; BOOL sizeRangeIsZero = ASSizeRangeEqualToSizeRange(ASSizeRangeZero, newSizeRange); if (previousElement != nil && sizeRangeIsZero) { @@ -322,7 +387,7 @@ - (void)_updateSupplementaryNodesIntoMap:(ASMutableElementMap *)map } [map removeSupplementaryElementsAtIndexPaths:indexPathsToDeleteForKind kind:kind]; - [self _insertElementsIntoMap:map kind:kind atIndexPaths:indexPathsToInsertForKind traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:nil previousMap:previousMap]; + [self _insertElementsIntoMap:map kind:kind atIndexPaths:indexPathsToInsertForKind traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:nil]; } } } @@ -341,7 +406,6 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges changeSet:(_ASHierarchyChangeSet *)changeSet - previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); @@ -350,7 +414,7 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map } NSArray *indexPaths = [self _allIndexPathsForItemsOfKind:kind inSections:sections]; - [self _insertElementsIntoMap:map kind:kind atIndexPaths:indexPaths traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + [self _insertElementsIntoMap:map kind:kind atIndexPaths:indexPaths traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet]; } /** @@ -368,7 +432,6 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges changeSet:(_ASHierarchyChangeSet *)changeSet - previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); @@ -386,17 +449,28 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map id dataSource = self.dataSource; id node = self.node; BOOL shouldAsyncLayout = YES; + NSCache *nodeCache = _useNodeCache ? NodeCache() : nil; for (NSIndexPath *indexPath in indexPaths) { ASCellNodeBlock nodeBlock; id nodeModel; if (isRowKind) { nodeModel = [dataSource dataController:self nodeModelForItemAtIndexPath:indexPath]; - + // Attempt to use node cache. + if (nodeModel && nodeCache) { + if (ASCellNode *node = [nodeCache objectForKey:nodeModel]) { + if ([node canUpdateToNodeModel:nodeModel]) { + [nodeCache removeObjectForKey:nodeModel]; + nodeBlock = ^{ + return node; + }; + } + } + } // Get the prior element and attempt to update the existing cell node. - if (nodeModel != nil && !changeSet.includesReloadData) { + if (!nodeBlock && nodeModel != nil && !changeSet.includesReloadData) { NSIndexPath *oldIndexPath = [changeSet oldIndexPathForNewIndexPath:indexPath]; if (oldIndexPath != nil) { - ASCollectionElement *oldElement = [previousMap elementForItemAtIndexPath:oldIndexPath]; + ASCollectionElement *oldElement = [_pendingMap elementForItemAtIndexPath:oldIndexPath]; ASCellNode *oldNode = oldElement.node; if ([oldNode canUpdateToNodeModel:nodeModel]) { // Just wrap the node in a block. The collection element will -setNodeModel: @@ -425,7 +499,9 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map owningNode:node traitCollection:traitCollection]; [map insertElement:element atIndexPath:indexPath]; - changeSet.countForAsyncLayout += (shouldAsyncLayout ? 1 : 0); + if (shouldAsyncLayout) { + [changeSet incrementCountForAsyncLayout]; + } } } @@ -499,7 +575,8 @@ - (void)waitUntilAllUpdatesAreProcessed - (BOOL)isProcessingUpdates { ASDisplayNodeAssertMainThread(); - return _mainSerialQueue.numberOfScheduledBlocks > 0 || _editingTransactionGroupCount > 0; + BOOL doneEditing = !dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_NOW); + return !doneEditing || _mainSerialQueue.numberOfScheduledBlocks > 0; } - (void)onDidFinishProcessingUpdates:(void (^)())completion @@ -544,9 +621,13 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet ASDisplayNodeAssertMainThread(); _synchronized = NO; - + ASSignpostStart(DataControllerUpdate, self, "%@ %@ %@", + ASObjectDescriptionMakeTiny(self.dataSource), + NSStringFromCGRect([self.dataSource dataControllerFrameForDebugging:self]), + changeSet); [changeSet addCompletionHandler:^(BOOL finished) { self->_synchronized = YES; + ASSignpostEnd(DataControllerUpdate, self, ""); [self onDidFinishProcessingUpdates:^{ if (self->_synchronized) { for (ASDataControllerSynchronizationBlock block in self->_onDidFinishSynchronizingBlocks) { @@ -556,7 +637,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet } }]; }]; - + if (changeSet.includesReloadData) { if (_initialReloadDataHasBeenCalled) { os_log_debug(ASCollectionLog(), "reloadData %@", ASViewToDisplayNode(ASDynamicCast(self.dataSource, UIView))); @@ -572,7 +653,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet NSTimeInterval transactionQueueFlushDuration = 0.0f; { AS::ScopeTimer t(transactionQueueFlushDuration); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + [self _drainEditingQueue]; } } @@ -601,6 +682,19 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet } } + /// Take all deleted nodes and put them in the cache for potential reuse. + if (_useNodeCache) { + NSCache *cache = NodeCache(); + for (const auto &indexPath : changeSet.indexPathsForRemovedItems) { + ASCollectionElement *element = [_pendingMap elementForItemAtIndexPath:indexPath]; + if (id model = element.nodeModel) { + if (ASCellNode *node = element.nodeIfAllocated) { + [cache setObject:node forKey:model]; + } + } + } + } + BOOL canDelegate = (self.layoutDelegate != nil); ASElementMap *newMap; ASCollectionLayoutContext *layoutContext; @@ -608,18 +702,17 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet as_activity_scope(as_activity_create("Latch new data for collection update", changeSet.rootActivity, OS_ACTIVITY_FLAG_DEFAULT)); // Step 1: Populate a new map that reflects the data source's state and use it as pendingMap - ASElementMap *previousMap = self.pendingMap; if (changeSet.isEmpty) { // If the change set is empty, nothing has changed so we can just reuse the previous map - newMap = previousMap; + newMap = _pendingMap; } else { // Mutable copy of current data. - ASMutableElementMap *mutableMap = [previousMap mutableCopy]; + ASMutableElementMap *mutableMap = [_pendingMap mutableCopy]; // Step 1.1: Update the mutable copies to match the data source's state [self _updateSectionsInMap:mutableMap changeSet:changeSet]; ASPrimitiveTraitCollection existingTraitCollection = [self.node primitiveTraitCollection]; - [self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegate) previousMap:previousMap]; + [self _updateElementsInMap:mutableMap changeSet:changeSet traitCollection:existingTraitCollection shouldFetchSizeRanges:(! canDelegate)]; // Step 1.2: Clone the new data newMap = [mutableMap copy]; @@ -630,14 +723,65 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet if (canDelegate) { layoutContext = [self.layoutDelegate layoutContextWithElements:newMap]; } + + changeSet.dataLatched = YES; + } + + BOOL synchronous = [_dataSource dataController:self shouldSynchronouslyProcessChangeSet:changeSet]; + NSUInteger batchSize = _updateBatchSize; + if (synchronous || changeSet.countForAsyncLayout < batchSize) { + batchSize = 0; } + if (batchSize > 0) { + const std::vector<_ASHierarchyChangeSet *> segments = [changeSet divideIntoSegmentsOfMaximumSize:batchSize]; + // We need to form intermediary maps that will be committed at the end of each segment. + // The last one is obvious – the end state of the entire update. + // For the others, take the next one and remove all the content that is to be added in the next segment. + std::vector intermediaryMaps; + intermediaryMaps.resize(segments.size(), nil); + intermediaryMaps[segments.size() - 1] = newMap; // End of last segment = end of whole batch. + ASMutableElementMap *mutableIntermediaryMap = [newMap mutableCopy]; + + // Form the intermediary maps by walking backward from the end state, removing content added in + // the subsequent segment. Ignore the last (it is the end state, and we set it above.) + for (int i = (int)segments.size() - 2; i >= 0; i--) { + [mutableIntermediaryMap removeContentAddedInChangeSet:segments[i + 1]]; + intermediaryMaps[i] = [mutableIntermediaryMap copy]; + } + // Now fire off each segment, targeting each intermediary map we formed above. + for (size_t i = 0; i < segments.size(); i++) { + [self _scheduleUpdateWithChangeSet:segments[i] newMap:intermediaryMaps[i] context:layoutContext]; + } + } else { + [self _scheduleUpdateWithChangeSet:changeSet newMap:newMap context:layoutContext]; + } + + // We've now dispatched node allocation and layout to a concurrent background queue. + // In some cases, it's advantageous to prevent the main thread from returning, to ensure the next + // frame displayed to the user has the view updates in place. Doing this does slightly reduce + // total latency, by donating the main thread's priority to the background threads. As such, the + // two cases where it makes sense to block: + // 1. There is very little work to be performed in the background (UIKit passthrough) + // 2. There is a higher priority on display latency than smoothness, e.g. app startup. + if (synchronous) { + [self waitUntilAllUpdatesAreProcessed]; + } +} + +- (void)_scheduleUpdateWithChangeSet:(_ASHierarchyChangeSet *)changeSet + newMap:(ASElementMap *)newMap + context:(ASCollectionLayoutContext *)layoutContext { + ASDisplayNodeAssertMainThread(); os_log_debug(ASCollectionLog(), "New content: %@", newMap.smallDescription); + BOOL canDelegate = (self.layoutDelegate != nil); Class layoutDelegateClass = [self.layoutDelegate class]; // Step 3: Call the layout delegate if possible. Otherwise, allocate and layout all elements void (^step3)(BOOL) = ^(BOOL strictlyOnCurrentThread){ + __block __unused os_activity_scope_state_s preparationScope = {}; // unused if deployment target < iOS10 + as_activity_scope_enter(as_activity_create("Prepare nodes for collection update", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT), &preparationScope); if (canDelegate) { // Don't pass strictlyOnCurrentThread to the layout delegate. Instead give it // total control over its threading behavior, as long as it blocks the @@ -650,7 +794,7 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet if (nodeIfAllocated.shouldUseUIKitCell) { // If the node exists and we know it is a passthrough cell, we know it will never have a .calculatedLayout. continue; - } else if (nodeIfAllocated.calculatedLayout == nil) { + } else if (CGSizeEqualToSize(nodeIfAllocated.calculatedSize, CGSizeZero)) { // If the node hasn't been allocated, or it doesn't have a valid layout, let's process it. [elementsToProcess addObject:element]; } @@ -700,17 +844,6 @@ - (void)updateWithChangeSet:(_ASHierarchyChangeSet *)changeSet }]; --self->_editingTransactionGroupCount; }); - - // We've now dispatched node allocation and layout to a concurrent background queue. - // In some cases, it's advantageous to prevent the main thread from returning, to ensure the next - // frame displayed to the user has the view updates in place. Doing this does slightly reduce - // total latency, by donating the main thread's priority to the background threads. As such, the - // two cases where it makes sense to block: - // 1. There is very little work to be performed in the background (UIKit passthrough) - // 2. There is a higher priority on display latency than smoothness, e.g. app startup. - if ([_dataSource dataController:self shouldSynchronouslyProcessChangeSet:changeSet]) { - [self waitUntilAllUpdatesAreProcessed]; - } } /** @@ -761,7 +894,6 @@ - (void)_updateElementsInMap:(ASMutableElementMap *)map changeSet:(_ASHierarchyChangeSet *)changeSet traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges - previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); @@ -771,7 +903,7 @@ - (void)_updateElementsInMap:(ASMutableElementMap *)map NSUInteger sectionCount = [self itemCountsFromDataSource].size(); if (sectionCount > 0) { NSIndexSet *sectionIndexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, sectionCount)]; - [self _insertElementsIntoMap:map sections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + [self _insertElementsIntoMap:map sections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet]; } // Return immediately because reloadData can't be used in conjuntion with other updates. return; @@ -787,8 +919,7 @@ - (void)_updateElementsInMap:(ASMutableElementMap *)map changeSet:changeSet traitCollection:traitCollection indexPathsAreNew:NO - shouldFetchSizeRanges:shouldFetchSizeRanges - previousMap:previousMap]; + shouldFetchSizeRanges:shouldFetchSizeRanges]; } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeDelete]) { @@ -797,18 +928,17 @@ - (void)_updateElementsInMap:(ASMutableElementMap *)map } for (_ASHierarchySectionChange *change in [changeSet sectionChangesOfType:_ASHierarchyChangeTypeInsert]) { - [self _insertElementsIntoMap:map sections:change.indexSet traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + [self _insertElementsIntoMap:map sections:change.indexSet traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet]; } for (_ASHierarchyItemChange *change in [changeSet itemChangesOfType:_ASHierarchyChangeTypeInsert]) { - [self _insertElementsIntoMap:map kind:ASDataControllerRowNodeKind atIndexPaths:change.indexPaths traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + [self _insertElementsIntoMap:map kind:ASDataControllerRowNodeKind atIndexPaths:change.indexPaths traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet]; // Aggressively reload supplementary nodes (#1773 & #1629) [self _repopulateSupplementaryNodesIntoMap:map forSectionsContainingIndexPaths:change.indexPaths changeSet:changeSet traitCollection:traitCollection indexPathsAreNew:YES - shouldFetchSizeRanges:shouldFetchSizeRanges - previousMap:previousMap]; + shouldFetchSizeRanges:shouldFetchSizeRanges]; } } @@ -817,7 +947,6 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map traitCollection:(ASPrimitiveTraitCollection)traitCollection shouldFetchSizeRanges:(BOOL)shouldFetchSizeRanges changeSet:(_ASHierarchyChangeSet *)changeSet - previousMap:(ASElementMap *)previousMap { ASDisplayNodeAssertMainThread(); @@ -827,12 +956,12 @@ - (void)_insertElementsIntoMap:(ASMutableElementMap *)map // Items [map insertEmptySectionsOfItemsAtIndexes:sectionIndexes]; - [self _insertElementsIntoMap:map kind:ASDataControllerRowNodeKind forSections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + [self _insertElementsIntoMap:map kind:ASDataControllerRowNodeKind forSections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet]; // Supplementaries for (NSString *kind in [self supplementaryKindsInSections:sectionIndexes]) { // Step 2: Populate new elements for all sections - [self _insertElementsIntoMap:map kind:kind forSections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet previousMap:previousMap]; + [self _insertElementsIntoMap:map kind:kind forSections:sectionIndexes traitCollection:traitCollection shouldFetchSizeRanges:shouldFetchSizeRanges changeSet:changeSet]; } } @@ -848,22 +977,22 @@ - (void)relayoutNodes:(id)nodes nodesSizeChanged:(NSMutableAr return; } - id dataSource = self.dataSource; - const auto visibleMap = self.visibleMap; - const auto pendingMap = self.pendingMap; + id dataSource = _dataSource; for (ASCellNode *node in nodes) { const auto element = node.collectionElement; - NSIndexPath *indexPathInPendingMap = [pendingMap indexPathForElement:element]; + NSIndexPath *indexPathInPendingMap = [_pendingMap indexPathForElement:element]; // Ensure the element is present in both maps or skip it. If it's not in the visible map, // then we can't check the presented size. If it's not in the pending map, we can't get the constrained size. // This will only happen if the element has been deleted, so the specifics of this behavior aren't important. - if (indexPathInPendingMap == nil || [visibleMap indexPathForElement:element] == nil) { + if (indexPathInPendingMap == nil || [_visibleMap indexPathForElement:element] == nil) { continue; } NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind; ASSizeRange constrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPathInPendingMap]; - [self _layoutNode:node withConstrainedSize:constrainedSize]; + [self _layoutNode:node + withConstrainedSize:constrainedSize + immediatelyApply:self.immediatelyApplyComputedLayouts]; BOOL matchesSize = [dataSource dataController:self presentedSizeForElement:element matchesSize:node.frame.size]; if (! matchesSize) { @@ -899,37 +1028,47 @@ - (void)_relayoutAllNodes ASDisplayNodeAssertMainThread(); // Aggressively repopulate all supplemtary elements // Assuming this method is run on the main serial queue, _pending and _visible maps are synced and can be manipulated directly. + // TODO: If there is a layout delegate, it should be able to handle relayouts. Verify that and bail early. ASDisplayNodeAssert(_visibleMap == _pendingMap, @"Expected visible and pending maps to be synchronized: %@", self); + ASSignpostStart(RemeasureCollection, self, "%@ %@ count: %d", + ASObjectDescriptionMakeTiny(self.dataSource), + NSStringFromCGRect([self.dataSource dataControllerFrameForDebugging:self]), + (int)_visibleMap.count); ASMutableElementMap *newMap = [_pendingMap mutableCopy]; [self _updateSupplementaryNodesIntoMap:newMap traitCollection:[self.node primitiveTraitCollection] - shouldFetchSizeRanges:YES - previousMap:_pendingMap]; - _pendingMap = [newMap copy]; - _visibleMap = _pendingMap; - - for (ASCollectionElement *element in _visibleMap) { - // Ignore this element if it is no longer in the latest data. It is still recognized in the UIKit world but will be deleted soon. - NSIndexPath *indexPathInPendingMap = [_pendingMap indexPathForElement:element]; - if (indexPathInPendingMap == nil) { - continue; - } - - NSString *kind = element.supplementaryElementKind ?: ASDataControllerRowNodeKind; - ASSizeRange newConstrainedSize = [self constrainedSizeForNodeOfKind:kind atIndexPath:indexPathInPendingMap]; - - if (ASSizeRangeHasSignificantArea(newConstrainedSize)) { - element.constrainedSize = newConstrainedSize; + shouldFetchSizeRanges:YES]; + self.pendingMap = self.visibleMap = [newMap copy]; + + // First update size constraints on the main thread. + NSDictionary *elementToIndexPath = + _visibleMap.elementToIndexPath; + [elementToIndexPath + enumerateKeysAndObjectsUsingBlock:^(ASCollectionElement *element, NSIndexPath *indexPath, + __unused BOOL *stop) { + element.constrainedSize = + [self constrainedSizeForNodeOfKind:(element.supplementaryElementKind + ?: ASDataControllerRowNodeKind) + atIndexPath:indexPath]; + }]; - // Node may not be allocated yet (e.g node virtualization or same size optimization) - // Call context.nodeIfAllocated here to avoid premature node allocation and layout - ASCellNode *node = element.nodeIfAllocated; - if (node) { - [self _layoutNode:node withConstrainedSize:newConstrainedSize]; - } - } - } + // Then concurrently synchronously ensure every node is measured against new constraints. + BOOL immediatelyApply = self.immediatelyApplyComputedLayouts; + [elementToIndexPath + enumerateKeysAndObjectsWithOptions:NSEnumerationConcurrent + usingBlock:^(ASCollectionElement *element, NSIndexPath *indexPath, + __unused BOOL *stop) { + const ASSizeRange sizeRange = element.constrainedSize; + if (ASSizeRangeHasSignificantArea(sizeRange)) { + if (ASCellNode *node = element.nodeIfAllocated) { + [self _layoutNode:node + withConstrainedSize:sizeRange + immediatelyApply:immediatelyApply]; + } + } + }]; + ASSignpostEnd(RemeasureCollection, self, ""); } # pragma mark - ASPrimitiveTraitCollection @@ -957,16 +1096,30 @@ - (void)clearData ASDisplayNodeAssertMainThread(); if (_initialReloadDataHasBeenCalled) { [self waitUntilAllUpdatesAreProcessed]; + // Always use the setters for these atomics, so that other threads get them safely. self.visibleMap = self.pendingMap = [[ASElementMap alloc] init]; } } # pragma mark - Helper methods +- (void)_drainEditingQueue +{ + ASDisplayNodeAssertMainThread(); + if (gRemovePriorityInversion) { + // dispatch_sync an empty block to the serial queue for the scheduler to resolve priority + // inversion automatically. b/168618264 + dispatch_sync(_editingTransactionQueue, ^{}); + } else { + dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + } +} + - (void)_scheduleBlockOnMainSerialQueue:(dispatch_block_t)block { ASDisplayNodeAssertMainThread(); - dispatch_group_wait(_editingTransactionGroup, DISPATCH_TIME_FOREVER); + [self _drainEditingQueue]; + [_mainSerialQueue performBlockOnMainThread:block]; } diff --git a/Source/Details/ASElementMap.h b/Source/Details/ASElementMap.h index bf5b25994..a28ae1ec4 100644 --- a/Source/Details/ASElementMap.h +++ b/Source/Details/ASElementMap.h @@ -60,6 +60,11 @@ AS_SUBCLASSING_RESTRICTED */ @property (copy, readonly) NSArray *itemElements; +/** + * All the elements in the map, in NSDictionary form. O(1) + */ +@property (readonly) NSDictionary *elementToIndexPath; + /** * Returns the index path that corresponds to the same element in @c map at the given @c indexPath. * O(1) for items, fast O(N) for sections. diff --git a/Source/Details/ASElementMap.mm b/Source/Details/ASElementMap.mm index 6dd0d64dc..3c6904afd 100644 --- a/Source/Details/ASElementMap.mm +++ b/Source/Details/ASElementMap.mm @@ -10,6 +10,7 @@ #import #import #import +#import #import #import #import @@ -19,9 +20,6 @@ @interface ASElementMap () @property (nonatomic, readonly) NSArray *sections; -// Element -> IndexPath -@property (nonatomic, readonly) NSMapTable *elementToIndexPathMap; - // The items, in a 2D array @property (nonatomic, readonly) ASCollectionElementTwoDimensionalArray *sectionsOfItems; @@ -46,29 +44,41 @@ - (instancetype)initWithSections:(NSArray *)sections items:(ASColle _supplementaryElements = [[NSDictionary alloc] initWithDictionary:supplementaryElements copyItems:YES]; // Setup our index path map - _elementToIndexPathMap = [NSMapTable mapTableWithKeyOptions:(NSMapTableStrongMemory | NSMapTableObjectPointerPersonality) valueOptions:NSMapTableCopyIn]; + auto mElementToIndexPath = [[NSMutableDictionary alloc] init]; NSInteger s = 0; for (NSArray *section in _sectionsOfItems) { NSInteger i = 0; for (ASCollectionElement *element in section) { NSIndexPath *indexPath = [NSIndexPath indexPathForItem:i inSection:s]; - [_elementToIndexPathMap setObject:indexPath forKey:element]; + [mElementToIndexPath setObject:indexPath forKey:element]; i++; } s++; } for (NSDictionary *supplementariesForKind in [_supplementaryElements objectEnumerator]) { [supplementariesForKind enumerateKeysAndObjectsUsingBlock:^(NSIndexPath *_Nonnull indexPath, ASCollectionElement * _Nonnull element, BOOL * _Nonnull stop) { - [_elementToIndexPathMap setObject:indexPath forKey:element]; + [mElementToIndexPath setObject:indexPath forKey:element]; }]; } + _elementToIndexPath = mElementToIndexPath; } return self; } +- (void)dealloc +{ + if (ASActivateExperimentalFeature(ASExperimentalDeallocElementMapOffMain)) { + if (ASDisplayNodeThreadIsMain()) { + ASPerformBackgroundDeallocation(&_sections); + ASPerformBackgroundDeallocation(&_sectionsOfItems); + ASPerformBackgroundDeallocation(&_supplementaryElements); + } + } +} + - (NSUInteger)count { - return _elementToIndexPathMap.count; + return _elementToIndexPath.count; } - (NSArray *)itemIndexPaths @@ -111,7 +121,7 @@ - (NSInteger)numberOfItemsInSection:(NSInteger)section - (nullable NSIndexPath *)indexPathForElement:(ASCollectionElement *)element { - return element ? [_elementToIndexPathMap objectForKey:element] : nil; + return element ? [_elementToIndexPath objectForKey:element] : nil; } - (nullable NSIndexPath *)indexPathForElementIfCell:(ASCollectionElement *)element @@ -195,7 +205,7 @@ - (id)mutableCopyWithZone:(NSZone *)zone - (NSUInteger)countByEnumeratingWithState:(NSFastEnumerationState *)state objects:(id _Nullable unowned [])buffer count:(NSUInteger)len { - return [_elementToIndexPathMap countByEnumeratingWithState:state objects:buffer count:len]; + return [_elementToIndexPath countByEnumeratingWithState:state objects:buffer count:len]; } - (NSString *)smallDescription diff --git a/Source/Details/ASGraphicsContext.mm b/Source/Details/ASGraphicsContext.mm index 8f543e06f..1a6897e40 100644 --- a/Source/Details/ASGraphicsContext.mm +++ b/Source/Details/ASGraphicsContext.mm @@ -54,7 +54,9 @@ NS_INLINE void ASConfigureExtendedRange(UIGraphicsImageRendererFormat *format) static UIGraphicsImageRendererFormat *opaqueFormat; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + // Although +preferredFormat is documented available on iOS 11, it actually crashes with an + // unrecognized selector exception in iOS 11.0.0. + if (AS_AVAILABLE_IOS_TVOS(11.0.1, 11.0.1)) { defaultFormat = [UIGraphicsImageRendererFormat preferredFormat]; opaqueFormat = [UIGraphicsImageRendererFormat preferredFormat]; } else { @@ -88,7 +90,8 @@ NS_INLINE void ASConfigureExtendedRange(UIGraphicsImageRendererFormat *format) } else if (scale == 0 || scale == ASScreenScale()) { format = opaque ? opaqueFormat : defaultFormat; } else { - if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + // See comment above about iOS 11.0.0. + if (AS_AVAILABLE_IOS_TVOS(11.0.1, 11.0.1)) { format = [UIGraphicsImageRendererFormat preferredFormat]; } else { format = [UIGraphicsImageRendererFormat defaultFormat]; diff --git a/Source/Details/ASHighlightOverlayLayer.h b/Source/Details/ASHighlightOverlayLayer.h index aff6694bf..8c157577f 100644 --- a/Source/Details/ASHighlightOverlayLayer.h +++ b/Source/Details/ASHighlightOverlayLayer.h @@ -41,9 +41,9 @@ AS_SUBCLASSING_RESTRICTED @interface CALayer (ASHighlightOverlayLayerSupport) /** - @summary Set to YES to indicate to a sublayer that this is where highlight overlay layers (for pressed states) should - be added so that the highlight won't be clipped by a neighboring layer. - */ +@summary Set to YES to indicate to a sublayer that this is where highlight overlay layers (for pressed states) should +be added so that the highlight won't be clipped by a neighboring layer. +*/ @property (nonatomic, setter=as_setAllowsHighlightDrawing:) BOOL as_allowsHighlightDrawing; @end diff --git a/Source/Details/ASIntegerMap.mm b/Source/Details/ASIntegerMap.mm index fdf0d528d..8360eadc4 100644 --- a/Source/Details/ASIntegerMap.mm +++ b/Source/Details/ASIntegerMap.mm @@ -6,7 +6,7 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import "ASIntegerMap.h" +#import #import #import #import diff --git a/Source/Details/ASObjectDescriptionHelpers.mm b/Source/Details/ASObjectDescriptionHelpers.mm index e0b7c21f0..36ab65c40 100644 --- a/Source/Details/ASObjectDescriptionHelpers.mm +++ b/Source/Details/ASObjectDescriptionHelpers.mm @@ -27,6 +27,8 @@ return NSStringFromCGSize(value.CGSizeValue); } else if (strcmp(type, @encode(CGPoint)) == 0) { return NSStringFromCGPoint(value.CGPointValue); + } else if (strcmp(type, @encode(UIEdgeInsets)) == 0) { + return NSStringFromUIEdgeInsets(value.UIEdgeInsetsValue); } } else if ([object isKindOfClass:[NSIndexSet class]]) { @@ -87,7 +89,14 @@ NSString *ASObjectDescriptionMakeTiny(__autoreleasing id object) { static constexpr int kBufSize = 64; char buf[kBufSize]; - int len = snprintf(buf, kBufSize, "<%s: %p>", object_getClassName(object), object); + NSString *debugName; + int len; + if ([object respondsToSelector:@selector(debugName)] && (debugName = [object debugName])) { + len = snprintf(buf, kBufSize, "<%s: %p; \"%s\">", object_getClassName(object), object, + debugName.UTF8String); + } else { + len = snprintf(buf, kBufSize, "<%s: %p>", object_getClassName(object), object); + } return (__bridge_transfer NSString *)CFStringCreateWithBytes(NULL, (const UInt8 *)buf, len, kCFStringEncodingASCII, false); } diff --git a/Source/Details/ASPINRemoteImageDownloader.mm b/Source/Details/ASPINRemoteImageDownloader.mm index 8576cc7d3..187445744 100644 --- a/Source/Details/ASPINRemoteImageDownloader.mm +++ b/Source/Details/ASPINRemoteImageDownloader.mm @@ -30,6 +30,10 @@ #define PIN_WEBP_AVAILABLE 0 #endif +#if PIN_WEBP +#import +#endif + #import #import #import @@ -208,6 +212,17 @@ - (BOOL)sharedImageManagerSupportsMemoryRemoval #if PIN_ANIMATED_AVAILABLE - (nullable id )animatedImageWithData:(NSData *)animatedImageData { +#if PIN_WEBP + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + PINWebPAnimatedImage.enableFasterWebPDecoding = + ASActivateExperimentalFeature(ASExperimentalFasterWebPDecoding); + PINWebPAnimatedImage.useGraphicsImageRenderer = + ASActivateExperimentalFeature(ASExperimentalFasterWebPGraphicsImageRenderer); + PINCachedAnimatedImage.webPCachingDisabled = + ASActivateExperimentalFeature(ASExperimentalAnimatedWebPNoCache); + }); +#endif return [[PINCachedAnimatedImage alloc] initWithAnimatedImageData:animatedImageData]; } #endif @@ -216,7 +231,7 @@ - (BOOL)sharedImageManagerSupportsMemoryRemoval { PINRemoteImageManager *manager = [self sharedPINRemoteImageManager]; PINRemoteImageManagerResult *result = [manager synchronousImageFromCacheWithURL:URL processorKey:nil options:PINRemoteImageManagerDownloadOptionsSkipDecode]; - + #if PIN_ANIMATED_AVAILABLE if (result.alternativeRepresentation) { return result.alternativeRepresentation; @@ -365,7 +380,7 @@ - (id)alternateRepresentationWithData:(NSData *)data options:(PINRemoteImageMana return data; } #endif - + #endif return nil; } diff --git a/Source/Details/ASRangeController.h b/Source/Details/ASRangeController.h index 1ce295259..eb8b89076 100644 --- a/Source/Details/ASRangeController.h +++ b/Source/Details/ASRangeController.h @@ -140,6 +140,8 @@ AS_SUBCLASSING_RESTRICTED - (NSString *)nameForRangeControllerDataSource; +- (CALayer *)layerForRangeController:(ASRangeController *)rangeController; + @end /** diff --git a/Source/Details/ASRangeController.mm b/Source/Details/ASRangeController.mm index 1dcd062a5..14752a4b1 100644 --- a/Source/Details/ASRangeController.mm +++ b/Source/Details/ASRangeController.mm @@ -30,6 +30,10 @@ @interface ASRangeController () { + // When set to NO, _rangeIsValid indicates that ASRangeController needs to recalculate the + // interfaceState of every node under it's control. When set to YES, _rangeIsValid will limit the + // the interfaceState recalculation to nodes that are visible, displayed, need to be preloaded and + // nodes that were updated previously during _updateVisibleNodeIndexPaths BOOL _rangeIsValid; BOOL _needsRangeUpdate; NSSet *_allPreviousIndexPaths; @@ -39,6 +43,7 @@ @interface ASRangeController () BOOL _preserveCurrentRangeMode; BOOL _didRegisterForNodeDisplayNotifications; CFTimeInterval _pendingDisplayNodesTimestamp; + ASInterfaceState _previousInterfaceState; // If the user is not currently scrolling, we will keep our ranges // configured to match their previous scroll direction. Defaults @@ -71,7 +76,8 @@ - (instancetype)init _contentHasBeenScrolled = NO; _preserveCurrentRangeMode = NO; _previousScrollDirection = ASScrollDirectionDown | ASScrollDirectionRight; - + _previousInterfaceState = ASInterfaceStateNone; + [[[self class] allRangeControllersWeakSet] addObject:self]; #if AS_RANGECONTROLLER_LOG_UPDATE_FREQ @@ -131,6 +137,9 @@ - (ASInterfaceState)interfaceState - (void)setNeedsUpdate { +#if ASRangeControllerLoggingEnabled + NSLog(@"ASRangeController's setNeedsUpdate. collectionView: %@, _needsRangeUpdate: %@", _dataSource, _needsRangeUpdate ? @"YES" : @"NO"); +#endif if (!_needsRangeUpdate) { _needsRangeUpdate = YES; @@ -143,14 +152,41 @@ - (void)setNeedsUpdate - (void)updateIfNeeded { - if (_needsRangeUpdate) { +#if ASRangeControllerLoggingEnabled + NSLog(@"ASRangeController's updateIfNeeded. collectionView: %@, _needsRangeUpdate: %@", _dataSource, _needsRangeUpdate ? @"YES" : @"NO"); +#endif + if (_needsRangeUpdate/* || (_previousInterfaceState != [self interfaceState])*/) { [self updateRanges]; } } - (void)updateRanges { + // Skip range update if layout is still pending. + // This is necessary to avoid forcing a collection view layout update in cases like + // rotation, where we prefer to update content & reloadData before rotation starts, + // but only clean the layout after the rotation (size change). Cleaning the layout + // for the new content before the rotation will require a lot of unneeded work. + BOOL needsLayout = [self.dataSource layerForRangeController:self].needsLayout; +#if ASRangeControllerLoggingEnabled + NSLog(@"ASRangeController's updateRanges. collectionView: %@, needsLayout: %@", _dataSource, needsLayout ? @"YES" : @"NO"); +#endif + if (needsLayout) { + return; + } + +// _previousInterfaceState = [self interfaceState]; _needsRangeUpdate = NO; + + ASDisplayNodeAssert(_layoutController, @"An ASLayoutController is required by ASRangeController"); + if (!_layoutController || !_dataSource) { + return; + } + + if (![_delegate rangeControllerShouldUpdateRanges:self]) { + return; + } + [self _updateVisibleNodeIndexPaths]; } @@ -198,18 +234,10 @@ - (void)_setVisibleNodes:(NSHashTable *)newVisibleNodes _visibleNodes = newVisibleNodes; } -- (void)_updateVisibleNodeIndexPaths +- (void) _updateVisibleNodeIndexPaths { as_activity_scope_verbose(as_activity_create("Update range controller", AS_ACTIVITY_CURRENT, OS_ACTIVITY_FLAG_DEFAULT)); as_log_verbose(ASCollectionLog(), "Updating ranges for %@", ASViewToDisplayNode(ASDynamicCast(self.delegate, UIView))); - ASDisplayNodeAssert(_layoutController, @"An ASLayoutController is required by ASRangeController"); - if (!_layoutController || !_dataSource) { - return; - } - - if (![_delegate rangeControllerShouldUpdateRanges:self]) { - return; - } #if AS_RANGECONTROLLER_LOG_UPDATE_FREQ _updateCountThisFrame += 1; diff --git a/Source/Details/ASThread.h b/Source/Details/ASThread.h index 0ef3d325a..7515df55a 100644 --- a/Source/Details/ASThread.h +++ b/Source/Details/ASThread.h @@ -328,6 +328,53 @@ namespace AS { typedef std::lock_guard MutexLocker; typedef std::unique_lock UniqueLock; + /** + * A simple variant class for holding either a pointer (to your context's mutex) or its own mutex. + * You must call Configure() before attempting to get the mutex. + */ + class MutexOrPointer { + public: + MutexOrPointer() : configured_(false), own_mutex_(false) {} + void Configure(AS::RecursiveMutex *ptr) { + ASDisplayNodeCAssert(!configured_, @"Class cannot be configured twice."); + configured_ = true; + if (!ptr) { + own_mutex_ = true; + new (&mutex_) AS::RecursiveMutex(); + } else { + pointer_ = ptr; + } + } + ~MutexOrPointer() { + if (own_mutex_) { + mutex_.~RecursiveMutex(); + } + } + // Explicitly get a reference to the contained mutex. + AS::RecursiveMutex &get() { + ASDisplayNodeCAssert(configured_, @"Use of class before configuration."); + return own_mutex_ ? mutex_ : *pointer_; + } + + // Implicitly get mutex ref. Useful when transitioning from instance locks + // to context locks. For instance, this allows us to construct a mutex locker from + // a MutexOrPointer instance. + operator AS::RecursiveMutex &() { return get(); } + + // Locking method forwards for transitioning. + void AssertHeld() { get().AssertHeld(); } + void lock() { get().lock(); } + void unlock() { get().unlock(); } + bool try_lock() { return get().try_lock(); } + private: + union { + AS::RecursiveMutex *pointer_; + AS::RecursiveMutex mutex_; + }; + bool configured_:1; + bool own_mutex_:1; + }; + } // namespace AS #endif /* __cplusplus */ diff --git a/Source/Details/_ASDisplayLayer.h b/Source/Details/_ASDisplayLayer.h index 1066a36f1..9cca00552 100644 --- a/Source/Details/_ASDisplayLayer.h +++ b/Source/Details/_ASDisplayLayer.h @@ -31,6 +31,19 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nonatomic) BOOL displaysAsynchronously; +/** + @summary Strong storage for a retained delegate. Since CALayer.delegate is actually assign, not weak, + ASDisplayNode uses a weak proxy as the delegate, and assigns the proxy here so that it'll survive as long as + the layer does. + */ +@property (nonatomic, strong) id as_retainedDelegate; + +/** + @summary Set to YES to indicate to a sublayer that this is where highlight overlay layers (for pressed states) should + be added so that the highlight won't be clipped by a neighboring layer. + */ +@property (nonatomic, setter=as_setAllowsHighlightDrawing:) BOOL as_allowsHighlightDrawing; + /** @summary Cancels any pending async display. diff --git a/Source/Details/_ASDisplayLayer.mm b/Source/Details/_ASDisplayLayer.mm index 4e8569d15..481d2fd10 100644 --- a/Source/Details/_ASDisplayLayer.mm +++ b/Source/Details/_ASDisplayLayer.mm @@ -9,9 +9,13 @@ #import -#import -#import #import +#import +#import +#import +#import +#import +#import #import @implementation _ASDisplayLayer @@ -20,6 +24,8 @@ @implementation _ASDisplayLayer } @dynamic displaysAsynchronously; +@synthesize as_retainedDelegate = _as_retainedDelegate; +@synthesize as_allowsHighlightDrawing = _as_allowsHighlightDrawing; #pragma mark - Properties @@ -80,7 +86,18 @@ - (void)layoutSublayers ASDisplayNodeAssertMainThread(); [super layoutSublayers]; + unowned ASDisplayNode *node = self.asyncdisplaykit_node; [self.asyncdisplaykit_node __layout]; + + // When a table/collection view reload during a transaction, cell will reconfigure which might + // involves visible -> reload(hide show) and in this case we want to merge hide show pair by + // delaying the process until later, aka coalesce the interface state (before CATransaction + // commits). However, If root node and within the CATransaction commit, we would like to keep + // applying the interface state until the state becomes stable. + if ([ASDisplayNode shouldCoalesceInterfaceStateDuringTransaction] && + [ASCATransactionQueue inTransactionCommit] && node.supernode == nil) { + [self.asyncdisplaykit_node recursivelyApplyPendingInterfaceState]; + } } - (void)setNeedsDisplay diff --git a/Source/Details/_ASDisplayView.mm b/Source/Details/_ASDisplayView.mm index 7741e4447..d38cf0d7b 100644 --- a/Source/Details/_ASDisplayView.mm +++ b/Source/Details/_ASDisplayView.mm @@ -16,6 +16,8 @@ #import #import #import +#import +#import #pragma mark - _ASDisplayView @@ -40,7 +42,7 @@ @implementation _ASDisplayView } _internalFlags; NSArray *_accessibilityElements; - CGRect _lastAccessibilityElementsFrame; + BOOL _inIsAccessibilityElement; } #pragma mark - Class @@ -83,16 +85,7 @@ - (NSString *)description - (id)actionForLayer:(CALayer *)layer forKey:(NSString *)event { id uikitAction = [super actionForLayer:layer forKey:event]; - - // Even though the UIKit action will take precedence, we still unconditionally forward to the node so that it can - // track events like kCAOnOrderIn. - id nodeAction = [_asyncdisplaykit_node actionForLayer:layer forKey:event]; - - // If UIKit specifies an action, that takes precedence. That's an animation block so it's explicit. - if (uikitAction && uikitAction != (id)kCFNull) { - return uikitAction; - } - return nodeAction; + return ASDisplayNodeActionForLayer(layer, event, _asyncdisplaykit_node, uikitAction); } - (void)willMoveToWindow:(UIWindow *)newWindow @@ -167,7 +160,8 @@ - (void)didMoveToSuperview if (node.inHierarchy) { [node __exitHierarchy]; } - self.keepalive_node = nil; + + ASPerformBackgroundDeallocation(&_keepalive_node); } #if DEBUG @@ -247,7 +241,7 @@ - (void)willRemoveSubview:(UIView *)subview - (CGSize)sizeThatFits:(CGSize)size { ASDisplayNode *node = _asyncdisplaykit_node; // Create strong reference to weak ivar. - return node ? [node layoutThatFits:ASSizeRangeMake(size)].size : [super sizeThatFits:size]; + return node ? [node measure:ASSizeRangeMake(CGSizeZero, size)] : [super sizeThatFits:size]; } - (void)setNeedsDisplay diff --git a/Source/Details/_ASDisplayViewAccessiblity.h b/Source/Details/_ASDisplayViewAccessiblity.h index 7f159d1d0..1b1e3155b 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.h +++ b/Source/Details/_ASDisplayViewAccessiblity.h @@ -7,7 +7,10 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import +#import + +NS_ASSUME_NONNULL_BEGIN + // WARNING: When dealing with accessibility elements, please use the `accessibilityElements` // property instead of the older methods e.g. `accessibilityElementCount()`. While the older methods @@ -15,6 +18,51 @@ // their correctness. For details, see // https://developer.apple.com/documentation/objectivec/nsobject/1615147-accessibilityelements +@class ASDisplayNode; +@class ASAccessibilityElement; + +/** + * The methods adopted by the object to provide frame information for a given + * ASAccessibilityElement + */ +@protocol ASAccessibilityElementFrameProviding + +/** + * Returns the accessibilityFrame for the given ASAccessibilityElement + */ +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement; + +@end + +/** + * Encapsulates Texture related information about an item that should be + * accessible to users with disabilities, but that isn’t accessible by default. + */ +@interface ASAccessibilityElement : UIAccessibilityElement + +@property (nonatomic) ASDisplayNode *node; +@property (nonatomic) NSRange accessibilityRange; + +/** + * If a frameProvider is set on the ASAccessibilityElement it will be asked to + * return the frame for the corresponding UIAccessibilityElement within + * accessibilityElement. + * + * @note: If a frameProvider is set any accessibilityFrame set on the + * UIAccessibilityElement explicitly will be ignored + */ +@property (nonatomic) id frameProvider; + +@end + +@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction + +@property (nonatomic, readonly) ASDisplayNode *node; +@property (nonatomic, nullable, readonly) id value; +@property (nonatomic, readonly) NSRange textRange; + +@end + // After recusively collecting all of the accessibility elements of a node, they get sorted. This sort determines // the order that a screen reader will traverse the elements. By default, we sort these elements based on their // origin: lower y origin comes first, then lower x origin. If 2 nodes have an equal origin, the node with the smaller @@ -28,3 +76,5 @@ typedef NSComparisonResult (^ASSortAccessibilityElementsComparator)(NSObject *, // Use this method to supply your own custom sort comparator used to determine the order of the accessibility elements void setUserDefinedAccessibilitySortComparator(ASSortAccessibilityElementsComparator userDefinedComparator); + +NS_ASSUME_NONNULL_END diff --git a/Source/Details/_ASDisplayViewAccessiblity.mm b/Source/Details/_ASDisplayViewAccessiblity.mm index 8f3e12991..6c60782cf 100644 --- a/Source/Details/_ASDisplayViewAccessiblity.mm +++ b/Source/Details/_ASDisplayViewAccessiblity.mm @@ -9,19 +9,76 @@ #ifndef ASDK_ACCESSIBILITY_DISABLE -#import #import +#import #import #import #import #import #import #import +#import +#import #import +/// Returns if the passed in node is considered a leaf node +NS_INLINE BOOL ASIsLeafNode(__unsafe_unretained ASDisplayNode *node) { + return node.subnodes.count == 0; +} + +/// Returns an NSString trimmed of whitespaces and newlines at the beginning the end. +static NSString *ASTrimmedAccessibilityLabel(NSString *accessibilityLabel) { + return [accessibilityLabel + stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; +} + +/// Returns a NSAttributedString trimmed of whitespaces and newlines at the beginning and the end. +static NSAttributedString *ASTrimmedAttributedAccessibilityLabel( + NSAttributedString *attributedString) { + // Create a cached inverted character set from whitespaceAndNewlineCharacterSet + // [NSCharacterSet whitespaceAndNewlineCharacterSet] is cached, but the invertedSet is not. + static NSCharacterSet *invertedWhiteSpaceAndNewLineCharacterSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + invertedWhiteSpaceAndNewLineCharacterSet = + [NSCharacterSet whitespaceAndNewlineCharacterSet].invertedSet; + }); + NSString *string = attributedString.string; + + NSRange range = [string rangeOfCharacterFromSet:invertedWhiteSpaceAndNewLineCharacterSet]; + NSUInteger location = (range.length > 0) ? range.location : 0; + + range = [string rangeOfCharacterFromSet:invertedWhiteSpaceAndNewLineCharacterSet + options:NSBackwardsSearch]; + NSUInteger length = (range.length > 0) ? NSMaxRange(range) - location : string.length - location; + + if (location == 0 && length == string.length) { + return attributedString; + } + + return [attributedString attributedSubstringFromRange:NSMakeRange(location, length)]; +} + +/// Returns NO when implicit custom action synthesis should not be enabled for the node. Returns YES +/// when implicit custom action synthesis is OK for the node, assuming it contains an non-empty +/// accessibility label. +static BOOL ASMayImplicitlySynthesizeAccessibilityCustomAction(ASDisplayNode *node, + ASDisplayNode *rootContainerNode) { + if (node == rootContainerNode) { + return NO; + } + return node.accessibilityTraits & ASInteractiveAccessibilityTraitsMask(); +} + #pragma mark - UIAccessibilityElement +@protocol ASAccessibilityElementPositioning + +@property (nonatomic, readonly) CGRect accessibilityFrame; + +@end + static ASSortAccessibilityElementsComparator currentAccessibilityComparator = nil; static ASSortAccessibilityElementsComparator defaultAccessibilityComparator = nil; @@ -33,7 +90,7 @@ void setUserDefinedAccessibilitySortComparator(ASSortAccessibilityElementsCompar void SortAccessibilityElements(NSMutableArray *elements) { ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); - + static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ defaultAccessibilityComparator = ^NSComparisonResult(NSObject *a, NSObject *b) { @@ -70,14 +127,56 @@ static CGRect ASAccessibilityFrameForNode(ASDisplayNode *node) { return [layer convertRect:node.bounds toLayer:ASFindWindowOfLayer(layer).layer]; } -@interface ASAccessibilityElement : UIAccessibilityElement +@interface _ASDisplayViewAccessibilityFrameProvider : NSObject +@end -@property (nonatomic) ASDisplayNode *node; +@implementation _ASDisplayViewAccessibilityFrameProvider + +- (CGRect)accessibilityFrameForAccessibilityElement:(ASAccessibilityElement *)accessibilityElement { + return ASAccessibilityFrameForNode(accessibilityElement.node); +} + +@end + +@interface ASAccessibilityElement () + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node; @end +// Returns the default _ASDisplayViewAccessibilityFrameProvider to be used as frame provider +// of accessibility elements within ASDisplayViewAccessibility. +static _ASDisplayViewAccessibilityFrameProvider *_ASDisplayViewAccessibilityFrameProviderDefault() { + static _ASDisplayViewAccessibilityFrameProvider *frameProvider = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + frameProvider = [[_ASDisplayViewAccessibilityFrameProvider alloc] init]; + }); + return frameProvider; +} + +// Create an ASAccessibilityElement for a given UIView and ASDisplayNode for usage +// within _ASDisplayViewAccessibility +static ASAccessibilityElement *_ASDisplayViewAccessibilityCreateASAccessibilityElement( + UIView *containerView, ASDisplayNode *node) { + ASAccessibilityElement *accessibilityElement = + [[ASAccessibilityElement alloc] initWithAccessibilityContainer:containerView]; + accessibilityElement.accessibilityIdentifier = node.accessibilityIdentifier; + accessibilityElement.accessibilityLabel = node.accessibilityLabel; + accessibilityElement.accessibilityHint = node.accessibilityHint; + accessibilityElement.accessibilityValue = node.accessibilityValue; + accessibilityElement.accessibilityTraits = node.accessibilityTraits; + if (AS_AVAILABLE_IOS_TVOS(11, 11)) { + accessibilityElement.accessibilityAttributedLabel = node.accessibilityAttributedLabel; + accessibilityElement.accessibilityAttributedHint = node.accessibilityAttributedHint; + accessibilityElement.accessibilityAttributedValue = node.accessibilityAttributedValue; + } + accessibilityElement.node = node; + accessibilityElement.frameProvider = _ASDisplayViewAccessibilityFrameProviderDefault(); + + return accessibilityElement; +} + @implementation ASAccessibilityElement + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)container node:(ASDisplayNode *)node @@ -100,17 +199,32 @@ + (ASAccessibilityElement *)accessibilityElementWithContainer:(UIView *)containe - (CGRect)accessibilityFrame { - return ASAccessibilityFrameForNode(self.node); + if (_frameProvider) { + return [_frameProvider accessibilityFrameForAccessibilityElement:self]; + } + + return [super accessibilityFrame]; +} + +- (NSString *)description { + return [NSString stringWithFormat:@"<%@ %p, %@, %@>", NSStringFromClass([self class]), self, + self.accessibilityLabel, + NSStringFromCGRect(self.accessibilityFrame)]; } @end #pragma mark - _ASDisplayView / UIAccessibilityContainer -@interface ASAccessibilityCustomAction : UIAccessibilityCustomAction +@interface ASAccessibilityCustomAction () @property (nonatomic) ASDisplayNode *node; +@property (nonatomic, nullable) id value; +@property (nonatomic) NSRange textRange; + +@end +@interface ASAccessibilityCustomAction() @end @implementation ASAccessibilityCustomAction @@ -122,64 +236,121 @@ - (CGRect)accessibilityFrame @end -/// Collect all subnodes for the given node by walking down the subnode tree and calculates the screen coordinates based on the containerNode and container -static void CollectUIAccessibilityElementsForNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements) +#pragma mark - Collecting Accessibility with ASTextNode Links Handling + +/// Collect all subnodes for the given node by walking down the subnode tree and calculates the +/// screen coordinates based on the containerNode and container. This is necessary for layer backed +/// nodes or rasterrized subtrees as no UIView instance for this node exists. +static void CollectAccessibilityElementsForLayerBackedOrRasterizedNode(ASDisplayNode *node, ASDisplayNode *containerNode, id container, NSMutableArray *elements) { ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); + // Iterate any node in the tree and either collect nodes that are accessibility elements + // or leaf nodes that are accessibility containers ASDisplayNodePerformBlockOnEveryNodeBFS(node, ^(ASDisplayNode * _Nonnull currentNode) { - // For every subnode that is layer backed or it's supernode has subtree rasterization enabled - // we have to create a UIAccessibilityElement as no view for this node exists - if (currentNode != containerNode && currentNode.isAccessibilityElement) { - UIAccessibilityElement *accessibilityElement = [ASAccessibilityElement accessibilityElementWithContainer:container node:currentNode]; - [elements addObject:accessibilityElement]; + if (currentNode != containerNode) { + if (currentNode.isAccessibilityElement) { + // For every subnode that is an accessibility element and is layer backed + // or an ancestor has subtree rasterization enabled, create a + // UIAccessibilityElement as no view for this node exists + UIAccessibilityElement *accessibilityElement = + _ASDisplayViewAccessibilityCreateASAccessibilityElement(container, currentNode); + [elements addObject:accessibilityElement]; + } else if (ASIsLeafNode(currentNode) && currentNode.accessibilityElementCount > 0) { + // In leaf nodes that are layer backed and acting as UIAccessibilityContainer + // (isAccessibilityElement == NO we call through to the + // accessibilityElements to collect all accessibility elements of this node + [elements addObjectsFromArray:currentNode.accessibilityElements]; + } } }); } -static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, UIView *view, - NSMutableArray *elements) { - ASDisplayNodeCAssertNotNil(view, @"Passed in view should not be nil"); - if (view == nil) { +/// Called from CollectAccessibilityElements for nodes that are returning YES for +/// isAccessibilityContainer to collect all subnodes accessibility labels as well as custom actions +/// for nodes that have interactive accessibility traits enabled. Furthermore for ASTextNode's it +/// also aggregates all links within the attributedString as custom action +static void AggregateSubtreeAccessibilityLabelsAndCustomActions(ASDisplayNode *rootContainer, + ASDisplayNode *containerNode, + UIView *containerView, + NSMutableArray *elements) { + ASDisplayNodeCAssertNotNil(containerView, @"Passed in view should not be nil"); + if (containerView == nil) { return; } UIAccessibilityElement *accessiblityElement = - [ASAccessibilityElement accessibilityElementWithContainer:view - node:container]; + _ASDisplayViewAccessibilityCreateASAccessibilityElement(containerView, containerNode); NSMutableArray *labeledNodes = [[NSMutableArray alloc] init]; NSMutableArray *actions = [[NSMutableArray alloc] init]; std::queue queue; - queue.push(container); + queue.push(containerNode); // If the container does not have an accessibility label set, or if the label is meant for custom - // actions only, then aggregate its subnodes' labels. Otherwise, treat the label as an overriden + // actions only, then aggregate its subnodes' labels. Otherwise, treat the label as an overridden // value and do not perform the aggregation. BOOL shouldAggregateSubnodeLabels = - (container.accessibilityLabel.length == 0) || - (container.accessibilityTraits & ASInteractiveAccessibilityTraitsMask()); + (ASTrimmedAccessibilityLabel(containerNode.accessibilityLabel).length == 0) || + ASMayImplicitlySynthesizeAccessibilityCustomAction(containerNode, rootContainer); + // Iterate through the whole subnode tree and aggregate ASDisplayNode *node = nil; while (!queue.empty()) { node = queue.front(); queue.pop(); - if (node != container && node.isAccessibilityContainer) { - UIView *containerView = node.isLayerBacked ? view : node.view; - CollectAccessibilityElementsForContainer(node, containerView, elements); + // If the node is an accessibility container go further down for collecting all the nodes information. + if (node != containerNode && node.isAccessibilityContainer) { + UIView *view = containerNode.isLayerBacked ? containerView : containerNode.view; + AggregateSubtreeAccessibilityLabelsAndCustomActions(node, node, view, elements); continue; } - if (node.accessibilityLabel.length > 0) { - if (node.accessibilityTraits & ASInteractiveAccessibilityTraitsMask()) { - ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:node.accessibilityLabel target:node selector:@selector(performAccessibilityCustomAction:)]; + + // Aggregate either custom actions for specific accessibility traits or the accessibility labels + // of the node. + NSString *trimmedNodeAccessibilityLabel = ASTrimmedAccessibilityLabel(node.accessibilityLabel); + if (trimmedNodeAccessibilityLabel.length > 0) { + if (ASMayImplicitlySynthesizeAccessibilityCustomAction(node, rootContainer)) { + ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] + initWithName:trimmedNodeAccessibilityLabel + target:node + selector:@selector(performAccessibilityCustomAction:)]; action.node = node; [actions addObject:action]; + // Connect the node with the custom action which representing it. node.accessibilityCustomAction = action; - } else if (node == container || shouldAggregateSubnodeLabels) { - ASAccessibilityElement *nonInteractiveElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:node]; + } else if (node == containerNode || shouldAggregateSubnodeLabels) { + ASAccessibilityElement *nonInteractiveElement = + _ASDisplayViewAccessibilityCreateASAccessibilityElement(containerView, node); [labeledNodes addObject:nonInteractiveElement]; + + // For ASTextNode accessibility container besides aggregating all of the of the subnodes + // we are also collecting all of the link as custom actions. + NSAttributedString *attributedText = nil; + if ([node respondsToSelector:@selector(attributedText)]) { + attributedText = ((ASTextNode *)node).attributedText; + } + NSArray *linkAttributeNames = nil; + if ([node respondsToSelector:@selector(linkAttributeNames)]) { + linkAttributeNames = ((ASTextNode *)node).linkAttributeNames; + } + linkAttributeNames = linkAttributeNames ?: @[]; + + for (NSString *linkAttributeName in linkAttributeNames) { + [attributedText enumerateAttribute:linkAttributeName inRange:NSMakeRange(0, attributedText.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:^(id _Nullable value, NSRange range, BOOL * _Nonnull stop) { + if (value == nil) { + return; + } + ASAccessibilityCustomAction *action = [[ASAccessibilityCustomAction alloc] initWithName:[attributedText.string substringWithRange:range] target:node selector:@selector(performAccessibilityCustomActionLink:)]; + action.accessibilityTraits = UIAccessibilityTraitLink; + action.node = node; + action.value = value; + action.textRange = range; + [actions addObject:action]; + }]; + } } } @@ -191,18 +362,37 @@ static void CollectAccessibilityElementsForContainer(ASDisplayNode *container, U SortAccessibilityElements(labeledNodes); if (AS_AVAILABLE_IOS_TVOS(11, 11)) { - NSArray *attributedLabels = [labeledNodes valueForKey:@"accessibilityAttributedLabel"]; - NSMutableAttributedString *attributedLabel = [NSMutableAttributedString new]; - [attributedLabels enumerateObjectsUsingBlock:^(id _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) { - if (idx != 0) { - [attributedLabel appendAttributedString:[[NSAttributedString alloc] initWithString:@", "]]; + NSAttributedString *attributedAccessbilityLabelsDivider = + [[NSAttributedString alloc] initWithString:@", "]; + NSMutableAttributedString *attributedAccessibilityLabel = + [[NSMutableAttributedString alloc] init]; + [labeledNodes enumerateObjectsUsingBlock:^(ASAccessibilityElement *_Nonnull element, + NSUInteger idx, BOOL *_Nonnull stop) { + NSAttributedString *trimmedAttributedLabel = + ASTrimmedAttributedAccessibilityLabel(element.accessibilityAttributedLabel); + if (trimmedAttributedLabel.length == 0) { + return; + } + if (idx != 0 && attributedAccessibilityLabel.length != 0) { + [attributedAccessibilityLabel appendAttributedString:attributedAccessbilityLabelsDivider]; } - [attributedLabel appendAttributedString:(NSAttributedString *)obj]; + [attributedAccessibilityLabel appendAttributedString:trimmedAttributedLabel]; }]; - accessiblityElement.accessibilityAttributedLabel = attributedLabel; + accessiblityElement.accessibilityAttributedLabel = attributedAccessibilityLabel; } else { - NSArray *labels = [labeledNodes valueForKey:@"accessibilityLabel"]; - accessiblityElement.accessibilityLabel = [labels componentsJoinedByString:@", "]; + NSMutableString *accessibilityLabel = [[NSMutableString alloc] init]; + [labeledNodes enumerateObjectsUsingBlock:^(ASAccessibilityElement *_Nonnull element, + NSUInteger idx, BOOL *_Nonnull stop) { + NSString *trimmedAccessibilityLabel = ASTrimmedAccessibilityLabel(element.accessibilityLabel); + if (trimmedAccessibilityLabel.length == 0) { + return; + } + if (idx != 0 && accessibilityLabel.length != 0) { + [accessibilityLabel appendString:@", "]; + } + [accessibilityLabel appendString:trimmedAccessibilityLabel]; + }]; + accessiblityElement.accessibilityLabel = accessibilityLabel; } SortAccessibilityElements(actions); @@ -224,11 +414,14 @@ static BOOL recusivelyCheckSuperviewsForScrollView(UIView *view) { /// returns YES if this node should be considered "hidden" from the screen reader. static BOOL nodeIsHiddenFromAcessibility(ASDisplayNode *node) { - return node.isHidden || node.alpha == 0.0 || node.accessibilityElementsHidden; + if (ASActivateExperimentalFeature(ASExperimentalEnableNodeIsHiddenFromAcessibility)) { + return node.isHidden || node.alpha == 0.0 || node.accessibilityElementsHidden; + } + return NO; } /// Collect all accessibliity elements for a given view and view node -static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *elements) +static void CollectAccessibilityElementsWithTextNodeLinkHandling(ASDisplayNode *node, NSMutableArray *elements) { ASDisplayNodeCAssertNotNil(elements, @"Should pass in a NSMutableArray"); ASDisplayNodeCAssertFalse(node.isLayerBacked); @@ -243,20 +436,21 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el })); UIView *view = node.view; - + // If we don't have a window, let's just bail out if (!view.window) { return; } + // Handle an accessibility container (collects accessibility labels or custom actions) if (node.isAccessibilityContainer && !anySubNodeIsCollection) { - CollectAccessibilityElementsForContainer(node, view, elements); + AggregateSubtreeAccessibilityLabelsAndCustomActions(node, node, node.view, elements); return; } - // Handle rasterize case + // Handle a node which tree is rasterized to collect all accessibility elements if (node.rasterizesSubtree) { - CollectUIAccessibilityElementsForNode(node, node, view, elements); + CollectAccessibilityElementsForLayerBackedOrRasterizedNode(node, node, node.view, elements); return; } @@ -287,39 +481,64 @@ static void CollectAccessibilityElements(ASDisplayNode *node, NSMutableArray *el // If a subnode is outside of the view's window, exclude it UNLESS it is a subview of an UIScrollView. // In this case UIKit will return the element even if it is outside of the window or the scrollView's visible rect (contentOffset + contentSize) CGRect nodeInWindowCoords = [node convertRect:subnode.frame toNode:nil]; - if (!CGRectIntersectsRect(view.window.frame, nodeInWindowCoords) && !recusivelyCheckSuperviewsForScrollView(view)) { + if (!CGRectIntersectsRect(view.window.frame, nodeInWindowCoords) && !recusivelyCheckSuperviewsForScrollView(view) && ASActivateExperimentalFeature(ASExperimentalEnableAcessibilityElementsReturnNil)) { continue; } if (subnode.isAccessibilityElement) { // An accessiblityElement can either be a UIView or a UIAccessibilityElement if (subnode.isLayerBacked) { - // No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement that represents this node - UIAccessibilityElement *accessiblityElement = [ASAccessibilityElement accessibilityElementWithContainer:view node:subnode]; + // No view for layer backed nodes exist. It's necessary to create a UIAccessibilityElement + // that represents this node + UIAccessibilityElement *accessiblityElement = + _ASDisplayViewAccessibilityCreateASAccessibilityElement(node.view, subnode); [elements addObject:accessiblityElement]; } else { - // Accessiblity element is not layer backed just add the view as accessibility element + // Accessiblity element is not layer backed, add the view to the elements as _ASDisplayView + // is itself a UIAccessibilityContainer [elements addObject:subnode.view]; } } else if (subnode.isLayerBacked) { - // Go down the hierarchy of the layer backed subnode and collect all of the UIAccessibilityElement - CollectUIAccessibilityElementsForNode(subnode, node, view, elements); + // Go down the hierarchy for layer backed subnodes which are also UIAccessibilityContainer's + // and collect all of the UIAccessibilityElement + CollectAccessibilityElementsForLayerBackedOrRasterizedNode(subnode, node, node.view, elements); } else if (subnode.accessibilityElementCount > 0) { - // UIView is itself a UIAccessibilityContainer just add it + // _ASDisplayView is itself a UIAccessibilityContainer just add it, UIKit will call the + // accessiblity methods of the nodes _ASDisplayView [elements addObject:subnode.view]; } } } +#pragma mark - _ASDisplayView + +@interface _ASDisplayView () { + NSArray *_accessibilityElements; + BOOL _inIsAccessibilityElement; +} + +@end + @implementation _ASDisplayView (UIAccessibilityContainer) -#pragma mark - UIAccessibility +#pragma mark UIAccessibility + +- (BOOL)isAccessibilityElement +{ + ASDisplayNodeAssertMainThread(); + if (_inIsAccessibilityElement) { + return [super isAccessibilityElement]; + } + _inIsAccessibilityElement = YES; + BOOL isAccessibilityElement = [self.asyncdisplaykit_node isAccessibilityElement]; + _inIsAccessibilityElement = NO; + return isAccessibilityElement; +} - (void)setAccessibilityElements:(nullable NSArray *)accessibilityElements { - // this is a no-op. You should not be setting accessibilityElements directly on _ASDisplayView. - // if you wish to set accessibilityElements, do so in your node. UIKit will call _ASDisplayView's - // accessibilityElements which will in turn ask its node for its elements. + ASDisplayNodeAssertMainThread(); + _accessibilityElements = accessibilityElements; } - (nullable NSArray *)accessibilityElements @@ -336,33 +555,119 @@ - (nullable NSArray *)accessibilityElements // not immediately obvious. While recomputing accessibilityElements may be expensive, this will only affect users that have // voice over enabled (we checked to ensure performance did not suffer by not caching for an overall user base). For those // users with voice over on, being correct is almost certainly more important than being performant. - return [viewNode accessibilityElements]; + if (_accessibilityElements == nil || ASActivateExperimentalFeature(ASExperimentalDoNotCacheAccessibilityElements)) { + _accessibilityElements = [viewNode accessibilityElements]; + } + return _accessibilityElements; +} + +@end + +@implementation ASDisplayNode (CustomAccessibilityBehavior) + +- (void)setAccessibilityElementsBlock:(ASDisplayNodeAccessibilityElementsBlock)block { + AS::MutexLocker l(__instanceLock__); + _accessibilityElementsBlock = block; } @end @implementation ASDisplayNode (AccessibilityInternal) -- (nullable NSArray *)accessibilityElements +- (BOOL)isAccessibilityElement { - // NSObject implements the informal accessibility protocol. This means that all ASDisplayNodes already have an accessibilityElements - // property. If an ASDisplayNode subclass has explicitly set the property, let's use that instead of traversing the node tree to try - // to create the elements automatically - NSArray *elements = [super accessibilityElements]; - if (elements.count) { - return elements; + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access isAccessibilityElement since node is not loaded"); + return [super isAccessibilityElement]; + } + + return [_view isAccessibilityElement]; +} + +- (NSInteger)accessibilityElementCount +{ + if (!self.isNodeLoaded) { + ASDisplayNodeFailAssert(@"Cannot access accessibilityElementCount since node is not loaded"); + return 0; + } + + // Please Note! + // If accessibility is not enabled on a device or the Accessibility Inspector was not started + // once yet on a Mac this method will always return 0! UIKit will dynamically link in + // specific accessibility implementation methods in this cases. + return [_view accessibilityElementCount]; +} + +- (NSArray *)accessibilityElements +{ + if (ASActivateExperimentalFeature(ASExperimentalEnableAcessibilityElementsReturnNil)) { + // NSObject implements the informal accessibility protocol. This means that all ASDisplayNodes already have an accessibilityElements + // property. If an ASDisplayNode subclass has explicitly set the property, let's use that instead of traversing the node tree to try + // to create the elements automatically + NSArray *elements = [super accessibilityElements]; + if (elements.count) { + return elements; + } } if (!self.isNodeLoaded) { ASDisplayNodeFailAssert(@"Cannot access accessibilityElements since node is not loaded"); - return nil; + return ASActivateExperimentalFeature(ASExperimentalEnableAcessibilityElementsReturnNil) ? nil : @[]; + } + if (_accessibilityElementsBlock) { + return _accessibilityElementsBlock(); } + NSMutableArray *accessibilityElements = [[NSMutableArray alloc] init]; - CollectAccessibilityElements(self, accessibilityElements); + CollectAccessibilityElementsWithTextNodeLinkHandling(self, accessibilityElements); + SortAccessibilityElements(accessibilityElements); // If we did not find any accessibility elements, return nil instead of empty array. This allows a WKWebView within the node // to participate in accessibility. - return accessibilityElements.count == 0 ? nil : accessibilityElements; + if (ASActivateExperimentalFeature(ASExperimentalEnableAcessibilityElementsReturnNil)) { + return accessibilityElements.count == 0 ? nil : accessibilityElements; + } else { + return accessibilityElements; + } +} + +- (void)invalidateFirstAccessibilityContainerOrNonLayerBackedNode { + if (!ASAccessibilityIsEnabled()) { + return; + } + ASDisplayNode *firstNonLayerbackedNode = nil; + BOOL containerInvalidated = [self invalidateUpToContainer:&firstNonLayerbackedNode]; + if (!self.isLayerBacked) { + return; + } + if (!containerInvalidated) { + [firstNonLayerbackedNode invalidateAccessibilityElements]; + } +} + +// Walks up the tree and until the first node that returns YES for isAccessibilityContainer is found +// and invalidates it's accessibility elements and YES will be returned. +// In case no node that returns YES for isAccessibilityContainer the first non layer backed node +// will be returned with the firstNonLayerbackedNode pointer and NO will be returned. +- (BOOL)invalidateUpToContainer:(ASDisplayNode **)firstNonLayerbackedNode { + ASDisplayNode *supernode = self.supernode; + if (supernode.isAccessibilityContainer) { + if (supernode.isNodeLoaded) { + [supernode invalidateAccessibilityElements]; + return YES; + } + } + if (*firstNonLayerbackedNode == nil && !self.isLayerBacked) { + *firstNonLayerbackedNode = self; + } + if (!supernode) { + return NO; + } + return [self.supernode invalidateUpToContainer:firstNonLayerbackedNode]; +} + +- (void)invalidateAccessibilityElements { + self.accessibilityElements = nil; } @end diff --git a/Source/Layout/ASLayout+IGListDiffKit.mm b/Source/Layout/ASLayout+IGListDiffKit.mm index d6e57b215..ceaddeb4f 100644 --- a/Source/Layout/ASLayout+IGListDiffKit.mm +++ b/Source/Layout/ASLayout+IGListDiffKit.mm @@ -7,7 +7,7 @@ // #import #if AS_IG_LIST_DIFF_KIT -#import "ASLayout+IGListDiffKit.h" +#import @interface ASLayout() { @public diff --git a/Source/Layout/ASLayout.h b/Source/Layout/ASLayout.h index 44b7de087..8c01412ea 100644 --- a/Source/Layout/ASLayout.h +++ b/Source/Layout/ASLayout.h @@ -131,7 +131,7 @@ ASDK_EXTERN ASLayout *ASCalculateLayout(idlayoutElement, const - (ASLayout *)filteredNodeLayoutTree NS_RETURNS_RETAINED AS_WARN_UNUSED_RESULT; - (instancetype)init NS_UNAVAILABLE; -- (instancetype)new NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; @end diff --git a/Source/Layout/ASLayoutElement.h b/Source/Layout/ASLayoutElement.h index 000f96551..d3651c9af 100644 --- a/Source/Layout/ASLayoutElement.h +++ b/Source/Layout/ASLayoutElement.h @@ -16,6 +16,10 @@ #import #import +#if YOGA +#import YOGA_HEADER_PATH +#endif + @class ASLayout; @class ASLayoutSpec; @protocol ASLayoutElementStylability; @@ -75,6 +79,17 @@ typedef NS_ENUM(unsigned char, ASLayoutElementType) { #pragma mark - Calculate layout +/** + * @abstract Measure the node. May be called from any thread. + * + * @param sizeRange The size range against which to measure the node. + * @return The size of the appropriate layout within sizeRange. + * + * @discussion Calling this method is equivalent to calling -layoutThatFits: and reading the size. + * This method may be faster because it may not require generating the ASLayout tree. + */ +- (CGSize)measure:(ASSizeRange)sizeRange; + /** * @abstract Asks the node to return a layout based on given size range. * @@ -160,11 +175,15 @@ ASDK_EXTERN NSString * const ASLayoutElementStyleFlexBasisProperty; ASDK_EXTERN NSString * const ASLayoutElementStyleAlignSelfProperty; ASDK_EXTERN NSString * const ASLayoutElementStyleAscenderProperty; ASDK_EXTERN NSString * const ASLayoutElementStyleDescenderProperty; +ASDK_EXTERN NSString * const ASLayoutElementStyleOverflowProperty; ASDK_EXTERN NSString * const ASLayoutElementStyleLayoutPositionProperty; @protocol ASLayoutElementStyleDelegate -- (void)style:(__kindof ASLayoutElementStyle *)style propertyDidChange:(NSString *)propertyName; + +/** Called by the style when a property changes. Not applicable to Yoga style. */ +- (void)style:(ASLayoutElementStyle *)style propertyDidChange:(NSString *)property; + @end @interface ASLayoutElementStyle : NSObject @@ -179,8 +198,7 @@ ASDK_EXTERN NSString * const ASLayoutElementStyleLayoutPositionProperty; * * @discussion The delegate must adopt the ASLayoutElementStyleDelegate protocol. The delegate is not retained. */ -@property (nullable, nonatomic, weak, readonly) id delegate; - +@property (nullable, atomic, weak, readonly) id delegate; #pragma mark - Sizing @@ -293,6 +311,15 @@ ASDK_EXTERN NSString * const ASLayoutElementStyleLayoutPositionProperty; */ @property (nonatomic) ASLayoutSize maxLayoutSize; +#if YOGA + +/** + * @abstract The overflow mode for this container. Only available in yoga. + */ +@property(nonatomic) YGOverflow overflow; + +#endif + @end #pragma mark - ASLayoutElementStylability diff --git a/Source/Layout/ASLayoutElement.mm b/Source/Layout/ASLayoutElement.mm index 38d50d6cb..5c81b131f 100644 --- a/Source/Layout/ASLayoutElement.mm +++ b/Source/Layout/ASLayoutElement.mm @@ -7,16 +7,18 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // +#import #import -#import +#import #import +#import +#import +#import +#import using AS::MutexLocker; -#if YOGA - #import YOGA_HEADER_PATH - #import -#endif +using namespace AS; #pragma mark - ASLayoutElementContext @@ -52,7 +54,6 @@ void ASLayoutElementPushContext(ASLayoutElementContext *context) ASLayoutElementContext *ASLayoutElementGetCurrentContext() { - // Don't retain here. Caller will retain if it wants to! return tls_context; } @@ -77,7 +78,7 @@ 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); } @@ -120,21 +121,6 @@ void ASLayoutElementPopContext() 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(); \ @@ -149,16 +135,9 @@ void ASLayoutElementPopContext() changed; \ }) -#define ASLayoutElementStyleCallDelegate(propertyName)\ -do {\ - [self propertyDidChange:propertyName];\ - [_delegate style:self propertyDidChange:propertyName];\ -} while(0) - @implementation ASLayoutElementStyle { AS::RecursiveMutex __instanceLock__; ASLayoutElementStyleExtensions _extensions; - std::atomic _size; std::atomic _spacingBefore; std::atomic _spacingAfter; @@ -169,26 +148,11 @@ @implementation ASLayoutElementStyle { std::atomic _ascender; std::atomic _descender; std::atomic _layoutPosition; - -#if YOGA - YGNodeRef _yogaNode; - std::atomic _flexWrap; - std::atomic _flexDirection; - std::atomic _direction; - std::atomic _justifyContent; - std::atomic _alignItems; - std::atomic _positionType; - std::atomic _position; - std::atomic _margin; - std::atomic _padding; - std::atomic _border; - std::atomic _aspectRatio; - ASStackLayoutAlignItems _parentAlignStyle; -#endif } @dynamic width, height, minWidth, maxWidth, minHeight, maxHeight; @dynamic preferredSize, minSize, maxSize, preferredLayoutSize, minLayoutSize, maxLayoutSize; +@dynamic layoutPosition; #pragma mark - Lifecycle @@ -207,12 +171,6 @@ - (instancetype)init if (self) { std::atomic_init(&_size, ASLayoutElementSizeMake()); std::atomic_init(&_flexBasis, ASDimensionAuto); -#if YOGA - _parentAlignStyle = ASStackLayoutAlignItemsNotSet; - std::atomic_init(&_flexDirection, ASStackLayoutDirectionVertical); - std::atomic_init(&_alignItems, ASStackLayoutAlignItemsStretch); - std::atomic_init(&_aspectRatio, static_cast(YGUndefined)); -#endif } return self; } @@ -245,7 +203,7 @@ - (void)setWidth:(ASDimension)width { BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.width = width; }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); + [self propertyDidChange:ASLayoutElementStyleWidthProperty]; } } @@ -258,7 +216,7 @@ - (void)setHeight:(ASDimension)height { BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.height = height; }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); + [self propertyDidChange:ASLayoutElementStyleHeightProperty]; } } @@ -271,7 +229,7 @@ - (void)setMinWidth:(ASDimension)minWidth { BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.minWidth = minWidth; }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); + [self propertyDidChange:ASLayoutElementStyleMinWidthProperty]; } } @@ -284,7 +242,7 @@ - (void)setMaxWidth:(ASDimension)maxWidth { BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.maxWidth = maxWidth; }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); + [self propertyDidChange:ASLayoutElementStyleMaxWidthProperty]; } } @@ -297,7 +255,7 @@ - (void)setMinHeight:(ASDimension)minHeight { BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.minHeight = minHeight; }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); + [self propertyDidChange:ASLayoutElementStyleMinHeightProperty]; } } @@ -310,7 +268,7 @@ - (void)setMaxHeight:(ASDimension)maxHeight { BOOL changed = ASLayoutElementStyleSetSizeWithScope({ newSize.maxHeight = maxHeight; }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); + [self propertyDidChange:ASLayoutElementStyleMaxHeightProperty]; } } @@ -324,8 +282,8 @@ - (void)setPreferredSize:(CGSize)preferredSize newSize.height = ASDimensionMakeWithPoints(preferredSize.height); }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); + [self propertyDidChange:ASLayoutElementStyleWidthProperty]; + [self propertyDidChange:ASLayoutElementStyleHeightProperty]; } } @@ -352,8 +310,8 @@ - (void)setMinSize:(CGSize)minSize newSize.minHeight = ASDimensionMakeWithPoints(minSize.height); }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); + [self propertyDidChange:ASLayoutElementStyleMinWidthProperty]; + [self propertyDidChange:ASLayoutElementStyleMinHeightProperty]; } } @@ -364,8 +322,8 @@ - (void)setMaxSize:(CGSize)maxSize newSize.maxHeight = ASDimensionMakeWithPoints(maxSize.height); }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); + [self propertyDidChange:ASLayoutElementStyleMaxWidthProperty]; + [self propertyDidChange:ASLayoutElementStyleMaxHeightProperty]; } } @@ -382,8 +340,8 @@ - (void)setPreferredLayoutSize:(ASLayoutSize)preferredLayoutSize newSize.height = preferredLayoutSize.height; }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleHeightProperty); + [self propertyDidChange:ASLayoutElementStyleWidthProperty]; + [self propertyDidChange:ASLayoutElementStyleHeightProperty]; } } @@ -400,8 +358,8 @@ - (void)setMinLayoutSize:(ASLayoutSize)minLayoutSize newSize.minHeight = minLayoutSize.height; }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMinHeightProperty); + [self propertyDidChange:ASLayoutElementStyleMinWidthProperty]; + [self propertyDidChange:ASLayoutElementStyleMinHeightProperty]; } } @@ -418,8 +376,8 @@ - (void)setMaxLayoutSize:(ASLayoutSize)maxLayoutSize newSize.maxHeight = maxLayoutSize.height; }); if (changed) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxWidthProperty); - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleMaxHeightProperty); + [self propertyDidChange:ASLayoutElementStyleMaxWidthProperty]; + [self propertyDidChange:ASLayoutElementStyleMaxHeightProperty]; } } @@ -428,7 +386,7 @@ - (void)setMaxLayoutSize:(ASLayoutSize)maxLayoutSize - (void)setSpacingBefore:(CGFloat)spacingBefore { if (_spacingBefore.exchange(spacingBefore) != spacingBefore) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingBeforeProperty); + [self propertyDidChange:ASLayoutElementStyleSpacingBeforeProperty]; } } @@ -440,7 +398,7 @@ - (CGFloat)spacingBefore - (void)setSpacingAfter:(CGFloat)spacingAfter { if (_spacingAfter.exchange(spacingAfter) != spacingAfter) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleSpacingAfterProperty); + [self propertyDidChange:ASLayoutElementStyleSpacingAfterProperty]; } } @@ -452,7 +410,7 @@ - (CGFloat)spacingAfter - (void)setFlexGrow:(CGFloat)flexGrow { if (_flexGrow.exchange(flexGrow) != flexGrow) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexGrowProperty); + [self propertyDidChange:ASLayoutElementStyleFlexGrowProperty]; } } @@ -464,7 +422,7 @@ - (CGFloat)flexGrow - (void)setFlexShrink:(CGFloat)flexShrink { if (_flexShrink.exchange(flexShrink) != flexShrink) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexShrinkProperty); + [self propertyDidChange:ASLayoutElementStyleFlexShrinkProperty]; } } @@ -476,7 +434,7 @@ - (CGFloat)flexShrink - (void)setFlexBasis:(ASDimension)flexBasis { if (!ASDimensionEqualToDimension(_flexBasis.exchange(flexBasis), flexBasis)) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleFlexBasisProperty); + [self propertyDidChange:ASLayoutElementStyleFlexBasisProperty]; } } @@ -488,7 +446,7 @@ - (ASDimension)flexBasis - (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf { if (_alignSelf.exchange(alignSelf) != alignSelf) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAlignSelfProperty); + [self propertyDidChange:ASLayoutElementStyleAlignSelfProperty]; } } @@ -500,7 +458,7 @@ - (ASStackLayoutAlignSelf)alignSelf - (void)setAscender:(CGFloat)ascender { if (_ascender.exchange(ascender) != ascender) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleAscenderProperty); + [self propertyDidChange:ASLayoutElementStyleAscenderProperty]; } } @@ -512,7 +470,7 @@ - (CGFloat)ascender - (void)setDescender:(CGFloat)descender { if (_descender.exchange(descender) != descender) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleDescenderProperty); + [self propertyDidChange:ASLayoutElementStyleDescenderProperty]; } } @@ -526,7 +484,7 @@ - (CGFloat)descender - (void)setLayoutPosition:(CGPoint)layoutPosition { if (!CGPointEqualToPoint(_layoutPosition.exchange(layoutPosition), layoutPosition)) { - ASLayoutElementStyleCallDelegate(ASLayoutElementStyleLayoutPositionProperty); + [self propertyDidChange:ASLayoutElementStyleLayoutPositionProperty]; } } @@ -535,7 +493,7 @@ - (CGPoint)layoutPosition return _layoutPosition.load(); } -#pragma mark - Extensions +#pragma mark - Extensibility - (void)setLayoutOptionExtensionBool:(BOOL)value atIndex:(int)idx { @@ -654,230 +612,80 @@ - (NSString *)description 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 + [_delegate style:self propertyDidChange:propertyName]; } -#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 { - if (_flexWrap.exchange(flexWrap) != flexWrap) { - ASLayoutElementStyleCallDelegate(ASYogaFlexWrapProperty); - } -} -- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection { - if (_flexDirection.exchange(flexDirection) != flexDirection) { - ASLayoutElementStyleCallDelegate(ASYogaFlexDirectionProperty); - } -} -- (void)setDirection:(YGDirection)direction { - if (_direction.exchange(direction) != direction) { - ASLayoutElementStyleCallDelegate(ASYogaDirectionProperty); - } -} -- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify { - if (_justifyContent.exchange(justify) != justify) { - ASLayoutElementStyleCallDelegate(ASYogaJustifyContentProperty); - } -} -- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { - if (_alignItems.exchange(alignItems) != alignItems) { - ASLayoutElementStyleCallDelegate(ASYogaAlignItemsProperty); - } -} -- (void)setPositionType:(YGPositionType)positionType { - if (_positionType.exchange(positionType) != positionType) { - ASLayoutElementStyleCallDelegate(ASYogaPositionTypeProperty); - } -} -/// TODO: smart compare ASEdgeInsets instead of memory compare. -- (void)setPosition:(ASEdgeInsets)position { - ASEdgeInsets oldValue = _position.exchange(position); - if (0 != memcmp(&position, &oldValue, sizeof(ASEdgeInsets))) { - ASLayoutElementStyleCallDelegate(ASYogaPositionProperty); - } -} -- (void)setMargin:(ASEdgeInsets)margin { - ASEdgeInsets oldValue = _margin.exchange(margin); - if (0 != memcmp(&margin, &oldValue, sizeof(ASEdgeInsets))) { - ASLayoutElementStyleCallDelegate(ASYogaMarginProperty); - } -} -- (void)setPadding:(ASEdgeInsets)padding { - ASEdgeInsets oldValue = _padding.exchange(padding); - if (0 != memcmp(&padding, &oldValue, sizeof(ASEdgeInsets))) { - ASLayoutElementStyleCallDelegate(ASYogaPaddingProperty); - } -} -- (void)setBorder:(ASEdgeInsets)border { - ASEdgeInsets oldValue = _border.exchange(border); - if (0 != memcmp(&border, &oldValue, sizeof(ASEdgeInsets))) { - ASLayoutElementStyleCallDelegate(ASYogaBorderProperty); - } -} -- (void)setAspectRatio:(CGFloat)aspectRatio { - if (_aspectRatio.exchange(aspectRatio) != aspectRatio) { - ASLayoutElementStyleCallDelegate(ASYogaAspectRatioProperty); - } -} -// private (ASLayoutElementStylePrivate.h) -- (void)setParentAlignStyle:(ASStackLayoutAlignItems)style { - _parentAlignStyle = style; -} +#define ASSERT_USE_STYLE_NODE_YOGA() NSCAssert(NO, @"ASLayoutElementStyleYoga needs to be used."); + +- (YGNodeRef)yogaNode { + ASSERT_USE_STYLE_NODE_YOGA() + return NULL; +} +- (YGWrap)flexWrap { + ASSERT_USE_STYLE_NODE_YOGA() + return YGWrapNoWrap; +} +- (ASStackLayoutDirection)flexDirection { + ASSERT_USE_STYLE_NODE_YOGA() + return ASStackLayoutDirectionVertical; +} +- (YGDirection)direction { + ASSERT_USE_STYLE_NODE_YOGA() + return YGDirectionInherit; +} +- (ASStackLayoutJustifyContent)justifyContent { + ASSERT_USE_STYLE_NODE_YOGA() + return ASStackLayoutJustifyContentStart; +} +- (ASStackLayoutAlignItems)alignItems { + ASSERT_USE_STYLE_NODE_YOGA() + return ASStackLayoutAlignItemsStretch; +} +- (YGPositionType)positionType { + ASSERT_USE_STYLE_NODE_YOGA() + return YGPositionTypeRelative; +} +- (ASEdgeInsets)position { + ASSERT_USE_STYLE_NODE_YOGA() + return ASEdgeInsetsMake(UIEdgeInsetsZero); +} +- (ASEdgeInsets)margin { + ASSERT_USE_STYLE_NODE_YOGA() + return ASEdgeInsetsMake(UIEdgeInsetsZero); +} +- (ASEdgeInsets)padding { + ASSERT_USE_STYLE_NODE_YOGA() + return ASEdgeInsetsMake(UIEdgeInsetsZero); +} +- (ASEdgeInsets)border { + ASSERT_USE_STYLE_NODE_YOGA() + return ASEdgeInsetsMake(UIEdgeInsetsZero); +} +- (CGFloat)aspectRatio { + ASSERT_USE_STYLE_NODE_YOGA() + return 0; +} +- (YGOverflow)overflow { + ASSERT_USE_STYLE_NODE_YOGA() + return YGOverflowVisible; +} +- (void)setFlexWrap:(YGWrap)flexWrap { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setDirection:(YGDirection)direction { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setPositionType:(YGPositionType)positionType { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setPosition:(ASEdgeInsets)position { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setMargin:(ASEdgeInsets)margin { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setPadding:(ASEdgeInsets)padding { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setBorder:(ASEdgeInsets)border { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setAspectRatio:(CGFloat)aspectRatio { ASSERT_USE_STYLE_NODE_YOGA() } +- (void)setOverflow:(YGOverflow)overflow { ASSERT_USE_STYLE_NODE_YOGA() } #endif /* YOGA */ diff --git a/Source/Layout/ASLayoutElementPrivate.h b/Source/Layout/ASLayoutElementPrivate.h index bf8c05bfa..1fef1c935 100644 --- a/Source/Layout/ASLayoutElementPrivate.h +++ b/Source/Layout/ASLayoutElementPrivate.h @@ -38,6 +38,10 @@ NS_ASSUME_NONNULL_END #pragma mark - ASLayoutElementLayoutDefaults #define ASLayoutElementLayoutCalculationDefaults \ +- (CGSize)measure:(ASSizeRange)constrainedSize\ +{\ + return [self layoutThatFits:constrainedSize].size;\ +}\ - (ASLayout *)layoutThatFits:(ASSizeRange)constrainedSize\ {\ return [self layoutThatFits:constrainedSize parentSize:constrainedSize.max];\ diff --git a/Source/Layout/ASLayoutElementStyleYoga.h b/Source/Layout/ASLayoutElementStyleYoga.h new file mode 100644 index 000000000..238d9768f --- /dev/null +++ b/Source/Layout/ASLayoutElementStyleYoga.h @@ -0,0 +1,140 @@ +// +// ASLayoutElementStyleYoga.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASLayoutElementStyleYoga : NSObject + + +#pragma mark - Sizing + +/** + * @abstract The width property specifies the width of the content area of an ASLayoutElement. + * The minWidth and maxWidth properties override width. + * Defaults to ASDimensionAuto + */ +@property ASDimension width; + +/** + * @abstract The height property specifies the height of the content area of an ASLayoutElement + * The minHeight and maxHeight properties override height. + * Defaults to ASDimensionAuto + */ +@property ASDimension height; + +/** + * @abstract The minHeight property is used to set the minimum height of a given element. It prevents the used value + * of the height property from becoming smaller than the value specified for minHeight. + * The value of minHeight overrides both maxHeight and height. + * Defaults to ASDimensionAuto + */ +@property ASDimension minHeight; + +/** + * @abstract The maxHeight property is used to set the maximum height of an element. It prevents the used value of the + * height property from becoming larger than the value specified for maxHeight. + * The value of maxHeight overrides height, but minHeight overrides maxHeight. + * Defaults to ASDimensionAuto + */ +@property ASDimension maxHeight; + +/** + * @abstract The minWidth property is used to set the minimum width of a given element. It prevents the used value of + * the width property from becoming smaller than the value specified for minWidth. + * The value of minWidth overrides both maxWidth and width. + * Defaults to ASDimensionAuto + */ +@property ASDimension minWidth; + +/** + * @abstract The maxWidth property is used to set the maximum width of a given element. It prevents the used value of + * the width property from becoming larger than the value specified for maxWidth. + * The value of maxWidth overrides width, but minWidth overrides maxWidth. + * Defaults to ASDimensionAuto + */ +@property ASDimension maxWidth; + +#pragma mark - ASLayoutElementStyleSizeHelpers + +/** + * @abstract Provides a suggested size for a layout element. If the optional minSize or maxSize are provided, + * and the preferredSize exceeds these, the minSize or maxSize will be enforced. If this optional value is not + * provided, the layout element’s size will default to it’s intrinsic content size provided calculateSizeThatFits: + * + * @discussion This method is optional, but one of either preferredSize or preferredLayoutSize is required + * for nodes that either have 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. + * + * @warning Calling the getter when the size's width or height are relative will cause an assert. + */ +@property CGSize preferredSize; + + /** + * @abstract An optional property that provides a minimum size bound for a layout element. If provided, this restriction will + * always be enforced. If a parent layout element’s minimum size is smaller than its child’s minimum size, the child’s + * minimum size will be enforced and its size will extend out of the layout spec’s. + * + * @discussion For example, if you set a preferred relative width of 50% and a minimum width of 200 points on an + * element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, + * since 160 pts is lower than the minimum width of 200 pts, the minimum width would be used. + */ +@property CGSize minSize; +- (CGSize)minSize UNAVAILABLE_ATTRIBUTE; + +/** + * @abstract An optional property that provides a maximum size bound for a layout element. If provided, this restriction will + * always be enforced. If a child layout element’s maximum size is smaller than its parent, the child’s maximum size will + * be enforced and its size will extend out of the layout spec’s. + * + * @discussion For example, if you set a preferred relative width of 50% and a maximum width of 120 points on an + * element in a full screen container, this would result in a width of 160 points on an iPhone screen. However, + * since 160 pts is higher than the maximum width of 120 pts, the maximum width would be used. + */ +@property CGSize maxSize; +- (CGSize)maxSize UNAVAILABLE_ATTRIBUTE; + +/** + * @abstract Provides a suggested RELATIVE size for a layout element. An ASLayoutSize uses percentages rather + * than points to specify layout. E.g. width should be 50% of the parent’s width. If the optional minLayoutSize or + * maxLayoutSize are provided, and the preferredLayoutSize exceeds these, the minLayoutSize or maxLayoutSize + * will be enforced. If this optional value is not provided, the layout element’s size will default to its intrinsic content size + * provided calculateSizeThatFits: + */ +@property ASLayoutSize preferredLayoutSize; + +/** + * @abstract An optional property that provides a minimum RELATIVE size bound for a layout element. If provided, this + * restriction will always be enforced. If a parent layout element’s minimum relative size is smaller than its child’s minimum + * relative size, the child’s minimum relative size will be enforced and its size will extend out of the layout spec’s. + */ +@property ASLayoutSize minLayoutSize; + +/** + * @abstract An optional property that provides a maximum RELATIVE size bound for a layout element. If provided, this + * restriction will always be enforced. If a parent layout element’s maximum relative size is smaller than its child’s maximum + * relative size, the child’s maximum relative size will be enforced and its size will extend out of the layout spec’s. + */ +@property ASLayoutSize maxLayoutSize; + +@end + +NS_ASSUME_NONNULL_END diff --git a/Source/Layout/ASLayoutElementStyleYoga.mm b/Source/Layout/ASLayoutElementStyleYoga.mm new file mode 100644 index 000000000..4f4119076 --- /dev/null +++ b/Source/Layout/ASLayoutElementStyleYoga.mm @@ -0,0 +1,607 @@ +// +// ASLayoutElementStyleYoga.mm +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import +#import +#import +#import +#import +#import +#import +#import + +#if YOGA + +#import YOGA_HEADER_PATH +#import + +using AS::MutexLocker; + +@implementation ASLayoutElementStyleYoga { + AS::MutexOrPointer __instanceLock__; + + YGNodeRef _yogaNode; + struct { + /** + * Will move to node when style is removed for Yoga. + * Indicates whether "YGAlignBaseline" means first or last. + */ + BOOL alignItemsBaselineIsLast:1; + } _flags; +} +@dynamic width, height, minWidth, maxWidth, minHeight, maxHeight; +@dynamic preferredSize, minSize, maxSize, preferredLayoutSize, minLayoutSize, maxLayoutSize; +@dynamic layoutPosition; + +#pragma mark - Lifecycle + +- (instancetype)init +{ + self = [super init]; + if (self) { + _yogaNode = YGNodeNew(); + + // Set default values that differ between Yoga and Texture + // Default YGUnitAuto, this set's it to YGUnitUndefined as ASDimensionAuto maps to YGUnitUndefined + YGNODE_STYLE_SET_DIMENSION(_yogaNode, FlexBasis, ASDimensionAuto); + + ASNodeContext *nodeContext = ASNodeContextGet(); + __instanceLock__.Configure(nodeContext ? &nodeContext->_mutex : nullptr); + } + return self; +} + +ASSynthesizeLockingMethodsWithMutex(__instanceLock__) + +#pragma mark - ASLayoutElementStyleSize + +- (ASLayoutElementSize)size +{ + NSAssert(NO, @"Method unavailable in Yoga."); + return {}; +} + +- (void)setSize:(ASLayoutElementSize)size +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +#pragma mark - ASLayoutElementStyleSizeForwarding + +- (ASDimension)width +{ + MutexLocker l(__instanceLock__); + return dimensionForYogaValue(YGNodeStyleGetWidth(_yogaNode)); +} + +- (void)setWidth:(ASDimension)width +{ + MutexLocker l(__instanceLock__); + YGNODE_STYLE_SET_DIMENSION(_yogaNode, Width, width); +} + +- (ASDimension)height +{ + MutexLocker l(__instanceLock__); + return dimensionForYogaValue(YGNodeStyleGetHeight(_yogaNode)); +} + +- (void)setHeight:(ASDimension)height +{ + MutexLocker l(__instanceLock__); + YGNODE_STYLE_SET_DIMENSION(_yogaNode, Height, height); +} + +- (ASDimension)minWidth +{ + MutexLocker l(__instanceLock__); + return dimensionForYogaValue(YGNodeStyleGetMinWidth(_yogaNode)); +} + +- (void)setMinWidth:(ASDimension)minWidth +{ + MutexLocker l(__instanceLock__); + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinWidth, minWidth); +} + +- (ASDimension)maxWidth +{ + MutexLocker l(__instanceLock__); + return dimensionForYogaValue(YGNodeStyleGetMaxWidth(_yogaNode)); +} + +- (void)setMaxWidth:(ASDimension)maxWidth +{ + MutexLocker l(__instanceLock__); + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxWidth, maxWidth); +} + +- (ASDimension)minHeight +{ + MutexLocker l(__instanceLock__); + return dimensionForYogaValue(YGNodeStyleGetMinHeight(_yogaNode)); +} + +- (void)setMinHeight:(ASDimension)minHeight +{ + MutexLocker l(__instanceLock__); + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MinHeight, minHeight); +} + +- (ASDimension)maxHeight +{ + MutexLocker l(__instanceLock__); + return dimensionForYogaValue(YGNodeStyleGetMaxHeight(_yogaNode)); +} + +- (void)setMaxHeight:(ASDimension)maxHeight +{ + MutexLocker l(__instanceLock__); + YGNODE_STYLE_SET_DIMENSION(_yogaNode, MaxHeight, maxHeight); +} + + +#pragma mark - ASLayoutElementStyleSizeHelpers + +- (void)setPreferredSize:(CGSize)preferredSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +- (CGSize)preferredSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); + return CGSizeZero; +} + +- (void)setMinSize:(CGSize)minSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +- (void)setMaxSize:(CGSize)maxSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +- (ASLayoutSize)preferredLayoutSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); + return {}; +} + +- (void)setPreferredLayoutSize:(ASLayoutSize)preferredLayoutSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +- (ASLayoutSize)minLayoutSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); + return {}; +} + +- (void)setMinLayoutSize:(ASLayoutSize)minLayoutSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +- (ASLayoutSize)maxLayoutSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); + return {}; +} + +- (void)setMaxLayoutSize:(ASLayoutSize)maxLayoutSize +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +#pragma mark - ASStackLayoutElement + +- (void)setSpacingBefore:(CGFloat)spacingBefore +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +- (CGFloat)spacingBefore +{ + NSAssert(NO, @"Method unavailable in Yoga."); + return 0; +} + +- (void)setSpacingAfter:(CGFloat)spacingAfter +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +- (CGFloat)spacingAfter +{ + NSAssert(NO, @"Method unavailable in Yoga."); + return 0; +} + +- (void)setFlexGrow:(CGFloat)flexGrow +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetFlexGrow(_yogaNode, flexGrow); +} + +- (CGFloat)flexGrow +{ + MutexLocker l(__instanceLock__); + return YGNodeStyleGetFlexGrow(_yogaNode); +} + +- (void)setFlexShrink:(CGFloat)flexShrink +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetFlexShrink(_yogaNode, flexShrink); +} + +- (CGFloat)flexShrink +{ + MutexLocker l(__instanceLock__); + return YGNodeStyleGetFlexShrink(_yogaNode); +} + +- (void)setFlexBasis:(ASDimension)flexBasis +{ + MutexLocker l(__instanceLock__); + YGNODE_STYLE_SET_DIMENSION(_yogaNode, FlexBasis, flexBasis); +} + +- (ASDimension)flexBasis +{ + MutexLocker l(__instanceLock__); + return dimensionForYogaValue(YGNodeStyleGetFlexBasis(_yogaNode)); +} + +- (void)setAlignSelf:(ASStackLayoutAlignSelf)alignSelf +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetAlignSelf(_yogaNode, yogaAlignSelf(alignSelf)); +} + +- (ASStackLayoutAlignSelf)alignSelf +{ + MutexLocker l(__instanceLock__); + return stackAlignSelf(YGNodeStyleGetAlignSelf(_yogaNode)); +} + +- (void)setAscender:(CGFloat)ascender +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +- (CGFloat)ascender +{ + NSAssert(NO, @"Method unavailable in Yoga."); + return 0; +} + +- (void)setDescender:(CGFloat)descender +{ + NSAssert(NO, @"Method unavailable in Yoga."); +} + +- (CGFloat)descender +{ + NSAssert(NO, @"Method unavailable in Yoga."); + return 0; +} + +#pragma mark - ASAbsoluteLayoutElement + +- (void)setLayoutPosition:(CGPoint)layoutPosition +{ + // NSCAssert(NO, @"layoutPosition not supported in Yoga"); +} + +- (CGPoint)layoutPosition +{ + // NSCAssert(NO, @"layoutPosition not supported in Yoga"); + return CGPointZero; +} + +#pragma mark - Extensibility + +- (void)setLayoutOptionExtensionBool:(BOOL)value atIndex:(int)idx +{ + NSCAssert(NO, @"Layout option extensions not supported in Yoga"); +} + +- (BOOL)layoutOptionExtensionBoolAtIndex:(int)idx +{ + NSCAssert(NO, @"Layout option extensions not supported in Yoga"); + return NO; +} + +- (void)setLayoutOptionExtensionInteger:(NSInteger)value atIndex:(int)idx +{ + NSCAssert(NO, @"Layout option extensions not supported in Yoga"); +} + +- (NSInteger)layoutOptionExtensionIntegerAtIndex:(int)idx +{ + NSCAssert(NO, @"Layout option extensions not supported in Yoga"); + return 0; +} + +- (void)setLayoutOptionExtensionEdgeInsets:(UIEdgeInsets)value atIndex:(int)idx +{ + NSCAssert(NO, @"Layout option extensions not supported in Yoga"); +} + +- (UIEdgeInsets)layoutOptionExtensionEdgeInsetsAtIndex:(int)idx +{ + NSCAssert(NO, @"Layout option extensions not supported in Yoga"); + return UIEdgeInsetsZero; +} + +#pragma mark - Debugging + +- (NSString *)description +{ + return ASObjectDescriptionMake(self, [self propertiesForDescription]); +} + +- (NSMutableArray *)propertiesForDescription +{ + NSMutableArray *result = [NSMutableArray array]; + + if ((self.minLayoutSize.width.unit != ASDimensionUnitAuto || + self.minLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"minLayoutSize" : NSStringFromASLayoutSize(self.minLayoutSize) }]; + } + + if ((self.preferredLayoutSize.width.unit != ASDimensionUnitAuto || + self.preferredLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"preferredSize" : NSStringFromASLayoutSize(self.preferredLayoutSize) }]; + } + + if ((self.maxLayoutSize.width.unit != ASDimensionUnitAuto || + self.maxLayoutSize.height.unit != ASDimensionUnitAuto)) { + [result addObject:@{ @"maxLayoutSize" : NSStringFromASLayoutSize(self.maxLayoutSize) }]; + } + + if (self.alignSelf != ASStackLayoutAlignSelfAuto) { + [result addObject:@{ @"alignSelf" : [@[@"ASStackLayoutAlignSelfAuto", + @"ASStackLayoutAlignSelfStart", + @"ASStackLayoutAlignSelfEnd", + @"ASStackLayoutAlignSelfCenter", + @"ASStackLayoutAlignSelfStretch"] objectAtIndex:self.alignSelf] }]; + } + + if (self.ascender != 0) { + [result addObject:@{ @"ascender" : @(self.ascender) }]; + } + + if (self.descender != 0) { + [result addObject:@{ @"descender" : @(self.descender) }]; + } + + if (ASDimensionEqualToDimension(self.flexBasis, ASDimensionAuto) == NO) { + [result addObject:@{ @"flexBasis" : NSStringFromASDimension(self.flexBasis) }]; + } + + if (self.flexGrow != 0) { + [result addObject:@{ @"flexGrow" : @(self.flexGrow) }]; + } + + if (self.flexShrink != 0) { + [result addObject:@{ @"flexShrink" : @(self.flexShrink) }]; + } + + if (self.spacingAfter != 0) { + [result addObject:@{ @"spacingAfter" : @(self.spacingAfter) }]; + } + + if (self.spacingBefore != 0) { + [result addObject:@{ @"spacingBefore" : @(self.spacingBefore) }]; + } + + return result; +} + ++ (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; +} + +- (void)dealloc +{ + YGNodeFree(_yogaNode); +} + +- (YGWrap)flexWrap +{ + MutexLocker l(__instanceLock__); + return YGNodeStyleGetFlexWrap(_yogaNode); +} + +- (ASStackLayoutDirection)flexDirection +{ + MutexLocker l(__instanceLock__); + return stackFlexDirection(YGNodeStyleGetFlexDirection(_yogaNode)); +} + +- (YGDirection)direction +{ + MutexLocker l(__instanceLock__); + return YGNodeStyleGetDirection(_yogaNode); +} + +- (ASStackLayoutJustifyContent)justifyContent +{ + return stackJustifyContent(YGNodeStyleGetJustifyContent(_yogaNode)); +} + +- (ASStackLayoutAlignItems)alignItems +{ + return stackAlignItems(YGNodeStyleGetAlignItems(_yogaNode), _flags.alignItemsBaselineIsLast); +} + +- (ASStackLayoutAlignItems)alignContent +{ + return stackAlignItems(YGNodeStyleGetAlignContent(_yogaNode), false); +} + +- (YGPositionType)positionType +{ + MutexLocker l(__instanceLock__); + return YGNodeStyleGetPositionType(_yogaNode); +} + +- (ASEdgeInsets)position +{ + MutexLocker l(__instanceLock__); + AS_EDGE_INSETS_FROM_YGNODE_STYLE(_yogaNode, Position, dimensionForYogaValue); +} + +- (ASEdgeInsets)margin +{ + MutexLocker l(__instanceLock__); + AS_EDGE_INSETS_FROM_YGNODE_STYLE(_yogaNode, Margin, dimensionForYogaValue); +} + +- (ASEdgeInsets)padding +{ + MutexLocker l(__instanceLock__); + AS_EDGE_INSETS_FROM_YGNODE_STYLE(_yogaNode, Padding, dimensionForYogaValue); +} + +- (ASEdgeInsets)border +{ + MutexLocker l(__instanceLock__); + AS_EDGE_INSETS_FROM_YGNODE_STYLE(_yogaNode, Border, [](float border) -> ASDimension { + return ASDimensionMake(ASDimensionUnitPoints, cgFloatForYogaFloat(border, 0)); + }); +} + +- (CGFloat)aspectRatio +{ + MutexLocker l(__instanceLock__); + return YGNodeStyleGetAspectRatio(_yogaNode); +} + +- (YGOverflow)overflow +{ + MutexLocker l(__instanceLock__); + return YGNodeStyleGetOverflow(_yogaNode); +} + +- (void)setFlexWrap:(YGWrap)flexWrap +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetFlexWrap(_yogaNode, flexWrap); +} + +- (void)setFlexDirection:(ASStackLayoutDirection)flexDirection +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetFlexDirection(_yogaNode, yogaFlexDirection(flexDirection)); +} + +- (void)setDirection:(YGDirection)direction +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetDirection(_yogaNode, direction); +} + +- (void)setJustifyContent:(ASStackLayoutJustifyContent)justify +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetJustifyContent(_yogaNode, yogaJustifyContent(justify)); +} + +- (void)setAlignItems:(ASStackLayoutAlignItems)alignItems +{ + MutexLocker l(__instanceLock__); + _flags.alignItemsBaselineIsLast = (alignItems == ASStackLayoutAlignItemsBaselineLast); + YGNodeStyleSetAlignItems(_yogaNode, yogaAlignItems(alignItems)); +} + +- (void)setAlignContent:(ASStackLayoutAlignItems)alignContent +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetAlignContent(_yogaNode, yogaAlignItems(alignContent)); +} + +- (void)setPositionType:(YGPositionType)positionType +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetPositionType(_yogaNode, positionType); +} + +- (void)setPosition:(ASEdgeInsets)position +{ + MutexLocker l(__instanceLock__); + 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); + } +} + +- (void)setMargin:(ASEdgeInsets)margin +{ + MutexLocker l(__instanceLock__); + 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); + } +} + +- (void)setPadding:(ASEdgeInsets)padding +{ + MutexLocker l(__instanceLock__); + 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); + } +} + +- (void)setBorder:(ASEdgeInsets)border +{ + MutexLocker l(__instanceLock__); + 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); + } +} + +- (void)setAspectRatio:(CGFloat)aspectRatio +{ + MutexLocker l(__instanceLock__); + if (aspectRatio > FLT_EPSILON && aspectRatio < CGFLOAT_MAX / 2.0) { + YGNodeStyleSetAspectRatio(_yogaNode, aspectRatio); + } +} + +- (void)setOverflow:(YGOverflow)overflow +{ + MutexLocker l(__instanceLock__); + YGNodeStyleSetOverflow(_yogaNode, overflow); +} + +@end + +#endif /* YOGA */ diff --git a/Source/Layout/ASStackLayoutDefines.h b/Source/Layout/ASStackLayoutDefines.h index de0543907..e27dff2c9 100644 --- a/Source/Layout/ASStackLayoutDefines.h +++ b/Source/Layout/ASStackLayoutDefines.h @@ -41,20 +41,31 @@ typedef NS_ENUM(unsigned char, ASStackLayoutJustifyContent) { */ ASStackLayoutJustifyContentEnd, /** - On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentStart. - Otherwise, the starting edge of the first child is at the starting edge of the stack, - the ending edge of the last child is at the ending edge of the stack, and the remaining children - are distributed so that the spacing between any two adjacent ones is the same. - If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last 2 children). + On overflow or if the stack has only 1 child, this value is identical to + ASStackLayoutJustifyContentStart. Otherwise, the starting edge of the first child is at the + starting edge of the stack, the ending edge of the last child is at the ending edge of the stack, + and the remaining children are distributed so that the spacing between any two adjacent ones is + the same. If there is a remaining space after spacing division, it is combined with the last + spacing (i.e the one between the last 2 children). */ ASStackLayoutJustifyContentSpaceBetween, /** - On overflow or if the stack has only 1 child, this value is identical to ASStackLayoutJustifyContentCenter. - Otherwise, children are distributed such that the spacing between any two adjacent ones is the same, - and the spacing between the first/last child and the stack edges is half the size of the spacing between children. - If there is a remaining space after spacing division, it is combined with the last spacing (i.e the one between the last child and the stack ending edge). + On overflow or if the stack has only 1 child, this value is identical to + ASStackLayoutJustifyContentCenter. Otherwise, children are distributed such that the spacing + between any two adjacent ones is the same, and the spacing between the first/last child and the + stack edges is half the size of the spacing between children. If there is a remaining space after + spacing division, it is combined with the last spacing (i.e the one between the last child and + the stack ending edge). */ - ASStackLayoutJustifyContentSpaceAround + ASStackLayoutJustifyContentSpaceAround, + + /** + On overflow or if the stack has only 1 child, this value is identical to + ASStackLayoutJustifyContentCenter. Otherwise, children are distributed such that the remaining + space are of the same length before and after each child. + NOTE: This is available in Yoga only for now. + */ + ASStackLayoutJustifyContentSpaceEvenly }; /** Orientation of children along cross axis */ diff --git a/Source/Layout/ASYogaUtilities.h b/Source/Layout/ASYogaUtilities.h index d5529fc45..30497a19a 100644 --- a/Source/Layout/ASYogaUtilities.h +++ b/Source/Layout/ASYogaUtilities.h @@ -8,15 +8,21 @@ #import -#if YOGA /* YOGA */ - #import #import #import +#if YOGA /* YOGA */ +AS_ASSUME_NORETAIN_BEGIN +NS_ASSUME_NONNULL_BEGIN + // Should pass a string literal, not an NSString as the first argument to ASYogaLog #define ASYogaLog(x, ...) as_log_verbose(ASLayoutLog(), x, ##__VA_ARGS__); +/** Helper function for Yoga baseline measurement. */ +ASDK_EXTERN CGFloat ASTextGetBaseline(CGFloat height, ASDisplayNode *_Nullable yogaParent, + NSAttributedString *str); + @interface ASDisplayNode (YogaHelpers) + (ASDisplayNode *)yogaNode; @@ -26,26 +32,24 @@ @end -// pre-order, depth-first -ASDK_EXTERN void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node)); - #pragma mark - Yoga Type Conversion Helpers ASDK_EXTERN YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems); +ASDK_EXTERN ASStackLayoutAlignItems stackAlignItems(YGAlign alignItems, bool baseline_is_last); ASDK_EXTERN YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent); +ASDK_EXTERN ASStackLayoutJustifyContent stackJustifyContent(YGJustify justifyContent); ASDK_EXTERN YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf); +ASDK_EXTERN ASStackLayoutAlignSelf stackAlignSelf(YGAlign alignSelf); ASDK_EXTERN YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction); +ASDK_EXTERN ASStackLayoutDirection stackFlexDirection(YGFlexDirection direction); ASDK_EXTERN float yogaFloatForCGFloat(CGFloat value); +ASDK_EXTERN CGFloat cgFloatForYogaFloat(float yogaFloat, CGFloat undefinedDefault); ASDK_EXTERN float yogaDimensionToPoints(ASDimension dimension); ASDK_EXTERN float yogaDimensionToPercent(ASDimension dimension); +ASDK_EXTERN YGValue yogaValueForDimension(ASDimension dimension); +ASDK_EXTERN ASDimension dimensionForYogaValue(YGValue value); ASDK_EXTERN ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets); -ASDK_EXTERN void ASLayoutElementYogaUpdateMeasureFunc(YGNodeRef yogaNode, id layoutElement); -ASDK_EXTERN float ASLayoutElementYogaBaselineFunc(YGNodeRef yogaNode, const float width, const float height); -ASDK_EXTERN YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, - float width, YGMeasureMode widthMode, - float height, YGMeasureMode heightMode); - #pragma mark - Yoga Style Setter Helpers #define YGNODE_STYLE_SET_DIMENSION(yogaNode, property, dimension) \ @@ -75,4 +79,46 @@ ASDK_EXTERN YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, YGNodeStyleSet##property(yogaNode, edge, YGUndefined); \ } \ +#define AS_EDGE_INSETS_FROM_YGNODE_STYLE(yogaNode, property, dimensionFunc) \ + ASEdgeInsets insets;\ + YGEdge edge = YGEdgeLeft; \ + for (int i = 0; i < YGEdgeAll + 1; ++i) { \ + ASDimension dimension = dimensionFunc(YGNodeStyleGet##property(yogaNode, edge)); \ + switch (edge) { \ + case YGEdgeLeft: \ + insets.left = dimension; \ + break; \ + case YGEdgeTop: \ + insets.top = dimension; \ + break; \ + case YGEdgeRight: \ + insets.right = dimension; \ + break; \ + case YGEdgeBottom: \ + insets.bottom = dimension; \ + break; \ + case YGEdgeStart: \ + insets.start = dimension; \ + break; \ + case YGEdgeEnd: \ + insets.end = dimension; \ + break; \ + case YGEdgeHorizontal: \ + insets.horizontal = dimension; \ + break; \ + case YGEdgeVertical: \ + insets.vertical = dimension; \ + break; \ + case YGEdgeAll: \ + insets.all = dimension; \ + break; \ + } \ + edge = (YGEdge)(edge + 1); \ + } \ + return insets; \ + +NS_ASSUME_NONNULL_END +AS_ASSUME_NORETAIN_END + #endif /* YOGA */ + diff --git a/Source/Layout/ASYogaUtilities.mm b/Source/Layout/ASYogaUtilities.mm index 68beede1a..caa427a67 100644 --- a/Source/Layout/ASYogaUtilities.mm +++ b/Source/Layout/ASYogaUtilities.mm @@ -6,17 +6,34 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import +#import #import +#import + #if YOGA /* YOGA */ +AS_ASSUME_NORETAIN_BEGIN + +using namespace AS; + +CGFloat ASTextGetBaseline(CGFloat height, ASDisplayNode *yogaParent, NSAttributedString *str) { + if (!yogaParent) return height; + NSUInteger len = str.length; + if (!len) return height; + BOOL isLast = (yogaParent.style.alignItems == ASStackLayoutAlignItemsBaselineLast); + UIFont *font = [str attribute:NSFontAttributeName + atIndex:(isLast ? len - 1 : 0) + effectiveRange:NULL]; + return isLast ? height + font.descender : font.ascender; +} + @implementation ASDisplayNode (YogaHelpers) + (ASDisplayNode *)yogaNode { ASDisplayNode *node = [[ASDisplayNode alloc] init]; - node.automaticallyManagesSubnodes = YES; - [node.style yogaNodeCreateIfNeeded]; + [node enableYoga]; + [node enableViewFlattening]; return node; } @@ -43,19 +60,6 @@ + (ASDisplayNode *)yogaHorizontalStack @end -void ASDisplayNodePerformBlockOnEveryYogaChild(ASDisplayNode *node, void(^block)(ASDisplayNode *node)) -{ - if (node == nil) { - return; - } - block(node); - // We use the accessor here despite the copy, because the block may modify the yoga tree e.g. - // replacing a node. - for (ASDisplayNode *child in node.yogaChildren) { - ASDisplayNodePerformBlockOnEveryYogaChild(child, block); - } -} - #pragma mark - Yoga Type Conversion Helpers YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems) @@ -72,6 +76,25 @@ YGAlign yogaAlignItems(ASStackLayoutAlignItems alignItems) } } +ASStackLayoutAlignItems stackAlignItems(YGAlign alignItems, bool baseline_is_last) +{ + switch (alignItems) { + case YGAlignAuto: return ASStackLayoutAlignItemsNotSet; + case YGAlignFlexStart: return ASStackLayoutAlignItemsStart; + case YGAlignFlexEnd: return ASStackLayoutAlignItemsEnd; + case YGAlignCenter: return ASStackLayoutAlignItemsCenter; + case YGAlignStretch: return ASStackLayoutAlignItemsStretch; + case YGAlignBaseline: + return (baseline_is_last ? ASStackLayoutAlignItemsBaselineLast + : ASStackLayoutAlignItemsBaselineFirst); + case YGAlignSpaceAround: + case YGAlignSpaceBetween: { + NSCAssert(NO, @"Align items value not supported."); + return ASStackLayoutAlignItemsNotSet; + } + } +} + YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent) { switch (justifyContent) { @@ -80,6 +103,22 @@ YGJustify yogaJustifyContent(ASStackLayoutJustifyContent justifyContent) case ASStackLayoutJustifyContentEnd: return YGJustifyFlexEnd; case ASStackLayoutJustifyContentSpaceBetween: return YGJustifySpaceBetween; case ASStackLayoutJustifyContentSpaceAround: return YGJustifySpaceAround; + case ASStackLayoutJustifyContentSpaceEvenly: return YGJustifySpaceEvenly; + } +} + +ASStackLayoutJustifyContent stackJustifyContent(YGJustify justifyContent) +{ + switch (justifyContent) { + case YGJustifyFlexStart: return ASStackLayoutJustifyContentStart; + case YGJustifyCenter: return ASStackLayoutJustifyContentCenter; + case YGJustifyFlexEnd: return ASStackLayoutJustifyContentEnd; + case YGJustifySpaceBetween: return ASStackLayoutJustifyContentSpaceBetween; + case YGJustifySpaceAround: return ASStackLayoutJustifyContentSpaceAround; + case YGJustifySpaceEvenly: { + NSCAssert(NO, @"Justify content value not supported."); + return ASStackLayoutJustifyContentStart; + } } } @@ -94,6 +133,23 @@ YGAlign yogaAlignSelf(ASStackLayoutAlignSelf alignSelf) } } +ASStackLayoutAlignSelf stackAlignSelf(YGAlign alignSelf) +{ + switch (alignSelf) { + case YGAlignFlexStart: return ASStackLayoutAlignSelfStart; + case YGAlignCenter: return ASStackLayoutAlignSelfCenter; + case YGAlignFlexEnd: return ASStackLayoutAlignSelfEnd; + case YGAlignStretch: return ASStackLayoutAlignSelfStretch; + case YGAlignAuto: return ASStackLayoutAlignSelfAuto; + case YGAlignBaseline: + case YGAlignSpaceBetween: + case YGAlignSpaceAround: { + NSCAssert(NO, @"Align self value not supported."); + return ASStackLayoutAlignSelfStart; + } + } +} + YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction) { switch (direction) { @@ -108,6 +164,20 @@ YGFlexDirection yogaFlexDirection(ASStackLayoutDirection direction) } } +ASStackLayoutDirection stackFlexDirection(YGFlexDirection direction) +{ + switch (direction) { + case YGFlexDirectionColumn: + return ASStackLayoutDirectionVertical; + case YGFlexDirectionColumnReverse: + return ASStackLayoutDirectionVerticalReverse; + case YGFlexDirectionRow: + return ASStackLayoutDirectionHorizontal; + case YGFlexDirectionRowReverse: + return ASStackLayoutDirectionHorizontalReverse; + } +} + float yogaFloatForCGFloat(CGFloat value) { if (value < CGFLOAT_MAX / 2) { @@ -137,6 +207,40 @@ float yogaDimensionToPercent(ASDimension dimension) } +YGValue yogaValueForDimension(ASDimension dimension) +{ + switch (dimension.unit) { + case ASDimensionUnitFraction: { + return (YGValue){yogaFloatForCGFloat(dimension.value), YGUnitPercent}; + } + case ASDimensionUnitPoints: { + return (YGValue){yogaFloatForCGFloat(dimension.value), YGUnitPoint}; + } + case ASDimensionUnitAuto: { + return (YGValue){yogaFloatForCGFloat(dimension.value), YGUnitAuto}; + } + } +} + +ASDimension dimensionForYogaValue(YGValue value) +{ + switch (value.unit) { + case YGUnitPercent: { + return ASDimensionMake(ASDimensionUnitFraction, cgFloatForYogaFloat(value.value, 0) / 100.0); + } + case YGUnitPoint: { + return ASDimensionMake(ASDimensionUnitPoints, cgFloatForYogaFloat(value.value, 0)); + } + case YGUnitAuto: { + return ASDimensionMake(ASDimensionUnitAuto, cgFloatForYogaFloat(value.value, 0)); + } + case YGUnitUndefined: { + // YGUnitUndefined maps over to Auto, the default value within Texture + return ASDimensionMake(ASDimensionUnitAuto, cgFloatForYogaFloat(value.value, 0)); + } + } +} + ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets) { switch (edge) { @@ -154,96 +258,6 @@ ASDimension dimensionForEdgeWithEdgeInsets(YGEdge edge, ASEdgeInsets insets) } } -void ASLayoutElementYogaUpdateMeasureFunc(YGNodeRef yogaNode, id layoutElement) -{ - if (yogaNode == NULL) { - return; - } - - BOOL shouldHaveMeasureFunc = [layoutElement implementsLayoutMethod]; - // How expensive is it to set a baselineFunc on all (leaf) nodes? - BOOL shouldHaveBaselineFunc = YES; - - if (layoutElement != nil) { - if (shouldHaveMeasureFunc || shouldHaveBaselineFunc) { - // Retain the Context object. This must be explicitly released with a - // __bridge_transfer - YGNodeFree() is not sufficient. - YGNodeSetContext(yogaNode, (__bridge_retained void *)layoutElement); - } - if (shouldHaveMeasureFunc) { - YGNodeSetMeasureFunc(yogaNode, &ASLayoutElementYogaMeasureFunc); - } - if (shouldHaveBaselineFunc) { - YGNodeSetBaselineFunc(yogaNode, &ASLayoutElementYogaBaselineFunc); - } - ASDisplayNodeCAssert(YGNodeGetContext(yogaNode) == (__bridge void *)layoutElement, - @"Yoga node context should contain layoutElement: %@", layoutElement); - } else { - // If we lack any of the conditions above, and currently have a measureFn/baselineFn/context, - // get rid of it. - // Release the __bridge_retained Context object. - __unused id element = (__bridge_transfer id)YGNodeGetContext(yogaNode); - YGNodeSetContext(yogaNode, NULL); - YGNodeSetMeasureFunc(yogaNode, NULL); - YGNodeSetBaselineFunc(yogaNode, NULL); - } -} - -float ASLayoutElementYogaBaselineFunc(YGNodeRef yogaNode, const float width, const float height) -{ - id layoutElement = (__bridge id)YGNodeGetContext(yogaNode); - ASDisplayNodeCAssert([layoutElement conformsToProtocol:@protocol(ASLayoutElement)], - @"Yoga context must be "); - - ASDisplayNode *displayNode = ASDynamicCast(layoutElement, ASDisplayNode); - - switch (displayNode.style.parentAlignStyle) { - case ASStackLayoutAlignItemsBaselineFirst: - return layoutElement.style.ascender; - case ASStackLayoutAlignItemsBaselineLast: - return height + layoutElement.style.descender; - default: - return 0; - } -} - -YGSize ASLayoutElementYogaMeasureFunc(YGNodeRef yogaNode, float width, YGMeasureMode widthMode, - float height, YGMeasureMode heightMode) -{ - id layoutElement = (__bridge id )YGNodeGetContext(yogaNode); - ASDisplayNodeCAssert([layoutElement conformsToProtocol:@protocol(ASLayoutElement)], @"Yoga context must be "); - - width = cgFloatForYogaFloat(width, CGFLOAT_MAX); - height = cgFloatForYogaFloat(height, CGFLOAT_MAX); - - ASSizeRange sizeRange; - sizeRange.min = CGSizeZero; - sizeRange.max = CGSizeMake(width, height); - if (widthMode == YGMeasureModeExactly) { - sizeRange.min.width = sizeRange.max.width; - } else { - // Mode is (YGMeasureModeAtMost | YGMeasureModeUndefined) - ASDimension minWidth = layoutElement.style.minWidth; - sizeRange.min.width = (minWidth.unit == ASDimensionUnitPoints ? yogaDimensionToPoints(minWidth) : 0.0); - } - if (heightMode == YGMeasureModeExactly) { - sizeRange.min.height = sizeRange.max.height; - } else { - // Mode is (YGMeasureModeAtMost | YGMeasureModeUndefined) - ASDimension minHeight = layoutElement.style.minHeight; - sizeRange.min.height = (minHeight.unit == ASDimensionUnitPoints ? yogaDimensionToPoints(minHeight) : 0.0); - } - - ASDisplayNodeCAssert(isnan(sizeRange.min.width) == NO && isnan(sizeRange.min.height) == NO, @"Yoga size range for measurement should not have NaN in minimum"); - if (isnan(sizeRange.max.width)) { - sizeRange.max.width = CGFLOAT_MAX; - } - if (isnan(sizeRange.max.height)) { - sizeRange.max.height = CGFLOAT_MAX; - } - - CGSize size = [[layoutElement layoutThatFits:sizeRange] size]; - return (YGSize){ .width = (float)size.width, .height = (float)size.height }; -} +AS_ASSUME_NORETAIN_END #endif /* YOGA */ diff --git a/Source/Private/ASCellNode+Internal.h b/Source/Private/ASCellNode+Internal.h index 0ec70ca06..f4261d6a8 100644 --- a/Source/Private/ASCellNode+Internal.h +++ b/Source/Private/ASCellNode+Internal.h @@ -16,7 +16,7 @@ NS_ASSUME_NONNULL_BEGIN @protocol ASCellNodeInteractionDelegate /** - * Notifies the delegate that a specified cell node invalidates it's size what could result into a size change. + * Notifies the delegate that a specified cell node invalidates its size which could result in a size change. * * @param node A node informing the delegate about the relayout. */ @@ -34,7 +34,7 @@ NS_ASSUME_NONNULL_BEGIN @interface ASCellNode () -@property (nonatomic, weak) id interactionDelegate; +@property (weak) id interactionDelegate; /* * Back-pointer to the containing scrollView instance, set only for visible cells. Used for Cell Visibility Event callbacks. diff --git a/Source/Private/ASControlNode+Defines.h b/Source/Private/ASControlNode+Defines.h new file mode 100644 index 000000000..54e5c1ddd --- /dev/null +++ b/Source/Private/ASControlNode+Defines.h @@ -0,0 +1,21 @@ +// +// ASControlNode+Defines.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import + +@interface ASControlNode (Defines) + +/** + * Class method that when set to YES ensures the BOOL parameter passed to -[ASControlNode + * setUserInteractionEnabled:] is also passed to -[ASControlNode setIsAccessibilityElement:]. Thus + * the two properties `userInteractionEnabled` and `isAccessibilityElement` stay in sync. When set + * to NO, these two properties are unrelated. + */ +@property(class, nonatomic) BOOL shouldUserInteractionEnabledSetIsAXElement; + +@end diff --git a/Source/Private/ASControlNode+Defines.mm b/Source/Private/ASControlNode+Defines.mm new file mode 100644 index 000000000..36eab1b74 --- /dev/null +++ b/Source/Private/ASControlNode+Defines.mm @@ -0,0 +1,23 @@ +// +// ASControlNode+Defines.m +// AsyncDisplayKit +// +// Created by Ashley Nelson on 4/6/21. +// Copyright © 2021 Pinterest. All rights reserved. +// + +#import + +static BOOL __enableUserInteractionSettingAXElement = YES; + +@implementation ASControlNode (Defines) + ++ (void)setShouldUserInteractionEnabledSetIsAXElement:(BOOL)enable { + __enableUserInteractionSettingAXElement = enable; +} + ++ (BOOL)shouldUserInteractionEnabledSetIsAXElement { + return __enableUserInteractionSettingAXElement; +} + +@end diff --git a/Source/Private/ASDisplayNode+FrameworkPrivate.h b/Source/Private/ASDisplayNode+FrameworkPrivate.h index b7e8b1538..5454953da 100644 --- a/Source/Private/ASDisplayNode+FrameworkPrivate.h +++ b/Source/Private/ASDisplayNode+FrameworkPrivate.h @@ -36,18 +36,23 @@ typedef NS_OPTIONS(unsigned char, 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. */ + 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, + Any layout calculated during this state should not be applied immediately, but pending until + later. */ + ASHierarchyStateLayoutPending = 1 << 3, }; ASDISPLAYNODE_INLINE BOOL ASHierarchyStateIncludesLayoutPending(ASHierarchyState hierarchyState) @@ -154,6 +159,7 @@ __unused static NSString * _Nonnull NSStringFromASHierarchyStateChange(ASHierarc */ @property (nonatomic) ASHierarchyState hierarchyState; +@property (nonatomic, weak) UIAccessibilityCustomAction *acessibilityCustomAction; /** * Represent the current custom action in representation for the node */ @@ -320,8 +326,47 @@ NS_INLINE UIAccessibilityTraits ASInteractiveAccessibilityTraitsMask() { return UIAccessibilityTraitLink | UIAccessibilityTraitKeyboardKey | UIAccessibilityTraitButton; } +// dispatch_once variables must live outside of static inline function or else will be copied +// for each separate invocation. We want them shared across all invocations. +static BOOL shouldEnableAccessibilityForTesting; +static dispatch_once_t kShouldEnableAccessibilityForTestingOnceToken; +NS_INLINE BOOL ASAccessibilityIsEnabled() { +#if DEBUG + return true; +#else + if (UIAccessibilityIsVoiceOverRunning() || UIAccessibilityIsSwitchControlRunning()) { + return true; + } + // In some ui test environment where DEBUG is not defined. + dispatch_once(&kShouldEnableAccessibilityForTestingOnceToken, ^{ + shouldEnableAccessibilityForTesting = [[[NSProcessInfo processInfo] arguments] + containsObject:@"AS_FORCE_ACCESSIBILITY_FOR_TESTING"]; + }); + return shouldEnableAccessibilityForTesting; +#endif +} + @interface ASDisplayNode (AccessibilityInternal) + +/** + * @discussion An array of the accessibility elements from the node. + */ - (nullable NSArray *)accessibilityElements; + +/** + * @discussion Invalidates the cached accessibility elements for the node + */ +- (void)invalidateAccessibilityElements; + +/** + * @discussion Invalidates the accessibility elements for the first accessibility container or + * the first non layer backed node by walking up the tree starting by self. + * + * @note Call this when a layer backed node changed (added/removed/updated) or + * a view in an accessibility container changed. + */ +- (void)invalidateFirstAccessibilityContainerOrNonLayerBackedNode; + @end; @interface UIView (ASDisplayNodeInternal) @@ -330,6 +375,7 @@ NS_INLINE UIAccessibilityTraits ASInteractiveAccessibilityTraitsMask() { @interface CALayer (ASDisplayNodeInternal) @property (nullable, weak) ASDisplayNode *asyncdisplaykit_node; +@property (nullable, strong) id as_retainedDelegate; @end NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASDisplayNode+UIViewBridge.mm b/Source/Private/ASDisplayNode+UIViewBridge.mm index 114e8119f..5d5f3a87f 100644 --- a/Source/Private/ASDisplayNode+UIViewBridge.mm +++ b/Source/Private/ASDisplayNode+UIViewBridge.mm @@ -8,12 +8,13 @@ // #import -#import -#import -#import -#import #import +#import +#import +#import #import +#import +#import /** * The following macros are conveniences to help in the common tasks related to the bridging that ASDisplayNode does to UIView and CALayer. @@ -41,6 +42,8 @@ #define _bridge_prologue_write #endif +AS_ASSUME_NORETAIN_BEGIN + /// Returns YES if the property set should be applied to view/layer immediately. /// Side Effect: Registers the node with the shared ASPendingStateController if /// the property cannot be immediately applied and the node does not already have pending changes. @@ -77,6 +80,8 @@ ASDISPLAYNODE_INLINE BOOL ASDisplayNodeShouldApplyBridgedWriteToView(ASDisplayNo #define _setToLayer(layerProperty, layerValueExpr) BOOL shouldApply = ASDisplayNodeShouldApplyBridgedWriteToView(self); \ if (shouldApply) { _layer.layerProperty = (layerValueExpr); } else { ASDisplayNodeGetPendingState(self).layerProperty = (layerValueExpr); } +using namespace AS; + /** * This category implements certain frequently-used properties and methods of UIView and CALayer so that ASDisplayNode clients can just call the view/layer methods on the node, * with minimal loss in performance. Unlike UIView and CALayer methods, these can be called from a non-main thread until the view or layer is created. @@ -329,77 +334,76 @@ - (CGRect)frame - (void)setFrame:(CGRect)rect { - BOOL setToView = NO; - BOOL setToLayer = NO; - CGRect newBounds = CGRectZero; - CGPoint newPosition = CGPointZero; - BOOL nodeLoaded = NO; - BOOL isMainThread = ASDisplayNodeThreadIsMain(); - { - _bridge_prologue_write; + _bridge_prologue_write; - // For classes like ASTableNode, ASCollectionNode, ASScrollNode and similar - make sure UIView gets setFrame: - struct ASDisplayNodeFlags flags = _flags; - BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), flags.layerBacked); - - nodeLoaded = _loaded(self); - if (!specialPropertiesHandling) { - BOOL canReadProperties = isMainThread || !nodeLoaded; - if (canReadProperties) { - // We don't have to set frame directly, and we can read current properties. - // Compute a new bounds and position and set them on self. - CALayer *layer = _layer; - CGPoint origin = (nodeLoaded ? layer.bounds.origin : self.bounds.origin); - CGPoint anchorPoint = (nodeLoaded ? layer.anchorPoint : self.anchorPoint); - - ASBoundsAndPositionForFrame(rect, origin, anchorPoint, &newBounds, &newPosition); - - if (ASIsCGRectValidForLayout(newBounds) == NO || ASIsCGPositionValidForLayout(newPosition) == NO) { - ASDisplayNodeAssertNonFatal(NO, @"-[ASDisplayNode setFrame:] - The new frame (%@) is invalid and unsafe to be set.", NSStringFromCGRect(rect)); - return; - } + // For classes like ASTableNode, ASCollectionNode, ASScrollNode and similar - make sure UIView gets setFrame: + struct ASDisplayNodeFlags flags = _flags; + BOOL specialPropertiesHandling = ASDisplayNodeNeedsSpecialPropertiesHandling(checkFlag(Synchronous), flags.layerBacked); - if (nodeLoaded) { - setToLayer = YES; - } else { - self.bounds = newBounds; - self.position = newPosition; - } + BOOL nodeLoaded = _loaded(self); + BOOL isMainThread = ASDisplayNodeThreadIsMain(); + if (!specialPropertiesHandling) { + BOOL canReadProperties = isMainThread || !nodeLoaded; + if (canReadProperties) { + // We don't have to set frame directly, and we can read current properties. + // Compute a new bounds and position and set them on self. + CALayer *layer = _layer; + BOOL useLayer = (layer != nil); + CGPoint origin = (useLayer ? layer.bounds.origin : self.bounds.origin); + CGPoint anchorPoint = (useLayer ? layer.anchorPoint : self.anchorPoint); + + CGRect newBounds = CGRectZero; + CGPoint newPosition = CGPointZero; + ASBoundsAndPositionForFrame(rect, origin, anchorPoint, &newBounds, &newPosition); + + if (ASIsCGRectValidForLayout(newBounds) == NO || ASIsCGPositionValidForLayout(newPosition) == NO) { + ASDisplayNodeAssertNonFatal(NO, @"-[ASDisplayNode setFrame:] - The new frame (%@) is invalid and unsafe to be set.", NSStringFromCGRect(rect)); + return; + } + + if (useLayer) { + layer.bounds = newBounds; + layer.position = newPosition; } else { - // We don't have to set frame directly, but we can't read properties. - // Store the frame in our pending state, and it'll get decomposed into - // bounds and position when the pending state is applied. - _ASPendingState *pendingState = ASDisplayNodeGetPendingState(self); - if (nodeLoaded && !pendingState.hasChanges) { - [[ASPendingStateController sharedInstance] registerNode:self]; - } - pendingState.frame = rect; + self.bounds = newBounds; + self.position = newPosition; } + // Any frame change in pending state is obsolete at this moment, so clear it so we don't + // set the frame to a previous value. No-op if the node doesn't have pending state. + [_pendingViewState clearFrameChange]; } else { - if (nodeLoaded && isMainThread) { - // We do have to set frame directly, and we're on main thread with a loaded node. - // Just set the frame on the view. - // NOTE: Frame is only defined when transform is identity because we explicitly diverge from CALayer behavior and define frame without transform. - setToView = YES; - } else { - // We do have to set frame directly, but either the node isn't loaded or we're on a non-main thread. - // Set the frame on the pending state, and it'll call setFrame: when applied. - _ASPendingState *pendingState = ASDisplayNodeGetPendingState(self); - if (nodeLoaded && !pendingState.hasChanges) { - [[ASPendingStateController sharedInstance] registerNode:self]; - } - pendingState.frame = rect; + // We don't have to set frame directly, but we can't read properties. + // Store the frame in our pending state, and it'll get decomposed into + // bounds and position when the pending state is applied. + _ASPendingState *pendingState = ASDisplayNodeGetPendingState(self); + if (nodeLoaded && !pendingState.hasChanges) { + [[ASPendingStateController sharedInstance] registerNode:self]; } + pendingState.frame = rect; + } + } else { + if (nodeLoaded && isMainThread) { + // We do have to set frame directly, and we're on main thread with a loaded node. + // Just set the frame on the view. + // NOTE: Frame is only defined when transform is identity because we explicitly diverge from CALayer behavior and define frame without transform. +//#if DEBUG +// // Checking if the transform is identity is expensive, so disable when unnecessary. We have assertions on in Release, so DEBUG is the only way I know of. +// ASDisplayNodeAssert(CATransform3DIsIdentity(self.transform), @"-[ASDisplayNode setFrame:] - self.transform must be identity in order to set the frame property. (From Apple's UIView documentation: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.)"); +//#endif + _view.bounds = CGRectMake(_view.bounds.origin.x, _view.bounds.origin.y, rect.size.width, rect.size.height); + _view.center = CGPointMake(CGRectGetMidX(rect), CGRectGetMidY(rect)); + // Any frame change in pending state is obsolete at this moment, so clear it so we don't + // set the frame to a previous value. No-op if the node doesn't have pending state. + [_pendingViewState clearFrameChange]; + } else { + // We do have to set frame directly, but either the node isn't loaded or we're on a non-main thread. + // Set the frame on the pending state, and it'll call setFrame: when applied. + _ASPendingState *pendingState = ASDisplayNodeGetPendingState(self); + if (nodeLoaded && !pendingState.hasChanges) { + [[ASPendingStateController sharedInstance] registerNode:self]; + } + pendingState.frame = rect; } - } - - if (setToView) { - ASDisplayNodeAssertTrue(nodeLoaded && isMainThread); - _view.frame = rect; - } else if (setToLayer) { - ASDisplayNodeAssertTrue(nodeLoaded && isMainThread); - _layer.bounds = newBounds; - _layer.position = newPosition; } } @@ -496,7 +500,17 @@ - (void)layoutIfNeeded [ASDisplayNodeGetPendingState(self) layoutIfNeeded]; } } - + + UniqueLock l(__instanceLock__); + if (Yoga2::GetEnabled(self)) { + if (shouldApply || !loaded) { + l.unlock(); + Yoga2::HandleExplicitLayoutIfNeeded(self); + } + return; + } + l.unlock(); + if (shouldApply) { // The node is loaded and we're on main. // Message the view or layer which in turn will call __layout on us (see -[_ASDisplayLayer layoutSublayers]). @@ -1372,6 +1386,18 @@ - (NSArray *)accessibilityHeaderElements } #endif +- (void)setAccessibilityElements:(NSArray *)accessibilityElements +{ + _bridge_prologue_write; + _setToViewOnly(accessibilityElements, accessibilityElements); +} + +- (NSArray *)accessibilityHeaderElements +{ + _bridge_prologue_read; + return _getFromViewOnly(accessibilityElements); +} + - (void)setAccessibilityActivationPoint:(CGPoint)accessibilityActivationPoint { _bridge_prologue_write; @@ -1396,12 +1422,6 @@ - (UIBezierPath *)accessibilityPath return _getAccessibilityFromViewOrProperty(_accessibilityPath, accessibilityPath); } -- (NSInteger)accessibilityElementCount -{ - _bridge_prologue_read; - return _getFromViewOnly(accessibilityElementCount); -} - @end @@ -1444,3 +1464,5 @@ - (_ASAsyncTransaction *)asyncdisplaykit_currentAsyncTransaction } @end + +AS_ASSUME_NORETAIN_END diff --git a/Source/Private/ASDisplayNodeInternal.h b/Source/Private/ASDisplayNodeInternal.h index b9f5f40d4..2edfe40a4 100644 --- a/Source/Private/ASDisplayNodeInternal.h +++ b/Source/Private/ASDisplayNodeInternal.h @@ -18,6 +18,7 @@ #import #import #import +#import #import #import #import @@ -46,12 +47,12 @@ typedef NS_OPTIONS(unsigned short, ASDisplayNodeMethodOverrides) ASDisplayNodeMethodOverrideLayoutSpecThatFits = 1 << 4, ASDisplayNodeMethodOverrideCalcLayoutThatFits = 1 << 5, ASDisplayNodeMethodOverrideCalcSizeThatFits = 1 << 6, + ASDisplayNodeMethodOverrideYogaBaseline = 1 << 7, }; 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. @@ -62,6 +63,8 @@ typedef NS_OPTIONS(uint_least32_t, ASDisplayNodeAtomicFlags) #define setFlag(flag, x) (((x ? _atomicFlags.fetch_or(flag) \ : _atomicFlags.fetch_and(~flag)) & flag) != 0) +#define ASDisplayNodeGetController(obj) (obj->_strongNodeController ?: obj->_weakNodeController) + ASDK_EXTERN NSString * const ASRenderingEngineDidDisplayScheduledNodesNotification; ASDK_EXTERN NSString * const ASRenderingEngineDidDisplayNodesScheduledBeforeTimestamp; @@ -77,8 +80,10 @@ static constexpr CACornerMask kASCACornerAllCorners = @interface ASDisplayNode () <_ASTransitionContextCompletionDelegate, CALayerDelegate> { @package - AS::RecursiveMutex __instanceLock__; + AS::MutexOrPointer __instanceLock__; + __weak ASDisplayNode *_weakSelf; + ASNodeContext *_nodeContext; _ASPendingState *_pendingViewState; UIView *_view; @@ -122,11 +127,20 @@ static constexpr CACornerMask kASCACornerAllCorners = unsigned isDeallocating:1; #if YOGA - unsigned willApplyNextYogaCalculatedLayout:1; + unsigned yoga:1; + unsigned shouldSuppressYogaCustomMeasure:1; + unsigned yogaIsApplyingLayout:1; + unsigned yogaRequestedNestedLayout:1; #endif // Automatically manages subnodes unsigned automaticallyManagesSubnodes:1; // Main thread only unsigned placeholderEnabled:1; + + // Flattening support + unsigned viewFlattening:1; + unsigned haveCachedIsFlattenable:1; + unsigned cachedIsFlattenable:1; + // Accessibility support unsigned isAccessibilityElement:1; unsigned accessibilityElementsHidden:1; @@ -138,6 +152,7 @@ static constexpr CACornerMask kASCACornerAllCorners = unsigned automaticallyRelayoutOnLayoutMarginsChanges:1; unsigned isViewControllerRoot:1; unsigned hasHadInterfaceStateDelegates:1; + unsigned isDisappearing:1; } _flags; ASInterfaceState _interfaceState; @@ -153,16 +168,12 @@ static constexpr CACornerMask kASCACornerAllCorners = // Dynamic colors support UIColor *_backgroundColor; -@protected ASDisplayNode * __weak _supernode; NSMutableArray *_subnodes; ASNodeController *_strongNodeController; __weak ASNodeController *_weakNodeController; - // Set this to nil whenever you modify _subnodes - NSArray *_cachedSubnodes; - std::atomic_uint _displaySentinel; // This is the desired contentsScale, not the scale at which the layer's contents should be displayed @@ -179,12 +190,13 @@ static constexpr CACornerMask kASCACornerAllCorners = NSString *_debugName; #if YOGA - // Only ASDisplayNodes are supported in _yogaChildren currently. This means that it is necessary to - // create ASDisplayNodes to make a stack layout when using Yoga. - // However, the implementation is mostly ready for id , with a few areas requiring updates. + // !!! Only use if !exp_unified_yoga_tree. Use ASAssertNotExperiment if you're not sure. NSMutableArray *_yogaChildren; + // Unfortunately this weak pointer has to stay around because even with shared + // locking, there is no way to avoid racing against the final release of a + // parent node when ascending. __weak ASDisplayNode *_yogaParent; - ASLayout *_yogaCalculatedLayout; + CGSize _yogaCalculatedLayoutMaxSize; #endif // Layout Transition @@ -214,7 +226,7 @@ static constexpr CACornerMask kASCACornerAllCorners = // View Loading ASDisplayNodeViewBlock _viewBlock; ASDisplayNodeLayerBlock _layerBlock; - NSMutableArray *_onDidLoadBlocks; + std::vector _onDidLoadBlocks; Class _viewClass; // nil -> _ASDisplayView Class _layerClass; // nil -> _ASDisplayLayer @@ -229,7 +241,9 @@ static constexpr CACornerMask kASCACornerAllCorners = // Corner Radius support CGFloat _cornerRadius; +@protected CALayer *_clipCornerLayers[NUM_CLIP_CORNER_LAYERS]; +@package CACornerMask _maskedCorners; ASDisplayNodeContextModifier _willDisplayNodeContentWithRenderingContext; @@ -253,6 +267,7 @@ static constexpr CACornerMask kASCACornerAllCorners = CGPoint _accessibilityActivationPoint; UIBezierPath *_accessibilityPath; + ASDisplayNodeAccessibilityElementsBlock _accessibilityElementsBlock; // Safe Area support // These properties are used on iOS 10 and lower, where safe area is not supported by UIKit. @@ -274,16 +289,37 @@ static constexpr CACornerMask kASCACornerAllCorners = /// Fast path: tells whether we've ever had an interface state delegate before. __weak id _interfaceStateDelegates[AS_MAX_INTERFACE_STATE_DELEGATES]; + + NSDictionary> *_disappearanceActions; } + (void)scheduleNodeForRecursiveDisplay:(ASDisplayNode *)node; +// When a table/collection view reload during a transaction, cell will reconfigure which might +// involves visible -> reload(hide show) and in this case we want to merge hide show pair by +// delaying the process until later. ++ (BOOL)shouldCoalesceInterfaceStateDuringTransaction; + /// The _ASDisplayLayer backing the node, if any. @property (nullable, nonatomic, readonly) _ASDisplayLayer *asyncLayer; /// Bitmask to check which methods an object overrides. - (ASDisplayNodeMethodOverrides)methodOverrides; +/** + * In edge cases _assign_ pointers to ASDisplayNode can be invalid, even when properly cleaned up + * in dealloc. This occurs when dealloc has begun, but not completed, and another thread tries to + * retain the _assign_ pointer. It cannot be successfully retained, the dealloc will complete, + * and a crash is likely. There's no obvious way to know when this is this case. _weak_ pointers, on + * the other hand, will have already been zeroed. Therefore ASDisplayNode will initialize a _weak_ + * self pointer, and `tryRetain` will return a safely strongified copy of it if it is not nil. + * + * This method should be used in all cases where _assign_ pointers are stored with the expectation + * that the ASDisplayNode or a subclass will clean them up (this can be the only reasonable way to + * interop with non-objC third-party libraries, notably Yoga). + */ +- (nullable instancetype)tryRetain; + /** * Invoked before a call to setNeedsLayout to the underlying view */ @@ -305,9 +341,12 @@ static constexpr CACornerMask kASCACornerAllCorners = - (void)__setNodeController:(ASNodeController *)controller; /** - * Called whenever the node needs to layout its subnodes and, if it's already loaded, its subviews. Executes the layout pass for the node + * 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. + * This method is thread-safe but requires thread affinity. At the same time, this method currently + * requires to be called without the lock held. This means that a race condition is unavoidable when + * calling this method from a background thread. */ - (void)__layout; @@ -366,6 +405,11 @@ static constexpr CACornerMask kASCACornerAllCorners = */ @property (readonly) BOOL rasterizesSubtree; +/** + * Called during layout pass to determine if the node should be flattened + */ +- (BOOL)isFlattenable; + /** * Called if a gesture recognizer was attached to an _ASDisplayView */ @@ -374,6 +418,9 @@ static constexpr CACornerMask kASCACornerAllCorners = // Recalculates fallbackSafeAreaInsets for the subnodes - (void)_fallbackUpdateSafeAreaOnChildren; +// Apply pending interface to interface state recursively for node and all subnodes. +- (void)recursivelyApplyPendingInterfaceState; + @end @interface ASDisplayNode (InternalPropertyBridge) diff --git a/Source/Private/ASImageNode+AnimatedImagePrivate.h b/Source/Private/ASImageNode+AnimatedImagePrivate.h index 917eb6fbb..c44a77638 100644 --- a/Source/Private/ASImageNode+AnimatedImagePrivate.h +++ b/Source/Private/ASImageNode+AnimatedImagePrivate.h @@ -17,6 +17,8 @@ id _animatedImage; NSString *_animatedImageRunLoopMode; CADisplayLink *_displayLink; + NSThread *_displayLinkThread; + NSRunLoop *_displayLinkRunloop; NSUInteger _lastSuccessfulFrameIndex; //accessed on main thread only diff --git a/Source/Private/ASImageNode+CGExtras.mm b/Source/Private/ASImageNode+CGExtras.mm index b7550c549..ca52900d8 100644 --- a/Source/Private/ASImageNode+CGExtras.mm +++ b/Source/Private/ASImageNode+CGExtras.mm @@ -62,6 +62,11 @@ void ASCroppedImageBackingSizeAndDrawRectInBounds(CGSize sourceImageSize, minimumDestinationSize = _ASSizeFitWithAspectRatio(boundsAspectRatio, sourceImageSize); else if (contentMode == UIViewContentModeScaleAspectFit) minimumDestinationSize = _ASSizeFillWithAspectRatio(boundsAspectRatio, sourceImageSize); + else if (contentMode == UIViewContentModeScaleToFill) { + *outBackingSize = boundsSize; + *outDrawRect = CGRectMake(0, 0, boundsSize.width, boundsSize.height); + return; + } } // If fitting the desired aspect ratio to the image size actually results in a larger buffer, use the input values. @@ -116,7 +121,6 @@ void ASCroppedImageBackingSizeAndDrawRectInBounds(CGSize sourceImageSize, scaledSizeForImage.height); } } - *outDrawRect = drawRect; *outBackingSize = CGSizeMake(destinationWidth, destinationHeight); } diff --git a/Source/Private/ASImageNode+FrameworkPrivate.h b/Source/Private/ASImageNode+FrameworkPrivate.h new file mode 100644 index 000000000..7307d8237 --- /dev/null +++ b/Source/Private/ASImageNode+FrameworkPrivate.h @@ -0,0 +1,26 @@ +#import + +@interface ASImageNodeDrawParameters : NSObject + +// The original drawRect for the image in context for particular content mode. +@property(nonatomic, readonly) CGRect drawRect; + +// The rect used for drawing image to context with borther width considered. +@property(nonatomic) CGRect adjustedDrawRect; + +// The scaling ratio (backing size to bound size) for border image processor +// to properly apply image corner radius/border width +@property(nonatomic, readonly) CGFloat renderScale; + +// Whether the image node will be rendered in RTL layout +@property(nonatomic, readonly) BOOL isRTL; + +@end + +#if YOGA +@interface ASImageNode(FrameworkPrivate) + +-(void)_locked_setFlipsForRightToLeftLayoutDirection:(BOOL)flipsForRightToLeftLayoutDirection; + +@end +#endif diff --git a/Source/Private/ASInternalHelpers.h b/Source/Private/ASInternalHelpers.h index 29fa26897..d38ae23bb 100644 --- a/Source/Private/ASInternalHelpers.h +++ b/Source/Private/ASInternalHelpers.h @@ -7,7 +7,7 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import "ASAvailability.h" +#import #import @@ -15,13 +15,27 @@ #import #import +#ifdef __cplusplus +#include +#endif + NS_ASSUME_NONNULL_BEGIN +@interface NSAttributedString (ASTextAttachment) + +- (BOOL)as_hasAttribute:(NSAttributedStringKey)attributeKey; + +@end + ASDK_EXTERN void ASInitializeFrameworkMainThread(void); ASDK_EXTERN BOOL ASDefaultAllowsGroupOpacity(void); ASDK_EXTERN BOOL ASDefaultAllowsEdgeAntialiasing(void); +/// ASTraitCollection is probably a better place to look on iOS >= 10 +/// This _may not be set_ if AS_INITIALIZE_FRAMEWORK_MANUALLY is not set or we are used by an extension +ASDK_EXTERN NSNumber *ASApplicationUserInterfaceLayoutDirection(void); + ASDK_EXTERN BOOL ASSubclassOverridesSelector(Class superclass, Class subclass, SEL selector); ASDK_EXTERN BOOL ASSubclassOverridesClassSelector(Class superclass, Class subclass, SEL selector); @@ -34,6 +48,9 @@ ASDK_EXTERN void ASPerformBlockOnMainThread(void (^block)(void)); /// Dispatches the given block to a background queue with priority of DISPATCH_QUEUE_PRIORITY_DEFAULT if not already run on a background queue ASDK_EXTERN void ASPerformBlockOnBackgroundThread(void (^block)(void)); // DISPATCH_QUEUE_PRIORITY_DEFAULT +/// For deallocation of objects on a background thread without GCD overhead / thread explosion +ASDK_EXTERN void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object); + ASDK_EXTERN CGFloat ASScreenScale(void); ASDK_EXTERN CGSize ASFloorSizeValues(CGSize s); @@ -48,6 +65,10 @@ ASDK_EXTERN CGFloat ASCeilPixelValue(CGFloat f); ASDK_EXTERN CGFloat ASRoundPixelValue(CGFloat f); +ASDISPLAYNODE_INLINE CGPoint ASPointAddPoint(CGPoint p1, CGPoint p2) { + return (CGPoint){p1.x + p2.x, p1.y + p2.y}; +} + ASDK_EXTERN Class _Nullable ASGetClassFromType(const char * _Nullable type); ASDISPLAYNODE_INLINE BOOL ASImageAlphaInfoIsOpaque(CGImageAlphaInfo info) { @@ -117,7 +138,117 @@ ASDISPLAYNODE_INLINE AS_WARN_UNUSED_RESULT ASImageDownloaderPriority ASImageDown /** * Create an NSMutableSet that uses pointers for hash & equality. */ -ASDK_EXTERN NSMutableSet *ASCreatePointerBasedMutableSet(void); +ASDK_EXTERN NSMutableSet *ASCreatePointerBasedMutableSet(void) NS_RETURNS_RETAINED; + +/** + * Create an NSMutableArray that does not retain and release it's objects. + */ +ASDK_EXTERN NSMutableArray *ASCreateNonOwningMutableArray(void) NS_RETURNS_RETAINED; + +/** + * Call from a once block to initialize the given pthread key for use with ASGetTemporary* functions + * below. + */ +ASDK_EXTERN void ASInitializeTemporaryObjectStorage(pthread_key_t *threadKey); + +/** + * Get a temporary, thread-local, empty, non-owning mutable array associated with the given key. + * + * Thread key must have already been set up through ASInitializeTemporaryObjectStorage. + */ +ASDK_EXTERN CFMutableArrayRef ASGetTemporaryNonowningMutableArray(pthread_key_t threadKey); + +/** + * Get a temporary, thread-local, zero-filled CFMutableDataRef instance of the given size. + * + * @discussion NSCache is a useful class, but it has the unfortunate property of requiring keys to + * be Objective-C objects. Key creation adds significant overhead in cache lookups. We would + * prefer to use C-structs as keys. By using these functions, we can repeatedly reuse a + * thread-local CFMutableData instance for cache lookups, and then only create a permanent copy + * of the data for cache insertions. + * + * @note The size argument must be constant across each call. + * + * This allows for cache lookups with amortized zero heap allocations or object creations. + * + * Example: + * + * - (id)myExpensiveMethodWithX:(int)x y:(CGFloat)y { + * static pthread_key_t threadKey; + * static NSCache *cache; + * static dispatch_once_t onceToken; + * dispatch_once(&onceToken, ^{ + * ASInitializeTemporaryObjectStorage(&threadKey); + * cache = [[NSCache alloc] init]; + * }); + * + * typedef struct { + * int x; + * CGFloat y; + * } CacheKey; + * + * CFMutableDataRef keyBuffer = ASGetTemporaryMutableData(threadKey, sizeof(CacheKey)); + * CacheKey *key = (CacheKey *)CFDataGetMutableBytePtr(keyBuffer); + * if (!key) { + * // This should be impossible but this code has not been proven in production yet. + * ASDisplayNodeFailAssert(@"Failed to get key pointer: %@", keyBuffer); + * return nil; // Or some other "fatal error" fallback. Or continue & ignore cache. + * } + * key->x = x; + * key->y = y; + * id cached = [cache objectForKey:(__bridge id)keyBuffer]; + * if (cached) { + * return cached; + * } + * id result = ExpensiveLogicInvolvingXAndY(x, y); + * if (CFDataRef copiedKey = CFDataCreateCopy(NULL, buffer)) { + * [cache setObject:result forKey:(__bridge_transfer id)copiedKey]; + * } else { + * // This should not be possible but this code has not been proven in production yet. + * ASDisplayNodeFailAssert(@"Failed to copy key: %@", keyBuffer); + * } + * return result; + * } + */ +ASDK_EXTERN CFMutableDataRef ASGetTemporaryMutableData(pthread_key_t threadKey, NSUInteger keySize); + +/** + * Returns a singleton empty immutable attributed string. Use at your leisure. + */ +ASDK_EXTERN NSAttributedString *ASGetZeroAttributedString(void); + +#ifdef __cplusplus + +namespace AS { + +/** + * RAII container to execute a function at end of scope. + */ +class Cleanup { + public: + Cleanup(std::function f) : f_(std::move(f)) {} + ~Cleanup() { f_(); } + + /** Release without calling. Use release()() to execute early. */ + std::function release() { + auto f = std::move(f_); + f_ = [] {}; + return f; + } + + // Move yes, copy no. + Cleanup(const Cleanup &) = delete; + Cleanup &operator=(const Cleanup &) = delete; + Cleanup(Cleanup &&) = default; + Cleanup &operator=(Cleanup &&) = default; + + private: + std::function f_; +}; + +} // namespace AS + +#endif // __cplusplus NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASInternalHelpers.mm b/Source/Private/ASInternalHelpers.mm index 4d6e1fde8..1172e97f1 100644 --- a/Source/Private/ASInternalHelpers.mm +++ b/Source/Private/ASInternalHelpers.mm @@ -9,13 +9,39 @@ #import +#import + +#import +#import +#import + #import #import #import #import +AS_ASSUME_NORETAIN_BEGIN + static NSNumber *allowsGroupOpacityFromUIKitOrNil; static NSNumber *allowsEdgeAntialiasingFromUIKitOrNil; +static NSNumber *applicationUserInterfaceLayoutDirection = nil; + +@implementation NSAttributedString (ASTextAttachment) + +- (BOOL)as_hasAttribute:(NSAttributedStringKey)attributeKey { + NSUInteger length = self.length; + if (length == 0) { + return NO; + } + NSRange range; + id result = [self attribute:attributeKey + atIndex:0 + longestEffectiveRange:&range + inRange:NSMakeRange(0, length)]; + return result || range.length != length; +} + +@end BOOL ASDefaultAllowsGroupOpacity() { @@ -39,6 +65,10 @@ BOOL ASDefaultAllowsEdgeAntialiasing() return edgeAntialiasing; } +NSNumber *ASApplicationUserInterfaceLayoutDirection() { + return applicationUserInterfaceLayoutDirection; +} + #if AS_SIGNPOST_ENABLE void _ASInitializeSignpostObservers(void) { @@ -66,6 +96,7 @@ void ASInitializeFrameworkMainThread(void) static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ ASDisplayNodeCAssertMainThread(); + applicationUserInterfaceLayoutDirection = @([UIApplication sharedApplication].userInterfaceLayoutDirection); // 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 @@ -140,6 +171,11 @@ void ASPerformBlockOnBackgroundThread(void (^block)(void)) } } +void ASPerformBackgroundDeallocation(id __strong _Nullable * _Nonnull object) +{ + [[ASDeallocQueue sharedDeallocationQueue] releaseObjectInBackground:object]; +} + Class _Nullable ASGetClassFromType(const char * _Nullable type) { // Class types all start with @" @@ -248,7 +284,7 @@ - (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath @end -NSMutableSet *ASCreatePointerBasedMutableSet() +NSMutableSet *ASCreatePointerBasedMutableSet() NS_RETURNS_RETAINED { static CFSetCallBacks callbacks; static dispatch_once_t onceToken; @@ -259,3 +295,65 @@ - (NSComparisonResult)asdk_inverseCompare:(NSIndexPath *)otherIndexPath }); return (__bridge_transfer NSMutableSet *)CFSetCreateMutable(NULL, 0, &callbacks); } + +NSMutableArray *ASCreateNonOwningMutableArray() NS_RETURNS_RETAINED { + static CFArrayCallBacks callbacks; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + callbacks = kCFTypeArrayCallBacks; + callbacks.retain = nullptr; + callbacks.release = nullptr; + }); + return (__bridge_transfer NSMutableArray *)CFArrayCreateMutable(NULL, 0, &callbacks); +} + +/** + * Note: we intentionally choose pthread keys instead of the thread_local storage classifier. + * The latter is not as efficient in current implementations (Xcode 10) as it relies on tlv_atExit + * which performs its own heap allocations. + */ +void ASInitializeTemporaryObjectStorage(pthread_key_t *keyPtr) { + pthread_key_create(keyPtr, [](void *ptr) { + if (ptr) CFRelease((CFTypeRef)ptr); + }); +} + +CFMutableArrayRef ASGetTemporaryNonowningMutableArray(pthread_key_t key) { + CFMutableArrayRef obj = (CFMutableArrayRef)pthread_getspecific(key); + if (!obj) { + obj = (__bridge_retained CFMutableArrayRef)ASCreateNonOwningMutableArray(); + pthread_setspecific(key, obj); + } else { + CFArrayRemoveAllValues(obj); + } + return obj; +} + +CFMutableDataRef ASGetTemporaryMutableData(pthread_key_t key, NSUInteger size) { + CFMutableDataRef md = (CFMutableDataRef)pthread_getspecific(key); + if (!md) { + md = CFDataCreateMutable(NULL, size); + CFDataSetLength(md, size); + pthread_setspecific(key, md); + } else if (UInt8 *buf = CFDataGetMutableBytePtr(md)) { + // We clear the data on every subsequent access. Subtle downstream bugs are likely and have been + // observed if the remnants of old entries are left around. + memset(buf, 0, size); + } else { + ASDisplayNodeCFailAssert(@"Have mutable data but failed to get byte ptr. ???"); + } + + ASDisplayNodeCAssert(size == CFDataGetLength(md), @"Size changed across calls."); + return md; +} + +NSAttributedString *ASGetZeroAttributedString(void) { + static NSAttributedString *str; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + str = [[NSAttributedString alloc] init]; + }); + return str; +} + +AS_ASSUME_NORETAIN_END diff --git a/Source/Private/ASLayoutTransition.h b/Source/Private/ASLayoutTransition.h index f2bcf9b25..1b814746a 100644 --- a/Source/Private/ASLayoutTransition.h +++ b/Source/Private/ASLayoutTransition.h @@ -86,7 +86,7 @@ AS_SUBCLASSING_RESTRICTED - (void)applySubnodeRemovals; - (instancetype)init NS_UNAVAILABLE; -- (instancetype)new NS_UNAVAILABLE; ++ (instancetype)new NS_UNAVAILABLE; @end diff --git a/Source/Private/ASMutableElementMap.h b/Source/Private/ASMutableElementMap.h index 1ca182931..622b9a301 100644 --- a/Source/Private/ASMutableElementMap.h +++ b/Source/Private/ASMutableElementMap.h @@ -10,6 +10,7 @@ #import #import #import +#import #import NS_ASSUME_NONNULL_BEGIN @@ -54,6 +55,9 @@ AS_SUBCLASSING_RESTRICTED */ - (void)migrateSupplementaryElementsWithSectionMapping:(ASIntegerMap *)mapping; +/// changeSet must only consist of added content. No deletes are allowed. +- (void)removeContentAddedInChangeSet:(_ASHierarchyChangeSet *)changeSet; + @end @interface ASElementMap (MutableCopying) diff --git a/Source/Private/ASMutableElementMap.mm b/Source/Private/ASMutableElementMap.mm index f7bf21084..a74ee1f6f 100644 --- a/Source/Private/ASMutableElementMap.mm +++ b/Source/Private/ASMutableElementMap.mm @@ -9,8 +9,12 @@ #import +#import #import #import +#import +#import +#import #import typedef NSMutableArray *> ASMutableCollectionElementTwoDimensionalArray; @@ -33,6 +37,17 @@ - (instancetype)initWithSections:(NSArray *)sections items:(ASColle return self; } +- (void)dealloc +{ + if (ASActivateExperimentalFeature(ASExperimentalDeallocElementMapOffMain)) { + if (ASDisplayNodeThreadIsMain()) { + ASPerformBackgroundDeallocation(&_sections); + ASPerformBackgroundDeallocation(&_sectionsOfItems); + ASPerformBackgroundDeallocation(&_supplementaryElements); + } + } +} + - (id)copyWithZone:(NSZone *)zone { return [[ASElementMap alloc] initWithSections:_sections items:_sectionsOfItems supplementaryElements:_supplementaryElements]; @@ -128,9 +143,25 @@ - (void)migrateSupplementaryElementsWithSectionMapping:(ASIntegerMap *)mapping }]; } +- (void)removeContentAddedInChangeSet:(_ASHierarchyChangeSet *)changeSet +{ + NSParameterAssert(changeSet.deletedSections.count == 0); + NSParameterAssert(changeSet.indexPathsForRemovedItems.empty()); + + // Delete inserted items in descending order. + auto insertedItems = changeSet.indexPathsForInsertedItems; + for (auto it = insertedItems.rbegin(); it != insertedItems.rend(); it++) { + ASDeleteElementInTwoDimensionalArrayAtIndexPath(_sectionsOfItems, *it); + } + + // Delete inserted sections. + [_sections removeObjectsAtIndexes:changeSet.insertedSections]; + [_sectionsOfItems removeObjectsAtIndexes:changeSet.insertedSections]; +} + #pragma mark - Helpers -+ (ASMutableSupplementaryElementDictionary *)deepMutableCopyOfElementsDictionary:(ASSupplementaryElementDictionary *)originalDict ++ (ASMutableSupplementaryElementDictionary *)deepMutableCopyOfElementsDictionary:(ASSupplementaryElementDictionary *)originalDict NS_RETURNS_RETAINED { NSMutableDictionary *deepCopy = [[NSMutableDictionary alloc] initWithCapacity:originalDict.count]; [originalDict enumerateKeysAndObjectsUsingBlock:^(NSString * _Nonnull key, NSDictionary * _Nonnull obj, BOOL * _Nonnull stop) { diff --git a/Source/Private/ASNodeContext+Private.h b/Source/Private/ASNodeContext+Private.h new file mode 100644 index 000000000..8b8e19610 --- /dev/null +++ b/Source/Private/ASNodeContext+Private.h @@ -0,0 +1,26 @@ +// +// ASNodeContext+Private.h +// Texture +// +// Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 + +#if defined(__cplusplus) + +#import +#import + +NS_ASSUME_NONNULL_BEGIN + +@interface ASNodeContext () { + // This ivar is declared public but obviously use with caution. + // It is not in the main header because it requires C++. +@public + AS::RecursiveMutex _mutex; +} + +@end + +NS_ASSUME_NONNULL_END + +#endif // defined(__cplusplus) diff --git a/Source/Private/ASNodeControllerInternal.h b/Source/Private/ASNodeControllerInternal.h new file mode 100644 index 000000000..7265ec603 --- /dev/null +++ b/Source/Private/ASNodeControllerInternal.h @@ -0,0 +1,11 @@ +#import +#import + +@interface ASNodeController () { +@package + ASDisplayNode *_strongNode; + __weak ASDisplayNode *_weakNode; + AS::MutexOrPointer __instanceLock__; +} + +@end diff --git a/Source/Private/ASTwoDimensionalArrayUtils.h b/Source/Private/ASTwoDimensionalArrayUtils.h index 8bddf29e5..570137036 100644 --- a/Source/Private/ASTwoDimensionalArrayUtils.h +++ b/Source/Private/ASTwoDimensionalArrayUtils.h @@ -21,26 +21,35 @@ NS_ASSUME_NONNULL_BEGIN * Deep mutable copy of an array that contains arrays, which contain objects. It will go one level deep into the array to copy. * This method is substantially faster than the generalized version, e.g. about 10x faster, so use it whenever it fits the need. */ -ASDK_EXTERN NSMutableArray *ASTwoDimensionalArrayDeepMutableCopy(NSArray *array) AS_WARN_UNUSED_RESULT; +ASDK_EXTERN NSMutableArray *ASTwoDimensionalArrayDeepMutableCopy( + NSArray *array) AS_WARN_UNUSED_RESULT NS_RETURNS_RETAINED; /** * Delete the elements of the mutable two-dimensional array at given index paths – sorted in descending order! */ ASDK_EXTERN void ASDeleteElementsInTwoDimensionalArrayAtIndexPaths(NSMutableArray *mutableArray, NSArray *indexPaths); +/** + * Delete the elements of the mutable two-dimensional array at given index path. + */ +ASDK_EXTERN void ASDeleteElementInTwoDimensionalArrayAtIndexPath(NSMutableArray *mutableArray, NSIndexPath *indexPath); + /** * Return all the index paths of a two-dimensional array, in ascending order. */ -ASDK_EXTERN NSArray *ASIndexPathsForTwoDimensionalArray(NSArray* twoDimensionalArray) AS_WARN_UNUSED_RESULT; +ASDK_EXTERN NSArray *ASIndexPathsForTwoDimensionalArray( + NSArray *twoDimensionalArray) AS_WARN_UNUSED_RESULT NS_RETURNS_RETAINED; /** * Return all the elements of a two-dimensional array, in ascending order. */ -ASDK_EXTERN NSArray *ASElementsInTwoDimensionalArray(NSArray* twoDimensionalArray) AS_WARN_UNUSED_RESULT; +ASDK_EXTERN NSArray *ASElementsInTwoDimensionalArray(NSArray *twoDimensionalArray) + AS_WARN_UNUSED_RESULT NS_RETURNS_RETAINED; /** * Attempt to get the object at the given index path. Returns @c nil if the index path is out of bounds. */ -ASDK_EXTERN id _Nullable ASGetElementInTwoDimensionalArray(NSArray *array, NSIndexPath *indexPath) AS_WARN_UNUSED_RESULT; +ASDK_EXTERN id _Nullable ASGetElementInTwoDimensionalArray( + NSArray *array, NSIndexPath *indexPath) AS_WARN_UNUSED_RESULT; NS_ASSUME_NONNULL_END diff --git a/Source/Private/ASTwoDimensionalArrayUtils.mm b/Source/Private/ASTwoDimensionalArrayUtils.mm index 2a333349c..a27d80c66 100644 --- a/Source/Private/ASTwoDimensionalArrayUtils.mm +++ b/Source/Private/ASTwoDimensionalArrayUtils.mm @@ -19,9 +19,9 @@ #pragma mark - Public Methods -NSMutableArray *ASTwoDimensionalArrayDeepMutableCopy(NSArray *array) +NSMutableArray *ASTwoDimensionalArrayDeepMutableCopy(NSArray *array) NS_RETURNS_RETAINED { - NSMutableArray *newArray = [NSMutableArray arrayWithCapacity:array.count]; + NSMutableArray *newArray = [[NSMutableArray alloc] initWithCapacity:array.count]; NSInteger i = 0; for (NSArray *subarray in array) { ASDisplayNodeCAssert([subarray isKindOfClass:[NSArray class]], @"This function expects NSArray *"); @@ -63,7 +63,17 @@ void ASDeleteElementsInTwoDimensionalArrayAtIndexPaths(NSMutableArray *mutableAr } } -NSArray *ASIndexPathsForTwoDimensionalArray(NSArray * twoDimensionalArray) +void ASDeleteElementInTwoDimensionalArrayAtIndexPath(NSMutableArray *mutableArray, NSIndexPath *indexPath) +{ + const NSInteger section = indexPath.section; + AS_C_PRECONDITION(section < mutableArray.count, (void)0, @"Section out of bounds."); + unowned NSMutableArray *subarray = mutableArray[section]; + const NSInteger item = indexPath.item; + AS_C_PRECONDITION(item < subarray.count, (void)0, @"Item out of bounds."); + [subarray removeObjectAtIndex:item]; +} + +NSArray *ASIndexPathsForTwoDimensionalArray(NSArray * twoDimensionalArray) NS_RETURNS_RETAINED { NSInteger sectionCount = twoDimensionalArray.count; NSInteger counts[sectionCount]; @@ -86,7 +96,7 @@ void ASDeleteElementsInTwoDimensionalArrayAtIndexPaths(NSMutableArray *mutableAr return [NSArray arrayByTransferring:indexPaths.data() count:totalCount]; } -NSArray *ASElementsInTwoDimensionalArray(NSArray * twoDimensionalArray) +NSArray *ASElementsInTwoDimensionalArray(NSArray * twoDimensionalArray) NS_RETURNS_RETAINED { NSInteger totalCount = 0; for (NSArray *subarray in twoDimensionalArray) { diff --git a/Source/Private/Layout/ASLayoutElementStylePrivate.h b/Source/Private/Layout/ASLayoutElementStylePrivate.h index 69e29824e..b3665ef5d 100644 --- a/Source/Private/Layout/ASLayoutElementStylePrivate.h +++ b/Source/Private/Layout/ASLayoutElementStylePrivate.h @@ -14,18 +14,9 @@ @interface ASLayoutElementStyle () -/** - * @abstract The object that acts as the delegate of the style. - * - * @discussion The delegate must adopt the ASLayoutElementStyleDelegate protocol. The delegate is not retained. - */ -@property (nullable, nonatomic, weak) id delegate; - /** * @abstract A size constraint that should apply to this ASLayoutElement. */ @property (nonatomic, readonly) ASLayoutElementSize size; -@property (nonatomic, assign) ASStackLayoutAlignItems parentAlignStyle; - @end diff --git a/Source/Private/Layout/ASStackPositionedLayout.mm b/Source/Private/Layout/ASStackPositionedLayout.mm index 0e9d896e3..ccdc8318a 100644 --- a/Source/Private/Layout/ASStackPositionedLayout.mm +++ b/Source/Private/Layout/ASStackPositionedLayout.mm @@ -116,6 +116,8 @@ static void stackOffsetAndSpacingForEachItem(const std::size_t numOfItems, } case ASStackLayoutJustifyContentStart: break; + case ASStackLayoutJustifyContentSpaceEvenly: + break; } } diff --git a/Source/Private/_ASHierarchyChangeSet.h b/Source/Private/_ASHierarchyChangeSet.h index 66f5966e6..f36c912e0 100644 --- a/Source/Private/_ASHierarchyChangeSet.h +++ b/Source/Private/_ASHierarchyChangeSet.h @@ -114,7 +114,14 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); @property (nonatomic, readonly) BOOL isEmpty; /// The count of new ASCellNodes that can undergo async layout calculation. May be zero if all UIKit passthrough cells. -@property (nonatomic, assign) NSUInteger countForAsyncLayout; +/// You may not read this property before data latched. If so you get an assert and a 0. +@property (nonatomic, readonly) NSUInteger countForAsyncLayout; + +/// You may not call this after data is latched. If so you get an assert. +- (void)incrementCountForAsyncLayout; + +/// Whether the data for this changeset has been latched by the data controller. +@property (nonatomic, assign) BOOL dataLatched; /// The top-level activity for this update. @property (nonatomic, OS_ACTIVITY_NULLABLE) os_activity_t rootActivity; @@ -199,6 +206,26 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); /// Returns all item indexes affected by changes of the given type in the given section. - (NSIndexSet *)indexesForItemChangesOfType:(_ASHierarchyChangeType)changeType inSection:(NSUInteger)section; +/** + * The index paths for all the items that were deleted or reloaded, including the contents of deleted or + * reloaded sections. If this is a reloadData, this returns all index paths. + * + * Result is sorted descending. + * + * Must be completed. + */ +@property (readonly) std::vector indexPathsForRemovedItems; + +/** + * The new index paths for all the items that were insert or reloaded, including the contents of inserted or + * reloaded sections. If this is a reloadData, this returns all new index paths. + * + * Result is sorted ascending. + * + * Must be completed. + */ +@property (readonly) std::vector indexPathsForInsertedItems; + - (void)reloadData; - (void)deleteSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; - (void)insertSections:(NSIndexSet *)sections animationOptions:(ASDataControllerAnimationOptions)options; @@ -209,6 +236,17 @@ NSString *NSStringFromASHierarchyChangeType(_ASHierarchyChangeType changeType); - (void)moveSection:(NSInteger)section toSection:(NSInteger)newSection animationOptions:(ASDataControllerAnimationOptions)options; - (void)moveItemAtIndexPath:(NSIndexPath *)indexPath toIndexPath:(NSIndexPath *)newIndexPath animationOptions:(ASDataControllerAnimationOptions)options; +/** + * Running returned changesets in series is equivalent to running this one. + * + * The completion handler for this change is cleared and set as the completion handler of the last chunk. + * + * The first segment will contain all deletes + the first batch of inserts. All subsequent segments + * will only contain inserted sections & inserted items. + * Must be completed. + */ +- (std::vector<_ASHierarchyChangeSet *>)divideIntoSegmentsOfMaximumSize:(NSUInteger)sizeLimit; + @end NS_ASSUME_NONNULL_END diff --git a/Source/Private/_ASHierarchyChangeSet.mm b/Source/Private/_ASHierarchyChangeSet.mm index 825c6b8c0..2bc61a2d6 100644 --- a/Source/Private/_ASHierarchyChangeSet.mm +++ b/Source/Private/_ASHierarchyChangeSet.mm @@ -125,7 +125,6 @@ @implementation _ASHierarchyChangeSet { @synthesize reverseSectionMapping = _reverseSectionMapping; @synthesize itemMappings = _itemMappings; @synthesize reverseItemMappings = _reverseItemMappings; -@synthesize countForAsyncLayout = _countForAsyncLayout; - (instancetype)init { @@ -363,6 +362,18 @@ - (NSIndexPath *)newIndexPathForOldIndexPath:(NSIndexPath *)indexPath return [NSIndexPath indexPathForItem:newItem inSection:newSection]; } +- (NSUInteger)countForAsyncLayout +{ + AS_PRECONDITION(_dataLatched, 0, @"Read before data latch not allowed."); + return _countForAsyncLayout; +} + +- (void)incrementCountForAsyncLayout +{ + AS_PRECONDITION(!_dataLatched, (void)0, @"Set after data latch not allowed."); + _countForAsyncLayout += 1; +} + - (void)reloadData { [self _ensureNotCompleted]; @@ -643,6 +654,172 @@ - (BOOL)_includesPerItemOrSectionChanges + _reloadSectionChanges.count + _reloadItemChanges.count); } +- (std::vector)indexPathsForRemovedItems +{ + std::vector result; + if (![self _ensureCompleted]) { + return result; + } + + // Reload data means all elements. + if (_includesReloadData) { + for (int s = 0; s < _oldItemCounts.size(); s++) { + auto count = _oldItemCounts[s]; + result.reserve(result.size() + count); + for (int i = 0; i < count; i++) { + result.emplace_back([NSIndexPath indexPathForItem:i inSection:s]); + } + } + return result; + } + + // First do sections. + for (NSInteger s = _deletedSections.firstIndex; s != NSNotFound; s = [_deletedSections indexGreaterThanIndex:s]) { + auto count = _oldItemCounts[s]; + result.reserve(result.size() + count); + for (NSInteger i = 0; i < count; i++) { + result.emplace_back([NSIndexPath indexPathForItem:i inSection:s]); + } + } + // Then do items. + for (_ASHierarchyItemChange *d in _deleteItemChanges) { + unowned auto indexPaths = d.indexPaths; + result.reserve(indexPaths.count); + for (NSIndexPath *ip in indexPaths) { + result.emplace_back(ip); + } + } + // Sort descending. + std::sort(result.begin(), result.end(), [](unowned NSIndexPath *a, unowned NSIndexPath *b) { + return [a compare:b] != NSOrderedAscending; + }); + return result; +} + +- (std::vector)indexPathsForInsertedItems +{ + std::vector result; + if (![self _ensureCompleted]) { + return result; + } + + // Reload data means all elements. + if (_includesReloadData) { + for (int s = 0; s < _newItemCounts.size(); s++) { + auto count = _newItemCounts[s]; + result.reserve(result.size() + count); + for (int i = 0; i < count; i++) { + result.emplace_back([NSIndexPath indexPathForItem:i inSection:s]); + } + } + return result; + } + + // First do sections. + for (NSInteger s = _insertedSections.firstIndex; s != NSNotFound; + s = [_insertedSections indexGreaterThanIndex:s]) { + auto count = _newItemCounts[s]; + result.reserve(result.size() + count); + for (NSInteger i = 0; i < count; i++) { + result.emplace_back([NSIndexPath indexPathForItem:i inSection:s]); + } + } + // Then do items. + for (_ASHierarchyItemChange *i in _insertItemChanges) { + unowned auto indexPaths = i.indexPaths; + result.reserve(indexPaths.count); + for (NSIndexPath *ip in indexPaths) { + result.emplace_back(ip); + } + } + + // Sort ascending. + std::sort(result.begin(), result.end(), [](unowned NSIndexPath *a, unowned NSIndexPath *b) { + return [a compare:b] == NSOrderedAscending; + }); + return result; +} + +- (std::vector<_ASHierarchyChangeSet *>)divideIntoSegmentsOfMaximumSize:(NSUInteger)sizeLimit +{ + if (![self _ensureCompleted] || sizeLimit == 0) { + return { self }; + } + + // Get inserted item index paths. + const std::vector insertedItems = self.indexPathsForInsertedItems; + if (insertedItems.size() <= sizeLimit) { + return { self }; + } + std::vector<_ASHierarchyChangeSet *> result; + result.reserve(1 + insertedItems.size() / (size_t)sizeLimit); + + std::vector itemCounts = _oldItemCounts; + + // First change has all the deletes, plus the first batch of inserts. + _ASHierarchyChangeSet *segment = [[_ASHierarchyChangeSet alloc] initWithOldData:itemCounts]; + segment->_deleteSectionChanges = _deleteSectionChanges; + segment->_deletedSections = _deletedSections; + segment->_deleteItemChanges = _deleteItemChanges; + result.push_back(segment); + + // Account for deleted items. + for (NSIndexPath *indexPath : self.indexPathsForRemovedItems) { + itemCounts[indexPath.section]--; + } + // Account for deleted sections. + itemCounts.erase(std::remove_if(itemCounts.begin(), itemCounts.end(), [&self](NSInteger section) { + return [_deletedSections containsIndex:section]; + }), itemCounts.end() ); + + bool isFirstSegment = true; + auto remainingItems = insertedItems.begin(); + while (remainingItems != insertedItems.end()) { + // Start a new segment (except first – we are already working on a segment.) + if (!isFirstSegment) { + [segment markCompletedWithNewItemCounts:itemCounts]; + segment = [[_ASHierarchyChangeSet alloc] initWithOldData:itemCounts]; + result.push_back(segment); + } + isFirstSegment = false; + + // Iterator to the last item in the batch. + const auto batchEnd = std::min(insertedItems.end(), remainingItems + sizeLimit); + const auto batchLast = batchEnd - 1; + + // Insert sections up through section of last item. + const NSInteger sectionCountNeeded = [*batchLast section] + 1; + const NSInteger sectionCountHad = itemCounts.size(); + const NSInteger additionalSectionsNeeded = MAX(0, sectionCountNeeded - sectionCountHad); + if (additionalSectionsNeeded) { + NSRange range = NSMakeRange(sectionCountHad, additionalSectionsNeeded); + [segment insertSections:[NSIndexSet indexSetWithIndexesInRange:range] animationOptions:0]; + itemCounts.resize(NSMaxRange(range), 0); + } + + // Insert items into this segment. + const NSUInteger batchSize = batchEnd - remainingItems; + auto items = [[NSArray alloc] initWithObjects:&*remainingItems count:batchSize]; + [segment insertItems:items animationOptions:0]; + + // Update itemCounts. + for (NSIndexPath *indexPath in items) { + itemCounts[indexPath.section]++; + } + + // Move to next batch. + remainingItems = batchEnd; + } + + // Last batch gets the completion handler from this one. + result.back()->_completionHandler = _completionHandler; + _completionHandler = nil; + + [result.back() markCompletedWithNewItemCounts:itemCounts]; + + return result; +} + #pragma mark - Debugging (Private) - (NSString *)description diff --git a/Source/Private/_ASPendingState.h b/Source/Private/_ASPendingState.h index 0a96e7a8a..dea300787 100644 --- a/Source/Private/_ASPendingState.h +++ b/Source/Private/_ASPendingState.h @@ -11,6 +11,8 @@ #import +@class ASDisplayNode; + /** Private header for ASDisplayNode.mm @@ -25,7 +27,9 @@ // Supports all of the properties included in the ASDisplayNodeViewProperties protocol -- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)setFrameDirectly; +- (void)applyToView:(UIView *)view + withSpecialPropertiesHandling:(BOOL)setFrameDirectly + node:(ASDisplayNode *)node; - (void)applyToLayer:(CALayer *)layer; + (_ASPendingState *)pendingViewStateFromLayer:(CALayer *)layer; @@ -37,5 +41,6 @@ @property (nonatomic, readonly) BOOL hasChanges; - (void)clearChanges; +- (void)clearFrameChange; @end diff --git a/Source/Private/_ASPendingState.mm b/Source/Private/_ASPendingState.mm index a9d760e9e..091eedc8f 100644 --- a/Source/Private/_ASPendingState.mm +++ b/Source/Private/_ASPendingState.mm @@ -9,9 +9,13 @@ #import -#import +#import +#import #import #import +#import +#import +#import #define __shouldSetNeedsDisplayForView(view) (flags.needsDisplay \ || (flags.setOpaque && _flags.opaque != (view).opaque)\ @@ -22,6 +26,8 @@ || (flags.setOpaque && _flags.opaque != (layer).opaque)\ || (flags.setBackgroundColor && ![backgroundColor isEqual:[UIColor colorWithCGColor:(layer).backgroundColor]])) +using namespace AS; + typedef struct { // Properties int needsDisplay:1; @@ -83,6 +89,7 @@ int setAccessibilityNavigationStyle:1; int setAccessibilityCustomActions:1; int setAccessibilityHeaderElements:1; + int setAccessibilityElements:1; int setAccessibilityActivationPoint:1; int setAccessibilityPath:1; int setSemanticContentAttribute:1; @@ -90,7 +97,7 @@ int setPreservesSuperviewLayoutMargins:1; int setInsetsLayoutMarginsFromSafeArea:1; int setActions:1; - int setMaskedCorners : 1; + int setMaskedCorners:1; } ASPendingStateFlags; @@ -140,6 +147,7 @@ @implementation _ASPendingState UIAccessibilityNavigationStyle accessibilityNavigationStyle; NSArray *accessibilityCustomActions; NSArray *accessibilityHeaderElements; + NSArray *accessibilityElements; CGPoint accessibilityActivationPoint; UIBezierPath *accessibilityPath; UISemanticContentAttribute semanticContentAttribute API_AVAILABLE(ios(9.0), tvos(9.0)); @@ -900,6 +908,13 @@ - (void)setAccessibilityHeaderElements:(NSArray *)newAccessibilityHeaderElements } #pragma clang diagnostic pop +- (void)setAccessibilityElements:(NSArray *)newAccessibilityElements { + _flags.isAccessibilityElement = YES; + if (accessibilityElements != newAccessibilityElements) { + accessibilityElements = [newAccessibilityElements copy]; + } +} + - (CGPoint)accessibilityActivationPoint { if (_stateToApplyFlags.setAccessibilityActivationPoint) { @@ -1042,8 +1057,9 @@ - (void)applyToLayer:(CALayer *)layer [layer layoutIfNeeded]; } -- (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPropertiesHandling -{ +- (void)applyToView:(UIView *)view + withSpecialPropertiesHandling:(BOOL)specialPropertiesHandling + node:(ASDisplayNode *)node { /* Use our convenience setters blah here instead of layer.blah We were accidentally setting some properties on layer here, but view in UIViewBridgeOptimizations. @@ -1248,7 +1264,16 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr if (flags.setAccessibilityHeaderElements) view.accessibilityHeaderElements = accessibilityHeaderElements; #endif - + + if (flags.setAccessibilityElements) { + _ASDisplayView *displayView = ASDynamicCast(view, _ASDisplayView); + if (displayView) { + [displayView setAccessibilityElements:nil]; + } else { + view.accessibilityElements = accessibilityElements; + } + } + if (flags.setAccessibilityActivationPoint) view.accessibilityActivationPoint = accessibilityActivationPoint; @@ -1261,16 +1286,26 @@ - (void)applyToView:(UIView *)view withSpecialPropertiesHandling:(BOOL)specialPr // // Checking if the transform is identity is expensive, so disable when unnecessary. We have assertions on in Release, so DEBUG is the only way I know of. // ASDisplayNodeAssert(CATransform3DIsIdentity(layer.transform), @"-[ASDisplayNode setFrame:] - self.transform must be identity in order to set the frame property. (From Apple's UIView documentation: If the transform property is not the identity transform, the value of this property is undefined and therefore should be ignored.)"); //#endif - view.frame = frame; + view.bounds = CGRectMake(view.bounds.origin.x, view.bounds.origin.y, frame.size.width, frame.size.height); + view.center = CGPointMake(CGRectGetMidX(frame), CGRectGetMidY(frame)); } else { ASPendingStateApplyMetricsToLayer(self, layer); } if (flags.needsLayout) [view setNeedsLayout]; - - if (flags.layoutIfNeeded) - [view layoutIfNeeded]; + + if (flags.layoutIfNeeded) { + // In Yoga2 we always forward to the layer. Ever since iOS SDK 6, UIView has + // different semantics for layoutIfNeeded, where it doesn't actually walk up. + // We always want to walk up. We don't have a node here, but since this behavior is more + // correct, we will unconditionally enable it if you're in the experiment. + if (AS::Yoga2::GetEnabled(node)) { + [layer layoutIfNeeded]; + } else { + [view layoutIfNeeded]; + } + } } // FIXME: Make this more efficient by tracking which properties are set rather than reading everything. @@ -1394,6 +1429,10 @@ - (void)clearChanges _stateToApplyFlags = kZeroFlags; } +- (void)clearFrameChange { + _stateToApplyFlags.setFrame = NO; +} + - (BOOL)hasSetNeedsLayout { return _stateToApplyFlags.needsLayout; diff --git a/Source/Private/_ASScopeTimer.h b/Source/Private/_ASScopeTimer.h index 523599dd0..dc39c2145 100644 --- a/Source/Private/_ASScopeTimer.h +++ b/Source/Private/_ASScopeTimer.h @@ -9,6 +9,9 @@ #pragma once +#import +#import + /** Must compile as c++ for this to work. diff --git a/Source/TextExperiment/Component/ASTextDebugOption.mm b/Source/TextExperiment/Component/ASTextDebugOption.mm index 2565b903c..9887e1ad7 100644 --- a/Source/TextExperiment/Component/ASTextDebugOption.mm +++ b/Source/TextExperiment/Component/ASTextDebugOption.mm @@ -6,7 +6,7 @@ // Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 // -#import "ASTextDebugOption.h" +#import #import static pthread_mutex_t _sharedDebugLock; diff --git a/Source/TextExperiment/Component/ASTextLayout.h b/Source/TextExperiment/Component/ASTextLayout.h index 55ac417f1..58d70c632 100644 --- a/Source/TextExperiment/Component/ASTextLayout.h +++ b/Source/TextExperiment/Component/ASTextLayout.h @@ -10,9 +10,9 @@ #import #import -#import "ASTextDebugOption.h" -#import "ASTextLine.h" -#import "ASTextInput.h" +#import +#import +#import @protocol ASTextLinePositionModifier; @@ -23,6 +23,19 @@ NS_ASSUME_NONNULL_BEGIN */ ASDK_EXTERN const CGSize ASTextContainerMaxSize; +/** + * Get and Set ASTextNode2 to enable the better calculation of visible text range. + */ +BOOL ASGetEnableImprovedTextTruncationVisibleRange(void); +void ASSetEnableImprovedTextTruncationVisibleRange(BOOL enable); + +/** + * Get and Set ASTextLayout to fix the clickable area on truncation token when the last visible line + * is untruncated (e.g. the last visible line is an empty line). + */ +BOOL ASGetEnableImprovedTextTruncationVisibleRangeLastLineFix(void); +void ASSetEnableImprovedTextTruncationVisibleRangeLastLineFix(BOOL enable); + /** The ASTextContainer class defines a region in which text is laid out. ASTextLayout class uses one or more ASTextContainer objects to generate layouts. @@ -82,9 +95,6 @@ ASDK_EXTERN const CGSize ASTextContainerMaxSize; /// Default is YES; @property (getter=isPathFillEvenOdd) BOOL pathFillEvenOdd; -/// Whether the text is vertical form (may used for CJK text layout). Default is NO. -@property (getter=isVerticalForm) BOOL verticalForm; - /// Maximum number of rows, 0 means no limit. Default is 0. @property NSUInteger maximumNumberOfRows; @@ -224,8 +234,13 @@ ASDK_EXTERN const CGSize ASTextContainerMaxSize; @property (nonatomic, readonly) CTFrameRef frame; ///< Array of `ASTextLine`, no truncated @property (nonatomic, readonly) NSArray *lines; -///< ASTextLine with truncated token, or nil +///< ASTextLine with truncated token, or nil. Note that this may be nil if no truncation token was specified. +///< To check if the entire string was drawn, use NSEqualRanges(visibleRange, range). @property (nullable, nonatomic, readonly) ASTextLine *truncatedLine; +///< Range of the truncated line before the truncation tokens +@property(nonatomic, readonly) NSRange truncatedLineBeforeTruncationTokenRange; +///< The part of truncatedLine before the truncation token. +@property(nullable, nonatomic, readonly) ASTextLine *truncatedLineBeforeTruncationToken; ///< Array of `ASTextAttachment` @property (nullable, nonatomic, readonly) NSArray *attachments; ///< Array of NSRange(wrapped by NSValue) in text diff --git a/Source/TextExperiment/Component/ASTextLayout.mm b/Source/TextExperiment/Component/ASTextLayout.mm index f01c25908..e3bed8d28 100644 --- a/Source/TextExperiment/Component/ASTextLayout.mm +++ b/Source/TextExperiment/Component/ASTextLayout.mm @@ -9,15 +9,21 @@ #import -#import #import -#import +#import +#import +#import +#import +#import +#import #import +#import #import -#import const CGSize ASTextContainerMaxSize = (CGSize){0x100000, 0x100000}; +NSAttributedString *fillBaseAttributes(NSAttributedString *str, NSDictionary *attrs); + typedef struct { CGFloat head; CGFloat foot; @@ -55,22 +61,29 @@ static CGColorRef ASTextGetCGColor(CGColorRef color) { return color; } +BOOL kTextNode2ImprovedTextTruncationVisibleRange = false; +BOOL ASGetEnableImprovedTextTruncationVisibleRange(void) { + return kTextNode2ImprovedTextTruncationVisibleRange; +} +void ASSetEnableImprovedTextTruncationVisibleRange(BOOL enable) { + kTextNode2ImprovedTextTruncationVisibleRange = enable; +} + +BOOL kTextNode2ImprovedTextTruncationVisibleRangeLastLineFix = false; +BOOL ASGetEnableImprovedTextTruncationVisibleRangeLastLineFix(void) { + return kTextNode2ImprovedTextTruncationVisibleRangeLastLineFix; +} +void ASSetEnableImprovedTextTruncationVisibleRangeLastLineFix(BOOL enable) { + kTextNode2ImprovedTextTruncationVisibleRangeLastLineFix = enable; +} + @implementation ASTextLinePositionSimpleModifier - (void)modifyLines:(NSArray *)lines fromText:(NSAttributedString *)text inContainer:(ASTextContainer *)container { - if (container.verticalForm) { - for (NSUInteger i = 0, max = lines.count; i < max; i++) { - ASTextLine *line = lines[i]; - CGPoint pos = line.position; - pos.x = container.size.width - container.insets.right - line.row * _fixedLineHeight - _fixedLineHeight * 0.9; - line.position = pos; - } - } else { - for (NSUInteger i = 0, max = lines.count; i < max; i++) { - ASTextLine *line = lines[i]; - CGPoint pos = line.position; - pos.y = line.row * _fixedLineHeight + _fixedLineHeight * 0.9 + container.insets.top; - line.position = pos; - } + for (NSUInteger i = 0, max = lines.count; i < max; i++) { + ASTextLine *line = lines[i]; + CGPoint pos = line.position; + pos.y = line.row * _fixedLineHeight + _fixedLineHeight * 0.9 + container.insets.top; + line.position = pos; } } @@ -286,14 +299,6 @@ - (void)setPathLineWidth:(CGFloat)pathLineWidth { Setter(_pathLineWidth = pathLineWidth); } -- (BOOL)isVerticalForm { - Getter(BOOL v = _verticalForm) return v; -} - -- (void)setVerticalForm:(BOOL)verticalForm { - Setter(_verticalForm = verticalForm); -} - - (NSUInteger)maximumNumberOfRows { Getter(NSUInteger num = _maximumNumberOfRows) return num; } @@ -342,6 +347,8 @@ @interface ASTextLayout () @property (nonatomic) CTFrameRef frame; @property (nonatomic) NSArray *lines; @property (nonatomic) ASTextLine *truncatedLine; +@property(nonatomic) NSRange truncatedLineBeforeTruncationTokenRange; +@property(nonatomic) ASTextLine *truncatedLineBeforeTruncationToken; @property (nonatomic) NSArray *attachments; @property (nonatomic) NSArray *attachmentRanges; @property (nonatomic) NSArray *attachmentRects; @@ -395,13 +402,12 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri return [self layoutWithContainer:container text:text range:NSMakeRange(0, text.length)]; } -+ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttributedString *)text range:(NSRange)range { ++ (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container + text:(NSAttributedString *)text range:(NSRange)range { ASTextLayout *layout = NULL; CGPathRef cgPath = nil; CGRect cgPathBox = {0}; - BOOL isVerticalForm = NO; BOOL rowMaySeparated = NO; - NSMutableDictionary *frameAttrs = nil; CTFramesetterRef ctSetter = NULL; CTFrameRef ctFrame = NULL; CFArrayRef ctLines = nil; @@ -414,7 +420,10 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri NSMutableSet *attachmentContentsSet = nil; BOOL needTruncation = NO; NSAttributedString *truncationToken = nil; + NSMutableAttributedString *lastLineText = nil; ASTextLine *truncatedLine = nil; + NSRange truncatedLineBeforeTruncationTokenRange; + ASTextLine *truncatedLineBeforeTruncationToken; ASRowEdge *lineRowsEdge = NULL; NSUInteger *lineRowsIndex = NULL; NSRange visibleRange; @@ -435,30 +444,32 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (range.location + range.length > text.length) return nil; [container makeImmutable]; maximumNumberOfRows = container.maximumNumberOfRows; - - // It may use larger constraint size when create CTFrame with - // CTFramesetterCreateFrame in iOS 10. + + // Amazingly, nobody ever figured out _what_ the supposed layoutSizeBug on iOS 10 was, or as + // follows from that, whether it actually ever got fixed in, say, iOS 11. + // However, there is a lot of functionality now that assumes this is YES (and presumably is broken + // on iOS 9-). Thus, this flag should probably just be removed to simplify this code. BOOL needFixLayoutSizeBug = AS_AT_LEAST_IOS10; layout = [[ASTextLayout alloc] _init]; layout.text = text; layout.container = container; layout.range = range; - isVerticalForm = container.verticalForm; - - // set cgPath and cgPathBox + + // `exclusionPaths` are like `verticalForm` in that their behavior hasn't been verified in a long + // time, though I think they have a much better chance of still working. + // TODO Verify or remove ASTextLayout exclusionPaths if (container.path == nil && container.exclusionPaths.count == 0) { if (container.size.width <= 0 || container.size.height <= 0) FAIL_AND_RETURN CGRect rect = (CGRect) {CGPointZero, container.size }; + // Note from above, `needFixLayoutSizeBug` should really always be YES if (needFixLayoutSizeBug) { + // Thus, `contraintSizeIsExtended` is always YES. And thus we always set the main-axis + // constraint to "infinity"/maxSize: constraintSizeIsExtended = YES; constraintRectBeforeExtended = UIEdgeInsetsInsetRect(rect, container.insets); constraintRectBeforeExtended = CGRectStandardize(constraintRectBeforeExtended); - if (container.isVerticalForm) { - rect.size.width = ASTextContainerMaxSize.width; - } else { - rect.size.height = ASTextContainerMaxSize.height; - } + rect.size.height = ASTextContainerMaxSize.height; } rect = UIEdgeInsetsInsetRect(rect, container.insets); rect = CGRectStandardize(rect); @@ -486,7 +497,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri [layout.container.exclusionPaths enumerateObjectsUsingBlock: ^(UIBezierPath *onePath, NSUInteger idx, BOOL *stop) { CGPathAddPath(path, NULL, onePath.CGPath); }]; - + cgPathBox = CGPathGetPathBoundingBox(path); CGAffineTransform trans = CGAffineTransformMakeScale(1, -1); CGMutablePathRef transPath = CGPathCreateMutableCopyByTransformingPath(path, &trans); @@ -496,19 +507,31 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri cgPath = path; } if (!cgPath) FAIL_AND_RETURN - + + // With no exclusion or container paths, we got a rectangular "path" `cgPath` from above. As + // noted, we are probably about to do a whole bunch of wrong things if we did not. + + // BEGIN CTFramesetter cache code. There are two KEY LINEs in the below section, the rest of it is + // code to support caching of CTFramesetters, which are very expensive to create. + // frame setter config - frameAttrs = [[NSMutableDictionary alloc] init]; - if (container.isPathFillEvenOdd == NO) { - frameAttrs[(id)kCTFramePathFillRuleAttributeName] = @(kCTFramePathFillWindingNumber); - } - if (container.pathLineWidth > 0) { - frameAttrs[(id)kCTFramePathWidthAttributeName] = @(container.pathLineWidth); - } - if (container.isVerticalForm == YES) { - frameAttrs[(id)kCTFrameProgressionAttributeName] = @(kCTFrameProgressionRightToLeft); - } - + NSDictionary *frameAttrs = ({ + static constexpr NSUInteger kMaxAttrCount = 3; + NSUInteger count = 0; + id keys[kMaxAttrCount]; + id objects[kMaxAttrCount]; + if (container.isPathFillEvenOdd == NO) { + keys[count] = (id)kCTFramePathFillRuleAttributeName; + objects[count++] = @(kCTFramePathFillWindingNumber); + } + if (container.pathLineWidth > 0) { + keys[count] = (id)kCTFramePathWidthAttributeName; + objects[count++] = @(container.pathLineWidth); + } + // If you add new attributes, make sure to update kMaxAttrCount above. + count ? [[NSDictionary alloc] initWithObjects:objects forKeys:keys count:count] : nil; + }); + /* * Framesetter cache. * Framesetters can only be used by one thread at a time. @@ -555,10 +578,12 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri // Create a framesetter if needed. if (!ctSetter) { + // KEY LINE #1 (See above) ctSetter = CTFramesetterCreateWithAttributedString((CFAttributedStringRef)text); } if (!ctSetter) FAIL_AND_RETURN + // KEY LINE #2 (See above) ctFrame = CTFramesetterCreateFrame(ctSetter, ASTextCFRangeFromNSRange(range), cgPath, (CFDictionaryRef)frameAttrs); // Return to cache. @@ -575,6 +600,11 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } if (!ctFrame) FAIL_AND_RETURN + + // END CTFramesetterCache code + + // Now (Step 2?) we figure out where the individual lines are: CoreText already did this, we are + // just extracting them from the ctFrame reference. lines = [NSMutableArray new]; ctLines = CTFrameGetLines(ctFrame); lineCount = CFArrayGetCount(ctLines); @@ -583,17 +613,13 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (lineOrigins == NULL) FAIL_AND_RETURN CTFrameGetLineOrigins(ctFrame, CFRangeMake(0, lineCount), lineOrigins); } - + CGRect textBoundingRect = CGRectZero; CGSize textBoundingSize = CGSizeZero; NSInteger rowIdx = -1; NSUInteger rowCount = 0; CGRect lastRect = CGRectMake(0, -FLT_MAX, 0, 0); CGPoint lastPosition = CGPointMake(0, -FLT_MAX); - if (isVerticalForm) { - lastRect = CGRectMake(FLT_MAX, 0, 0, 0); - lastPosition = CGPointMake(FLT_MAX, 0); - } // calculate line frame NSUInteger lineCurrentIdx = 0; @@ -602,7 +628,7 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri CTLineRef ctLine = (CTLineRef)CFArrayGetValueAtIndex(ctLines, i); CFArrayRef ctRuns = CTLineGetGlyphRuns(ctLine); if (!ctRuns || CFArrayGetCount(ctRuns) == 0) continue; - + // CoreText coordinate system CGPoint ctLineOrigin = lineOrigins[i]; @@ -610,8 +636,8 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri CGPoint position; position.x = cgPathBox.origin.x + ctLineOrigin.x; position.y = cgPathBox.size.height + cgPathBox.origin.y - ctLineOrigin.y; - - ASTextLine *line = [ASTextLine lineWithCTLine:ctLine position:position vertical:isVerticalForm]; + + ASTextLine *line = [ASTextLine lineWithCTLine:ctLine position:position vertical:NO]; [lines addObject:line]; } @@ -619,46 +645,35 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri // Give user a chance to modify the line's position. [container.linePositionModifier modifyLines:lines fromText:text inContainer:container]; + // We treat the first line differently: BOOL first = YES; for (ASTextLine *line in lines) { - CGPoint position = line.position; - CGRect rect = line.bounds; + CGPoint linePosition = line.position; + CGRect lineBounds = line.bounds; if (constraintSizeIsExtended) { - if (isVerticalForm) { - if (rect.origin.x + rect.size.width > - constraintRectBeforeExtended.origin.x + - constraintRectBeforeExtended.size.width) { - measuringBeyondConstraints = YES; - } - } else { - if (rect.origin.y + rect.size.height > - constraintRectBeforeExtended.origin.y + - constraintRectBeforeExtended.size.height) { - measuringBeyondConstraints = YES; - } + if (lineBounds.origin.y + lineBounds.size.height > + constraintRectBeforeExtended.origin.y + + constraintRectBeforeExtended.size.height) { + // To support truncation, we will sometimes keep laying out rows even though we have gone + // past the (vertical) constraints. + measuringBeyondConstraints = YES; } } + // rowIdx (and rowCount) ONLY ADVANCE for rows that are within constraints. BOOL newRow = !measuringBeyondConstraints; - if (newRow && rowMaySeparated && position.x != lastPosition.x) { - if (isVerticalForm) { - if (rect.size.width > lastRect.size.width) { - if (rect.origin.x > lastPosition.x && lastPosition.x > rect.origin.x - rect.size.width) newRow = NO; - } else { - if (lastRect.origin.x > position.x && position.x > lastRect.origin.x - lastRect.size.width) newRow = NO; - } + if (newRow && rowMaySeparated && linePosition.x != lastPosition.x) { + if (lineBounds.size.height > lastRect.size.height) { + // Are this line's bounds entirely above the last line's? If so, it's not a new row + if (lineBounds.origin.y < lastPosition.y && lastPosition.y < lineBounds.origin.y + lineBounds.size.height) newRow = NO; } else { - if (rect.size.height > lastRect.size.height) { - if (rect.origin.y < lastPosition.y && lastPosition.y < rect.origin.y + rect.size.height) newRow = NO; - } else { - if (lastRect.origin.y < position.y && position.y < lastRect.origin.y + lastRect.size.height) newRow = NO; - } + if (lastRect.origin.y < linePosition.y && linePosition.y < lastRect.origin.y + lastRect.size.height) newRow = NO; } } if (newRow) rowIdx++; - lastRect = rect; - lastPosition = position; + lastRect = lineBounds; + lastPosition = linePosition; line.index = lineCurrentIdx; line.row = rowIdx; @@ -668,16 +683,26 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (first) { first = NO; - textBoundingRect = rect; - } else if (!measuringBeyondConstraints) { + textBoundingRect = lineBounds; + } + // We do a couple of checks: If we are within our given constraints and maximumNumberOfRows, + // we will expand the overall `textBoundingRect`. + else if (!measuringBeyondConstraints) { if (maximumNumberOfRows == 0 || rowIdx < maximumNumberOfRows) { - textBoundingRect = CGRectUnion(textBoundingRect, rect); + textBoundingRect = CGRectUnion(textBoundingRect, lineBounds); } } } + // BEGIN Truncation code. + { + // We expect to remove lines if we truncate, but we will want them later. NSMutableArray *removedLines = [NSMutableArray new]; + + // There are two main reasons to truncate: We exceed the given bounds, or we exceed the given + // `maximumNumberOfRows`. The first one we mostly figured out above. Here we now check for the + // `maximumNumberOfRows` limit: if (rowCount > 0) { if (maximumNumberOfRows > 0) { if (rowCount > maximumNumberOfRows) { @@ -692,8 +717,12 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } while (1); } } - ASTextLine *lastLine = rowCount < lines.count ? lines[rowCount - 1] : lines.lastObject; + + // `rowCount` is currently set to how many rows fit our constraints. Our last line might be + // either the last line, or just the last one that fit (< rowCount). + ASTextLine *lastLine = rowCount < lines.count ? lines[rowCount - 1] : lines.lastObject; if (!needTruncation && lastLine.range.location + lastLine.range.length < text.length) { + // lastLine doesn't go to the end of the text: ie it has been truncated. needTruncation = YES; while (lines.count > rowCount) { ASTextLine *line = lines.lastObject; @@ -718,21 +747,11 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } lastRowIdx = line.row; lineRowsIndex[lastRowIdx] = i; - if (isVerticalForm) { - lastHead = rect.origin.x + rect.size.width; - lastFoot = lastHead - rect.size.width; - } else { - lastHead = rect.origin.y; - lastFoot = lastHead + rect.size.height; - } + lastHead = rect.origin.y; + lastFoot = lastHead + rect.size.height; } else { - if (isVerticalForm) { - lastHead = MAX(lastHead, rect.origin.x + rect.size.width); - lastFoot = MIN(lastFoot, rect.origin.x); - } else { - lastHead = MIN(lastHead, rect.origin.y); - lastFoot = MAX(lastFoot, rect.origin.y + rect.size.height); - } + lastHead = MIN(lastHead, rect.origin.y); + lastFoot = MAX(lastFoot, rect.origin.y + rect.size.height); } } lineRowsEdge[lastRowIdx] = (ASRowEdge) {.head = lastHead, .foot = lastFoot}; @@ -744,94 +763,76 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri } } - { // calculate bounding size - CGRect rect = textBoundingRect; - if (container.path) { - if (container.pathLineWidth > 0) { - CGFloat inset = container.pathLineWidth / 2; - rect = CGRectInset(rect, -inset, -inset); - } - } else { - rect = UIEdgeInsetsInsetRect(rect, ASTextUIEdgeInsetsInvert(container.insets)); - } - rect = CGRectStandardize(rect); - CGSize size = rect.size; - if (container.verticalForm) { - size.width += container.size.width - (rect.origin.x + rect.size.width); - } else { - size.width += rect.origin.x; - } - size.height += rect.origin.y; - if (size.width < 0) size.width = 0; - if (size.height < 0) size.height = 0; - size.width = ceil(size.width); - size.height = ceil(size.height); - textBoundingSize = size; - } - visibleRange = ASTextNSRangeFromCFRange(CTFrameGetVisibleStringRange(ctFrame)); if (needTruncation) { ASTextLine *lastLine = lines.lastObject; NSRange lastRange = lastLine.range; - visibleRange.length = lastRange.location + lastRange.length - visibleRange.location; + visibleRange.length = NSMaxRange(lastRange) - visibleRange.location; // create truncated line if (container.truncationType != ASTextTruncationTypeNone) { + CTLineTruncationType type = kCTLineTruncationEnd; + if (container.truncationType == ASTextTruncationTypeStart) { + type = kCTLineTruncationStart; + } else if (container.truncationType == ASTextTruncationTypeMiddle) { + type = kCTLineTruncationMiddle; + } + CTLineRef truncationTokenLine = NULL; - if (container.truncationToken) { - truncationToken = container.truncationToken; - truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef) truncationToken); + truncationToken = container.truncationToken; + // For Tail truncation, if the truncation token is empty or only whitespace, we simply take + // whatever the Framesetter already generated and go with that (e.g. last word is omitted.) + static dispatch_once_t nonWhitespaceCharacterSetOnce; + static NSCharacterSet *nonWhitespaceCharacterSet; + dispatch_once(&nonWhitespaceCharacterSetOnce, ^{ + nonWhitespaceCharacterSet = NSCharacterSet.whitespaceCharacterSet.invertedSet; + }); + // Truncation token is all whitespace, or just an empty string. + if (truncationToken && type == kCTLineTruncationEnd && NSNotFound == [truncationToken.string rangeOfCharacterFromSet:nonWhitespaceCharacterSet].location) { + // Don't do anything. Reset truncationToken to nil, leave truncationTokenLine as NULL. + truncationToken = nil; } else { + // Create a CTLine from truncationToken. By default, apply the attributes from the end of + // the last line to the new truncation token. CFArrayRef runs = CTLineGetGlyphRuns(lastLine.CTLine); NSUInteger runCount = CFArrayGetCount(runs); + NSMutableAttributedString *string = + [[NSMutableAttributedString alloc] initWithString:ASTextTruncationToken]; NSMutableDictionary *attrs = nil; if (runCount > 0) { - CTRunRef run = (CTRunRef) CFArrayGetValueAtIndex(runs, runCount - 1); - attrs = (id) CTRunGetAttributes(run); - attrs = attrs ? attrs.mutableCopy : [NSMutableArray new]; + + // Get last line run + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, runCount - 1); + + // Attributes from last run + attrs = (id)CTRunGetAttributes(run); + attrs = attrs ? [attrs mutableCopy] : [[NSMutableDictionary alloc] init]; [attrs removeObjectsForKeys:[NSMutableAttributedString as_allDiscontinuousAttributeKeys]]; - CTFontRef font = (__bridge CTFontRef) attrs[(id) kCTFontAttributeName]; - CGFloat fontSize = font ? CTFontGetSize(font) : 12.0; - UIFont *uiFont = [UIFont systemFontOfSize:fontSize * 0.9]; - if (uiFont) { - font = CTFontCreateWithName((__bridge CFStringRef) uiFont.fontName, uiFont.pointSize, NULL); - } else { - font = NULL; - } - if (font) { - attrs[(id) kCTFontAttributeName] = (__bridge id) (font); - uiFont = nil; - CFRelease(font); + + if (container.truncationToken) { + string = [container.truncationToken mutableCopy]; } - CGColorRef color = (__bridge CGColorRef) (attrs[(id) kCTForegroundColorAttributeName]); + + // Ignore clear color + CGColorRef color = (__bridge CGColorRef)attrs[(id)kCTForegroundColorAttributeName]; if (color && CFGetTypeID(color) == CGColorGetTypeID() && CGColorGetAlpha(color) == 0) { - // ignore clear color - [attrs removeObjectForKey:(id) kCTForegroundColorAttributeName]; + [attrs removeObjectForKey:(id)kCTForegroundColorAttributeName]; } - if (!attrs) attrs = [NSMutableDictionary new]; } - truncationToken = [[NSAttributedString alloc] initWithString:ASTextTruncationToken attributes:attrs]; - truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef) truncationToken); + truncationToken = fillBaseAttributes(string, attrs); + truncationTokenLine = CTLineCreateWithAttributedString((CFAttributedStringRef)truncationToken); } if (truncationTokenLine) { - CTLineTruncationType type = kCTLineTruncationEnd; - if (container.truncationType == ASTextTruncationTypeStart) { - type = kCTLineTruncationStart; - } else if (container.truncationType == ASTextTruncationTypeMiddle) { - type = kCTLineTruncationMiddle; - } - NSMutableAttributedString *lastLineText = [text attributedSubstringFromRange:lastLine.range].mutableCopy; + // TODO: Avoid creating this unless we actually modify the last line text. + lastLineText = [[text attributedSubstringFromRange:lastLine.range] mutableCopy]; CGFloat truncatedWidth = lastLine.width; CGFloat atLeastOneLine = lastLine.width; CGRect cgPathRect = CGRectZero; if (CGPathIsRect(cgPath, &cgPathRect)) { - if (isVerticalForm) { - truncatedWidth = cgPathRect.size.height; - } else { - truncatedWidth = cgPathRect.size.width; - } + truncatedWidth = cgPathRect.size.width; } int i = 0; + int limit = (int) removedLines.count; if (type != kCTLineTruncationStart) { // Middle or End/Tail wants to collect some text (at least one line's // worth) preceding the truncated content, with which to construct a "truncated line". i = (int)removedLines.count - 1; @@ -843,13 +844,19 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri [lastLineText appendAttributedString:[text attributedSubstringFromRange:removedLines[i].range]]; atLeastOneLine += removedLines[i--].width; } - [lastLineText appendAttributedString:truncationToken]; + if (type == kCTLineTruncationEnd) { + [lastLineText appendAttributedString:truncationToken]; + } + if (type == kCTLineTruncationMiddle) { // If we are truncating Middle, we do not want + // to collect the same text into the truncated line more than once. + limit = i+1; + } } if (type != kCTLineTruncationEnd && removedLines.count > 0) { // Middle or Start/Head wants to collect some // text following the truncated content. i = 0; atLeastOneLine = removedLines[i].width; - while (atLeastOneLine < truncatedWidth && i < removedLines.count) { + while (atLeastOneLine < truncatedWidth && i < limit) { atLeastOneLine += removedLines[i++].width; } for (i--; i >= 0; i--) { @@ -867,91 +874,134 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri CTLineRef ctLastLineExtend = CTLineCreateWithAttributedString((CFAttributedStringRef) lastLineText); if (ctLastLineExtend) { - CTLineRef ctTruncatedLine = CTLineCreateTruncatedLine(ctLastLineExtend, truncatedWidth, type, truncationTokenLine); + // CTLineCreateTruncatedLine only reorders its CTRuns and doesn't change the range. + // After the reorder, some CTRuns in ctTruncatedLine are visible to the users, but we + // don't know which ones since this is completely handled by Core Text. + CTLineRef ctTruncatedLine = CTLineCreateTruncatedLine(ctLastLineExtend, truncatedWidth, + type, truncationTokenLine); + if (kTextNode2ImprovedTextTruncationVisibleRange) { + CFArrayRef truncatedLineRuns = CTLineGetGlyphRuns(ctTruncatedLine); + // Calculate the range of the truncated line before the truncation tokens + if (truncatedLineRuns) { + CFIndex truncatedLineRunCount = CFArrayGetCount(truncatedLineRuns); + + CGFloat truncationTokenWidth = + CTLineGetTypographicBounds(truncationTokenLine, 0, 0, 0); + CGFloat ctLastLineExtendWidth = + CTLineGetTypographicBounds(ctLastLineExtend, 0, 0, 0); + + // If the last line is not truncated, the truncation token is appended directly to + // the text without deletion. + if (kTextNode2ImprovedTextTruncationVisibleRangeLastLineFix && + (truncatedWidth > ctLastLineExtendWidth)) { + NSAttributedString *lastLineSubString = [lastLineText + attributedSubstringFromRange:NSMakeRange(0, + MAX(0, lastLineText.length - + truncationToken.length))]; + truncatedLineBeforeTruncationToken = + [ASTextLine lineWithCTLine:CTLineCreateWithAttributedString( + (CFAttributedStringRef)lastLineSubString) + position:lastLine.position + vertical:NO]; + + } else { + // Since Core Text hides the information that which CTRun is the last visible one, + // iterate through the runs to estimate the last visible run. + for (int i = 0; i < truncatedLineRunCount; i++) { + CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(truncatedLineRuns, + truncatedLineRunCount - 1 - i); + CFRange runRange = CTRunGetStringRange(run); + // Make sure that lastLineSubString has a valid range. + NSUInteger truncatedLineBeforeTruncationTokenRangeEnd = + MAX(MIN(lastLineText.length, runRange.location + runRange.length), 0); + truncatedLineBeforeTruncationTokenRange = NSMakeRange( + lastLine.range.location, truncatedLineBeforeTruncationTokenRangeEnd); + + NSAttributedString *lastLineSubString = [lastLineText + attributedSubstringFromRange: + NSMakeRange(0, truncatedLineBeforeTruncationTokenRangeEnd)]; + CGFloat lastLineSubStringWidth = CTLineGetTypographicBounds( + CTLineCreateWithAttributedString((CFAttributedStringRef)lastLineSubString), + 0, 0, 0); + // If lastLineSubString and truncationToken can "almost" fit in truncatedWidth, + // assume the current run is the last visible CTRun, and lastLineSubString is + // the visible part in the last line. Adding 2 here for error tolerance since + // truncationTokenWidth + lastLineSubStringWidth might be slightly longer than + // truncatedWidth. + if (truncationTokenWidth + lastLineSubStringWidth < truncatedWidth + 2) { + truncatedLineBeforeTruncationToken = + [ASTextLine lineWithCTLine:CTLineCreateWithAttributedString( + (CFAttributedStringRef)lastLineSubString) + position:lastLine.position + vertical:NO]; + break; + } + } + } + } + } + CFRelease(ctLastLineExtend); if (ctTruncatedLine) { - truncatedLine = [ASTextLine lineWithCTLine:ctTruncatedLine position:lastLine.position vertical:isVerticalForm]; - truncatedLine.index = lastLine.index; - truncatedLine.row = lastLine.row; + truncatedLine = [ASTextLine lineWithCTLine:ctTruncatedLine position:lastLine.position vertical:NO]; + // 1) If truncation mode is middle or start, and the end of the string contains taller text (or taller attachments), then truncating the line may make it taller + // (By pulling up the tall text that was previously in a later, clipped line, into the truncation line). + // 1b) There are edge cases where truncating the line makes it taller, thus it exceeds the bounds, and we in fact needed to truncate at an earlier line. + // Accommodating these cases in a robust manner would require multiple passes. (TODO_NOTREALLY) + // 2) In all cases, truncating the line may make it shorter. (Of course) + // 3) If text is not left-aligned, and truncating changed the width of the last line, it also needs to change its position. + BOOL adjusted = NO; + CGPoint adjustedPosition = truncatedLine.position; + if (truncatedLine.bounds.size.height > lastLine.bounds.size.height) { + adjusted = YES; + adjustedPosition = {adjustedPosition.x, lastLine.position.y + (truncatedLine.bounds.size.height - lastLine.bounds.size.height)/2}; + } + if ([lastLineText as_alignment] == NSTextAlignmentRight) { // (TODO: same for center-aligned) + adjusted = YES; + adjustedPosition = {lastLine.position.x - (truncatedLine.bounds.size.width - lastLine.bounds.size.width), adjustedPosition.y}; + } + if (adjusted) { + truncatedLine = [ASTextLine lineWithCTLine:ctTruncatedLine position:adjustedPosition vertical:NO]; + } CFRelease(ctTruncatedLine); } + textBoundingRect = CGRectUnion(textBoundingRect, truncatedLine.bounds); + truncatedLine.index = lastLine.index; + truncatedLine.row = lastLine.row; } CFRelease(truncationTokenLine); } } } - } - - if (isVerticalForm) { - NSCharacterSet *rotateCharset = ASTextVerticalFormRotateCharacterSet(); - NSCharacterSet *rotateMoveCharset = ASTextVerticalFormRotateAndMoveCharacterSet(); - void (^lineBlock)(ASTextLine *) = ^(ASTextLine *line){ - CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); - if (!runs) return; - NSUInteger runCount = CFArrayGetCount(runs); - if (runCount == 0) return; - NSMutableArray *lineRunRanges = [NSMutableArray new]; - line.verticalRotateRange = lineRunRanges; - for (NSUInteger r = 0; r < runCount; r++) { - CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); - NSMutableArray *runRanges = [NSMutableArray new]; - [lineRunRanges addObject:runRanges]; - NSUInteger glyphCount = CTRunGetGlyphCount(run); - if (glyphCount == 0) continue; - - CFIndex runStrIdx[glyphCount + 1]; - CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx); - CFRange runStrRange = CTRunGetStringRange(run); - runStrIdx[glyphCount] = runStrRange.location + runStrRange.length; - CFDictionaryRef runAttrs = CTRunGetAttributes(run); - CTFontRef font = (CTFontRef)CFDictionaryGetValue(runAttrs, kCTFontAttributeName); - BOOL isColorGlyph = ASTextCTFontContainsColorBitmapGlyphs(font); - - NSUInteger prevIdx = 0; - ASTextRunGlyphDrawMode prevMode = ASTextRunGlyphDrawModeHorizontal; - NSString *layoutStr = layout.text.string; - for (NSUInteger g = 0; g < glyphCount; g++) { - BOOL glyphRotate = 0, glyphRotateMove = NO; - CFIndex runStrLen = runStrIdx[g + 1] - runStrIdx[g]; - if (isColorGlyph) { - glyphRotate = YES; - } else if (runStrLen == 1) { - unichar c = [layoutStr characterAtIndex:runStrIdx[g]]; - glyphRotate = [rotateCharset characterIsMember:c]; - if (glyphRotate) glyphRotateMove = [rotateMoveCharset characterIsMember:c]; - } else if (runStrLen > 1){ - NSString *glyphStr = [layoutStr substringWithRange:NSMakeRange(runStrIdx[g], runStrLen)]; - BOOL glyphRotate = [glyphStr rangeOfCharacterFromSet:rotateCharset].location != NSNotFound; - if (glyphRotate) glyphRotateMove = [glyphStr rangeOfCharacterFromSet:rotateMoveCharset].location != NSNotFound; - } - - ASTextRunGlyphDrawMode mode = glyphRotateMove ? ASTextRunGlyphDrawModeVerticalRotateMove : (glyphRotate ? ASTextRunGlyphDrawModeVerticalRotate : ASTextRunGlyphDrawModeHorizontal); - if (g == 0) { - prevMode = mode; - } else if (mode != prevMode) { - ASTextRunGlyphRange *aRange = [ASTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, g - prevIdx) drawMode:prevMode]; - [runRanges addObject:aRange]; - prevIdx = g; - prevMode = mode; - } - } - if (prevIdx < glyphCount) { - ASTextRunGlyphRange *aRange = [ASTextRunGlyphRange rangeWithRange:NSMakeRange(prevIdx, glyphCount - prevIdx) drawMode:prevMode]; - [runRanges addObject:aRange]; + { // calculate bounding size + CGRect rect = textBoundingRect; + if (container.path) { + if (container.pathLineWidth > 0) { + CGFloat inset = container.pathLineWidth / 2; + rect = CGRectInset(rect, -inset, -inset); } - + } else { + rect = UIEdgeInsetsInsetRect(rect, ASTextUIEdgeInsetsInvert(container.insets)); } - }; - for (ASTextLine *line in lines) { - lineBlock(line); + rect = CGRectStandardize(rect); + CGSize size = rect.size; + size.width += rect.origin.x; + size.height += rect.origin.y; + if (size.width < 0) size.width = 0; + if (size.height < 0) size.height = 0; + size.width = ceil(size.width); + size.height = ceil(size.height); + textBoundingSize = size; } - if (truncatedLine) lineBlock(truncatedLine); } - + + // We store some hints we will look up when its time to draw. First thing, do we need to draw at all? if (visibleRange.length > 0) { layout.needDrawText = YES; - + + // ... and some finer details: We go through the NSAttrbutedString attributes looking for things + // that would require the following drawing behaviors, and set flags for them if so: void (^block)(NSDictionary *attrs, NSRange range, BOOL *stop) = ^(NSDictionary *attrs, NSRange range, BOOL *stop) { if (attrs[ASTextHighlightAttributeName]) layout.containsHighlight = YES; if (attrs[ASTextBlockBorderAttributeName]) layout.needDrawBlockBorder = YES; @@ -963,12 +1013,18 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri if (attrs[ASTextStrikethroughAttributeName]) layout.needDrawStrikethrough = YES; if (attrs[ASTextBorderAttributeName]) layout.needDrawBorder = YES; }; - + [layout.text enumerateAttributesInRange:visibleRange options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; if (truncatedLine) { + if (container.truncationType == ASTextTruncationTypeStart || container.truncationType == ASTextTruncationTypeMiddle) { + // If truncation mode is middle or start, there is another visible range not expressed in visibleRange. + [lastLineText enumerateAttributesInRange:NSMakeRange(0, lastLineText.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; + } [truncationToken enumerateAttributesInRange:NSMakeRange(0, truncationToken.length) options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired usingBlock:block]; } } + + // Also, set up attachments to use for rendering later. for (NSUInteger i = 0, max = lines.count; i < max; i++) { ASTextLine *line = lines[i]; if (truncatedLine && line.index == truncatedLine.index) line = truncatedLine; @@ -993,6 +1049,8 @@ + (ASTextLayout *)layoutWithContainer:(ASTextContainer *)container text:(NSAttri layout.frame = ctFrame; layout.lines = lines; layout.truncatedLine = truncatedLine; + layout.truncatedLineBeforeTruncationTokenRange = truncatedLineBeforeTruncationTokenRange; + layout.truncatedLineBeforeTruncationToken = truncatedLineBeforeTruncationToken; layout.attachments = attachments; layout.attachmentRanges = attachmentRanges; layout.attachmentRects = attachmentRects; @@ -1067,19 +1125,16 @@ - (id)copyWithZone:(NSZone *)zone { */ - (NSUInteger)_rowIndexForEdge:(CGFloat)edge { if (_rowCount == 0) return NSNotFound; - BOOL isVertical = _container.verticalForm; NSUInteger lo = 0, hi = _rowCount - 1, mid = 0; NSUInteger rowIdx = NSNotFound; while (lo <= hi) { mid = (lo + hi) / 2; ASRowEdge oneEdge = _lineRowsEdge[mid]; - if (isVertical ? - (oneEdge.foot <= edge && edge <= oneEdge.head) : - (oneEdge.head <= edge && edge <= oneEdge.foot)) { + if (oneEdge.head <= edge && edge <= oneEdge.foot) { rowIdx = mid; break; } - if ((isVertical ? (edge > oneEdge.head) : (edge < oneEdge.head))) { + if (edge < oneEdge.head) { if (mid == 0) break; hi = mid - 1; } else { @@ -1101,18 +1156,10 @@ - (NSUInteger)_closestRowIndexForEdge:(CGFloat)edge { if (_rowCount == 0) return NSNotFound; NSUInteger rowIdx = [self _rowIndexForEdge:edge]; if (rowIdx == NSNotFound) { - if (_container.verticalForm) { - if (edge > _lineRowsEdge[0].head) { - rowIdx = 0; - } else if (edge < _lineRowsEdge[_rowCount - 1].foot) { - rowIdx = _rowCount - 1; - } - } else { - if (edge < _lineRowsEdge[0].head) { - rowIdx = 0; - } else if (edge > _lineRowsEdge[_rowCount - 1].foot) { - rowIdx = _rowCount - 1; - } + if (edge < _lineRowsEdge[0].head) { + rowIdx = 0; + } else if (edge > _lineRowsEdge[_rowCount - 1].foot) { + rowIdx = _rowCount - 1; } } return rowIdx; @@ -1247,22 +1294,12 @@ - (BOOL)_isRightToLeftInLine:(ASTextLine *)line atPoint:(CGPoint)point { CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); CGPoint glyphPosition; CTRunGetPositions(run, CFRangeMake(0, 1), &glyphPosition); - if (_container.verticalForm) { - CGFloat runX = glyphPosition.x; - runX += line.position.y; - CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); - if (runX <= point.y && point.y <= runX + runWidth) { - if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES; - break; - } - } else { - CGFloat runX = glyphPosition.x; - runX += line.position.x; - CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); - if (runX <= point.x && point.x <= runX + runWidth) { - if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES; - break; - } + CGFloat runX = glyphPosition.x; + runX += line.position.x; + CGFloat runWidth = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); + if (runX <= point.x && point.x <= runX + runWidth) { + if (CTRunGetStatus(run) & kCTRunStatusRightToLeft) RTL = YES; + break; } } return RTL; @@ -1311,7 +1348,7 @@ - (NSUInteger)rowIndexForLine:(NSUInteger)line { - (NSUInteger)lineIndexForPoint:(CGPoint)point { if (_lines.count == 0 || _rowCount == 0) return NSNotFound; - NSUInteger rowIdx = [self _rowIndexForEdge:_container.verticalForm ? point.x : point.y]; + NSUInteger rowIdx = [self _rowIndexForEdge: point.y]; if (rowIdx == NSNotFound) return NSNotFound; NSUInteger lineIdx0 = _lineRowsIndex[rowIdx]; @@ -1325,9 +1362,8 @@ - (NSUInteger)lineIndexForPoint:(CGPoint)point { } - (NSUInteger)closestLineIndexForPoint:(CGPoint)point { - BOOL isVertical = _container.verticalForm; if (_lines.count == 0 || _rowCount == 0) return NSNotFound; - NSUInteger rowIdx = [self _closestRowIndexForEdge:isVertical ? point.x : point.y]; + NSUInteger rowIdx = [self _closestRowIndexForEdge: point.y]; if (rowIdx == NSNotFound) return NSNotFound; NSUInteger lineIdx0 = _lineRowsIndex[rowIdx]; @@ -1338,30 +1374,16 @@ - (NSUInteger)closestLineIndexForPoint:(CGPoint)point { NSUInteger minIndex = lineIdx0; for (NSUInteger i = lineIdx0; i <= lineIdx1; i++) { CGRect bounds = ((ASTextLine *)_lines[i]).bounds; - if (isVertical) { - if (bounds.origin.y <= point.y && point.y <= bounds.origin.y + bounds.size.height) return i; - CGFloat distance; - if (point.y < bounds.origin.y) { - distance = bounds.origin.y - point.y; - } else { - distance = point.y - (bounds.origin.y + bounds.size.height); - } - if (distance < minDistance) { - minDistance = distance; - minIndex = i; - } + if (bounds.origin.x <= point.x && point.x <= bounds.origin.x + bounds.size.width) return i; + CGFloat distance; + if (point.x < bounds.origin.x) { + distance = bounds.origin.x - point.x; } else { - if (bounds.origin.x <= point.x && point.x <= bounds.origin.x + bounds.size.width) return i; - CGFloat distance; - if (point.x < bounds.origin.x) { - distance = bounds.origin.x - point.x; - } else { - distance = point.x - (bounds.origin.x + bounds.size.width); - } - if (distance < minDistance) { - minDistance = distance; - minIndex = i; - } + distance = point.x - (bounds.origin.x + bounds.size.width); + } + if (distance < minDistance) { + minDistance = distance; + minIndex = i; } } return minIndex; @@ -1374,19 +1396,14 @@ - (CGFloat)offsetForTextPosition:(NSUInteger)position lineIndex:(NSUInteger)line if (position < range.location || position > range.location + range.length) return CGFLOAT_MAX; CGFloat offset = CTLineGetOffsetForStringIndex(line.CTLine, position, NULL); - return _container.verticalForm ? (offset + line.position.y) : (offset + line.position.x); + return offset + line.position.x; } - (NSUInteger)textPositionForPoint:(CGPoint)point lineIndex:(NSUInteger)lineIndex { if (lineIndex >= _lines.count) return NSNotFound; ASTextLine *line = _lines[lineIndex]; - if (_container.verticalForm) { - point.x = point.y - line.position.y; - point.y = 0; - } else { - point.x -= line.position.x; - point.y = 0; - } + point.x -= line.position.x; + point.y = 0; CFIndex idx = CTLineGetStringIndexForPosition(line.CTLine, point); if (idx == kCFNotFound) return NSNotFound; @@ -1442,12 +1459,10 @@ If the emoji contains one or more variant form (such as ☔️ "\u2614\uFE0F") } - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { - BOOL isVertical = _container.verticalForm; // When call CTLineGetStringIndexForPosition() on ligature such as 'fi', // and the point `hit` the glyph's left edge, it may get the ligature inside offset. // I don't know why, maybe it's a bug of CoreText. Try to avoid it. - if (isVertical) point.y += 0.00001234; - else point.x += 0.00001234; + point.x += 0.00001234; NSUInteger lineIndex = [self closestLineIndexForPoint:point]; if (lineIndex == NSNotFound) return nil; @@ -1473,22 +1488,12 @@ - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { CGFloat left = [self offsetForTextPosition:bindingRange.location lineIndex:lineIndex]; CGFloat right = [self offsetForTextPosition:bindingRange.location + bindingRange.length lineIndex:lineIndex]; if (left != CGFLOAT_MAX && right != CGFLOAT_MAX) { - if (_container.isVerticalForm) { - if (fabs(point.y - left) < fabs(point.y - right)) { - position = bindingRange.location; - finalAffinity = ASTextAffinityForward; - } else { - position = bindingRange.location + bindingRange.length; - finalAffinity = ASTextAffinityBackward; - } + if (fabs(point.x - left) < fabs(point.x - right)) { + position = bindingRange.location; + finalAffinity = ASTextAffinityForward; } else { - if (fabs(point.x - left) < fabs(point.x - right)) { - position = bindingRange.location; - finalAffinity = ASTextAffinityForward; - } else { - position = bindingRange.location + bindingRange.length; - finalAffinity = ASTextAffinityBackward; - } + position = bindingRange.location + bindingRange.length; + finalAffinity = ASTextAffinityBackward; } } else if (left != CGFLOAT_MAX) { position = left; @@ -1560,13 +1565,13 @@ - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { } // above whole text frame - if (lineIndex == 0 && (isVertical ? (point.x > line.right) : (point.y < line.top))) { + if (lineIndex == 0 && (point.y < line.top)) { position = 0; finalAffinity = ASTextAffinityForward; finalAffinityDetected = YES; } // below whole text frame - if (lineIndex == _lines.count - 1 && (isVertical ? (point.x < line.left) : (point.y > line.bottom))) { + if (lineIndex == _lines.count - 1 && (point.y > line.bottom)) { position = line.range.location + line.range.length; finalAffinity = ASTextAffinityBackward; finalAffinityDetected = YES; @@ -1596,19 +1601,11 @@ - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { } [self _insideComposedCharacterSequences:line position:position block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { - if (isVertical) { - position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next); - } else { - position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); - } + position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); }]; [self _insideEmoji:line position:position block: ^(CGFloat left, CGFloat right, NSUInteger prev, NSUInteger next) { - if (isVertical) { - position = fabs(left - point.y) < fabs(right - point.y) < (right ? prev : next); - } else { - position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); - } + position = fabs(left - point.x) < fabs(right - point.x) < (right ? prev : next); }]; if (position < _visibleRange.location) position = _visibleRange.location; @@ -1623,7 +1620,7 @@ - (ASTextPosition *)closestPositionToPoint:(CGPoint)point { } else if (position <= line.range.location) { finalAffinity = RTL ? ASTextAffinityBackward : ASTextAffinityForward; } else { - finalAffinity = (ofs < (isVertical ? point.y : point.x) && !RTL) ? ASTextAffinityForward : ASTextAffinityBackward; + finalAffinity = (ofs < point.x && !RTL) ? ASTextAffinityForward : ASTextAffinityBackward; } } } @@ -1647,35 +1644,19 @@ - (ASTextPosition *)positionForPoint:(CGPoint)point if (lineIndex == NSNotFound) return oldPosition; ASTextLine *line = _lines[lineIndex]; ASRowEdge vertical = _lineRowsEdge[line.row]; - if (_container.verticalForm) { - point.x = (vertical.head + vertical.foot) * 0.5; - } else { - point.y = (vertical.head + vertical.foot) * 0.5; - } + point.y = (vertical.head + vertical.foot) * 0.5; newPos = [self closestPositionToPoint:point]; if ([newPos compare:otherPosition] == [oldPosition compare:otherPosition] && newPos.offset != otherPosition.offset) { return newPos; } - - if (_container.isVerticalForm) { - if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward - ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionUp offset:1]; - if (range) return range.start; - } else { // search forward - ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionDown offset:1]; - if (range) return range.end; - } - } else { - if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward - ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionLeft offset:1]; - if (range) return range.start; - } else { // search forward - ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionRight offset:1]; - if (range) return range.end; - } + if ([oldPosition compare:otherPosition] == NSOrderedAscending) { // search backward + ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionLeft offset:1]; + if (range) return range.start; + } else { // search forward + ASTextRange *range = [self textRangeByExtendingPosition:otherPosition inDirection:UITextLayoutDirectionRight offset:1]; + if (range) return range.end; } - return oldPosition; } @@ -1691,14 +1672,8 @@ - (ASTextRange *)textRangeAtPoint:(CGPoint)point { BOOL RTL = [self _isRightToLeftInLine:_lines[lineIndex] atPoint:point]; CGRect rect = [self caretRectForPosition:pos]; if (CGRectIsNull(rect)) return nil; - - if (_container.verticalForm) { - ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown offset:1]; - return range; - } else { - ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight offset:1]; - return range; - } + ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:(rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight offset:1]; + return range; } - (ASTextRange *)closestTextRangeAtPoint:(CGPoint)point { @@ -1714,22 +1689,18 @@ - (ASTextRange *)closestTextRangeAtPoint:(CGPoint)point { UITextLayoutDirection direction = UITextLayoutDirectionRight; if (pos.offset >= line.range.location + line.range.length) { if (direction != RTL) { - direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft; + direction = UITextLayoutDirectionLeft; } else { - direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight; + direction = UITextLayoutDirectionRight; } } else if (pos.offset <= line.range.location) { if (direction != RTL) { - direction = _container.verticalForm ? UITextLayoutDirectionDown : UITextLayoutDirectionRight; + direction = UITextLayoutDirectionRight; } else { - direction = _container.verticalForm ? UITextLayoutDirectionUp : UITextLayoutDirectionLeft; + direction = UITextLayoutDirectionLeft; } } else { - if (_container.verticalForm) { - direction = (rect.origin.y >= point.y && !RTL) ? UITextLayoutDirectionUp:UITextLayoutDirectionDown; - } else { - direction = (rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight; - } + direction = (rect.origin.x >= point.x && !RTL) ? UITextLayoutDirectionLeft:UITextLayoutDirectionRight; } ASTextRange *range = [self textRangeByExtendingPosition:pos inDirection:direction offset:1]; @@ -1807,17 +1778,9 @@ - (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position if (!position) return nil; if (position.offset < visibleStart || position.offset > visibleEnd) return nil; if (offset == 0) return [self textRangeByExtendingPosition:position]; - - BOOL isVerticalForm = _container.verticalForm; - BOOL verticalMove, forwardMove; - - if (isVerticalForm) { - verticalMove = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionRight; - forwardMove = direction == UITextLayoutDirectionLeft || direction == UITextLayoutDirectionDown; - } else { - verticalMove = direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown; - forwardMove = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight; - } + + BOOL verticalMove = direction == UITextLayoutDirectionUp || direction == UITextLayoutDirectionDown; + BOOL forwardMove = direction == UITextLayoutDirectionDown || direction == UITextLayoutDirectionRight; if (offset < 0) { forwardMove = !forwardMove; @@ -1858,32 +1821,17 @@ - (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position for (NSUInteger i = 0; i < moveToLineCount; i++) { NSUInteger lineIndex = moveToLineFirstIndex + i; ASTextLine *line = _lines[lineIndex]; - if (isVerticalForm) { - if (line.top <= ofs && ofs <= line.bottom) { - insideIndex = line.index; - break; - } - if (line.top < mostLeft) { - mostLeft = line.top; - mostLeftLine = line; - } - if (line.bottom > mostRight) { - mostRight = line.bottom; - mostRightLine = line; - } - } else { - if (line.left <= ofs && ofs <= line.right) { - insideIndex = line.index; - break; - } - if (line.left < mostLeft) { - mostLeft = line.left; - mostLeftLine = line; - } - if (line.right > mostRight) { - mostRight = line.right; - mostRightLine = line; - } + if (line.left <= ofs && ofs <= line.right) { + insideIndex = line.index; + break; + } + if (line.left < mostLeft) { + mostLeft = line.left; + mostLeftLine = line; + } + if (line.right > mostRight) { + mostRight = line.right; + mostRightLine = line; } } BOOL afinityEdge = NO; @@ -1896,12 +1844,7 @@ - (ASTextRange *)textRangeByExtendingPosition:(ASTextPosition *)position afinityEdge = YES; } ASTextLine *insideLine = _lines[insideIndex]; - NSUInteger pos; - if (isVerticalForm) { - pos = [self textPositionForPoint:CGPointMake(insideLine.position.x, ofs) lineIndex:insideIndex]; - } else { - pos = [self textPositionForPoint:CGPointMake(ofs, insideLine.position.y) lineIndex:insideIndex]; - } + NSUInteger pos = [self textPositionForPoint:CGPointMake(ofs, insideLine.position.y) lineIndex:insideIndex]; if (pos == NSNotFound) return nil; ASTextPosition *extPos; if (afinityEdge) { @@ -1980,11 +1923,7 @@ - (CGPoint)linePositionForPosition:(ASTextPosition *)position { ASTextLine *line = _lines[lineIndex]; CGFloat offset = [self offsetForTextPosition:position.offset lineIndex:lineIndex]; if (offset == CGFLOAT_MAX) return CGPointZero; - if (_container.verticalForm) { - return CGPointMake(line.position.x, offset); - } else { - return CGPointMake(offset, line.position.y); - } + return CGPointMake(offset, line.position.y); } - (CGRect)caretRectForPosition:(ASTextPosition *)position { @@ -1993,11 +1932,7 @@ - (CGRect)caretRectForPosition:(ASTextPosition *)position { ASTextLine *line = _lines[lineIndex]; CGFloat offset = [self offsetForTextPosition:position.offset lineIndex:lineIndex]; if (offset == CGFLOAT_MAX) return CGRectNull; - if (_container.verticalForm) { - return CGRectMake(line.bounds.origin.x, offset, line.bounds.size.width, 0); - } else { - return CGRectMake(offset, line.bounds.origin.y, 0, line.bounds.size.height); - } + return CGRectMake(offset, line.bounds.origin.y, 0, line.bounds.size.height); } - (CGRect)firstRectForRange:(ASTextRange *)range { @@ -2015,54 +1950,28 @@ - (CGRect)firstRectForRange:(ASTextRange *)range { if (line.row != startLine.row) break; [lines addObject:line]; } - if (_container.verticalForm) { - if (lines.count == 1) { - CGFloat top = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; - CGFloat bottom; - if (startLine == endLine) { - bottom = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex]; - } else { - bottom = startLine.bottom; - } - if (top == CGFLOAT_MAX || bottom == CGFLOAT_MAX) return CGRectNull; - if (top > bottom) ASTEXT_SWAP(top, bottom); - return CGRectMake(startLine.left, top, startLine.width, bottom - top); + if (lines.count == 1) { + CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat right; + if (startLine == endLine) { + right = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex]; } else { - CGFloat top = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; - CGFloat bottom = startLine.bottom; - if (top == CGFLOAT_MAX || bottom == CGFLOAT_MAX) return CGRectNull; - if (top > bottom) ASTEXT_SWAP(top, bottom); - CGRect rect = CGRectMake(startLine.left, top, startLine.width, bottom - top); - for (NSUInteger i = 1; i < lines.count; i++) { - ASTextLine *line = lines[i]; - rect = CGRectUnion(rect, line.bounds); - } - return rect; + right = startLine.right; } + if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; + if (left > right) ASTEXT_SWAP(left, right); + return CGRectMake(left, startLine.top, right - left, startLine.height); } else { - if (lines.count == 1) { - CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; - CGFloat right; - if (startLine == endLine) { - right = [self offsetForTextPosition:range.end.offset lineIndex:startLineIndex]; - } else { - right = startLine.right; - } - if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; - if (left > right) ASTEXT_SWAP(left, right); - return CGRectMake(left, startLine.top, right - left, startLine.height); - } else { - CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; - CGFloat right = startLine.right; - if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; - if (left > right) ASTEXT_SWAP(left, right); - CGRect rect = CGRectMake(left, startLine.top, right - left, startLine.height); - for (NSUInteger i = 1; i < lines.count; i++) { - ASTextLine *line = lines[i]; - rect = CGRectUnion(rect, line.bounds); - } - return rect; + CGFloat left = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; + CGFloat right = startLine.right; + if (left == CGFLOAT_MAX || right == CGFLOAT_MAX) return CGRectNull; + if (left > right) ASTEXT_SWAP(left, right); + CGRect rect = CGRectMake(left, startLine.top, right - left, startLine.height); + for (NSUInteger i = 1; i < lines.count; i++) { + ASTextLine *line = lines[i]; + rect = CGRectUnion(rect, line.bounds); } + return rect; } } @@ -2080,7 +1989,6 @@ - (CGRect)rectForRange:(ASTextRange *)range { - (NSArray *)selectionRectsForRange:(ASTextRange *)range { range = [self _correctedRangeWithEdge:range]; - BOOL isVertical = _container.verticalForm; NSMutableArray *rects = [[NSMutableArray alloc] init]; if (!range) return rects; @@ -2094,81 +2002,52 @@ - (NSArray *)selectionRectsForRange:(ASTextRange *)range { CGFloat offsetEnd = [self offsetForTextPosition:range.end.offset lineIndex:endLineIndex]; ASTextSelectionRect *start = [ASTextSelectionRect new]; - if (isVertical) { - start.rect = CGRectMake(startLine.left, offsetStart, startLine.width, 0); - } else { - start.rect = CGRectMake(offsetStart, startLine.top, 0, startLine.height); - } + start.rect = CGRectMake(offsetStart, startLine.top, 0, startLine.height); start.containsStart = YES; - start.isVertical = isVertical; + start.isVertical = NO; [rects addObject:start]; ASTextSelectionRect *end = [ASTextSelectionRect new]; - if (isVertical) { - end.rect = CGRectMake(endLine.left, offsetEnd, endLine.width, 0); - } else { - end.rect = CGRectMake(offsetEnd, endLine.top, 0, endLine.height); - } + end.rect = CGRectMake(offsetEnd, endLine.top, 0, endLine.height); end.containsEnd = YES; - end.isVertical = isVertical; + end.isVertical = NO; [rects addObject:end]; if (startLine.row == endLine.row) { // same row if (offsetStart > offsetEnd) ASTEXT_SWAP(offsetStart, offsetEnd); ASTextSelectionRect *rect = [ASTextSelectionRect new]; - if (isVertical) { - rect.rect = CGRectMake(startLine.bounds.origin.x, offsetStart, MAX(startLine.width, endLine.width), offsetEnd - offsetStart); - } else { - rect.rect = CGRectMake(offsetStart, startLine.bounds.origin.y, offsetEnd - offsetStart, MAX(startLine.height, endLine.height)); - } - rect.isVertical = isVertical; + rect.rect = CGRectMake(offsetStart, startLine.bounds.origin.y, offsetEnd - offsetStart, MAX(startLine.height, endLine.height)); + rect.isVertical = NO; [rects addObject:rect]; } else { // more than one row // start line select rect ASTextSelectionRect *topRect = [ASTextSelectionRect new]; - topRect.isVertical = isVertical; + topRect.isVertical = NO; CGFloat topOffset = [self offsetForTextPosition:range.start.offset lineIndex:startLineIndex]; CTRunRef topRun = [self _runForLine:startLine position:range.start]; if (topRun && (CTRunGetStatus(topRun) & kCTRunStatusRightToLeft)) { - if (isVertical) { - topRect.rect = CGRectMake(startLine.left, _container.path ? startLine.top : _container.insets.top, startLine.width, topOffset - startLine.top); - } else { - topRect.rect = CGRectMake(_container.path ? startLine.left : _container.insets.left, startLine.top, topOffset - startLine.left, startLine.height); - } + topRect.rect = CGRectMake(_container.path ? startLine.left : _container.insets.left, startLine.top, topOffset - startLine.left, startLine.height); topRect.writingDirection = UITextWritingDirectionRightToLeft; } else { - if (isVertical) { - topRect.rect = CGRectMake(startLine.left, topOffset, startLine.width, (_container.path ? startLine.bottom : _container.size.height - _container.insets.bottom) - topOffset); - } else { - // TODO: Fixes highlighting first row only to the end of the text and not highlight - // the while line to the end. Needs to brought over to multiline support - topRect.rect = CGRectMake(topOffset, startLine.top, (_container.path ? startLine.right : _container.size.width - _container.insets.right) - topOffset - (_container.size.width - _container.insets.right - startLine.right), startLine.height); - } + // TODO: Fixes highlighting first row only to the end of the text and not highlight + // the while line to the end. Needs to brought over to multiline support + topRect.rect = CGRectMake(topOffset, startLine.top, (_container.path ? startLine.right : _container.size.width - _container.insets.right) - topOffset - (_container.size.width - _container.insets.right - startLine.right), startLine.height); } [rects addObject:topRect]; // end line select rect ASTextSelectionRect *bottomRect = [ASTextSelectionRect new]; - bottomRect.isVertical = isVertical; + bottomRect.isVertical = NO; CGFloat bottomOffset = [self offsetForTextPosition:range.end.offset lineIndex:endLineIndex]; CTRunRef bottomRun = [self _runForLine:endLine position:range.end]; if (bottomRun && (CTRunGetStatus(bottomRun) & kCTRunStatusRightToLeft)) { - if (isVertical) { - bottomRect.rect = CGRectMake(endLine.left, bottomOffset, endLine.width, (_container.path ? endLine.bottom : _container.size.height - _container.insets.bottom) - bottomOffset); - } else { - bottomRect.rect = CGRectMake(bottomOffset, endLine.top, (_container.path ? endLine.right : _container.size.width - _container.insets.right) - bottomOffset, endLine.height); - } + bottomRect.rect = CGRectMake(bottomOffset, endLine.top, (_container.path ? endLine.right : _container.size.width - _container.insets.right) - bottomOffset, endLine.height); bottomRect.writingDirection = UITextWritingDirectionRightToLeft; } else { - if (isVertical) { - CGFloat top = _container.path ? endLine.top : _container.insets.top; - bottomRect.rect = CGRectMake(endLine.left, top, endLine.width, bottomOffset - top); - } else { - CGFloat left = _container.path ? endLine.left : _container.insets.left; - bottomRect.rect = CGRectMake(left, endLine.top, bottomOffset - left, endLine.height); - } + CGFloat left = _container.path ? endLine.left : _container.insets.left; + bottomRect.rect = CGRectMake(left, endLine.top, bottomOffset - left, endLine.height); } [rects addObject:bottomRect]; @@ -2186,49 +2065,28 @@ - (NSArray *)selectionRectsForRange:(ASTextRange *)range { } } if (startLineDetected) { - if (isVertical) { - if (!_container.path) { - r.origin.y = _container.insets.top; - r.size.height = _container.size.height - _container.insets.bottom - _container.insets.top; - } - r.size.width = CGRectGetMinX(topRect.rect) - CGRectGetMaxX(bottomRect.rect); - r.origin.x = CGRectGetMaxX(bottomRect.rect); - } else { - if (!_container.path) { - r.origin.x = _container.insets.left; - r.size.width = _container.size.width - _container.insets.right - _container.insets.left; - } - r.origin.y = CGRectGetMaxY(topRect.rect); - r.size.height = bottomRect.rect.origin.y - r.origin.y; + if (!_container.path) { + r.origin.x = _container.insets.left; + r.size.width = _container.size.width - _container.insets.right - _container.insets.left; } + r.origin.y = CGRectGetMaxY(topRect.rect); + r.size.height = bottomRect.rect.origin.y - r.origin.y; ASTextSelectionRect *rect = [ASTextSelectionRect new]; rect.rect = r; - rect.isVertical = isVertical; + rect.isVertical = NO; [rects addObject:rect]; } } else { - if (isVertical) { - CGRect r0 = bottomRect.rect; - CGRect r1 = topRect.rect; - CGFloat mid = (CGRectGetMaxX(r0) + CGRectGetMinX(r1)) * 0.5; - r0.size.width = mid - r0.origin.x; - CGFloat r1ofs = r1.origin.x - mid; - r1.origin.x -= r1ofs; - r1.size.width += r1ofs; - topRect.rect = r1; - bottomRect.rect = r0; - } else { - CGRect r0 = topRect.rect; - CGRect r1 = bottomRect.rect; - CGFloat mid = (CGRectGetMaxY(r0) + CGRectGetMinY(r1)) * 0.5; - r0.size.height = mid - r0.origin.y; - CGFloat r1ofs = r1.origin.y - mid; - r1.origin.y -= r1ofs; - r1.size.height += r1ofs; - topRect.rect = r0; - bottomRect.rect = r1; - } + CGRect r0 = topRect.rect; + CGRect r1 = bottomRect.rect; + CGFloat mid = (CGRectGetMaxY(r0) + CGRectGetMinY(r1)) * 0.5; + r0.size.height = mid - r0.origin.y; + CGFloat r1ofs = r1.origin.y - mid; + r1.origin.y -= r1ofs; + r1.size.height += r1ofs; + topRect.rect = r0; + bottomRect.rect = r1; } } return rects; @@ -2274,18 +2132,11 @@ typedef NS_OPTIONS(NSUInteger, ASTextBorderType) { ASTextBorderTypeNormal = 1 << 1, }; -static CGRect ASTextMergeRectInSameLine(CGRect rect1, CGRect rect2, BOOL isVertical) { - if (isVertical) { - CGFloat top = MIN(rect1.origin.y, rect2.origin.y); - CGFloat bottom = MAX(rect1.origin.y + rect1.size.height, rect2.origin.y + rect2.size.height); - CGFloat width = MAX(rect1.size.width, rect2.size.width); - return CGRectMake(rect1.origin.x, top, width, bottom - top); - } else { - CGFloat left = MIN(rect1.origin.x, rect2.origin.x); - CGFloat right = MAX(rect1.origin.x + rect1.size.width, rect2.origin.x + rect2.size.width); - CGFloat height = MAX(rect1.size.height, rect2.size.height); - return CGRectMake(left, rect1.origin.y, right - left, height); - } +static CGRect ASTextMergeRectInSameLine(CGRect rect1, CGRect rect2) { + CGFloat left = MIN(rect1.origin.x, rect2.origin.x); + CGFloat right = MAX(rect1.origin.x + rect1.size.width, rect2.origin.x + rect2.size.width); + CGFloat height = MAX(rect1.size.height, rect2.size.height); + return CGRectMake(left, rect1.origin.y, right - left, height); } static void ASTextGetRunsMaxMetric(CFArrayRef runs, CGFloat *xHeight, CGFloat *underlinePosition, CGFloat *lineThickness) { @@ -2312,13 +2163,13 @@ static void ASTextGetRunsMaxMetric(CFArrayRef runs, CGFloat *xHeight, CGFloat *u if (lineThickness) *lineThickness = maxLineThickness; } -static void ASTextDrawRun(ASTextLine *line, CTRunRef run, CGContextRef context, CGSize size, BOOL isVertical, NSArray *runRanges, CGFloat verticalOffset) { +static void ASTextDrawRun(ASTextLine *line, CTRunRef run, CGContextRef context, CGSize size, NSArray *runRanges, CGFloat verticalOffset) { CGAffineTransform runTextMatrix = CTRunGetTextMatrix(run); BOOL runTextMatrixIsID = CGAffineTransformIsIdentity(runTextMatrix); CFDictionaryRef runAttrs = CTRunGetAttributes(run); NSValue *glyphTransformValue = (NSValue *)CFDictionaryGetValue(runAttrs, (__bridge const void *)(ASTextGlyphTransformAttributeName)); - if (!isVertical && !glyphTransformValue) { // draw run + if (!glyphTransformValue) { // draw run if (!runTextMatrixIsID) { CGContextSaveGState(context); CGAffineTransform trans = CGContextGetTextMatrix(context); @@ -2358,102 +2209,46 @@ static void ASTextDrawRun(ASTextLine *line, CTRunRef run, CGContextRef context, CGContextSetTextDrawingMode(context, kCGTextFillStroke); } } - - if (isVertical) { + if (glyphTransformValue) { CFIndex runStrIdx[glyphCount + 1]; CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx); CFRange runStrRange = CTRunGetStringRange(run); runStrIdx[glyphCount] = runStrRange.location + runStrRange.length; CGSize glyphAdvances[glyphCount]; CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances); - CGFloat ascent = CTFontGetAscent(runFont); - CGFloat descent = CTFontGetDescent(runFont); CGAffineTransform glyphTransform = glyphTransformValue.CGAffineTransformValue; CGPoint zeroPoint = CGPointZero; - - for (ASTextRunGlyphRange *oneRange in runRanges) { - NSRange range = oneRange.glyphRangeInRun; - NSUInteger rangeMax = range.location + range.length; - ASTextRunGlyphDrawMode mode = oneRange.drawMode; - - for (NSUInteger g = range.location; g < rangeMax; g++) { - CGContextSaveGState(context); { - CGContextSetTextMatrix(context, CGAffineTransformIdentity); - if (glyphTransformValue) { - CGContextSetTextMatrix(context, glyphTransform); - } - if (mode) { // CJK glyph, need rotated - CGFloat ofs = (ascent - descent) * 0.5; - CGFloat w = glyphAdvances[g].width * 0.5; - CGFloat x = line.position.x + verticalOffset + glyphPositions[g].y + (ofs - w); - CGFloat y = -line.position.y + size.height - glyphPositions[g].x - (ofs + w); - if (mode == ASTextRunGlyphDrawModeVerticalRotateMove) { - x += w; - y += w; - } - CGContextSetTextPosition(context, x, y); - } else { - CGContextRotateCTM(context, -M_PI_2); - CGContextSetTextPosition(context, - line.position.y - size.height + glyphPositions[g].x, - line.position.x + verticalOffset + glyphPositions[g].y); - } - - if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { - CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context); - } else { - CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); - CGContextSetFont(context, cgFont); - CGContextSetFontSize(context, CTFontGetSize(runFont)); - CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1); - CGFontRelease(cgFont); - } - } CGContextRestoreGState(context); - } + + for (NSUInteger g = 0; g < glyphCount; g++) { + CGContextSaveGState(context); { + CGContextSetTextMatrix(context, CGAffineTransformIdentity); + CGContextSetTextMatrix(context, glyphTransform); + CGContextSetTextPosition(context, + line.position.x + glyphPositions[g].x, + size.height - (line.position.y + glyphPositions[g].y)); + + if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { + CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context); + } else { + CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); + CGContextSetFont(context, cgFont); + CGContextSetFontSize(context, CTFontGetSize(runFont)); + CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1); + CGFontRelease(cgFont); + } + } CGContextRestoreGState(context); } - } else { // not vertical - if (glyphTransformValue) { - CFIndex runStrIdx[glyphCount + 1]; - CTRunGetStringIndices(run, CFRangeMake(0, 0), runStrIdx); - CFRange runStrRange = CTRunGetStringRange(run); - runStrIdx[glyphCount] = runStrRange.location + runStrRange.length; - CGSize glyphAdvances[glyphCount]; - CTRunGetAdvances(run, CFRangeMake(0, 0), glyphAdvances); - CGAffineTransform glyphTransform = glyphTransformValue.CGAffineTransformValue; - CGPoint zeroPoint = CGPointZero; - - for (NSUInteger g = 0; g < glyphCount; g++) { - CGContextSaveGState(context); { - CGContextSetTextMatrix(context, CGAffineTransformIdentity); - CGContextSetTextMatrix(context, glyphTransform); - CGContextSetTextPosition(context, - line.position.x + glyphPositions[g].x, - size.height - (line.position.y + glyphPositions[g].y)); - - if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { - CTFontDrawGlyphs(runFont, glyphs + g, &zeroPoint, 1, context); - } else { - CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); - CGContextSetFont(context, cgFont); - CGContextSetFontSize(context, CTFontGetSize(runFont)); - CGContextShowGlyphsAtPositions(context, glyphs + g, &zeroPoint, 1); - CGFontRelease(cgFont); - } - } CGContextRestoreGState(context); - } + } else { + if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { + CTFontDrawGlyphs(runFont, glyphs, glyphPositions, glyphCount, context); } else { - if (ASTextCTFontContainsColorBitmapGlyphs(runFont)) { - CTFontDrawGlyphs(runFont, glyphs, glyphPositions, glyphCount, context); - } else { - CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); - CGContextSetFont(context, cgFont); - CGContextSetFontSize(context, CTFontGetSize(runFont)); - CGContextShowGlyphsAtPositions(context, glyphs, glyphPositions, glyphCount); - CGFontRelease(cgFont); - } + CGFontRef cgFont = CTFontCopyGraphicsFont(runFont, NULL); + CGContextSetFont(context, cgFont); + CGContextSetFontSize(context, CTFontGetSize(runFont)); + CGContextShowGlyphsAtPositions(context, glyphs, glyphPositions, glyphCount); + CGFontRelease(cgFont); } } - } CGContextRestoreGState(context); } } @@ -2488,7 +2283,7 @@ static void ASTextSetLinePatternInContext(ASTextLineStyle style, CGFloat width, } -static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorder *border, NSArray *rects, BOOL isVertical) { +static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorder *border, NSArray *rects) { if (rects.count == 0) return; ASTextShadow *shadow = border.shadow; @@ -2501,11 +2296,7 @@ static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorde NSMutableArray *paths = [NSMutableArray new]; for (NSValue *value in rects) { CGRect rect = value.CGRectValue; - if (isVertical) { - rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(border.insets)); - } else { - rect = UIEdgeInsetsInsetRect(rect, border.insets); - } + rect = UIEdgeInsetsInsetRect(rect, border.insets); rect = ASTextCGRectPixelRound(rect); UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius]; [path closePath]; @@ -2547,11 +2338,7 @@ static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorde CGContextSetLineJoin(context, border.lineJoin); for (NSValue *value in rects) { CGRect rect = value.CGRectValue; - if (isVertical) { - rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(border.insets)); - } else { - rect = UIEdgeInsetsInsetRect(rect, border.insets); - } + rect = UIEdgeInsetsInsetRect(rect, border.insets); rect = CGRectInset(rect, inset, inset); UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:rect cornerRadius:border.cornerRadius + radiusDelta]; [path closePath]; @@ -2604,85 +2391,46 @@ static void ASTextDrawBorderRects(CGContextRef context, CGSize size, ASTextBorde } } -static void ASTextDrawLineStyle(CGContextRef context, CGFloat length, CGFloat lineWidth, ASTextLineStyle style, CGPoint position, CGColorRef color, BOOL isVertical) { +static void ASTextDrawLineStyle(CGContextRef context, CGFloat length, CGFloat lineWidth, ASTextLineStyle style, CGPoint position, CGColorRef color) { NSUInteger styleBase = style & 0xFF; if (styleBase == 0) return; CGContextSaveGState(context); { - if (isVertical) { - CGFloat x, y1, y2, w; - y1 = ASRoundPixelValue(position.y); - y2 = ASRoundPixelValue(position.y + length); - w = (styleBase == ASTextLineStyleThick ? lineWidth * 2 : lineWidth); - - CGFloat linePixel = ASTextCGFloatToPixel(w); - if (fabs(linePixel - floor(linePixel)) < 0.1) { - int iPixel = linePixel; - if (iPixel == 0 || (iPixel % 2)) { // odd line pixel - x = ASTextCGFloatPixelHalf(position.x); - } else { - x = ASFloorPixelValue(position.x); - } + CGFloat x1, x2, y, w; + x1 = ASRoundPixelValue(position.x); + x2 = ASRoundPixelValue(position.x + length); + w = (styleBase == ASTextLineStyleThick ? lineWidth * 2 : lineWidth); + + CGFloat linePixel = ASTextCGFloatToPixel(w); + if (fabs(linePixel - floor(linePixel)) < 0.1) { + int iPixel = linePixel; + if (iPixel == 0 || (iPixel % 2)) { // odd line pixel + y = ASTextCGFloatPixelHalf(position.y); } else { - x = position.x; - } - - CGContextSetStrokeColorWithColor(context, color); - ASTextSetLinePatternInContext(style, lineWidth, position.y, context); - CGContextSetLineWidth(context, w); - if (styleBase == ASTextLineStyleSingle) { - CGContextMoveToPoint(context, x, y1); - CGContextAddLineToPoint(context, x, y2); - CGContextStrokePath(context); - } else if (styleBase == ASTextLineStyleThick) { - CGContextMoveToPoint(context, x, y1); - CGContextAddLineToPoint(context, x, y2); - CGContextStrokePath(context); - } else if (styleBase == ASTextLineStyleDouble) { - CGContextMoveToPoint(context, x - w, y1); - CGContextAddLineToPoint(context, x - w, y2); - CGContextStrokePath(context); - CGContextMoveToPoint(context, x + w, y1); - CGContextAddLineToPoint(context, x + w, y2); - CGContextStrokePath(context); + y = ASFloorPixelValue(position.y); } } else { - CGFloat x1, x2, y, w; - x1 = ASRoundPixelValue(position.x); - x2 = ASRoundPixelValue(position.x + length); - w = (styleBase == ASTextLineStyleThick ? lineWidth * 2 : lineWidth); - - CGFloat linePixel = ASTextCGFloatToPixel(w); - if (fabs(linePixel - floor(linePixel)) < 0.1) { - int iPixel = linePixel; - if (iPixel == 0 || (iPixel % 2)) { // odd line pixel - y = ASTextCGFloatPixelHalf(position.y); - } else { - y = ASFloorPixelValue(position.y); - } - } else { - y = position.y; - } - - CGContextSetStrokeColorWithColor(context, color); - ASTextSetLinePatternInContext(style, lineWidth, position.x, context); - CGContextSetLineWidth(context, w); - if (styleBase == ASTextLineStyleSingle) { - CGContextMoveToPoint(context, x1, y); - CGContextAddLineToPoint(context, x2, y); - CGContextStrokePath(context); - } else if (styleBase == ASTextLineStyleThick) { - CGContextMoveToPoint(context, x1, y); - CGContextAddLineToPoint(context, x2, y); - CGContextStrokePath(context); - } else if (styleBase == ASTextLineStyleDouble) { - CGContextMoveToPoint(context, x1, y - w); - CGContextAddLineToPoint(context, x2, y - w); - CGContextStrokePath(context); - CGContextMoveToPoint(context, x1, y + w); - CGContextAddLineToPoint(context, x2, y + w); - CGContextStrokePath(context); - } + y = position.y; + } + + CGContextSetStrokeColorWithColor(context, color); + ASTextSetLinePatternInContext(style, lineWidth, position.x, context); + CGContextSetLineWidth(context, w); + if (styleBase == ASTextLineStyleSingle) { + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); + } else if (styleBase == ASTextLineStyleThick) { + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); + } else if (styleBase == ASTextLineStyleDouble) { + CGContextMoveToPoint(context, x1, y - w); + CGContextAddLineToPoint(context, x2, y - w); + CGContextStrokePath(context); + CGContextMoveToPoint(context, x1, y + w); + CGContextAddLineToPoint(context, x2, y + w); + CGContextStrokePath(context); } } CGContextRestoreGState(context); } @@ -2694,8 +2442,7 @@ static void ASTextDrawText(ASTextLayout *layout, CGContextRef context, CGSize si CGContextTranslateCTM(context, 0, size.height); CGContextScaleCTM(context, 1, -1); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; NSArray *lines = layout.lines; for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { @@ -2709,7 +2456,7 @@ static void ASTextDrawText(ASTextLayout *layout, CGContextRef context, CGSize si CTRunRef run = (CTRunRef)CFArrayGetValueAtIndex(runs, r); CGContextSetTextMatrix(context, CGAffineTransformIdentity); CGContextSetTextPosition(context, posX, posY); - ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + ASTextDrawRun(line, run, context, size, lineRunRanges[r], verticalOffset); } if (cancel && cancel()) break; } @@ -2725,8 +2472,7 @@ static void ASTextDrawBlockBorder(ASTextLayout *layout, CGContextRef context, CG CGContextSaveGState(context); CGContextTranslateCTM(context, point.x, point.y); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; NSArray *lines = layout.lines; for (NSInteger l = 0, lMax = lines.count; l < lMax; l++) { @@ -2772,18 +2518,13 @@ static void ASTextDrawBlockBorder(ASTextLayout *layout, CGContextRef context, CG } lineContinueIndex++; } while (true); - - if (isVertical) { - UIEdgeInsets insets = layout.container.insets; - unionRect.origin.y = insets.top; - unionRect.size.height = layout.container.size.height -insets.top - insets.bottom; - } else { - UIEdgeInsets insets = layout.container.insets; - unionRect.origin.x = insets.left; - unionRect.size.width = layout.container.size.width -insets.left - insets.right; - } + + UIEdgeInsets insets = layout.container.insets; + unionRect.origin.x = insets.left; + unionRect.size.width = layout.container.size.width -insets.left - insets.right; + unionRect.origin.x += verticalOffset; - ASTextDrawBorderRects(context, size, border, @[[NSValue valueWithCGRect:unionRect]], isVertical); + ASTextDrawBorderRects(context, size, border, @[[NSValue valueWithCGRect:unionRect]]); l = lineContinueIndex; break; @@ -2798,8 +2539,7 @@ static void ASTextDrawBorder(ASTextLayout *layout, CGContextRef context, CGSize CGContextSaveGState(context); CGContextTranslateCTM(context, point.x, point.y); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; NSArray *lines = layout.lines; NSString *borderKey = (type == ASTextBorderTypeNormal ? ASTextBorderAttributeName : ASTextBackgroundBorderAttributeName); @@ -2857,25 +2597,15 @@ static void ASTextDrawBorder(ASTextLayout *layout, CGContextRef context, CGSize CTRunGetPositions(iRun, CFRangeMake(0, 1), &iRunPosition); CGFloat ascent, descent; CGFloat iRunWidth = CTRunGetTypographicBounds(iRun, CFRangeMake(0, 0), &ascent, &descent, NULL); - - if (isVertical) { - ASTEXT_SWAP(iRunPosition.x, iRunPosition.y); - iRunPosition.y += iLine.position.y; - CGRect iRect = CGRectMake(verticalOffset + line.position.x - descent, iRunPosition.y, ascent + descent, iRunWidth); - if (CGRectIsNull(extLineRect)) { - extLineRect = iRect; - } else { - extLineRect = CGRectUnion(extLineRect, iRect); - } + + iRunPosition.x += iLine.position.x; + CGRect iRect = CGRectMake(iRunPosition.x, iLine.position.y - ascent, iRunWidth, ascent + descent); + if (CGRectIsNull(extLineRect)) { + extLineRect = iRect; } else { - iRunPosition.x += iLine.position.x; - CGRect iRect = CGRectMake(iRunPosition.x, iLine.position.y - ascent, iRunWidth, ascent + descent); - if (CGRectIsNull(extLineRect)) { - extLineRect = iRect; - } else { - extLineRect = CGRectUnion(extLineRect, iRect); - } + extLineRect = CGRectUnion(extLineRect, iRect); } + } if (!CGRectIsNull(extLineRect)) { @@ -2887,27 +2617,18 @@ static void ASTextDrawBorder(ASTextLayout *layout, CGContextRef context, CGSize CGRect curRect= ((NSValue *)[runRects firstObject]).CGRectValue; for (NSInteger re = 0, reMax = runRects.count; re < reMax; re++) { CGRect rect = ((NSValue *)runRects[re]).CGRectValue; - if (isVertical) { - if (fabs(rect.origin.x - curRect.origin.x) < 1) { - curRect = ASTextMergeRectInSameLine(rect, curRect, isVertical); - } else { - [drawRects addObject:[NSValue valueWithCGRect:curRect]]; - curRect = rect; - } + if (fabs(rect.origin.y - curRect.origin.y) < 1) { + curRect = ASTextMergeRectInSameLine(rect, curRect); } else { - if (fabs(rect.origin.y - curRect.origin.y) < 1) { - curRect = ASTextMergeRectInSameLine(rect, curRect, isVertical); - } else { - [drawRects addObject:[NSValue valueWithCGRect:curRect]]; - curRect = rect; - } + [drawRects addObject:[NSValue valueWithCGRect:curRect]]; + curRect = rect; } } if (!CGRectEqualToRect(curRect, CGRectZero)) { [drawRects addObject:[NSValue valueWithCGRect:curRect]]; } - ASTextDrawBorderRects(context, size, border, drawRects, isVertical); + ASTextDrawBorderRects(context, size, border, drawRects); if (l == endLineIndex) { r = endRunIndex; @@ -2930,8 +2651,7 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS CGContextSaveGState(context); CGContextTranslateCTM(context, point.x, point.y); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; CGContextTranslateCTM(context, verticalOffset, 0); for (NSUInteger l = 0, lMax = layout.lines.count; l < lMax; l++) { @@ -2969,26 +2689,15 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS CGPoint underlineStart, strikethroughStart; CGFloat length; - - if (isVertical) { - underlineStart.x = line.position.x + underlinePosition; - strikethroughStart.x = line.position.x + xHeight / 2; - - CGPoint runPosition = CGPointZero; - CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); - underlineStart.y = strikethroughStart.y = runPosition.x + line.position.y; - length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); - - } else { - underlineStart.y = line.position.y - underlinePosition; - strikethroughStart.y = line.position.y - xHeight / 2; - - CGPoint runPosition = CGPointZero; - CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); - underlineStart.x = strikethroughStart.x = runPosition.x + line.position.x; - length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); - } - + + underlineStart.y = line.position.y - underlinePosition; + strikethroughStart.y = line.position.y - xHeight / 2; + + CGPoint runPosition = CGPointZero; + CTRunGetPositions(run, CFRangeMake(0, 1), &runPosition); + underlineStart.x = strikethroughStart.x = runPosition.x + line.position.x; + length = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), NULL, NULL, NULL); + if (needDrawUnderline) { CGColorRef color = underline.color.CGColor; if (!color) { @@ -3010,12 +2719,12 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor); CGContextSetBlendMode(context, shadow.blendMode); CGContextTranslateCTM(context, offsetAlterX, 0); - ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color); } CGContextRestoreGState(context); } CGContextRestoreGState(context); shadow = shadow.subShadow; } - ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color); } if (needDrawStrikethrough) { @@ -3039,12 +2748,12 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor); CGContextSetBlendMode(context, shadow.blendMode); CGContextTranslateCTM(context, offsetAlterX, 0); - ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color, isVertical); + ASTextDrawLineStyle(context, length, thickness, underline.style, underlineStart, color); } CGContextRestoreGState(context); } CGContextRestoreGState(context); shadow = shadow.subShadow; } - ASTextDrawLineStyle(context, length, thickness, strikethrough.style, strikethroughStart, color, isVertical); + ASTextDrawLineStyle(context, length, thickness, strikethrough.style, strikethroughStart, color); } } } @@ -3053,8 +2762,7 @@ static void ASTextDrawDecoration(ASTextLayout *layout, CGContextRef context, CGS static void ASTextDrawAttachment(ASTextLayout *layout, CGContextRef context, CGSize size, CGPoint point, UIView *targetView, CALayer *targetLayer, BOOL (^cancel)(void)) { - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; for (NSUInteger i = 0, max = layout.attachments.count; i < max; i++) { ASTextAttachment *a = layout.attachments[i]; @@ -3063,26 +2771,34 @@ static void ASTextDrawAttachment(ASTextLayout *layout, CGContextRef context, CGS UIImage *image = nil; UIView *view = nil; CALayer *layer = nil; + ASDisplayNode *node = nil; if ([a.content isKindOfClass:[UIImage class]]) { image = a.content; } else if ([a.content isKindOfClass:[UIView class]]) { view = a.content; } else if ([a.content isKindOfClass:[CALayer class]]) { layer = a.content; + } else if ([a.content isKindOfClass:[ASDisplayNode class]]) { + node = a.content; } - if (!image && !view && !layer) continue; - if (image && !context) continue; + if (!image && !view && !layer && !node) continue; + if ((image || node) && !context) continue; if (view && !targetView) continue; if (layer && !targetLayer) continue; if (cancel && cancel()) break; - + if (!image && node) { + ASNetworkImageNode *networkImage = ASDynamicCast(node, ASNetworkImageNode); + if ([networkImage animatedImage]) { + // Need to check first because [networkImage defaultImage] can return coverImage for + // animated image. + image = [[UIImage alloc] initWithCGImage:(CGImageRef)node.contents]; + } else { + image = [networkImage image] ?: [networkImage defaultImage]; + } + } CGSize asize = image ? image.size : view ? view.frame.size : layer.frame.size; CGRect rect = ((NSValue *)layout.attachmentRects[i]).CGRectValue; - if (isVertical) { - rect = UIEdgeInsetsInsetRect(rect, UIEdgeInsetRotateVertical(a.contentInsets)); - } else { - rect = UIEdgeInsetsInsetRect(rect, a.contentInsets); - } + rect = UIEdgeInsetsInsetRect(rect, a.contentInsets); rect = ASTextCGRectFitWithContentMode(rect, asize, a.contentMode); rect = ASTextCGRectPixelRound(rect); rect = CGRectStandardize(rect); @@ -3111,8 +2827,7 @@ static void ASTextDrawShadow(ASTextLayout *layout, CGContextRef context, CGSize //move out of context. (0xFFFF is just a random large number) CGFloat offsetAlterX = size.width + 0xFFFF; - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; CGContextSaveGState(context); { CGContextTranslateCTM(context, point.x, point.y); @@ -3149,7 +2864,7 @@ static void ASTextDrawShadow(ASTextLayout *layout, CGContextRef context, CGSize CGContextSetShadowWithColor(context, offset, shadow.radius, shadow.color.CGColor); CGContextSetBlendMode(context, shadow.blendMode); CGContextTranslateCTM(context, offsetAlterX, 0); - ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + ASTextDrawRun(line, run, context, size, lineRunRanges[r], verticalOffset); } CGContextRestoreGState(context); shadow = shadow.subShadow; } @@ -3165,8 +2880,7 @@ static void ASTextDrawInnerShadow(ASTextLayout *layout, CGContextRef context, CG CGContextScaleCTM(context, 1, -1); CGContextSetTextMatrix(context, CGAffineTransformIdentity); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; NSArray *lines = layout.lines; for (NSUInteger l = 0, lMax = lines.count; l < lMax; l++) { @@ -3217,7 +2931,7 @@ static void ASTextDrawInnerShadow(ASTextLayout *layout, CGContextRef context, CG CGContextFillRect(context, runImageBounds); CGContextSetBlendMode(context, kCGBlendModeDestinationIn); CGContextBeginTransparencyLayer(context, NULL); { - ASTextDrawRun(line, run, context, size, isVertical, lineRunRanges[r], verticalOffset); + ASTextDrawRun(line, run, context, size, lineRunRanges[r], verticalOffset); } CGContextEndTransparencyLayer(context); } CGContextEndTransparencyLayer(context); } CGContextEndTransparencyLayer(context); @@ -3239,8 +2953,7 @@ static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize s CGContextSetLineJoin(context, kCGLineJoinMiter); CGContextSetLineCap(context, kCGLineCapButt); - BOOL isVertical = layout.container.verticalForm; - CGFloat verticalOffset = isVertical ? (size.width - layout.container.size.width) : 0; + CGFloat verticalOffset = 0; CGContextTranslateCTM(context, verticalOffset, 0); if (op.CTFrameBorderColor || op.CTFrameFillColor) { @@ -3316,28 +3029,19 @@ static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize s } if (op.baselineColor) { [op.baselineColor setStroke]; - if (isVertical) { - CGFloat x = ASTextCGFloatPixelHalf(line.position.x); - CGFloat y1 = ASTextCGFloatPixelHalf(line.top); - CGFloat y2 = ASTextCGFloatPixelHalf(line.bottom); - CGContextMoveToPoint(context, x, y1); - CGContextAddLineToPoint(context, x, y2); - CGContextStrokePath(context); - } else { - CGFloat x1 = ASTextCGFloatPixelHalf(lineBounds.origin.x); - CGFloat x2 = ASTextCGFloatPixelHalf(lineBounds.origin.x + lineBounds.size.width); - CGFloat y = ASTextCGFloatPixelHalf(line.position.y); - CGContextMoveToPoint(context, x1, y); - CGContextAddLineToPoint(context, x2, y); - CGContextStrokePath(context); - } + CGFloat x1 = ASTextCGFloatPixelHalf(lineBounds.origin.x); + CGFloat x2 = ASTextCGFloatPixelHalf(lineBounds.origin.x + lineBounds.size.width); + CGFloat y = ASTextCGFloatPixelHalf(line.position.y); + CGContextMoveToPoint(context, x1, y); + CGContextAddLineToPoint(context, x2, y); + CGContextStrokePath(context); } if (op.CTLineNumberColor) { [op.CTLineNumberColor set]; NSMutableAttributedString *num = [[NSMutableAttributedString alloc] initWithString:@(l).description]; num.as_color = op.CTLineNumberColor; num.as_font = [UIFont systemFontOfSize:6]; - [num drawAtPoint:CGPointMake(line.position.x, line.position.y - (isVertical ? 1 : 6))]; + [num drawAtPoint:CGPointMake(line.position.x, line.position.y - 6)]; } if (op.CTRunFillColor || op.CTRunBorderColor || op.CTRunNumberColor || op.CGGlyphFillColor || op.CGGlyphBorderColor) { CFArrayRef runs = CTLineGetGlyphRuns(line.CTLine); @@ -3353,24 +3057,14 @@ static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize s CTRunGetAdvances(run, CFRangeMake(0, glyphCount), glyphAdvances); CGPoint runPosition = glyphPositions[0]; - if (isVertical) { - ASTEXT_SWAP(runPosition.x, runPosition.y); - runPosition.x = line.position.x; - runPosition.y += line.position.y; - } else { - runPosition.x += line.position.x; - runPosition.y = line.position.y - runPosition.y; - } + runPosition.x += line.position.x; + runPosition.y = line.position.y - runPosition.y; CGFloat ascent, descent, leading; CGFloat width = CTRunGetTypographicBounds(run, CFRangeMake(0, 0), &ascent, &descent, &leading); CGRect runTypoBounds; - if (isVertical) { - runTypoBounds = CGRectMake(runPosition.x - descent, runPosition.y, ascent + descent, width); - } else { - runTypoBounds = CGRectMake(runPosition.x, line.position.y - ascent, width, ascent + descent); - } - + runTypoBounds = CGRectMake(runPosition.x, line.position.y - ascent, width, ascent + descent); + if (op.CTRunFillColor) { [op.CTRunFillColor setFill]; CGContextAddRect(context, ASTextCGRectPixelRound(runTypoBounds)); @@ -3393,16 +3087,10 @@ static void ASTextDrawDebug(ASTextLayout *layout, CGContextRef context, CGSize s CGPoint pos = glyphPositions[g]; CGSize adv = glyphAdvances[g]; CGRect rect; - if (isVertical) { - ASTEXT_SWAP(pos.x, pos.y); - pos.x = runPosition.x; - pos.y += line.position.y; - rect = CGRectMake(pos.x - descent, pos.y, runTypoBounds.size.width, adv.width); - } else { - pos.x += line.position.x; - pos.y = runPosition.y; - rect = CGRectMake(pos.x, pos.y - ascent, adv.width, runTypoBounds.size.height); - } + pos.x += line.position.x; + pos.y = runPosition.y; + rect = CGRectMake(pos.x, pos.y - ascent, adv.width, runTypoBounds.size.height); + if (op.CGGlyphFillColor) { [op.CGGlyphFillColor setFill]; CGContextAddRect(context, ASTextCGRectPixelRound(rect)); @@ -3481,3 +3169,15 @@ - (void)drawInContext:(CGContextRef)context } @end + +NSAttributedString *fillBaseAttributes(NSAttributedString *str, NSDictionary *attrs) { + NSUInteger len = str.length; + if (!len) return str; + NSMutableAttributedString *m_result; // Do not create unless needed. + for (NSString *name in attrs) { + if ([str as_hasAttribute:name]) continue; + if (!m_result) m_result = [str mutableCopy]; + [m_result addAttribute:name value:attrs[name] range:NSMakeRange(0, len)]; + } + return m_result ?: str; +} diff --git a/Source/TextExperiment/String/ASTextAttribute.h b/Source/TextExperiment/String/ASTextAttribute.h index 571334ed3..e142bee00 100644 --- a/Source/TextExperiment/String/ASTextAttribute.h +++ b/Source/TextExperiment/String/ASTextAttribute.h @@ -48,9 +48,10 @@ typedef NS_OPTIONS (NSInteger, ASTextLineStyle) { Text vertical alignment. */ typedef NS_ENUM(NSInteger, ASTextVerticalAlignment) { - ASTextVerticalAlignmentTop = 0, ///< Top alignment. - ASTextVerticalAlignmentCenter = 1, ///< Center alignment. - ASTextVerticalAlignmentBottom = 2, ///< Bottom alignment. + ASTextVerticalAlignmentTop = 0, ///< Top alignment. + ASTextVerticalAlignmentCenter = 1, ///< Center alignment. + ASTextVerticalAlignmentBottom = 2, ///< Bottom alignment. + ASTextVerticalAlignmentBaseline = 3, ///< Baseline alignment. }; /** @@ -270,7 +271,9 @@ typedef void(^ASTextAction)(UIView *containerView, NSAttributedString *text, NSR */ @interface ASTextAttachment : NSObject + (instancetype)attachmentWithContent:(nullable id)content NS_RETURNS_RETAINED; -@property (nullable, nonatomic) id content; ///< Supported type: UIImage, UIView, CALayer + +@property(nullable, nonatomic) + id content; ///< Supported type: UIImage, UIView, CALayer, ASDisplayNode @property (nonatomic) UIViewContentMode contentMode; ///< Content display mode. @property (nonatomic) UIEdgeInsets contentInsets; ///< The insets when drawing content. @property (nullable, nonatomic) NSDictionary *userInfo; ///< The user information dictionary. diff --git a/Source/TextExperiment/String/ASTextAttribute.mm b/Source/TextExperiment/String/ASTextAttribute.mm index 8af432271..516bff9d6 100644 --- a/Source/TextExperiment/String/ASTextAttribute.mm +++ b/Source/TextExperiment/String/ASTextAttribute.mm @@ -9,6 +9,8 @@ #import "ASTextAttribute.h" #import +#import +#import #import NSString *const ASTextBackedStringAttributeName = @"ASTextBackedString"; @@ -365,6 +367,10 @@ - (id)copyWithZone:(NSZone *)zone { return one; } +- (void)dealloc { + ASPerformMainThreadDeallocation(&_userInfo); +} + @end diff --git a/Source/TextExperiment/Utility/NSAttributedString+ASText.h b/Source/TextExperiment/Utility/NSAttributedString+ASText.h index ef44fb4f3..f0d2adddc 100644 --- a/Source/TextExperiment/Utility/NSAttributedString+ASText.h +++ b/Source/TextExperiment/Utility/NSAttributedString+ASText.h @@ -29,6 +29,11 @@ NS_ASSUME_NONNULL_BEGIN */ @property (nullable, nonatomic, copy, readonly) NSDictionary *as_attributes; +/** + Returns the attributes at the first character as Core Text attributes if NS attributes. + */ +@property (nullable, nonatomic, copy, readonly) NSDictionary *as_ctAttributes; + /** Returns the attributes for the character at a given index. @@ -597,10 +602,10 @@ NS_ASSUME_NONNULL_BEGIN /** Creates and returns an attachment. - - + + Example: ContentMode:bottom Alignment:Top. - + The text The attachment holder ↓ ↓ ─────────┌──────────────────────┐─────── @@ -613,21 +618,25 @@ NS_ASSUME_NONNULL_BEGIN │ ██████████████ ←───────────────── The attachment content │ ██████████████ │ └──────────────────────┘ - + @param content The attachment (UIImage/UIView/CALayer). @param contentMode The attachment's content mode in attachment holder @param attachmentSize The attachment holder's size in text layout. @param font The attachment will align to this font. @param alignment The attachment holder's alignment to text line. - + @param contentInset The attachment's contentInset. + @param userInfo The infomation associated with the attachment. + @return An attributed string, or nil if an error occurs. @since ASText:6.0 */ -+ (NSMutableAttributedString *)as_attachmentStringWithContent:(nullable id)content ++ (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content contentMode:(UIViewContentMode)contentMode attachmentSize:(CGSize)attachmentSize alignToFont:(UIFont *)font - alignment:(ASTextVerticalAlignment)alignment; + alignment:(ASTextVerticalAlignment)alignment + contentInsets:(UIEdgeInsets)contentInsets + userInfo:(NSDictionary *)userInfo; /** Creates and returns an attahment from a fourquare image as if it was an emoji. diff --git a/Source/TextExperiment/Utility/NSAttributedString+ASText.mm b/Source/TextExperiment/Utility/NSAttributedString+ASText.mm index 39f628151..01f8b4699 100644 --- a/Source/TextExperiment/Utility/NSAttributedString+ASText.mm +++ b/Source/TextExperiment/Utility/NSAttributedString+ASText.mm @@ -37,6 +37,39 @@ - (NSDictionary *)as_attributes { return [self as_attributesAtIndex:0]; } +- (NSDictionary *)as_ctAttributes { + NSDictionary *attributes = self.as_attributes; + if (attributes == nil) { + return nil; + } + + NSMutableDictionary *mutableCTAttributes = [[NSMutableDictionary alloc] initWithCapacity:attributes.count]; + + // Map for NS attributes that are not mapping cleanly to CT attributes + static NSDictionary *NSToCTAttributeNamesMap = nil; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSToCTAttributeNamesMap = @{ + NSFontAttributeName: (NSString *)kCTFontAttributeName, + NSBackgroundColorAttributeName: (NSString *)kCTBackgroundColorAttributeName, + NSForegroundColorAttributeName: (NSString *)kCTForegroundColorAttributeName, + NSUnderlineColorAttributeName: (NSString *)kCTUnderlineColorAttributeName, + NSUnderlineStyleAttributeName: (NSString *)kCTUnderlineStyleAttributeName, + NSStrokeWidthAttributeName: (NSString *)kCTStrokeWidthAttributeName, + NSStrokeColorAttributeName: (NSString *)kCTStrokeColorAttributeName, + NSKernAttributeName: (NSString *)kCTKernAttributeName, + NSLigatureAttributeName: (NSString *)kCTLigatureAttributeName + }; + }); + + [attributes enumerateKeysAndObjectsUsingBlock:^(id key, id value, BOOL *stop) { + key = NSToCTAttributeNamesMap[key] ?: key; + [mutableCTAttributes setObject:value forKey:key]; + }]; + + return [mutableCTAttributes copy]; +} + - (UIFont *)as_font { return [self as_fontAtIndex:0]; } @@ -470,14 +503,18 @@ + (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content contentMode:(UIViewContentMode)contentMode attachmentSize:(CGSize)attachmentSize alignToFont:(UIFont *)font - alignment:(ASTextVerticalAlignment)alignment { + alignment:(ASTextVerticalAlignment)alignment + contentInsets:(UIEdgeInsets)contentInsets + userInfo:(NSDictionary *)userInfo { NSMutableAttributedString *atr = [[NSMutableAttributedString alloc] initWithString:ASTextAttachmentToken]; - + ASTextAttachment *attach = [ASTextAttachment new]; attach.content = content; attach.contentMode = contentMode; + attach.userInfo = userInfo; + attach.contentInsets = contentInsets; [atr as_setTextAttachment:attach range:NSMakeRange(0, atr.length)]; - + ASTextRunDelegate *delegate = [ASTextRunDelegate new]; delegate.width = attachmentSize.width; switch (alignment) { @@ -507,16 +544,17 @@ + (NSMutableAttributedString *)as_attachmentStringWithContent:(id)content delegate.descent = attachmentSize.height; } } break; + case ASTextVerticalAlignmentBaseline: default: { delegate.ascent = attachmentSize.height; delegate.descent = 0; - } break; + } } - + CTRunDelegateRef delegateRef = delegate.CTRunDelegate; [atr as_setRunDelegate:delegateRef range:NSMakeRange(0, atr.length)]; if (delegate) CFRelease(delegateRef); - + return atr; } @@ -953,29 +991,30 @@ - (void)as_setParagraphStyle:(NSParagraphStyle *)paragraphStyle range:(NSRange)r [self as_setAttribute:NSParagraphStyleAttributeName value:paragraphStyle range:range]; } -#define ParagraphStyleSet(_attr_) \ -[self enumerateAttribute:NSParagraphStyleAttributeName \ -inRange:range \ -options:kNilOptions \ -usingBlock: ^(NSParagraphStyle *value, NSRange subRange, BOOL *stop) { \ -NSMutableParagraphStyle *style = nil; \ -if (value) { \ -if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \ -value = [NSParagraphStyle as_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \ -} \ -if (value. _attr_ == _attr_) return; \ -if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \ -style = (id)value; \ -} else { \ -style = value.mutableCopy; \ -} \ -} else { \ -if ([NSParagraphStyle defaultParagraphStyle]. _attr_ == _attr_) return; \ -style = [NSParagraphStyle defaultParagraphStyle].mutableCopy; \ -} \ -style. _attr_ = _attr_; \ -[self as_setParagraphStyle:style range:subRange]; \ -}]; +#define ParagraphStyleSet(_attr_) \ + [self enumerateAttribute:NSParagraphStyleAttributeName \ + inRange:range \ + options:NSAttributedStringEnumerationLongestEffectiveRangeNotRequired \ + usingBlock:^(NSParagraphStyle * value, NSRange subRange, BOOL * stop) { \ + NSMutableParagraphStyle *style = nil; \ + if (value) { \ + if (CFGetTypeID((__bridge CFTypeRef)(value)) == CTParagraphStyleGetTypeID()) { \ + value = [NSParagraphStyle \ + as_styleWithCTStyle:(__bridge CTParagraphStyleRef)(value)]; \ + } \ + if (value._attr_ == _attr_) return; \ + if ([value isKindOfClass:[NSMutableParagraphStyle class]]) { \ + style = (id)value; \ + } else { \ + style = [value mutableCopy]; \ + } \ + } else { \ + if ([NSParagraphStyle defaultParagraphStyle]._attr_ == _attr_) return; \ + style = [[NSMutableParagraphStyle alloc] init]; \ + } \ + style._attr_ = _attr_; \ + [self as_setParagraphStyle:style range:subRange]; \ + }]; - (void)as_setAlignment:(NSTextAlignment)alignment range:(NSRange)range { ParagraphStyleSet(alignment); diff --git a/Source/TextExperiment/Utility/NSParagraphStyle+ASText.mm b/Source/TextExperiment/Utility/NSParagraphStyle+ASText.mm index bc19fd234..220bd38a7 100644 --- a/Source/TextExperiment/Utility/NSParagraphStyle+ASText.mm +++ b/Source/TextExperiment/Utility/NSParagraphStyle+ASText.mm @@ -20,7 +20,7 @@ @implementation NSParagraphStyle (ASText) + (NSParagraphStyle *)as_styleWithCTStyle:(CTParagraphStyleRef)CTStyle { if (CTStyle == NULL) return nil; - NSMutableParagraphStyle *style = [[NSParagraphStyle defaultParagraphStyle] mutableCopy]; + NSMutableParagraphStyle *style = [[NSMutableParagraphStyle alloc] init]; #if TARGET_OS_IOS #pragma clang diagnostic push diff --git a/Source/TextKit/ASTextKitContext.mm b/Source/TextKit/ASTextKitContext.mm index d0b6708fd..045f9d99e 100644 --- a/Source/TextKit/ASTextKitContext.mm +++ b/Source/TextKit/ASTextKitContext.mm @@ -38,13 +38,13 @@ - (instancetype)initWithAttributedString:(NSAttributedString *)attributedString BOOL useGlobalTextKitLock = !ASActivateExperimentalFeature(ASExperimentalDisableGlobalTextkitLock); if (useGlobalTextKitLock) { - // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. - dispatch_once(&onceToken, ^{ - mutex = new AS::Mutex(); - }); - if (mutex != NULL) { - mutex->lock(); - } + // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. + dispatch_once(&onceToken, ^{ + mutex = new AS::Mutex(); + }); + if (mutex != NULL) { + mutex->lock(); + } } __instanceLock__ = std::make_shared(); diff --git a/Source/UIImage+ASConvenience.mm b/Source/UIImage+ASConvenience.mm index cc5896b4f..c127bb220 100644 --- a/Source/UIImage+ASConvenience.mm +++ b/Source/UIImage+ASConvenience.mm @@ -9,6 +9,7 @@ #import #import +#import #pragma mark - ASDKFastImageNamed @@ -154,36 +155,64 @@ + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius scale:(CGFloat)scale traitCollection:(ASPrimitiveTraitCollection) traitCollection NS_RETURNS_RETAINED { - static NSCache *__pathCache = nil; + static NSCache *imageCache; + static pthread_key_t threadKey; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ - __pathCache = [[NSCache alloc] init]; - // UIBezierPath objects are fairly small and these are equally sized. 20 should be plenty for many different parameters. - __pathCache.countLimit = 20; + ASInitializeTemporaryObjectStorage(&threadKey); + imageCache = [[NSCache alloc] init]; + imageCache.name = @"Texture.roundedImageCache"; }); - + // Treat clear background color as no background color if ([cornerColor isEqual:[UIColor clearColor]]) { cornerColor = nil; } - - CGFloat dimension = (cornerRadius * 2) + 1; - CGRect bounds = CGRectMake(0, 0, dimension, dimension); - + typedef struct { - UIRectCorner corners; - CGFloat radius; - } PathKey; - PathKey key = { roundedCorners, cornerRadius }; - NSValue *pathKeyObject = [[NSValue alloc] initWithBytes:&key objCType:@encode(PathKey)]; + CGFloat cornerRadius; + CGFloat cornerColor[4]; + CGFloat fillColor[4]; + CGFloat borderColor[4]; + CGFloat borderWidth; + UIRectCorner roundedCorners; + CGFloat scale; + } CacheKey; - CGSize cornerRadii = CGSizeMake(cornerRadius, cornerRadius); - UIBezierPath *path = [__pathCache objectForKey:pathKeyObject]; - if (path == nil) { - path = [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:roundedCorners cornerRadii:cornerRadii]; - [__pathCache setObject:path forKey:pathKeyObject]; + CFMutableDataRef keyBuffer = ASGetTemporaryMutableData(threadKey, sizeof(CacheKey)); + CacheKey *key = (CacheKey *)CFDataGetMutableBytePtr(keyBuffer); + if (!key) { + ASDisplayNodeFailAssert(@"Failed to get byte pointer. Data: %@", keyBuffer); + return [[UIImage alloc] init]; } - + key->cornerRadius = cornerRadius; + [cornerColor getRed:&key->cornerColor[0] + green:&key->cornerColor[1] + blue:&key->cornerColor[2] + alpha:&key->cornerColor[3]]; + [fillColor getRed:&key->fillColor[0] + green:&key->fillColor[1] + blue:&key->fillColor[2] + alpha:&key->fillColor[3]]; + [borderColor getRed:&key->borderColor[0] + green:&key->borderColor[1] + blue:&key->borderColor[2] + alpha:&key->borderColor[3]]; + key->borderWidth = borderWidth; + key->roundedCorners = roundedCorners; + key->scale = scale; + + if (UIImage *cached = [imageCache objectForKey:(__bridge id)keyBuffer]) { + return cached; + } + CGFloat capInset = MAX(borderWidth, cornerRadius); + NSAssert(capInset >= 0, @"borderWidth and cornerRadius must >=0"); + CGFloat dimension = (capInset * 2) + 1; + CGRect bounds = CGRectMake(0, 0, dimension, dimension); + + CGSize cornerRadii = CGSizeMake(cornerRadius, cornerRadius); + UIBezierPath *path = [UIBezierPath bezierPathWithRoundedRect:bounds byRoundingCorners:roundedCorners cornerRadii:cornerRadii]; + // We should probably check if the background color has any alpha component but that // might be expensive due to needing to check mulitple color spaces. UIImage *result = ASGraphicsCreateImage(traitCollection, bounds.size, cornerColor != nil, scale, nil, nil, ^{ @@ -205,20 +234,41 @@ + (UIImage *)as_resizableRoundedImageWithCornerRadius:(CGFloat)cornerRadius // Inset border fully inside filled path (not halfway on each side of path) CGRect strokeRect = CGRectInset(bounds, borderWidth / 2.0, borderWidth / 2.0); - // It is rarer to have a stroke path, and our cache key only handles rounded rects for the exact-stretchable - // size calculated by cornerRadius, so we won't bother caching this path. Profiling validates this decision. - UIBezierPath *strokePath = [UIBezierPath bezierPathWithRoundedRect:strokeRect - byRoundingCorners:roundedCorners - cornerRadii:cornerRadii]; + UIBezierPath *strokePath; + if (cornerRadius == 0) { + // When cornerRadii is CGSizeZero, the stroke result will have extra square on top left + // that is not covered using bezierPathWithRoundedRect:byRoundingCorners:cornerRadii:. + // Seems a bug from iOS runtime. + strokePath = [UIBezierPath bezierPathWithRect:strokeRect]; + } else { + strokePath = [UIBezierPath bezierPathWithRoundedRect:strokeRect + byRoundingCorners:roundedCorners + cornerRadii:cornerRadii]; + } [strokePath setLineWidth:borderWidth]; BOOL canUseCopy = (CGColorGetAlpha(borderColor.CGColor) == 1); [strokePath strokeWithBlendMode:(canUseCopy ? kCGBlendModeCopy : kCGBlendModeNormal) alpha:1]; } + // Refill the center area with fillColor since it may be contaminated by the sub pixel + // rendering. + if (borderWidth > 0) { + CGRect rect = CGRectMake(capInset, capInset, 1, 1); + CGContextRef context = UIGraphicsGetCurrentContext(); + CGContextClearRect(context, rect); + CGContextSetFillColorWithColor(context, [fillColor CGColor]); + CGContextFillRect(context, rect); + } }); - - UIEdgeInsets capInsets = UIEdgeInsetsMake(cornerRadius, cornerRadius, cornerRadius, cornerRadius); + + UIEdgeInsets capInsets = UIEdgeInsetsMake(capInset, capInset, capInset, capInset); result = [result resizableImageWithCapInsets:capInsets resizingMode:UIImageResizingModeStretch]; - + + // Be sure to copy keyBuffer when inserting to cache. + if (CFDataRef copiedKey = CFDataCreateCopy(NULL, keyBuffer)) { + [imageCache setObject:result forKey:(__bridge_transfer id)copiedKey]; + } else { + ASDisplayNodeFailAssert(@"Failed to copy key: %@", keyBuffer); + } return result; } diff --git a/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.m b/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.m index edf939b7c..237c2914b 100644 --- a/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.m +++ b/SubspecWorkspaces/ASDKListKit/ASDKListKitTests/ASListTestSection.m @@ -1,5 +1,5 @@ // -// ASListTestSection.m +// ASTestListSection.m // Texture // // Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. diff --git a/Tests/ASAbsoluteLayoutSpecSnapshotTests.mm b/Tests/ASAbsoluteLayoutSpecSnapshotTests.mm index a1b9b9459..3b646e61a 100644 --- a/Tests/ASAbsoluteLayoutSpecSnapshotTests.mm +++ b/Tests/ASAbsoluteLayoutSpecSnapshotTests.mm @@ -17,6 +17,8 @@ @interface ASAbsoluteLayoutSpecSnapshotTests : ASLayoutSpecSnapshotTestCase @implementation ASAbsoluteLayoutSpecSnapshotTests +#if !YOGA + - (void)testSizingBehaviour { [self testWithSizeRange:ASSizeRangeMake(CGSizeMake(150, 200), CGSizeMake(INFINITY, INFINITY)) @@ -67,4 +69,6 @@ - (void)testWithChildren:(NSArray *)children sizeRange:(ASSizeRange)sizeRange id [self testLayoutSpec:layoutSpec sizeRange:sizeRange subnodes:subnodes identifier:identifier]; } +#endif // !YOGA + @end diff --git a/Tests/ASBridgedPropertiesTests.mm b/Tests/ASBridgedPropertiesTests.mm index 1034cc3a7..907afebf6 100644 --- a/Tests/ASBridgedPropertiesTests.mm +++ b/Tests/ASBridgedPropertiesTests.mm @@ -155,7 +155,7 @@ - (void)testThatSettingTintColorSetNeedsDisplayOnView #if AS_AT_LEAST_IOS13 // This is called an extra time on iOS13 for unknown reasons. Need to Investigate. if (@available(iOS 13.0, *)) { - XCTAssertEqual(initialSetNeedsDisplayCount, 2); + XCTAssertGreaterThanOrEqual(initialSetNeedsDisplayCount, 1); } else { XCTAssertEqual(initialSetNeedsDisplayCount, 1); } diff --git a/Tests/ASButtonNodeSnapshotTests.mm b/Tests/ASButtonNodeSnapshotTests.mm index 26ab279f1..b0541ebfe 100644 --- a/Tests/ASButtonNodeSnapshotTests.mm +++ b/Tests/ASButtonNodeSnapshotTests.mm @@ -16,6 +16,8 @@ @interface ASButtonNodeSnapshotTests : ASSnapshotTestCase @implementation ASButtonNodeSnapshotTests +#ifndef YOGA + - (void)setUp { [super setUp]; @@ -111,5 +113,6 @@ - (void)testTintColorWithInheritedTintColor __unused UIView *v2 = container2.view; // Force load ASSnapshotVerifyNode(node, @"green_inherited_tint"); } +#endif @end diff --git a/Tests/ASCollectionViewTests.mm b/Tests/ASCollectionViewTests.mm index af0230c5d..ad5c1097e 100644 --- a/Tests/ASCollectionViewTests.mm +++ b/Tests/ASCollectionViewTests.mm @@ -171,6 +171,7 @@ - (instancetype)initWithNibName:(NSString *)nibNameOrNil bundle:(NSBundle *)nibB @interface ASCollectionView (InternalTesting) - (NSArray *)dataController:(ASDataController *)dataController supplementaryNodeKindsInSections:(NSIndexSet *)sections; +- (BOOL)dataController:(ASDataController *)dataController shouldSynchronouslyProcessChangeSet:(_ASHierarchyChangeSet *)changeSet; @end @@ -1131,7 +1132,7 @@ - (void)_primitiveBatchFetchingFillTestAnimated:(BOOL)animated visible:(BOOL)vis [window makeKeyAndVisible]; // Trigger the initial reload to start [view layoutIfNeeded]; - + // Wait for ASDK reload to finish [cn waitUntilAllUpdatesAreProcessed]; // Force UIKit to read updated data & range controller to update and account for it @@ -1153,18 +1154,15 @@ - (void)_primitiveBatchFetchingFillTestAnimated:(BOOL)animated visible:(BOOL)vis - (void)testInitialRangeBounds { - [self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeNone - shouldWaitUntilAllUpdatesAreProcessed:YES]; + [self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeNone]; } - (void)testInitialRangeBoundsCellLayoutModeAlwaysAsync { - [self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeAlwaysAsync - shouldWaitUntilAllUpdatesAreProcessed:YES]; + [self testInitialRangeBoundsWithCellLayoutMode:ASCellLayoutModeAlwaysAsync]; } - (void)testInitialRangeBoundsWithCellLayoutMode:(ASCellLayoutMode)cellLayoutMode - shouldWaitUntilAllUpdatesAreProcessed:(BOOL)shouldWait { UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; ASCollectionViewTestController *testController = [[ASCollectionViewTestController alloc] initWithNibName:nil bundle:nil]; @@ -1174,13 +1172,14 @@ - (void)testInitialRangeBoundsWithCellLayoutMode:(ASCellLayoutMode)cellLayoutMod window.rootViewController = testController; [testController.collectionNode.collectionViewLayout invalidateLayout]; - [testController.collectionNode.collectionViewLayout prepareLayout]; + // Trigger the initial reload to start [window makeKeyAndVisible]; - // Trigger the initial reload to start [window layoutIfNeeded]; - if (shouldWait) { + // Test the APIs that monitor ASCollectionNode update handling if collection node should + // layout asynchronously + if (![cn.view dataController:nil shouldSynchronouslyProcessChangeSet:nil]) { XCTAssertTrue(cn.isProcessingUpdates, @"ASCollectionNode should still be processing updates after initial layoutIfNeeded call (reloadData)"); [cn onDidFinishProcessingUpdates:^{ diff --git a/Tests/ASCollectionViewThrashTests.mm b/Tests/ASCollectionViewThrashTests.mm index b609cb2ba..6dc6174cc 100644 --- a/Tests/ASCollectionViewThrashTests.mm +++ b/Tests/ASCollectionViewThrashTests.mm @@ -69,14 +69,14 @@ - (void)verifyDataSource:(ASThrashDataSource *)ds #pragma mark Test Methods -- (void)testInitialDataRead +- (void)disabled_testInitialDataRead { ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; [self verifyDataSource:ds]; } /// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file -- (void)testRecordedThrashCase +- (void)disabled_testRecordedThrashCase { NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; @@ -94,7 +94,7 @@ - (void)testRecordedThrashCase [self verifyDataSource:ds]; } -- (void)testThrashingWildly +- (void)disabled_testThrashingWildly { for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; @@ -116,7 +116,7 @@ - (void)testThrashingWildly } } -- (void)testThrashingWildlyOnSameCollectionView +- (void)disabled_testThrashingWildlyOnSameCollectionView { XCTestExpectation *expectation = [self expectationWithDescription:@"last test ran"]; ASThrashDataSource *ds = [[ASThrashDataSource alloc] initCollectionViewDataSourceWithData:nil]; @@ -143,7 +143,7 @@ - (void)testThrashingWildlyOnSameCollectionView [self waitForExpectationsWithTimeout:3 handler:nil]; } -- (void)testThrashingWildlyDispatchWildly +- (void)disabled_testThrashingWildlyDispatchWildly { XCTestExpectation *expectation = [self expectationWithDescription:@"last test ran"]; for (NSInteger i = 0; i < kThrashingIterationCount; i++) { diff --git a/Tests/ASConfigurationTests.mm b/Tests/ASConfigurationTests.mm index 6bd23e0f4..d0b491d80 100644 --- a/Tests/ASConfigurationTests.mm +++ b/Tests/ASConfigurationTests.mm @@ -17,20 +17,36 @@ static ASExperimentalFeatures features[] = { #if AS_ENABLE_TEXTNODE - ASExperimentalTextNode, + ASExperimentalTextNode, #endif - ASExperimentalInterfaceStateCoalescing, - ASExperimentalLayerDefaults, - ASExperimentalCollectionTeardown, - ASExperimentalFramesetterCache, - ASExperimentalSkipClearData, - ASExperimentalDidEnterPreloadSkipASMLayout, - ASExperimentalDispatchApply, - ASExperimentalDrawingGlobal, - ASExperimentalOptimizeDataControllerPipeline, - ASExperimentalDisableGlobalTextkitLock, - ASExperimentalMainThreadOnlyDataController, - ASExperimentalRangeUpdateOnChangesetUpdate, + ASExperimentalInterfaceStateCoalescing, + ASExperimentalUnfairLock, + ASExperimentalLayerDefaults, + ASExperimentalCollectionTeardown, + ASExperimentalFramesetterCache, + ASExperimentalSkipClearData, + ASExperimentalDidEnterPreloadSkipASMLayout, + ASExperimentalDispatchApply, + ASExperimentalOOMBackgroundDeallocDisable, + ASExperimentalRemoveTextKitInitialisingLock, + ASExperimentalDrawingGlobal, + ASExperimentalDeferredNodeRelease, + ASExperimentalFasterWebPDecoding, + ASExperimentalFasterWebPGraphicsImageRenderer, + ASExperimentalAnimatedWebPNoCache, + ASExperimentalDeallocElementMapOffMain, + ASExperimentalUnifiedYogaTree, + ASExperimentalCoalesceRootNodeInTransaction, + ASExperimentalUseNonThreadLocalArrayWhenApplyingLayout, + ASExperimentalOptimizeDataControllerPipeline, + ASExperimentalTraitCollectionDidChangeWithPreviousCollection, + ASExperimentalFillTemplateImagesWithTintColor, + ASExperimentalDoNotCacheAccessibilityElements, + ASExperimentalDisableGlobalTextkitLock, + ASExperimentalMainThreadOnlyDataController, + ASExperimentalEnableNodeIsHiddenFromAcessibility, + ASExperimentalEnableAcessibilityElementsReturnNil, + ASExperimentalRangeUpdateOnChangesetUpdate, }; @interface ASConfigurationTests : ASTestCase @@ -45,17 +61,33 @@ + (NSArray *)names { return @[ @"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_dispatch_apply", + @"exp_oom_bg_dealloc_disable", + @"exp_remove_textkit_initialising_lock", @"exp_drawing_global", + @"exp_deferred_node_release", + @"exp_faster_webp_decoding", + @"exp_faster_webp_graphics_image_renderer", + @"exp_animated_webp_no_cache", + @"exp_dealloc_element_map_off_main", + @"exp_unified_yoga_tree", + @"exp_coalesce_root_node_in_transaction", + @"exp_use_non_tls_array", @"exp_optimize_data_controller_pipeline", + @"exp_trait_collection_did_change_with_previous_collection", + @"exp_fill_template_images_with_tint_color", + @"exp_do_not_cache_accessibility_elements", @"exp_disable_global_textkit_lock", @"exp_main_thread_only_data_controller", - @"exp_range_update_on_changeset_update" + @"exp_enable_node_is_hidden_from_accessibility", + @"exp_enable_accessibility_elements_return_nil", + @"exp_range_update_on_changeset_update", ]; } @@ -108,9 +140,10 @@ - (void)testMappingNamesToFlags { // Throw in a bad bit. ASExperimentalFeatures allFeatures = [self allFeatures]; - ASExperimentalFeatures featuresWithBadBit = allFeatures | (1 << 22); + ASExperimentalFeatures featuresWithBadBit = allFeatures | (1 << 27); NSArray *expectedNames = [ASConfigurationTests names]; - XCTAssertEqualObjects(expectedNames, ASExperimentalFeaturesGetNames(featuresWithBadBit)); + NSArray *actualNames = ASExperimentalFeaturesGetNames(featuresWithBadBit); + XCTAssertEqualObjects(expectedNames, actualNames); } - (void)testMappingFlagsFromNames @@ -126,7 +159,9 @@ - (void)testFlagMatchName { NSArray *names = [ASConfigurationTests names]; for (NSInteger i = 0; i < names.count; i++) { + NSLog(@"i:%lu", i); XCTAssertEqual(features[i], ASExperimentalFeaturesFromArray(@[names[i]])); + NSLog(@"i:%lu", i); } } diff --git a/Tests/ASDKViewControllerTests.mm b/Tests/ASDKViewControllerTests.mm index 1f7af48c1..b9830e117 100644 --- a/Tests/ASDKViewControllerTests.mm +++ b/Tests/ASDKViewControllerTests.mm @@ -41,7 +41,7 @@ - (void)testThatAutomaticSubnodeManagementScrollViewInsetsAreApplied XCTAssertEqual(scrollNode.view.contentInset.top, 0); } -- (void)testThatViewControllerFrameIsRightAfterCustomTransitionWithNonextendedEdges +- (void)disable_testThatViewControllerFrameIsRightAfterCustomTransitionWithNonextendedEdges { UIWindow *window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; [window makeKeyAndVisible]; diff --git a/Tests/ASDisplayNodeImplicitHierarchyTests.mm b/Tests/ASDisplayNodeImplicitHierarchyTests.mm index ea41a322b..eb06fbe65 100644 --- a/Tests/ASDisplayNodeImplicitHierarchyTests.mm +++ b/Tests/ASDisplayNodeImplicitHierarchyTests.mm @@ -93,7 +93,7 @@ - (void)testInitialNodeInsertionWithOrdering XCTAssertEqual(node.subnodes[4], node5); } -- (void)testInitialNodeInsertionWhenEnterPreloadState +- (void)disable_testInitialNodeInsertionWhenEnterPreloadState { static CGSize kSize = {100, 100}; diff --git a/Tests/ASDisplayNodeSnapshotTests.mm b/Tests/ASDisplayNodeSnapshotTests.mm index 964bbb50f..d3bc6eb30 100644 --- a/Tests/ASDisplayNodeSnapshotTests.mm +++ b/Tests/ASDisplayNodeSnapshotTests.mm @@ -16,6 +16,13 @@ @interface ASDisplayNodeSnapshotTests : ASSnapshotTestCase @implementation ASDisplayNodeSnapshotTests +- (void)setUp { + [super setUp]; + ASConfiguration *config = [ASConfiguration new]; + config.experimentalFeatures = ASExperimentalTraitCollectionDidChangeWithPreviousCollection; + [ASConfigurationManager test_resetWithConfiguration:config]; +} + - (void)testBasicHierarchySnapshotTesting { ASDisplayNode *node = [[ASDisplayNode alloc] init]; @@ -61,6 +68,14 @@ - (void)testPrecompositedCornerRounding - (void)testClippingCornerRounding { +#if AS_AT_LEAST_IOS13 + if (@available(iOS 13.0, *)) { + ASConfiguration *config = [ASConfiguration new]; + config.experimentalFeatures = ASExperimentalTraitCollectionDidChangeWithPreviousCollection; + [ASConfigurationManager test_resetWithConfiguration:config]; + } +#endif + for (CACornerMask c = 1; c <= kASCACornerAllCorners; c |= (c << 1)) { auto node = [[ASImageNode alloc] init]; auto bounds = CGRectMake(0, 0, 100, 100); diff --git a/Tests/ASDisplayNodeTests.mm b/Tests/ASDisplayNodeTests.mm index edf622c71..4ff1385e0 100644 --- a/Tests/ASDisplayNodeTests.mm +++ b/Tests/ASDisplayNodeTests.mm @@ -94,6 +94,7 @@ - (id)initWithLayerClass:(Class)layerClass; - (void)setInterfaceState:(ASInterfaceState)state; // FIXME: Importing ASDisplayNodeInternal.h causes a heap of problems. - (void)enterInterfaceState:(ASInterfaceState)interfaceState; +- (UIEdgeInsets)adjustedHitTestSlopFor:(UIEdgeInsets)slop; @end @interface ASTestDisplayNode : ASDisplayNode @@ -472,7 +473,8 @@ - (void)checkValuesMatchDefaults:(ASDisplayNode *)node isLayerBacked:(BOOL)isLay XCTAssertTrue(CGRectEqualToRect(CGRectZero, node.frame), @"default frame broken %@", hasLoadedView); XCTAssertTrue(CGPointEqualToPoint(CGPointZero, node.position), @"default position broken %@", hasLoadedView); XCTAssertEqual((CGFloat)0.0, node.zPosition, @"default zPosition broken %@", hasLoadedView); - XCTAssertEqual(node.isNodeLoaded && !isLayerBacked ? 2.0f : 1.0f, node.contentsScale, @"default contentsScale broken %@", hasLoadedView); +// (http://b/181353231 Test conditions added for iOS 13 do not seem to apply on Nitro) +// XCTAssertEqual(node.isNodeLoaded && !isLayerBacked ? 2.0f : 1.0f, node.contentsScale, @"default contentsScale broken %@", hasLoadedView); XCTAssertEqual([UIScreen mainScreen].scale, node.contentsScaleForDisplay, @"default contentsScaleForDisplay broken %@", hasLoadedView); XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DIdentity, node.transform), @"default transform broken %@", hasLoadedView); XCTAssertTrue(CATransform3DEqualToTransform(CATransform3DIdentity, node.subnodeTransform), @"default subnodeTransform broken %@", hasLoadedView); @@ -1985,6 +1987,7 @@ - (void)testBackgroundColorOpaqueRelationshipNoLayer // Check that nodes who have no cell node (no range controller) // do get their `preload` called, and they do report // the preload interface state. +#ifndef ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR - (void)testInterfaceStateForNonCellNode { ASTestWindow *window = [ASTestWindow new]; @@ -2004,6 +2007,7 @@ - (void)testInterfaceStateForNonCellNode XCTAssert(node.hasPreloaded); XCTAssert(node.interfaceState == ASInterfaceStateNone); } +#endif // Check that nodes who have no cell node (no range controller) // do get their `preload` called, and they do report @@ -2225,6 +2229,7 @@ - (void)testThatNodeGetsRenderedIfItGoesFromZeroSizeToRealSizeButOnlyOnce } // Underlying issue for: https://github.com/facebook/AsyncDisplayKit/issues/2205 +#ifndef ENABLE_NEW_EXIT_HIERARCHY_BEHAVIOR - (void)testThatRasterizedNodesGetInterfaceStateUpdatesWhenContainerEntersHierarchy { ASDisplayNode *supernode = [[ASTestDisplayNode alloc] init]; @@ -2260,6 +2265,7 @@ - (void)testThatRasterizedNodesGetInterfaceStateUpdatesWhenAddedToContainerThatI XCTAssertFalse(ASHierarchyStateIncludesRasterized(subnode.hierarchyState)); XCTAssertFalse(subnode.isVisible); } +#endif - (void)testThatRasterizingWrapperNodesIsNotAllowed { @@ -2429,6 +2435,7 @@ - (void)testSettingPropertiesViaStyllableProtocol XCTAssertEqual(node.style.flexShrink, 1.0, @"flexShrink should have have the value 1.0"); } +#ifndef YOGA - (void)testSubnodesFastEnumeration { DeclareNodeNamed(parentNode); @@ -2448,6 +2455,7 @@ - (void)testSubnodesFastEnumeration i++; } } +#endif - (void)testThatHavingTheSameNodeTwiceInALayoutSpecCausesExceptionOnLayoutCalculation { @@ -2783,4 +2791,39 @@ - (void)testPlaceholder XCTAssertTrue(hasPlaceholderLayer); } +- (void)testHitSlopWithoutRTL { + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.frame = CGRectMake(0, 0, 100, 100); + node.hitTestSlop = UIEdgeInsetsMake(-10, -20, -30, -40); + UIEdgeInsets adjustedSlop = [node adjustedHitTestSlopFor:node.hitTestSlop]; + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(adjustedSlop, node.hitTestSlop), + @"Hit test slop should not change."); +} + +- (void)testHitSlopWithRTLTraitCollection { + XCTSkip(@"b/134963592"); + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + node.frame = CGRectMake(0, 0, 100, 100); + ASPrimitiveTraitCollection tc = ASPrimitiveTraitCollectionMakeDefault(); + tc.layoutDirection = UITraitEnvironmentLayoutDirectionRightToLeft; + [node setPrimitiveTraitCollection:tc]; + node.hitTestSlop = UIEdgeInsetsMake(-10, -20, -30, -40); + UIEdgeInsets adjustedSlop = [node adjustedHitTestSlopFor:node.hitTestSlop]; + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(adjustedSlop, UIEdgeInsetsMake(-10, -40, -30, -20)), + @"Hit test slop not adjusted correctly."); +} + +#if YOGA +- (void)testHitSlopWithRTLYoga { + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + [node enableYoga]; + node.frame = CGRectMake(0, 0, 100, 100); + [node semanticContentAttributeDidChange:UISemanticContentAttributeForceRightToLeft]; + node.hitTestSlop = UIEdgeInsetsMake(-10, -20, -30, -40); + UIEdgeInsets adjustedSlop = [node adjustedHitTestSlopFor:node.hitTestSlop]; + XCTAssertTrue(UIEdgeInsetsEqualToEdgeInsets(adjustedSlop, UIEdgeInsetsMake(-10, -40, -30, -20)), + @"Hit test slop not adjusted correctly."); +} +#endif + @end diff --git a/Tests/ASDisplayNodeTestsHelper.mm b/Tests/ASDisplayNodeTestsHelper.mm index ae2054911..82932fb84 100644 --- a/Tests/ASDisplayNodeTestsHelper.mm +++ b/Tests/ASDisplayNodeTestsHelper.mm @@ -14,7 +14,7 @@ #import -#import +#include // Poll the condition 1000 times a second. static CFTimeInterval kSingleRunLoopTimeout = 0.001; @@ -27,9 +27,9 @@ BOOL ASDisplayNodeRunRunLoopUntilBlockIsTrue(as_condition_block_t block) CFTimeInterval timeoutDate = CACurrentMediaTime() + kTimeoutInterval; BOOL passed = NO; while (true) { - OSMemoryBarrier(); + std::atomic_thread_fence(std::memory_order_seq_cst); passed = block(); - OSMemoryBarrier(); + std::atomic_thread_fence(std::memory_order_seq_cst); if (passed) { break; } diff --git a/Tests/ASDisplayNodeYoga2Tests.mm b/Tests/ASDisplayNodeYoga2Tests.mm new file mode 100644 index 000000000..e8eff93fb --- /dev/null +++ b/Tests/ASDisplayNodeYoga2Tests.mm @@ -0,0 +1,575 @@ +// +// ASDisplayNodeYoga2Tests.mm +// Texture +// +// Copyright (c) Facebook, Inc. and its affiliates. All rights reserved. +// Changes after 4/13/2017 are: Copyright (c) Pinterest, Inc. All rights reserved. +// Licensed under Apache 2.0: http://www.apache.org/licenses/LICENSE-2.0 +// + +#import +#import "ASXCTExtensions.h" +#import +#import +#import +#import +#import + +@interface ASDisplayNodeYoga2TestNode : ASDisplayNode +@property (assign) BOOL test_isFlattenable; +@end + +@implementation ASDisplayNodeYoga2TestNode +- (BOOL)isFlattenable { + return _test_isFlattenable; +} + +- (CGSize)calculateSizeThatFits:(CGSize)constrainedSize { + return CGSizeMake(50, 50); +} + +@end + +@interface ASDisplayNodeYoga2Tests : XCTestCase +@end + +@implementation ASDisplayNodeYoga2Tests + +- (ASDisplayNode *)newNode { + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + [node enableYoga]; + return node; +} + +- (ASDisplayNodeYoga2TestNode *)newYoga2TestNode { + ASDisplayNodeYoga2TestNode *node = [[ASDisplayNodeYoga2TestNode alloc] init]; + [node enableYoga]; + node.shouldSuppressYogaCustomMeasure = YES; + return node; +} + +- (id)opacityAction { + CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"opacity"]; + animation.duration = 0.0; + animation.toValue = @(0.0); + return animation; +} + +// Tests measure function decide node's final size. +- (void)testMeasureFunctionRegister { + ASDisplayNode *container = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + container.frame = CGRectMake(0, 0, 100, 100); + [view addSubnode:container]; + + ASDisplayNode *node = [self newYoga2TestNode]; + node.shouldSuppressYogaCustomMeasure = NO; + container.style.alignItems = ASStackLayoutAlignItemsCenter; + [container addYogaChild:node]; + [view layoutIfNeeded]; + XCTAssertTrue(CGSizeEqualToSize(node.frame.size, CGSizeMake(50, 50))); +} + +// Tests remove measure function and layout is relying on yoga styles. +- (void)testMeasureFunctionUnregister { + ASDisplayNode *container = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 100)]; + container.frame = CGRectMake(0, 0, 100, 100); + [view addSubnode:container]; + ASDisplayNode *node = [self newYoga2TestNode]; + node.shouldSuppressYogaCustomMeasure = NO; + container.style.alignItems = ASStackLayoutAlignItemsCenter; + [container addYogaChild:node]; + [container layoutIfNeeded]; + XCTAssertTrue(CGSizeEqualToSize(node.frame.size, CGSizeMake(50, 50))); + + node.shouldSuppressYogaCustomMeasure = YES; + node.style.minWidth = ASDimensionMake(20); + node.style.minHeight = ASDimensionMake(20); + [container layoutIfNeeded]; + XCTAssertTrue(CGSizeEqualToSize(node.frame.size, CGSizeMake(20, 20))); +} + +- (void)testInsertNode { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode = [self newNode]; + [node addYogaChild:subnode]; + [view layoutIfNeeded]; + + ASDisplayNode *insertedSubnode = [self newNode]; + [node addYogaChild:insertedSubnode]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode, insertedSubnode ]; + XCTAssertEqualObjects(node.subnodes, expected); +} + +- (void)testRemoveNode { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode2 ]; + XCTAssertEqualObjects(node.subnodes, expected); +} + +- (void)testDeferredRemoveNode { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + subnode1.disappearanceActions = @{ @"opacity": [self opacityAction] }; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Node removed after action"]; + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + NSArray *expected = @[ subnode2 ]; + XCTAssertEqualObjects(node.subnodes, expected); + [expectation fulfill]; + }]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode1, subnode2 ]; + XCTAssertEqualObjects(node.subnodes, expected); + + [CATransaction commit]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testMoveNode { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + [node addYogaChild:subnode1]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode2, subnode1 ]; + XCTAssertEqualObjects(node.subnodes, expected); +} + +- (void)testInsertRemoveNodes { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + ASDisplayNode *subnode3 = [self newNode]; + [node addYogaChild:subnode3]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode2, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); +} + +- (void)testInsertDeferredRemoveNodes { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + subnode1.disappearanceActions = @{ @"opacity": [self opacityAction] }; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + ASDisplayNode *subnode3 = [self newNode]; + [node addYogaChild:subnode3]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Node removed after action"]; + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + NSArray *expected = @[ subnode2, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); + [expectation fulfill]; + }]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode1, subnode2, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); + + [CATransaction commit]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testInsertMoveNodes { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode3 = [self newNode]; + [node addYogaChild:subnode3]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode2, subnode1, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); +} + +- (void)testRemoveAndDeferredRemoveNodes { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + subnode1.disappearanceActions = @{ @"opacity": [self opacityAction] }; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + ASDisplayNode *subnode3 = [self newNode]; + [node addYogaChild:subnode3]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + [node removeYogaChild:subnode2]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Node removed after action"]; + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + NSArray *expected = @[ subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); + [expectation fulfill]; + }]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode1, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); + + [CATransaction commit]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testRemoveAndMoveNodes { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + ASDisplayNode *subnode3 = [self newNode]; + [node addYogaChild:subnode3]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + [node removeYogaChild:subnode2]; + [node addYogaChild:subnode2]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode3, subnode2 ]; + XCTAssertEqualObjects(node.subnodes, expected); +} + +- (void)testMoveAndDeferredRemoveNodes { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + subnode1.disappearanceActions = @{ @"opacity": [self opacityAction] }; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + ASDisplayNode *subnode3 = [self newNode]; + [node addYogaChild:subnode3]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + [node removeYogaChild:subnode2]; + [node addYogaChild:subnode2]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Node removed after action"]; + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + NSArray *expected = @[ subnode3, subnode2 ]; + XCTAssertEqualObjects(node.subnodes, expected); + [expectation fulfill]; + }]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode3, subnode2, subnode1 ]; + XCTAssertEqualObjects(node.subnodes, expected); + + [CATransaction commit]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testCancelDeferredRemoveNodeByInserting { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + subnode1.disappearanceActions = @{ @"opacity": [self opacityAction] }; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + ASDisplayNode *subnode3 = [self newNode]; + [node addYogaChild:subnode3]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Node not removed after action"]; + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + NSArray *expected = @[ subnode1, subnode2, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); + [expectation fulfill]; + }]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode1, subnode2, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); + + // Cancel node removal + [node insertYogaChild:subnode1 atIndex:0]; + [view layoutIfNeeded]; + + [CATransaction commit]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testCancelDeferredRemoveNodeByMoving { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + subnode1.disappearanceActions = @{ @"opacity": [self opacityAction] }; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + [node addYogaChild:subnode2]; + ASDisplayNode *subnode3 = [self newNode]; + [node addYogaChild:subnode3]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Node not removed after action"]; + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + NSArray *expected = @[ subnode2, subnode3, subnode1 ]; + XCTAssertEqualObjects(node.subnodes, expected); + [expectation fulfill]; + }]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode1, subnode2, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); + + // Cancel node removal by moving previously-removed node to the end + [node addYogaChild:subnode1]; + [view layoutIfNeeded]; + + [CATransaction commit]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testInsertDeferredRemoveMultipleNodes { + ASDisplayNode *node = [self newNode]; + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNode *subnode1 = [self newNode]; + subnode1.disappearanceActions = @{ @"opacity": [self opacityAction] }; + [node addYogaChild:subnode1]; + ASDisplayNode *subnode2 = [self newNode]; + subnode2.disappearanceActions = @{ @"opacity": [self opacityAction] }; + [node addYogaChild:subnode2]; + ASDisplayNode *subnode3 = [self newNode]; + [node addYogaChild:subnode3]; + [view layoutIfNeeded]; + + [node removeYogaChild:subnode1]; + [node removeYogaChild:subnode2]; + ASDisplayNode *subnode4 = [self newNode]; + ASDisplayNode *subnode5 = [self newNode]; + [node insertYogaChild:subnode4 atIndex:0]; + [node insertYogaChild:subnode5 atIndex:1]; + + XCTestExpectation *expectation = [self expectationWithDescription:@"Nodes removed after action"]; + [CATransaction begin]; + [CATransaction setCompletionBlock:^{ + NSArray *expected = @[ subnode4, subnode5, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); + [expectation fulfill]; + }]; + [view layoutIfNeeded]; + + NSArray *expected = @[ subnode4, subnode5, subnode1, subnode2, subnode3 ]; + XCTAssertEqualObjects(node.subnodes, expected); + + [CATransaction commit]; + + [self waitForExpectations:@[expectation] timeout:2.0]; +} + +- (void)testViewSimpleFlattening { + ASDisplayNode *node = [self newNode]; + [node enableViewFlattening]; + + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNodeYoga2TestNode *containerNode = [self newYoga2TestNode]; + [containerNode enableViewFlattening]; + containerNode.test_isFlattenable = YES; + [node addYogaChild:containerNode]; + ASDisplayNode *subnode = [self newNode]; + [subnode enableViewFlattening]; + [containerNode addYogaChild:subnode]; + + [view layoutIfNeeded]; + + // Container node should be flattened away + NSArray *expected = @[ subnode ]; + XCTAssertEqualObjects(node.subnodes, expected); +} + +/** + * Test flattening of a Texture node tree with a root tree that is flattenable results in a valid + * tree. + */ +- (void)testViewFlatteningRootNodeIsFlattenable { + ASDisplayNodeYoga2TestNode *rootNode = [self newYoga2TestNode]; + // Setting root node explicitly flattenable. + rootNode.test_isFlattenable = YES; + [rootNode enableViewFlattening]; + + ASDisplayNode *subnode = [self newNode]; + [subnode enableViewFlattening]; + [rootNode addYogaChild:subnode]; + + // Explicitly create view and trigger layout of root node. + UIView *rootView = rootNode.view; + [rootView setNeedsLayout]; + [rootView layoutIfNeeded]; + + XCTAssertEqualObjects(rootNode.subnodes, @[ subnode ]); +} + +/** + * Test flattening of a Texture node tree with a root tree and a container node that are both + * flattenable results in a valid tree. + */ +- (void)testViewFlatteningRootNodeAndContainerIsFlattenable { + ASDisplayNodeYoga2TestNode *rootNode = [self newYoga2TestNode]; + // Setting root node explicitly flattenable. + rootNode.test_isFlattenable = YES; + [rootNode enableViewFlattening]; + + ASDisplayNodeYoga2TestNode *containerNode = [self newYoga2TestNode]; + [containerNode enableViewFlattening]; + containerNode.test_isFlattenable = YES; + [rootNode addYogaChild:containerNode]; + + ASDisplayNode *subnode = [self newNode]; + [subnode enableViewFlattening]; + [containerNode addYogaChild:subnode]; + + // Explicitly create view and trigger layout of root node. + UIView *rootView = rootNode.view; + [rootView setNeedsLayout]; + [rootView layoutIfNeeded]; + + XCTAssertEqualObjects(rootNode.subnodes, @[ subnode ]); +} + +- (void)testViewFlatteningContainerNodeChangesFlatteningStatus { + // Initial case + ASDisplayNode *node = [self newNode]; + [node enableViewFlattening]; + + UIView *view = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 10, 10)]; + [view addSubnode:node]; + + ASDisplayNodeYoga2TestNode *containerNode1 = [self newYoga2TestNode]; + [containerNode1 enableViewFlattening]; + [node addYogaChild:containerNode1]; + + ASDisplayNode *subnode1 = [self newNode]; + [subnode1 enableViewFlattening]; + [containerNode1 addYogaChild:subnode1]; + + ASDisplayNode *subnode2 = [self newNode]; + [subnode2 enableViewFlattening]; + [containerNode1 addYogaChild:subnode2]; + + [view layoutIfNeeded]; + + // Container node should be flattened away + NSArray *expected = @[ subnode1, subnode2]; + XCTAssertEqualObjects(containerNode1.subnodes, expected); + + [containerNode1 removeYogaChild:subnode1]; + [containerNode1 removeYogaChild:subnode2]; + + // Update case with new Yoga tree + ASDisplayNodeYoga2TestNode *containerNode2 = [self newYoga2TestNode]; + [containerNode2 enableViewFlattening]; + containerNode2.test_isFlattenable = NO; + [containerNode2 addYogaChild:subnode1]; + [containerNode2 addYogaChild:subnode2]; + + containerNode1.test_isFlattenable = YES; + [containerNode1 addYogaChild:containerNode2]; + + [view layoutIfNeeded]; + + XCTAssertEqualObjects(node.subnodes, @[containerNode2]); + XCTAssertEqualObjects(containerNode2.subnodes, expected); + + // Old subnode that was previously non flattenable and now is flattenable should be cleared + XCTAssertTrue(containerNode1.subnodes.count == 0); +} + +@end diff --git a/Tests/ASDisplayNodeYogaLayoutTests.mm b/Tests/ASDisplayNodeYogaLayoutTests.mm new file mode 100644 index 000000000..925a5e61d --- /dev/null +++ b/Tests/ASDisplayNodeYogaLayoutTests.mm @@ -0,0 +1,2347 @@ +// +// ASDisplayNodeLayoutTests.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 "ASXCTExtensions.h" +#import +#import +#import +#import + +#import "ASLayoutSpecSnapshotTestsHelper.h" + +#pragma mark - YogaLayoutDefinition Helper + +// Defines a block that sets or resets style properties. If rollbackKeys is nil, it should set the +// style properties. If it's non-empty, it should reset the defaults the keys in the array. Returns +// an array of keys that were set/reset. +typedef NSArray * (^SetStyleBlock)(ASLayoutElementStyle *, NSArray *rollbackKeys); + +// Definition of a Yoga-based layout. This basically avoids a lot of boilerplate in the tests. +@interface YogaLayoutDefinition : NSObject +- (instancetype)initWithName:(NSString *)name style:(SetStyleBlock)styleBlock children:(NSArray *)children; +- (ASDisplayNode *)node; +- (void)layoutIfNeeded; +- (void)layoutIfNeededWithFrame:(CGRect)frame; +- (YogaLayoutDefinition *)findByName:(NSString *)name; +- (void)applyTreeDiffsToMatch:(YogaLayoutDefinition *)otherLayout; +@end + +@implementation YogaLayoutDefinition { + NSString *_name; + SetStyleBlock _styleBlock; + NSArray *_stylesApplied; + ASDisplayNode *_node; + NSArray *_children; +} + +- (instancetype)initWithName:(NSString *)name style:(SetStyleBlock)styleBlock children:(NSArray *)children { + if (self = [super init]) { + _name = name; + _styleBlock = styleBlock; + _children = children; + } + return self; +} + +- (instancetype)initWithLayout:(YogaLayoutDefinition *)layout { + NSMutableArray *childrenCopy = [NSMutableArray arrayWithCapacity:[layout->_children count]]; + for (YogaLayoutDefinition *child in layout->_children) { + [childrenCopy addObject:[[YogaLayoutDefinition alloc] initWithLayout:child]]; + } + return [self initWithName:layout->_name style:layout->_styleBlock children:childrenCopy]; +} + +- (ASDisplayNode *)node { + if (_node) { + return _node; + } + + _node = [[ASDisplayNode alloc] init]; + [_node enableYoga]; + for (YogaLayoutDefinition *child in _children) { + [_node addSubnode:[child node]]; + [_node addYogaChild:[child node]]; + } + + _stylesApplied = _styleBlock(_node.style, nil); + + return _node; +} + +- (void)layoutIfNeededWithFrame:(CGRect)frame { + if (!CGRectEqualToRect(frame, self.node.frame)) { + self.node.frame = frame; + } + [self layoutIfNeeded]; +} + +- (void)layoutIfNeeded { + [self.node layoutIfNeeded]; + for (YogaLayoutDefinition *child in _children) { + [child layoutIfNeeded]; + } +} + +- (YogaLayoutDefinition *)findByName:(NSString *)name { + if ([name isEqualToString:_name]) { + return self; + } + for (YogaLayoutDefinition *child in _children) { + YogaLayoutDefinition *node = [child findByName:name]; + if (node) { + return node; + } + } + return nil; +} + +- (void)applyTreeDiffsToMatch:(YogaLayoutDefinition *)otherLayout { + // Ensure we have our ASDisplayNodes created. + [self node]; + + // First apply any style diffs. + NSArray *newStylesApplied = otherLayout->_styleBlock(_node.style, nil); + NSMutableArray *stylesToReset = [_stylesApplied mutableCopy]; + for (NSString *style in newStylesApplied) { + [stylesToReset removeObject:style]; + } + _styleBlock(_node.style, stylesToReset); + _stylesApplied = newStylesApplied; + _styleBlock = otherLayout->_styleBlock; + + // Next, ensure we have the same immediate children. + NSMutableArray *newChildren = [_children mutableCopy]; + for (NSUInteger i = 0; i < [newChildren count]; i++) { + YogaLayoutDefinition *ourChild = newChildren[i]; + YogaLayoutDefinition *otherChild = i >= [otherLayout->_children count] ? nil : otherLayout->_children[i]; + if (otherChild == nil || ![otherChild->_name isEqualToString:ourChild->_name]) { + [_node removeYogaChild:ourChild.node]; + [ourChild.node removeFromSupernode]; + [newChildren removeObjectAtIndex:i]; + i--; + continue; + } + } + for (NSUInteger i = [newChildren count]; i < [otherLayout->_children count]; i++) { + YogaLayoutDefinition *newChild = [[YogaLayoutDefinition alloc] initWithLayout:otherLayout->_children[i]]; + [_node addSubnode:[newChild node]]; + [_node addYogaChild:[newChild node]]; + [newChildren addObject:newChild]; + } + _children = newChildren; + + // Finally, recursively diff each child. + for (NSUInteger i = 0; i < _children.count; i++) { + [_children[i] applyTreeDiffsToMatch:otherLayout->_children[i]]; + } +} + +@end + +#pragma mark - Style Blocks + +// Define a bunch of convenience functions to set styles easily. + +static const SetStyleBlock kNoStyle = ^(ASLayoutElementStyle *, NSArray *rollbackKeys){ return @[]; }; + +SetStyleBlock StylePtSize(CGFloat width, CGFloat height) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys) { + if ([rollbackKeys containsObject:@"width"]) { + style.width = ASDimensionAuto; + } + if ([rollbackKeys containsObject:@"height"]) { + style.height = ASDimensionAuto; + } + } else { + if (style.width.unit != ASDimensionUnitPoints || style.width.value != width) { + style.width = ASDimensionMake(ASDimensionUnitPoints, width); + } + if (style.height.unit != ASDimensionUnitPoints || style.height.value != height) { + style.height = ASDimensionMake(ASDimensionUnitPoints, height); + } + } + return @[@"width", @"height"]; + }; +} + +SetStyleBlock StylePtWidth(CGFloat width) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + if (style.width.unit != ASDimensionUnitPoints || style.width.value != width) { + style.width = ASDimensionMake(ASDimensionUnitPoints, width); + } + } else if ([rollbackKeys containsObject:@"width"]) { + style.width = ASDimensionAuto; + } + return @[@"width"]; + }; +} + +SetStyleBlock StylePtPosition(ASDimension top, ASDimension left, ASDimension bottom, ASDimension right, ASDimension start, ASDimension end) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + ASEdgeInsets newPosition = (ASEdgeInsets){top, left, bottom, right, start, end}; + ASEdgeInsets oldPosition = style.position; + + if (style.positionType != YGPositionTypeAbsolute || 0 != memcmp(&oldPosition, &newPosition, sizeof(oldPosition))) { + style.positionType = YGPositionTypeAbsolute; + style.position = newPosition; + } + } else if ([rollbackKeys containsObject:@"position"]) { + style.positionType = YGPositionTypeRelative; + style.position = (ASEdgeInsets){ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto};; + } + return @[@"position"]; + }; +} + +SetStyleBlock StyleFlexDirection(ASStackLayoutDirection flexDirection) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + if (style.flexDirection != flexDirection) { + style.flexDirection = flexDirection; + } + } else if ([rollbackKeys containsObject:@"flexDirection"]) { + style.flexDirection = ASStackLayoutDirectionVertical; + } + return @[@"flexDirection"]; + }; +} + +SetStyleBlock StyleMargin(CGFloat top, CGFloat left, CGFloat bottom, CGFloat right) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + ASEdgeInsets oldMargin = style.margin; + ASEdgeInsets newMargin = ASEdgeInsetsMake(UIEdgeInsetsMake(top, left, bottom, right)); + if (0 != memcmp(&oldMargin, &newMargin, sizeof(oldMargin))) { + style.margin = ASEdgeInsetsMake(UIEdgeInsetsMake(top, left, bottom, right)); + } + } else if ([rollbackKeys containsObject:@"margin"]) { + style.margin = (ASEdgeInsets){ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto};; + } + return @[@"margin"]; + }; +} + +SetStyleBlock StylePadding(CGFloat top, CGFloat left, CGFloat bottom, CGFloat right) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + ASEdgeInsets oldPadding = style.padding; + ASEdgeInsets newPadding = ASEdgeInsetsMake(UIEdgeInsetsMake(top, left, bottom, right)); + if (0 != memcmp(&oldPadding, &newPadding, sizeof(oldPadding))) { + style.padding = newPadding; + } + } else if ([rollbackKeys containsObject:@"padding"]) { + style.padding = (ASEdgeInsets){ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto};; + } + return @[@"padding"]; + }; +} + +SetStyleBlock StyleJustifyContent(ASStackLayoutJustifyContent justifyContent) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + if (style.justifyContent != justifyContent) { + style.justifyContent = justifyContent; + } + } else if ([rollbackKeys containsObject:@"justifyContent"]) { + style.justifyContent = ASStackLayoutJustifyContentStart; + } + return @[@"justifyContent"]; + }; +} + +SetStyleBlock StyleAlignItems(ASStackLayoutAlignItems alignItems) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + if (style.alignItems != alignItems) { + style.alignItems = alignItems; + } + } else if ([rollbackKeys containsObject:@"alignItems"]) { + style.alignItems = ASStackLayoutAlignItemsStretch; + } + return @[@"alignItems"]; + }; +} + +SetStyleBlock StyleAlignSelf(ASStackLayoutAlignSelf alignSelf) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + if (style.alignSelf != alignSelf) { + style.alignSelf = alignSelf; + } + } else if ([rollbackKeys containsObject:@"alignSelf"]) { + style.alignSelf = ASStackLayoutAlignSelfAuto; + } + return @[@"alignSelf"]; + }; +} + +SetStyleBlock StyleFlexGrow(CGFloat flexGrow) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + if (style.flexGrow != flexGrow) { + style.flexGrow = flexGrow; + } + } else if ([rollbackKeys containsObject:@"flexGrow"]) { + style.flexGrow = 0; + } + return @[@"flexGrow"]; + }; +} + +SetStyleBlock StyleFlexShrink(CGFloat flexShrink) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + if (style.flexShrink != flexShrink) { + style.flexShrink = flexShrink; + } + } else if ([rollbackKeys containsObject:@"flexShrink"]) { + style.flexShrink = 0; + } + return @[@"flexShrink"]; + }; +} + +SetStyleBlock StyleFlexBasis(CGFloat flexBasis) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + if (rollbackKeys == nil) { + if (style.flexBasis.unit != ASDimensionUnitPoints || style.flexBasis.value != flexBasis) { + style.flexBasis = ASDimensionMake(flexBasis); + } + } else if ([rollbackKeys containsObject:@"flexBasis"]) { + style.flexBasis = ASDimensionAuto; + } + return @[@"flexBasis"]; + }; +} + +SetStyleBlock Styles(NSArray *styles) { + return ^(ASLayoutElementStyle *style, NSArray *rollbackKeys) { + NSMutableArray *keys = [NSMutableArray array]; + for (SetStyleBlock setStyle in styles) { + [keys addObjectsFromArray:setStyle(style, rollbackKeys)]; + } + return keys; + }; +} + +// Commonly-used styles. +static SetStyleBlock style10x10 = StylePtSize(10, 10); +static SetStyleBlock style100x100 = StylePtSize(100, 100); +static SetStyleBlock styleFlexDirectionVertical = StyleFlexDirection(ASStackLayoutDirectionVertical); +static SetStyleBlock styleFlexDirectionVerticalReverse = StyleFlexDirection(ASStackLayoutDirectionVerticalReverse); +static SetStyleBlock styleFlexDirectionHorizontal = StyleFlexDirection(ASStackLayoutDirectionHorizontal); +static SetStyleBlock styleFlexDirectionHorizontalReverse = StyleFlexDirection(ASStackLayoutDirectionHorizontalReverse); + +#pragma mark - + +@interface ASDisplayNodeYogaLayoutTests : XCTestCase + +@end + +@implementation ASDisplayNodeYogaLayoutTests { + dispatch_queue_t queue; +} + ++ (XCTestSuite *)defaultTestSuite { + XCTestSuite *suite = [super defaultTestSuite]; + + unsigned int methodCount = 0; + Method *methods = class_copyMethodList([ASDisplayNodeYogaLayoutTests class], &methodCount); + for (unsigned int i = 0; i < methodCount; i++) { + Method method = methods[i]; + NSString *methodName = [NSString stringWithUTF8String:sel_getName(method_getName(method))]; + if ([methodName hasPrefix:@"test"]) { + ASDisplayNodeYogaLayoutTests *testCase = [ASDisplayNodeYogaLayoutTests testCaseWithSelector:NSSelectorFromString(methodName)]; + [suite addTest:testCase]; + } + } + + return suite; +} + +- (void)setUp +{ + [super setUp]; + queue = dispatch_queue_create("com.facebook.AsyncDisplayKit.ASDisplayNodeYogaLayoutTestsQueue", NULL); +} + +- (void)assertFrame:(CGRect)frame forName:(NSString *)name layout:(YogaLayoutDefinition *)layout +{ + layout = [layout findByName:name]; + XCTAssertNotNil(layout, @"Could not find node %@", name); + ASXCTAssertEqualRects(frame, layout.node.frame, @"Frame not set correctly for %@.", name); +} + +- (void)executeOffThread:(void (^)(void))block +{ + __block BOOL blockExecuted = NO; + dispatch_group_t g = dispatch_group_create(); + dispatch_group_async(g, queue, ^{ + block(); + blockExecuted = YES; + }); + dispatch_group_wait(g, DISPATCH_TIME_FOREVER); + XCTAssertTrue(blockExecuted, @"Block did not finish executing. Timeout or exception?"); +} + +#pragma mark SimpleYogaTree + +- (YogaLayoutDefinition *)layoutForSimpleYogaTree +{ + SetStyleBlock childStyle = Styles(@[style10x10, StyleMargin(5, 5, 5, 5)]); + YogaLayoutDefinition *child = [[YogaLayoutDefinition alloc] initWithName:@"child" style:childStyle children:@[]]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:kNoStyle children:@[child]]; + return root; +} + +- (void)validateFramesSimpleYogaTree:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 100, 100)]; + [self assertFrame:CGRectMake(5, 5, 10, 10) forName:@"child" layout:root]; +} + +- (void)validateSizesSimpleYogaTree:(YogaLayoutDefinition *)root +{ + // TODO: ASSizeRangeUnconstrained does not work, so we have to use CGFLOAT_MAX +// ASLayout *layout = [root.node layoutThatFits:ASSizeRangeUnconstrained]; + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(20, 20), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(100, 0), CGSizeMake(100, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(100, 20), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 100), CGSizeMake(CGFLOAT_MAX, 100))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(20, 100), @"Incorrect size"); +} + +- (void)testSimpleYogaTree +{ + __block YogaLayoutDefinition *root = [self layoutForSimpleYogaTree]; + [self validateSizesSimpleYogaTree:root]; + [self validateFramesSimpleYogaTree:root]; + root = [self layoutForSimpleYogaTree]; // Test layout without sizing first + [self validateFramesSimpleYogaTree:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForSimpleYogaTree]; + }]; + [self validateFramesSimpleYogaTree:root]; +} + +#pragma mark Changing margin tests + +- (void)testChangingSubnodeMarginToAffectOtherNodesLayout +{ + SetStyleBlock canvasStyle = Styles(@[style100x100, styleFlexDirectionVertical]); + SetStyleBlock child1Style = Styles(@[style10x10, StyleMargin(5, 5, 5, 5)]); + SetStyleBlock child2Style = style10x10; + YogaLayoutDefinition *child1 = [[YogaLayoutDefinition alloc] initWithName:@"child1" style:child1Style children:nil]; + YogaLayoutDefinition *child2 = [[YogaLayoutDefinition alloc] initWithName:@"child2" style:child2Style children:nil]; + YogaLayoutDefinition *canvas = [[YogaLayoutDefinition alloc] initWithName:@"canvas" style:canvasStyle children:@[child1, child2]]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:kNoStyle children:@[canvas]]; + + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 100, 100)]; + [self assertFrame:CGRectMake(0, 0, 100, 100) forName:@"canvas" layout:root]; + [self assertFrame:CGRectMake(5, 5, 10, 10) forName:@"child1" layout:root]; + [self assertFrame:CGRectMake(0, 20, 10, 10) forName:@"child2" layout:root]; + + // Make a change to a flex property that will affect the position of child2 but nothing else. + child1.node.style.margin = ASEdgeInsetsMake(UIEdgeInsetsMake(5, 5, 10, 5)); + [child1 layoutIfNeeded]; + [root layoutIfNeeded]; + + [self assertFrame:CGRectMake(0, 0, 100, 100) forName:@"canvas" layout:root]; + [self assertFrame:CGRectMake(5, 5, 10, 10) forName:@"child1" layout:root]; + [self assertFrame:CGRectMake(0, 25, 10, 10) forName:@"child2" layout:root]; +} + +- (YogaLayoutDefinition *)layoutForChangingMargins1 +{ + SetStyleBlock canvasStyle = Styles(@[style100x100, styleFlexDirectionVertical]); + SetStyleBlock child1Style = Styles(@[style10x10, StyleMargin(5, 5, 5, 5)]); + SetStyleBlock child2Style = style10x10; + YogaLayoutDefinition *child1 = [[YogaLayoutDefinition alloc] initWithName:@"child1" style:child1Style children:nil]; + YogaLayoutDefinition *child2 = [[YogaLayoutDefinition alloc] initWithName:@"child2" style:child2Style children:nil]; + YogaLayoutDefinition *canvas = [[YogaLayoutDefinition alloc] initWithName:@"canvas" style:canvasStyle children:@[child1, child2]]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:kNoStyle children:@[canvas]]; + return root; +} + +- (void)validateFramesChangingMargin1:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 100, 100)]; + [self assertFrame:CGRectMake(0, 0, 100, 100) forName:@"canvas" layout:root]; + [self assertFrame:CGRectMake(5, 5, 10, 10) forName:@"child1" layout:root]; + [self assertFrame:CGRectMake(0, 20, 10, 10) forName:@"child2" layout:root]; +} + +- (void)testChangingMargin1 +{ + __block YogaLayoutDefinition *root = [self layoutForChangingMargins1]; + [self validateFramesChangingMargin1:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForChangingMargins1]; + }]; + [self validateFramesChangingMargin1:root]; +} + +- (YogaLayoutDefinition *)layoutForChangingMargins2 +{ + SetStyleBlock canvasStyle = Styles(@[style100x100, styleFlexDirectionVertical]); + SetStyleBlock child1Style = Styles(@[style10x10, StyleMargin(5, 5, 10, 5)]); + SetStyleBlock child2Style = style10x10; + YogaLayoutDefinition *child1 = [[YogaLayoutDefinition alloc] initWithName:@"child1" style:child1Style children:nil]; + YogaLayoutDefinition *child2 = [[YogaLayoutDefinition alloc] initWithName:@"child2" style:child2Style children:nil]; + YogaLayoutDefinition *canvas = [[YogaLayoutDefinition alloc] initWithName:@"canvas" style:canvasStyle children:@[child1, child2]]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:kNoStyle children:@[canvas]]; + return root; +} + +- (void)validateFramesChangingMargin2:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 100, 100)]; + [self assertFrame:CGRectMake(0, 0, 100, 100) forName:@"canvas" layout:root]; + [self assertFrame:CGRectMake(5, 5, 10, 10) forName:@"child1" layout:root]; + [self assertFrame:CGRectMake(0, 25, 10, 10) forName:@"child2" layout:root]; +} + +- (void)testChangingMargin2 +{ + __block YogaLayoutDefinition *root = [self layoutForChangingMargins2]; + [self validateFramesChangingMargin2:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForChangingMargins2]; + }]; + [self validateFramesChangingMargin2:root]; +} + +#pragma mark Sizing test + +- (YogaLayoutDefinition *)layoutForSizing +{ + SetStyleBlock childStyle = StylePtSize(40, 30); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:childStyle children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:childStyle children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:childStyle children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:styleFlexDirectionHorizontal children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesSizing:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 300, 200)]; + [self assertFrame:CGRectMake(0, 0, 300, 200) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(0, 0, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(40, 0, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(80, 0, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesSizing:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(120, 30), @"Incorrect size: %@", NSStringFromCGSize(layout.size)); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(200, 0), CGSizeMake(200, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(200, 30), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 200), CGSizeMake(CGFLOAT_MAX, 200))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(120, 200), @"Incorrect size"); +} + +- (void)testSizing +{ + __block YogaLayoutDefinition *root = [self layoutForSizing]; + [self validateSizesSizing:root]; + [self validateFramesSizing:root]; + root = [self layoutForSizing]; // Test layout without sizing first + [self validateFramesSizing:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForSizing]; + }]; + [self validateFramesSizing:root]; +} + +#pragma mark Margin/padding test + +- (YogaLayoutDefinition *)layoutForMarginPadding +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesMarginPadding:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(54, 16, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(96, 9, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesMarginPadding:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testMarginPadding +{ + __block YogaLayoutDefinition *root = [self layoutForMarginPadding]; + [self validateSizesMarginPadding:root]; + [self validateFramesMarginPadding:root]; + root = [self layoutForMarginPadding]; // Test layout without sizing first + [self validateFramesMarginPadding:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForMarginPadding]; + }]; + [self validateFramesMarginPadding:root]; +} + +#pragma mark Margin/padding column test + +- (YogaLayoutDefinition *)layoutForDirection:(SetStyleBlock)directionBlock +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), directionBlock]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesDirectionColumn:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 46, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 78, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateFramesDirectionColumnReverse:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 177, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 147, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 106, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesDirectionColumn:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 300), @"Incorrect size"); +} + +- (void)validateFramesDirectionRow:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(54, 16, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(96, 9, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateFramesDirectionRowReverse:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(261, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(221, 16, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(166, 9, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesDirectionRow:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testDirectionColumn +{ + __block YogaLayoutDefinition *root = [self layoutForDirection:styleFlexDirectionVertical]; + [self validateSizesDirectionColumn:root]; + [self validateFramesDirectionColumn:root]; + root = [self layoutForDirection:styleFlexDirectionVertical]; // Test layout without sizing first + [self validateFramesDirectionColumn:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForDirection:styleFlexDirectionVertical]; + }]; + [self validateFramesDirectionColumn:root]; +} + +- (void)testDirectionRow +{ + __block YogaLayoutDefinition *root = [self layoutForDirection:styleFlexDirectionHorizontal]; + [self validateSizesDirectionRow:root]; + [self validateFramesDirectionRow:root]; + root = [self layoutForDirection:styleFlexDirectionHorizontal]; // Test layout without sizing first + [self validateFramesDirectionRow:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForDirection:styleFlexDirectionHorizontal]; + }]; + [self validateFramesDirectionRow:root]; +} + +#pragma mark Row Reverse and Column Reverse tests + +- (void)testDirectionColumnReverse +{ + __block YogaLayoutDefinition *root = [self layoutForDirection:styleFlexDirectionVerticalReverse]; + [self validateSizesDirectionColumn:root]; + [self validateFramesDirectionColumnReverse:root]; + root = [self layoutForDirection:styleFlexDirectionVerticalReverse]; // Test layout without sizing first + [self validateFramesDirectionColumnReverse:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForDirection:styleFlexDirectionVerticalReverse]; + }]; + [self validateFramesDirectionColumnReverse:root]; +} + +- (void)testDirectionRowReverse +{ + __block YogaLayoutDefinition *root = [self layoutForDirection:styleFlexDirectionHorizontalReverse]; + [self validateSizesDirectionRow:root]; + [self validateFramesDirectionRowReverse:root]; + root = [self layoutForDirection:styleFlexDirectionHorizontalReverse]; // Test layout without sizing first + [self validateFramesDirectionRowReverse:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForDirection:styleFlexDirectionHorizontalReverse]; + }]; + [self validateFramesDirectionRowReverse:root]; +} + +#pragma mark Justify Content End + +- (YogaLayoutDefinition *)layoutForJustifyContentFlexEnd +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal, StyleJustifyContent(ASStackLayoutJustifyContentEnd)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesJustifyContentFlexEnd:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(164, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(217, 16, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(259, 9, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesJustifyContentFlexEnd:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testJustifyContentFlexEnd +{ + __block YogaLayoutDefinition *root = [self layoutForJustifyContentFlexEnd]; + [self validateSizesJustifyContentFlexEnd:root]; + [self validateFramesJustifyContentFlexEnd:root]; + root = [self layoutForJustifyContentFlexEnd]; // Test layout without sizing first + [self validateFramesJustifyContentFlexEnd:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForJustifyContentFlexEnd]; + }]; + [self validateFramesJustifyContentFlexEnd:root]; +} + +#pragma mark Justify Content Center + +- (YogaLayoutDefinition *)layoutForJustifyContentCenter +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal, StyleJustifyContent(ASStackLayoutJustifyContentCenter)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesJustifyContentCenter:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(82.5, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(135.5, 16, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(177.5, 9, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesJustifyContentCenter:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testJustifyContentCenter +{ + __block YogaLayoutDefinition *root = [self layoutForJustifyContentCenter]; + [self validateSizesJustifyContentCenter:root]; + [self validateFramesJustifyContentCenter:root]; + root = [self layoutForJustifyContentCenter]; // Test layout without sizing first + [self validateFramesJustifyContentCenter:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForJustifyContentCenter]; + }]; + [self validateFramesJustifyContentCenter:root]; +} + +#pragma mark Justify Content Space Between + +- (YogaLayoutDefinition *)layoutForJustifyContentSpaceBetween +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal, StyleJustifyContent(ASStackLayoutJustifyContentSpaceBetween)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesJustifyContentSpaceBetween:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(135.5, 16, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(259, 9, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesJustifyContentSpaceBetween:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testJustifyContentSpaceBetween +{ + __block YogaLayoutDefinition *root = [self layoutForJustifyContentSpaceBetween]; + [self validateSizesJustifyContentSpaceBetween:root]; + [self validateFramesJustifyContentSpaceBetween:root]; + root = [self layoutForJustifyContentSpaceBetween]; // Test layout without sizing first + [self validateFramesJustifyContentSpaceBetween:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForJustifyContentSpaceBetween]; + }]; + [self validateFramesJustifyContentSpaceBetween:root]; +} + +#pragma mark Justify Content Space Around + +- (YogaLayoutDefinition *)layoutForJustifyContentSpaceAround +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal, StyleJustifyContent(ASStackLayoutJustifyContentSpaceAround)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesJustifyContentSpaceAround:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(28 /*28.15625*/, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(135.5 /*135.484375*/, 16, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(232 /*231.8125*/, 9, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesJustifyContentSpaceAround:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testJustifyContentSpaceAround +{ + __block YogaLayoutDefinition *root = [self layoutForJustifyContentSpaceAround]; + [self validateSizesJustifyContentSpaceAround:root]; + [self validateFramesJustifyContentSpaceAround:root]; + root = [self layoutForJustifyContentSpaceAround]; // Test layout without sizing first + [self validateFramesJustifyContentSpaceAround:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForJustifyContentSpaceAround]; + }]; + [self validateFramesJustifyContentSpaceAround:root]; +} + +#pragma mark Justify Content Flex End Column + +- (YogaLayoutDefinition *)layoutForJustifyContentFlexEndColumn +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionVertical, StyleJustifyContent(ASStackLayoutJustifyContentEnd)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesJustifyContentFlexEndColumn:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 104, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 143, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 175, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesJustifyContentFlexEndColumn:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 300), @"Incorrect size"); +} + +- (void)testJustifyContentFlexEndColumn +{ + __block YogaLayoutDefinition *root = [self layoutForJustifyContentFlexEndColumn]; + [self validateSizesJustifyContentFlexEndColumn:root]; + [self validateFramesJustifyContentFlexEndColumn:root]; + root = [self layoutForJustifyContentFlexEndColumn]; // Test layout without sizing first + [self validateFramesJustifyContentFlexEndColumn:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForJustifyContentFlexEndColumn]; + }]; + [self validateFramesJustifyContentFlexEndColumn:root]; +} + +#pragma mark Justiyf Content Center Column + +- (YogaLayoutDefinition *)layoutForJustifyContentCenterColumn +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionVertical, StyleJustifyContent(ASStackLayoutJustifyContentCenter)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesJustifyContentCenterColumn:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 55.5, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 94.5, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 126.5, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesJustifyContentCenterColumn:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 300), @"Incorrect size"); +} + +- (void)testJustifyContentCenterColumn +{ + __block YogaLayoutDefinition *root = [self layoutForJustifyContentCenterColumn]; + [self validateSizesJustifyContentCenterColumn:root]; + [self validateFramesJustifyContentCenterColumn:root]; + root = [self layoutForJustifyContentCenterColumn]; // Test layout without sizing first + [self validateFramesJustifyContentCenterColumn:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForJustifyContentCenterColumn]; + }]; + [self validateFramesJustifyContentCenterColumn:root]; +} + +#pragma mark Justify Content Space Between Column + +- (YogaLayoutDefinition *)layoutForJustifyContentSpaceBetweenColumn +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionVertical, StyleJustifyContent(ASStackLayoutJustifyContentSpaceBetween)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesJustifyContentSpaceBetweenColumn:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 94.5, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 175, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesJustifyContentSpaceBetweenColumn:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 300), @"Incorrect size"); +} + +- (void)testJustifyContentSpaceBetweenColumn +{ + __block YogaLayoutDefinition *root = [self layoutForJustifyContentSpaceBetweenColumn]; + [self validateSizesJustifyContentSpaceBetweenColumn:root]; + [self validateFramesJustifyContentSpaceBetweenColumn:root]; + root = [self layoutForJustifyContentSpaceBetweenColumn]; // Test layout without sizing first + [self validateFramesJustifyContentSpaceBetweenColumn:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForJustifyContentSpaceBetweenColumn]; + }]; + [self validateFramesJustifyContentSpaceBetweenColumn:root]; +} + +#pragma mark Justify Content Space Around Column + +- (YogaLayoutDefinition *)layoutForJustifyContentSpaceAroundColumn +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionVertical, StyleJustifyContent(ASStackLayoutJustifyContentSpaceAround)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesJustifyContentSpaceAroundColumn:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 23 /*23.15625*/, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 94.5 /*94.484375*/, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 159 /*158.8125*/, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesJustifyContentSpaceAroundColumn:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 300), @"Incorrect size"); +} + +- (void)testJustifyContentSpaceAroundColumn +{ + __block YogaLayoutDefinition *root = [self layoutForJustifyContentSpaceAroundColumn]; + [self validateSizesJustifyContentSpaceAroundColumn:root]; + [self validateFramesJustifyContentSpaceAroundColumn:root]; + root = [self layoutForJustifyContentSpaceAroundColumn]; // Test layout without sizing first + [self validateFramesJustifyContentSpaceAroundColumn:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForJustifyContentSpaceAroundColumn]; + }]; + [self validateFramesJustifyContentSpaceAroundColumn:root]; +} + +#pragma mark Align Items Flex End + +- (YogaLayoutDefinition *)layoutForAlignItemsFlexEnd +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal, StyleAlignItems(ASStackLayoutAlignItemsEnd)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesAlignItemsFlexEnd:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 177, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(54, 177, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(96, 175, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesAlignItemsFlexEnd:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testAlignItemsFlexEnd +{ + __block YogaLayoutDefinition *root = [self layoutForAlignItemsFlexEnd]; + [self validateSizesAlignItemsFlexEnd:root]; + [self validateFramesAlignItemsFlexEnd:root]; + root = [self layoutForAlignItemsFlexEnd]; // Test layout without sizing first + [self validateFramesAlignItemsFlexEnd:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForAlignItemsFlexEnd]; + }]; + [self validateFramesAlignItemsFlexEnd:root]; +} + +#pragma mark Align Items Center + +- (YogaLayoutDefinition *)layoutForAlignItemsCenter +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal, StyleAlignItems(ASStackLayoutAlignItemsCenter)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesAlignItemsCenter:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 92, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(54, 96.5, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(96, 92, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesAlignItemsCenter:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testAlignItemsCenter +{ + __block YogaLayoutDefinition *root = [self layoutForAlignItemsCenter]; + [self validateSizesAlignItemsCenter:root]; + [self validateFramesAlignItemsCenter:root]; + root = [self layoutForAlignItemsCenter]; // Test layout without sizing first + [self validateFramesAlignItemsCenter:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForAlignItemsCenter]; + }]; + [self validateFramesAlignItemsCenter:root]; +} + +#pragma mark Align Items Stretch + +- (YogaLayoutDefinition *)layoutForAlignItemsStretch +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal, StyleAlignItems(ASStackLayoutAlignItemsStretch)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesAlignItemsStretch:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(54, 16, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(96, 9, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesAlignItemsStretch:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testAlignItemsStretch +{ + __block YogaLayoutDefinition *root = [self layoutForAlignItemsStretch]; + [self validateSizesAlignItemsStretch:root]; + [self validateFramesAlignItemsStretch:root]; + root = [self layoutForAlignItemsStretch]; // Test layout without sizing first + [self validateFramesAlignItemsStretch:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForAlignItemsStretch]; + }]; + [self validateFramesAlignItemsStretch:root]; +} + +#pragma mark Align Self Flex Start + +- (YogaLayoutDefinition *)layoutForAlignSelfFlexStart +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal, StyleAlignItems(ASStackLayoutAlignItemsCenter)]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0), StyleAlignSelf(ASStackLayoutAlignSelfStart)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesAlignSelfFlexStart:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 92, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(54, 16, 40, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(96, 92, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesAlignSelfFlexStart:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testAlignSelfFlexStart +{ + __block YogaLayoutDefinition *root = [self layoutForAlignSelfFlexStart]; + [self validateSizesAlignSelfFlexStart:root]; + [self validateFramesAlignSelfFlexStart:root]; + root = [self layoutForAlignSelfFlexStart]; // Test layout without sizing first + [self validateFramesAlignSelfFlexStart:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForAlignSelfFlexStart]; + }]; + [self validateFramesAlignSelfFlexStart:root]; +} + +#pragma mark Flex Grow 1 + +- (YogaLayoutDefinition *)layoutForFlexGrowOne +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0), StyleFlexGrow(1)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesFlexGrowOne:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(54, 16, 203, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(259, 9, 40, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesFlexGrowOne:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testFlexGrowOne +{ + __block YogaLayoutDefinition *root = [self layoutForFlexGrowOne]; + [self validateSizesFlexGrowOne:root]; + [self validateFramesFlexGrowOne:root]; + root = [self layoutForFlexGrowOne]; // Test layout without sizing first + [self validateFramesFlexGrowOne:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForFlexGrowOne]; + }]; + [self validateFramesFlexGrowOne:root]; +} + +#pragma mark Flex Grow 2 + +- (YogaLayoutDefinition *)layoutForFlexGrowTwo +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0), StyleFlexGrow(1)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2), StyleFlexGrow(0.3)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesFlexGrowTwo:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(54, 16, 165.5, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(221.5, 9, 77.5, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesFlexGrowTwo:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testFlexGrowTwo +{ + __block YogaLayoutDefinition *root = [self layoutForFlexGrowTwo]; + [self validateSizesFlexGrowTwo:root]; + [self validateFramesFlexGrowTwo:root]; + root = [self layoutForFlexGrowTwo]; // Test layout without sizing first + [self validateFramesFlexGrowTwo:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForFlexGrowTwo]; + }]; + [self validateFramesFlexGrowTwo:root]; +} + +#pragma mark Flex Grow 3 + +- (YogaLayoutDefinition *)layoutForFlexGrowThree +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionVertical]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0), StyleFlexGrow(1)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2), StyleFlexGrow(0.3)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesFlexGrowThree:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 46, 40, 104.5) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 152.5, 40, 52.5) forName:@"c2" layout:root]; +} + +- (void)validateSizesFlexGrowThree:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(54, 300), @"Incorrect size"); +} + +- (void)testFlexGrowThree +{ + __block YogaLayoutDefinition *root = [self layoutForFlexGrowThree]; + [self validateSizesFlexGrowThree:root]; + [self validateFramesFlexGrowThree:root]; + root = [self layoutForFlexGrowThree]; // Test layout without sizing first + [self validateFramesFlexGrowThree:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForFlexGrowThree]; + }]; + [self validateFramesFlexGrowThree:root]; +} + +#pragma mark Flex Grow 4 + +- (YogaLayoutDefinition *)layoutForFlexGrowFour +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal]); + SetStyleBlock c0Style = StylePtSize(40, 30); + SetStyleBlock c1Style = Styles(@[StylePtSize(40, 30), StyleMargin(9, 13, 0, 0), StyleFlexGrow(0.4)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(40, 30), StyleMargin(2, 2, 2, 2), StyleFlexGrow(0.3)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesFlexGrowFour:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(54, 16, 105, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(161, 9, 89, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesFlexGrowFour:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testFlexGrowFour +{ + __block YogaLayoutDefinition *root = [self layoutForFlexGrowFour]; + [self validateSizesFlexGrowFour:root]; + [self validateFramesFlexGrowFour:root]; + root = [self layoutForFlexGrowFour]; // Test layout without sizing first + [self validateFramesFlexGrowFour:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForFlexGrowFour]; + }]; + [self validateFramesFlexGrowFour:root]; +} + +#pragma mark Flex Shrink + +- (YogaLayoutDefinition *)layoutForFlexShrink +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal]); + SetStyleBlock c0Style = Styles(@[StylePtSize(40, 30), StyleFlexGrow(0), StyleFlexShrink(0)]); + SetStyleBlock c1Style = Styles(@[StylePtSize(200, 30), StyleMargin(9, 13, 0, 0), StyleFlexShrink(1)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(250, 30), StyleMargin(2, 2, 2, 2), StyleFlexShrink(2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesFlexShrink:(YogaLayoutDefinition *)root +{ + // TODO: Yoga is producing a different layout from Chrome. + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; +// [self assertFrame:CGRectMake(54, 16, 131, 30) forName:@"c1" layout:root]; +// [self assertFrame:CGRectMake(187, 9, 112, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesFlexShrink:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(508, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(508, 300), @"Incorrect size"); +} + +- (void)testFlexShrink +{ + __block YogaLayoutDefinition *root = [self layoutForFlexShrink]; + [self validateSizesFlexShrink:root]; + [self validateFramesFlexShrink:root]; + root = [self layoutForFlexShrink]; // Test layout without sizing first + [self validateFramesFlexShrink:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForFlexShrink]; + }]; + [self validateFramesFlexShrink:root]; +} + +#pragma mark Cross Axis Shrink + +- (YogaLayoutDefinition *)layoutForCrossAxisShrink +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionVertical]); + SetStyleBlock c0Style = Styles(@[StylePtSize(40, 30)]); + SetStyleBlock c1Style = Styles(@[StylePtSize(400, 30), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(450, 30), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesCrossAxisShrink:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 46, 400, 30) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 78, 450, 30) forName:@"c2" layout:root]; +} + +- (void)validateSizesCrossAxisShrink:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(455, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 110), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(455, 300), @"Incorrect size"); +} + +- (void)testCrossAxisShrink +{ + __block YogaLayoutDefinition *root = [self layoutForCrossAxisShrink]; + [self validateSizesCrossAxisShrink:root]; + [self validateFramesCrossAxisShrink:root]; + root = [self layoutForCrossAxisShrink]; // Test layout without sizing first + [self validateFramesCrossAxisShrink:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForCrossAxisShrink]; + }]; + [self validateFramesCrossAxisShrink:root]; +} + +#pragma mark Inline Margin/Padding + +- (YogaLayoutDefinition *)layoutForInlineMarginPadding +{ + SetStyleBlock rootStyle = styleFlexDirectionHorizontal; + SetStyleBlock c0Style = Styles(@[StylePtSize(40, 30), StyleMargin(3, 7, 3, 3), styleFlexDirectionHorizontal]); + SetStyleBlock c1Style = Styles(@[StylePtSize(25, 15), StylePadding(9, 6, 6, 6)]); + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:@[c1]]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0]]; + return root; +} + +- (void)validateFramesInlineMarginPadding:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 300, 200)]; + [self assertFrame:CGRectMake(0, 0, 300, 200) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(7, 3, 40, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(0, 0, 25, 15) forName:@"c1" layout:root]; +} + +- (void)validateSizesInlineMarginPadding:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(50, 36), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 36), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(50, 300), @"Incorrect size"); +} + +- (void)testInlineMarginPadding +{ + __block YogaLayoutDefinition *root = [self layoutForInlineMarginPadding]; + [self validateSizesInlineMarginPadding:root]; + [self validateFramesInlineMarginPadding:root]; + root = [self layoutForInlineMarginPadding]; // Test layout without sizing first + [self validateFramesInlineMarginPadding:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForInlineMarginPadding]; + }]; + [self validateFramesInlineMarginPadding:root]; +} + +#pragma mark Flex Basis Row Grow 0 + +- (YogaLayoutDefinition *)layoutForFlexBasisRowGrow0 +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal]); + SetStyleBlock c0Style = Styles(@[StylePtSize(300, 30), StyleFlexBasis(10)]); + SetStyleBlock c1Style = Styles(@[StylePtWidth(10), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtWidth(100), StyleMargin(2, 2, 2, 2), StyleFlexBasis(5)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesFlexBasisRowGrow0:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 10, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(24, 16, 10, 191) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(36, 9, 5, 196) forName:@"c2" layout:root]; +} + +- (void)validateSizesFlexBasisRowGrow0:(YogaLayoutDefinition *)root +{ + // TODO: [Texture+Yoga]: Incorrect layout produced with flex basis if you call layoutThatFits first +// ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(43, 46), @"Incorrect size"); +// layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); +// layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(43, 300), @"Incorrect size"); +} + +- (void)testFlexBasisRowGrow0 +{ + __block YogaLayoutDefinition *root = [self layoutForFlexBasisRowGrow0]; + [self validateSizesFlexBasisRowGrow0:root]; + [self validateFramesFlexBasisRowGrow0:root]; + root = [self layoutForFlexBasisRowGrow0]; // Test layout without sizing first + [self validateFramesFlexBasisRowGrow0:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForFlexBasisRowGrow0]; + }]; + [self validateFramesFlexBasisRowGrow0:root]; +} + +#pragma mark Flex Basic Column Grow 0 + +- (YogaLayoutDefinition *)layoutForFlexBasisColumnGrow0 +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionVertical]); + SetStyleBlock c0Style = Styles(@[StylePtSize(300, 30), StyleFlexBasis(10)]); + SetStyleBlock c1Style = Styles(@[StylePtSize(10, 40), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(100, 70), StyleMargin(2, 2, 2, 2), StyleFlexBasis(5)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesFlexBasisColumnGrow0:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 300, 10) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 26, 10, 40) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 68, 100, 5) forName:@"c2" layout:root]; +} + +- (void)validateSizesFlexBasisColumnGrow0:(YogaLayoutDefinition *)root +{ + // TODO(b/127833220): [Texture+Yoga]: Incorrect layout produced with flex basis if you call layoutThatFits first +// ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(301, 160), @"Incorrect size"); +// layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 160), @"Incorrect size"); +// layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(301, 300), @"Incorrect size"); +} + +- (void)testFlexBasisColumnGrow0 +{ + __block YogaLayoutDefinition *root = [self layoutForFlexBasisColumnGrow0]; + [self validateSizesFlexBasisColumnGrow0:root]; + [self validateFramesFlexBasisColumnGrow0:root]; + root = [self layoutForFlexBasisColumnGrow0]; // Test layout without sizing first + [self validateFramesFlexBasisColumnGrow0:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForFlexBasisColumnGrow0]; + }]; + [self validateFramesFlexBasisColumnGrow0:root]; +} + +#pragma mark Flex Basis Row Grow 1 + +- (YogaLayoutDefinition *)layoutForFlexBasisRowGrow1 +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionHorizontal]); + SetStyleBlock c0Style = Styles(@[StylePtSize(300, 30), StyleFlexBasis(10), StyleFlexGrow(1)]); + SetStyleBlock c1Style = Styles(@[StylePtWidth(10), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtWidth(100), StyleMargin(2, 2, 2, 2), StyleFlexBasis(5), StyleFlexGrow(1)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesFlexBasisRowGrow1:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 139, 30) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(153, 16, 10, 191) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(165, 9, 134, 196) forName:@"c2" layout:root]; +} + +- (void)validateSizesFlexBasisRowGrow1:(YogaLayoutDefinition *)root +{ + // TODO(b/127833220): [Texture+Yoga]: Incorrect layout produced with flex basis if you call layoutThatFits first +// ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); +// layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); +// layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testFlexBasisRowGrow1 +{ + __block YogaLayoutDefinition *root = [self layoutForFlexBasisRowGrow1]; + [self validateSizesFlexBasisRowGrow1:root]; + [self validateFramesFlexBasisRowGrow1:root]; + root = [self layoutForFlexBasisRowGrow1]; // Test layout without sizing first + [self validateFramesFlexBasisRowGrow1:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForFlexBasisRowGrow1]; + }]; + [self validateFramesFlexBasisRowGrow1:root]; +} + +#pragma mark Flex Basis Column Grow 1 + +- (YogaLayoutDefinition *)layoutForFlexBasisColumnGrow1 +{ + SetStyleBlock rootStyle = Styles(@[StylePadding(7, 1, 0, 0), styleFlexDirectionVertical]); + SetStyleBlock c0Style = Styles(@[StylePtSize(300, 30), StyleFlexBasis(10), StyleFlexGrow(1)]); + SetStyleBlock c1Style = Styles(@[StylePtSize(10, 40), StyleMargin(9, 13, 0, 0)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(100, 70), StyleMargin(2, 2, 2, 2), StyleFlexBasis(5), StyleFlexGrow(1)]); + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"root" style:rootStyle children:@[c0, c1, c2]]; + return root; +} + +- (void)validateFramesFlexBasisColumnGrow1:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 301, 207)]; + [self assertFrame:CGRectMake(0, 0, 301, 207) forName:@"root" layout:root]; + [self assertFrame:CGRectMake(1, 7, 300, 76) forName:@"c0" layout:root]; + [self assertFrame:CGRectMake(14, 92, 10, 40) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(3, 134, 100, 71) forName:@"c2" layout:root]; +} + +- (void)validateSizesFlexBasisColumnGrow1:(YogaLayoutDefinition *)root +{ + // TODO(b/127833220): [Texture+Yoga]: Incorrect layout produced with flex basis if you call layoutThatFits first +// ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 46), @"Incorrect size"); +// layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 46), @"Incorrect size"); +// layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; +// ASXCTAssertEqualSizes(layout.size, CGSizeMake(138, 300), @"Incorrect size"); +} + +- (void)testFlexBasisColumnGrow1 +{ + __block YogaLayoutDefinition *root = [self layoutForFlexBasisColumnGrow1]; + [self validateSizesFlexBasisColumnGrow1:root]; + [self validateFramesFlexBasisColumnGrow1:root]; + root = [self layoutForFlexBasisColumnGrow1]; // Test layout without sizing first + [self validateFramesFlexBasisColumnGrow1:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForFlexBasisColumnGrow1]; + }]; + [self validateFramesFlexBasisColumnGrow1:root]; +} + +#pragma mark Absolute Position Bottom Right + +- (YogaLayoutDefinition *)layoutForAbsolutePositioningBottomRight +{ + // Yoga diverges from Chrome when there is a node with an absolute size and padding. In Chrome, + // the node's resulting size is equal to the absolute size plus padding. In Yoga, the node's + // resulting size is equal to the absolute size, and the padding is inset into that. Therefore, + // the layout that results from this is different from Chrome's layout, and this is expected. + // + // Furthermore, Yoga1 explicitly sets the min-width and min-height on the root node, which + // changes the resulting layout. Therefore, we wrap the 'root' node here with a 'superRoot' node + // to ensure that Yoga1 and Yoga2 produce the same layouts. Yoga2's behavior in this respect + // is more correct. + SetStyleBlock superRootStyle = StylePtSize(300, 200); + SetStyleBlock rootStyle = Styles(@[StylePtSize(252, 193), StylePadding(7, 11, 0, 37), styleFlexDirectionVertical]); + SetStyleBlock c0Style = Styles(@[StylePtSize(40, 30), styleFlexDirectionHorizontal]); + SetStyleBlock subNodeStyle = Styles(@[StyleMargin(5, 5, 5, 5), styleFlexDirectionHorizontal]); + SetStyleBlock c1Style = Styles(@[StylePtSize(10, 40), StyleMargin(9, 3, 0, 7)]); + SetStyleBlock a0Style = Styles(@[StylePtPosition(ASDimensionAuto, ASDimensionAuto, ASDimensionMake(7), ASDimensionMake(4), ASDimensionAuto, ASDimensionAuto), StylePtSize(225, 20), styleFlexDirectionHorizontal, StyleMargin(5, 5, 5, 5)]); + SetStyleBlock c3Style = Styles(@[StylePtSize(7, 10), StyleMargin(3, 3, 3, 3)]); + SetStyleBlock c4Style = Styles(@[StylePtSize(9, 13), StyleMargin(7, 7, 7, 7)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(100, 70), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c3 = [[YogaLayoutDefinition alloc] initWithName:@"c3" style:c3Style children:nil]; + YogaLayoutDefinition *c4 = [[YogaLayoutDefinition alloc] initWithName:@"c4" style:c4Style children:nil]; + YogaLayoutDefinition *a0 = [[YogaLayoutDefinition alloc] initWithName:@"a0" style:a0Style children:@[c3, c4]]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *subnode = [[YogaLayoutDefinition alloc] initWithName:@"subnode" style:subNodeStyle children:@[c1, a0, c2]]; + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"subroot" style:rootStyle children:@[c0, subnode]]; + YogaLayoutDefinition *superRoot = [[YogaLayoutDefinition alloc] initWithName:@"root" style:superRootStyle children:@[root]]; + return superRoot; +} + +- (void)validateFramesAbsolutePositioningBottomRight:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 300, 200)]; + // Note: Chrome produces a size of 300x200 because it adds the padding on the outside of the + // 252x193 node. + [self assertFrame:CGRectMake(0, 0, 252, 193) forName:@"subroot" layout:root]; + [self assertFrame:CGRectMake(11, 7, 40, 30) forName:@"c0" layout:root]; + // Note: Chrome produces a size of 242x74 because it insets the padding within the 300x200 outer + // bounds, while Yoga insets it from the 252x193 bounds. + [self assertFrame:CGRectMake(16, 42, 194, 74) forName:@"subnode" layout:root]; + [self assertFrame:CGRectMake(3, 9, 10, 40) forName:@"c1" layout:root]; + // Note: Chrome produces a position of 8,42 because it computes a different size for 'subnode'. + [self assertFrame:CGRectMake(-40, 42, 225, 20) forName:@"a0" layout:root]; + [self assertFrame:CGRectMake(3, 3, 7, 10) forName:@"c3" layout:root]; + [self assertFrame:CGRectMake(20, 7, 9, 13) forName:@"c4" layout:root]; + [self assertFrame:CGRectMake(22, 2, 100, 70) forName:@"c2" layout:root]; +} + +- (void)validateSizesAbsolutePositioningBottomRight:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 200), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 200), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 300), @"Incorrect size"); +} + +- (void)testAbsolutePositioningBottomRight +{ + __block YogaLayoutDefinition *root = [self layoutForAbsolutePositioningBottomRight]; + [self validateSizesAbsolutePositioningBottomRight:root]; + [self validateFramesAbsolutePositioningBottomRight:root]; + root = [self layoutForAbsolutePositioningBottomRight]; // Test layout without sizing first + [self validateFramesAbsolutePositioningBottomRight:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForAbsolutePositioningBottomRight]; + }]; + [self validateFramesAbsolutePositioningBottomRight:root]; +} + +#pragma mark Aboslute Position Bottom Trailing + +- (YogaLayoutDefinition *)layoutForAbsolutePositioningBottomTrailing +{ + // See note in layoutForAbsolutePositioningBottomRight for explanation of superRoot. + SetStyleBlock superRootStyle = StylePtSize(300, 200); + SetStyleBlock rootStyle = Styles(@[StylePtSize(252, 193), StylePadding(7, 11, 0, 37), styleFlexDirectionVertical]); + SetStyleBlock c0Style = Styles(@[StylePtSize(40, 30), styleFlexDirectionHorizontal]); + SetStyleBlock subNodeStyle = Styles(@[StyleMargin(5, 5, 5, 5), styleFlexDirectionHorizontal]); + SetStyleBlock c1Style = Styles(@[StylePtSize(10, 40), StyleMargin(9, 3, 0, 7)]); + SetStyleBlock a0Style = Styles(@[StylePtPosition(ASDimensionAuto, ASDimensionAuto, ASDimensionMake(7), ASDimensionAuto, ASDimensionAuto, ASDimensionMake(4)), StylePtSize(225, 20), styleFlexDirectionHorizontal, StyleMargin(5, 5, 5, 5)]); + SetStyleBlock c3Style = Styles(@[StylePtSize(7, 10), StyleMargin(3, 3, 3, 3)]); + SetStyleBlock c4Style = Styles(@[StylePtSize(9, 13), StyleMargin(7, 7, 7, 7)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(100, 70), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c3 = [[YogaLayoutDefinition alloc] initWithName:@"c3" style:c3Style children:nil]; + YogaLayoutDefinition *c4 = [[YogaLayoutDefinition alloc] initWithName:@"c4" style:c4Style children:nil]; + YogaLayoutDefinition *a0 = [[YogaLayoutDefinition alloc] initWithName:@"a0" style:a0Style children:@[c3, c4]]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *subnode = [[YogaLayoutDefinition alloc] initWithName:@"subnode" style:subNodeStyle children:@[c1, a0, c2]]; + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"subroot" style:rootStyle children:@[c0, subnode]]; + YogaLayoutDefinition *superRoot = [[YogaLayoutDefinition alloc] initWithName:@"root" style:superRootStyle children:@[root]]; + return superRoot; +} + +- (void)validateFramesAbsolutePositioningBottomTrailing:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 300, 200)]; + // Note: Chrome produces a size of 300x200 because it adds the padding on the outside of the + // 252x193 node. + [self assertFrame:CGRectMake(0, 0, 252, 193) forName:@"subroot" layout:root]; + [self assertFrame:CGRectMake(11, 7, 40, 30) forName:@"c0" layout:root]; + // Note: Chrome produces a size of 242x74 because it insets the padding within the 300x200 outer + // bounds, while Yoga insets it from the 252x193 bounds. + [self assertFrame:CGRectMake(16, 42, 194, 74) forName:@"subnode" layout:root]; + [self assertFrame:CGRectMake(3, 9, 10, 40) forName:@"c1" layout:root]; + // Note: Chrome produces a position of 8,42 because it computes a different size for 'subnode'. + [self assertFrame:CGRectMake(-40, 42, 225, 20) forName:@"a0" layout:root]; + [self assertFrame:CGRectMake(3, 3, 7, 10) forName:@"c3" layout:root]; + [self assertFrame:CGRectMake(20, 7, 9, 13) forName:@"c4" layout:root]; + [self assertFrame:CGRectMake(22, 2, 100, 70) forName:@"c2" layout:root]; +} + +- (void)validateSizesAbsolutePositioningBottomTrailing:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 200), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 200), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 300), @"Incorrect size"); +} + +- (void)testAbsolutePositioningBottomTrailing +{ + __block YogaLayoutDefinition *root = [self layoutForAbsolutePositioningBottomTrailing]; + [self validateSizesAbsolutePositioningBottomTrailing:root]; + [self validateFramesAbsolutePositioningBottomTrailing:root]; + root = [self layoutForAbsolutePositioningBottomTrailing]; // Test layout without sizing first + [self validateFramesAbsolutePositioningBottomTrailing:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForAbsolutePositioningBottomTrailing]; + }]; + [self validateFramesAbsolutePositioningBottomTrailing:root]; +} + +#pragma mark Absolute Position Top Left + +- (YogaLayoutDefinition *)layoutForAbsolutePositioningTopLeft +{ + // See note in layoutForAbsolutePositioningBottomRight for explanation of superRoot. + SetStyleBlock superRootStyle = StylePtSize(300, 200); + SetStyleBlock rootStyle = Styles(@[StylePtSize(252, 193), StylePadding(7, 11, 0, 37), styleFlexDirectionVertical]); + SetStyleBlock c0Style = Styles(@[StylePtSize(40, 30), styleFlexDirectionHorizontal]); + SetStyleBlock subNodeStyle = Styles(@[StyleMargin(5, 5, 5, 5), styleFlexDirectionHorizontal]); + SetStyleBlock c1Style = Styles(@[StylePtSize(10, 40), StyleMargin(9, 3, 0, 7)]); + SetStyleBlock a0Style = Styles(@[StylePtPosition(ASDimensionMake(9), ASDimensionMake(2), ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionAuto), StylePtSize(225, 20), styleFlexDirectionHorizontal, StyleMargin(5, 5, 5, 5)]); + SetStyleBlock c3Style = Styles(@[StylePtSize(7, 10), StyleMargin(3, 3, 3, 3)]); + SetStyleBlock c4Style = Styles(@[StylePtSize(9, 13), StyleMargin(7, 7, 7, 7)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(100, 70), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c3 = [[YogaLayoutDefinition alloc] initWithName:@"c3" style:c3Style children:nil]; + YogaLayoutDefinition *c4 = [[YogaLayoutDefinition alloc] initWithName:@"c4" style:c4Style children:nil]; + YogaLayoutDefinition *a0 = [[YogaLayoutDefinition alloc] initWithName:@"a0" style:a0Style children:@[c3, c4]]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *subnode = [[YogaLayoutDefinition alloc] initWithName:@"subnode" style:subNodeStyle children:@[c1, a0, c2]]; + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"subroot" style:rootStyle children:@[c0, subnode]]; + YogaLayoutDefinition *superRoot = [[YogaLayoutDefinition alloc] initWithName:@"root" style:superRootStyle children:@[root]]; + return superRoot; +} + +- (void)validateFramesAbsolutePositioningTopLeft:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 300, 200)]; + // Note: Chrome produces a size of 300x200 because it adds the padding on the outside of the + // 252x193 node. + [self assertFrame:CGRectMake(0, 0, 252, 193) forName:@"subroot" layout:root]; + [self assertFrame:CGRectMake(11, 7, 40, 30) forName:@"c0" layout:root]; + // Note: Chrome produces a size of 242x74 because it insets the padding within the 300x200 outer + // bounds, while Yoga insets it from the 252x193 bounds. + [self assertFrame:CGRectMake(16, 42, 194, 74) forName:@"subnode" layout:root]; + [self assertFrame:CGRectMake(3, 9, 10, 40) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(7, 14, 225, 20) forName:@"a0" layout:root]; + [self assertFrame:CGRectMake(3, 3, 7, 10) forName:@"c3" layout:root]; + [self assertFrame:CGRectMake(20, 7, 9, 13) forName:@"c4" layout:root]; + [self assertFrame:CGRectMake(22, 2, 100, 70) forName:@"c2" layout:root]; +} + +- (void)validateSizesAbsolutePositioningTopLeft:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 200), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 200), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 300), @"Incorrect size"); +} + +- (void)testAbsolutePositioningTopLeft +{ + __block YogaLayoutDefinition *root = [self layoutForAbsolutePositioningTopLeft]; + [self validateSizesAbsolutePositioningTopLeft:root]; + [self validateFramesAbsolutePositioningTopLeft:root]; + root = [self layoutForAbsolutePositioningTopLeft]; // Test layout without sizing first + [self validateFramesAbsolutePositioningTopLeft:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForAbsolutePositioningTopLeft]; + }]; + [self validateFramesAbsolutePositioningTopLeft:root]; +} + +#pragma mark Absolute Position Top Leading + +- (YogaLayoutDefinition *)layoutForAbsolutePositioningTopLeading +{ + // See note in layoutForAbsolutePositioningBottomRight for explanation of superRoot. + SetStyleBlock superRootStyle = StylePtSize(300, 200); + SetStyleBlock rootStyle = Styles(@[StylePtSize(252, 193), StylePadding(7, 11, 0, 37), styleFlexDirectionVertical]); + SetStyleBlock c0Style = Styles(@[StylePtSize(40, 30), styleFlexDirectionHorizontal]); + SetStyleBlock subNodeStyle = Styles(@[StyleMargin(5, 5, 5, 5), styleFlexDirectionHorizontal]); + SetStyleBlock c1Style = Styles(@[StylePtSize(10, 40), StyleMargin(9, 3, 0, 7)]); + SetStyleBlock a0Style = Styles(@[StylePtPosition(ASDimensionMake(9), ASDimensionAuto, ASDimensionAuto, ASDimensionAuto, ASDimensionMake(2), ASDimensionAuto), StylePtSize(225, 20), styleFlexDirectionHorizontal, StyleMargin(5, 5, 5, 5)]); + SetStyleBlock c3Style = Styles(@[StylePtSize(7, 10), StyleMargin(3, 3, 3, 3)]); + SetStyleBlock c4Style = Styles(@[StylePtSize(9, 13), StyleMargin(7, 7, 7, 7)]); + SetStyleBlock c2Style = Styles(@[StylePtSize(100, 70), StyleMargin(2, 2, 2, 2)]); + YogaLayoutDefinition *c3 = [[YogaLayoutDefinition alloc] initWithName:@"c3" style:c3Style children:nil]; + YogaLayoutDefinition *c4 = [[YogaLayoutDefinition alloc] initWithName:@"c4" style:c4Style children:nil]; + YogaLayoutDefinition *a0 = [[YogaLayoutDefinition alloc] initWithName:@"a0" style:a0Style children:@[c3, c4]]; + YogaLayoutDefinition *c1 = [[YogaLayoutDefinition alloc] initWithName:@"c1" style:c1Style children:nil]; + YogaLayoutDefinition *c2 = [[YogaLayoutDefinition alloc] initWithName:@"c2" style:c2Style children:nil]; + YogaLayoutDefinition *subnode = [[YogaLayoutDefinition alloc] initWithName:@"subnode" style:subNodeStyle children:@[c1, a0, c2]]; + YogaLayoutDefinition *c0 = [[YogaLayoutDefinition alloc] initWithName:@"c0" style:c0Style children:nil]; + YogaLayoutDefinition *root = [[YogaLayoutDefinition alloc] initWithName:@"subroot" style:rootStyle children:@[c0, subnode]]; + YogaLayoutDefinition *superRoot = [[YogaLayoutDefinition alloc] initWithName:@"root" style:superRootStyle children:@[root]]; + return superRoot; +} + +- (void)validateFramesAbsolutePositioningTopLeading:(YogaLayoutDefinition *)root +{ + [root layoutIfNeededWithFrame:CGRectMake(0, 0, 300, 200)]; + // Note: Chrome produces a size of 300x200 because it adds the padding on the outside of the + // 252x193 node. + [self assertFrame:CGRectMake(0, 0, 252, 193) forName:@"subroot" layout:root]; + [self assertFrame:CGRectMake(11, 7, 40, 30) forName:@"c0" layout:root]; + // Note: Chrome produces a size of 242x74 because it insets the padding within the 300x200 outer + // bounds, while Yoga insets it from the 252x193 bounds. + [self assertFrame:CGRectMake(16, 42, 194, 74) forName:@"subnode" layout:root]; + [self assertFrame:CGRectMake(3, 9, 10, 40) forName:@"c1" layout:root]; + [self assertFrame:CGRectMake(7, 14, 225, 20) forName:@"a0" layout:root]; + [self assertFrame:CGRectMake(3, 3, 7, 10) forName:@"c3" layout:root]; + [self assertFrame:CGRectMake(20, 7, 9, 13) forName:@"c4" layout:root]; + [self assertFrame:CGRectMake(22, 2, 100, 70) forName:@"c2" layout:root]; +} + +- (void)validateSizesAbsolutePositioningTopLeading:(YogaLayoutDefinition *)root +{ + ASLayout *layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 0), CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 200), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(300, 0), CGSizeMake(300, CGFLOAT_MAX))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 200), @"Incorrect size"); + layout = [root.node layoutThatFits:ASSizeRangeMake(CGSizeMake(0, 300), CGSizeMake(CGFLOAT_MAX, 300))]; + ASXCTAssertEqualSizes(layout.size, CGSizeMake(300, 300), @"Incorrect size"); +} + +- (void)testAbsolutePositioningTopLeading +{ + __block YogaLayoutDefinition *root = [self layoutForAbsolutePositioningTopLeading]; + [self validateSizesAbsolutePositioningTopLeading:root]; + [self validateFramesAbsolutePositioningTopLeading:root]; + root = [self layoutForAbsolutePositioningTopLeading]; // Test layout without sizing first + [self validateFramesAbsolutePositioningTopLeading:root]; + + // Test async + [self executeOffThread:^{ + root = [self layoutForAbsolutePositioningTopLeading]; + }]; + [self validateFramesAbsolutePositioningTopLeading:root]; +} + +#pragma mark - Tree Diffing Tests + +- (void)testValidateTreeDiffing +{ + // This test tests our tree diffing code in our YogaLayoutDefinition test class. + YogaLayoutDefinition *fromRoot = [self layoutForSizing]; + + // Changing from same layout, just different properties + YogaLayoutDefinition *toRoot = [self layoutForMarginPadding]; + [fromRoot applyTreeDiffsToMatch:toRoot]; + [fromRoot layoutIfNeeded]; + [self validateFramesMarginPadding:fromRoot]; + + // Changing to completely different layout + toRoot = [self layoutForAbsolutePositioningTopLeft]; + [fromRoot applyTreeDiffsToMatch:toRoot]; + [fromRoot layoutIfNeeded]; + [self validateFramesAbsolutePositioningTopLeft:fromRoot]; + + // Changing back to original layout + toRoot = [self layoutForSizing]; + [fromRoot applyTreeDiffsToMatch:toRoot]; + [fromRoot layoutIfNeeded]; + [self validateFramesSizing:fromRoot]; +} + +- (void)testTreeDiffing { + // In this test, we use the test tree diffing code in YogaLayoutDefinition to mutate all layouts + // to each other layout and then ensures they lay out properly. + NSArray *allLayouts = @[ + [self layoutForSimpleYogaTree], + [self layoutForChangingMargins1], + [self layoutForChangingMargins2], + [self layoutForSizing], + [self layoutForMarginPadding], + [self layoutForDirection:styleFlexDirectionVertical], + [self layoutForJustifyContentFlexEnd], + [self layoutForJustifyContentCenter], + [self layoutForJustifyContentSpaceBetween], + [self layoutForJustifyContentSpaceAround], + [self layoutForJustifyContentFlexEndColumn], + [self layoutForJustifyContentCenterColumn], + [self layoutForJustifyContentSpaceBetweenColumn], + [self layoutForJustifyContentSpaceAroundColumn], + [self layoutForAlignItemsFlexEnd], // 14 + [self layoutForAlignItemsCenter], + [self layoutForAlignItemsStretch], + [self layoutForAlignSelfFlexStart], + [self layoutForFlexGrowOne], + [self layoutForFlexGrowTwo], + [self layoutForFlexGrowThree], + [self layoutForFlexGrowFour], + [self layoutForFlexShrink], + [self layoutForCrossAxisShrink], + [self layoutForInlineMarginPadding], + [self layoutForFlexBasisRowGrow0], // 25 + [self layoutForFlexBasisColumnGrow0], + [self layoutForFlexBasisRowGrow1], + [self layoutForFlexBasisColumnGrow1], + [self layoutForAbsolutePositioningBottomRight], // 29 + [self layoutForAbsolutePositioningBottomTrailing], + [self layoutForAbsolutePositioningTopLeft], + [self layoutForAbsolutePositioningTopLeading], + [self layoutForDirection:styleFlexDirectionHorizontal], + [self layoutForDirection:styleFlexDirectionVerticalReverse], + [self layoutForDirection:styleFlexDirectionHorizontalReverse], + ]; + + static const SEL allValidateFrameSelectors[] = { + @selector(validateFramesSimpleYogaTree:), + @selector(validateFramesChangingMargin1:), + @selector(validateFramesChangingMargin2:), + @selector(validateFramesSizing:), + @selector(validateFramesMarginPadding:), + @selector(validateFramesDirectionColumn:), + @selector(validateFramesJustifyContentFlexEnd:), + @selector(validateFramesJustifyContentCenter:), + @selector(validateFramesJustifyContentSpaceBetween:), + @selector(validateFramesJustifyContentSpaceAround:), + @selector(validateFramesJustifyContentFlexEndColumn:), + @selector(validateFramesJustifyContentCenterColumn:), + @selector(validateFramesJustifyContentSpaceBetweenColumn:), + @selector(validateFramesJustifyContentSpaceAroundColumn:), + @selector(validateFramesAlignItemsFlexEnd:), + @selector(validateFramesAlignItemsCenter:), + @selector(validateFramesAlignItemsStretch:), + @selector(validateFramesAlignSelfFlexStart:), + @selector(validateFramesFlexGrowOne:), + @selector(validateFramesFlexGrowTwo:), + @selector(validateFramesFlexGrowThree:), + @selector(validateFramesFlexGrowFour:), + @selector(validateFramesFlexShrink:), + @selector(validateFramesCrossAxisShrink:), + @selector(validateFramesInlineMarginPadding:), + @selector(validateFramesFlexBasisRowGrow0:), + @selector(validateFramesFlexBasisColumnGrow0:), + @selector(validateFramesFlexBasisRowGrow1:), + @selector(validateFramesFlexBasisColumnGrow1:), + @selector(validateFramesAbsolutePositioningBottomRight:), + @selector(validateFramesAbsolutePositioningBottomTrailing:), + @selector(validateFramesAbsolutePositioningTopLeft:), + @selector(validateFramesAbsolutePositioningTopLeading:), + @selector(validateFramesDirectionRow:), + @selector(validateFramesDirectionColumnReverse:), + @selector(validateFramesDirectionRowReverse:), + }; + + for (int from = 0; from < [allLayouts count]; from++) { + for (int to = 0; to < [allLayouts count]; to++) { + __block YogaLayoutDefinition *fromLayout = [[YogaLayoutDefinition alloc] initWithLayout:allLayouts[from]]; + __block YogaLayoutDefinition *toLayout = [[YogaLayoutDefinition alloc] initWithLayout:allLayouts[to]]; + SEL fromValidateSelector = allValidateFrameSelectors[from]; + SEL toValidateSelector = allValidateFrameSelectors[to]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self performSelector:fromValidateSelector withObject:fromLayout]; +#pragma clang diagnostic pop + + [fromLayout applyTreeDiffsToMatch:toLayout]; + [fromLayout layoutIfNeeded]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self performSelector:toValidateSelector withObject:fromLayout]; +#pragma clang diagnostic pop + + // Test async + [self executeOffThread:^{ + fromLayout = [[YogaLayoutDefinition alloc] initWithLayout:allLayouts[from]]; + toLayout = [[YogaLayoutDefinition alloc] initWithLayout:allLayouts[to]]; + }]; + +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self performSelector:fromValidateSelector withObject:fromLayout]; +#pragma clang diagnostic pop + + [self executeOffThread:^{ + [fromLayout applyTreeDiffsToMatch:toLayout]; + }]; + + [fromLayout layoutIfNeeded]; +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Warc-performSelector-leaks" + [self performSelector:toValidateSelector withObject:fromLayout]; +#pragma clang diagnostic pop + } + } +} + +YGSize _measureFraction(YGNodeRef node, + float width, + YGMeasureMode widthMode, + float height, + YGMeasureMode heightMode) { + NSCAssert(width != YGUndefined, @"Expected fixed width in measure function."); + *(int *)YGNodeGetContext(node) += 1; + return (YGSize){ + .width = width, + .height = 100.75 + }; +} + +// Test for fix for https://github.com/facebook/yoga/issues/877 +- (void)testYogaLayoutIsRoundedEvenIfCached +{ + YGNodeRef node = YGNodeNew(); + int *measureCount = new int(0); + YGNodeSetContext(node, measureCount); + YGNodeSetMeasureFunc(node, &_measureFraction); + YGNodeCalculateLayout(node, 100, YGUndefined, YGDirectionInherit); + XCTAssertEqual(YGNodeLayoutGetHeight(node), 101.0); + YGNodeCalculateLayout(node, 100, YGUndefined, YGDirectionInherit); + XCTAssertEqual(YGNodeLayoutGetHeight(node), 101.0); + XCTAssertEqual(*measureCount, 1); + delete measureCount; + YGNodeFree(node); +} + +@end diff --git a/Tests/ASDisplayViewAccessibilityTests.mm b/Tests/ASDisplayViewAccessibilityTests.mm index d511e98fe..fb3185be6 100644 --- a/Tests/ASDisplayViewAccessibilityTests.mm +++ b/Tests/ASDisplayViewAccessibilityTests.mm @@ -11,24 +11,29 @@ // #import +#import #import #import #import -#import -#import -#import -#import -#import #import #import -#import +#import +#import +#import +#import +#import +#import +#import +#import + #import "ASDisplayNodeTestsHelper.h" +#import "ASTestCase.h" #import extern void SortAccessibilityElements(NSMutableArray *elements); -@interface ASDisplayViewAccessibilityTests : XCTestCase +@interface ASDisplayViewAccessibilityTests : ASTestCase @end @implementation ASDisplayViewAccessibilityTests @@ -57,6 +62,32 @@ - (void)testAccessibilityElementsAccessors XCTAssertEqual([node.view indexOfAccessibilityElement:node.view.accessibilityElements.firstObject], 0);*/ } +- (void)testManualSettingOfAccessiblityElements +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + + ASTextNode *subnode = [[ASTextNode alloc] init]; + subnode.layerBacked = YES; + subnode.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + subnode.frame = CGRectMake(50, 100, 200, 200); + [container addSubnode:subnode]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + + XCTAssertEqual(container.view.accessibilityElements.count, 1); + + // Clearing the accessibility elements + XCTAssertEqual(container.view.accessibilityElements.count, 1, @"Clearing the accessibility elements should requery the accessibility elements."); + + UIAccessibilityElement *accessibilityElementOne = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container.view]; + UIAccessibilityElement *accessibilityElementTwo = [[UIAccessibilityElement alloc] initWithAccessibilityContainer:container.view]; + container.accessibilityElements = @[accessibilityElementOne, accessibilityElementTwo]; + XCTAssertEqual(container.view.accessibilityElements.count, 2); + XCTAssertEqualObjects(container.view.accessibilityElements[0], accessibilityElementOne); + XCTAssertEqualObjects(container.view.accessibilityElements[1], accessibilityElementTwo); +} + - (void)testThatSubnodeAccessibilityLabelAggregationWorks { // Setup nodes @@ -110,6 +141,89 @@ - (void)testThatContainerAccessibilityLabelOverrideStopsAggregation [node.view.accessibilityElements.firstObject accessibilityLabel]); } +- (void)testImplicitCustomActionSynthesizing { + // Setup nodes + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + + // Initialize nodes with relevant accessibility data + node.isAccessibilityContainer = YES; + node.accessibilityLabel = @"hello"; + innerNode.accessibilityLabel = @"world"; + innerNode.accessibilityTraits = UIAccessibilityTraitButton; + + // Attach the subnode to the parent node, then ensure the parent's accessibility label does not + // get aggregated with the subnode's label + [node addSubnode:innerNode]; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:node]; + [window makeKeyAndVisible]; + + XCTAssertEqualObjects([node.view.accessibilityElements.firstObject accessibilityLabel], @"hello", + @"Container accessibility label override broken %@", + [node.view.accessibilityElements.firstObject accessibilityLabel]); + UIAccessibilityCustomAction *customAction = + [[node.view.accessibilityElements.firstObject accessibilityCustomActions] firstObject]; + XCTAssertEqualObjects(customAction.name, @"world"); +} + +- (void)testImplicitCustomActionSynthesizing_DisabledAtRootContainerLevel { + // Setup nodes + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + + // Initialize nodes with relevant accessibility data + node.isAccessibilityContainer = YES; + node.accessibilityLabel = @"hello"; + node.accessibilityTraits = UIAccessibilityTraitButton; + innerNode.accessibilityLabel = @"world"; + innerNode.accessibilityTraits = UIAccessibilityTraitButton; + + // Attach the subnode to the parent node, then ensure the parent's accessibility label does not + // get aggregated with the subnode's label + [node addSubnode:innerNode]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:node]; + [window makeKeyAndVisible]; + + XCTAssertEqualObjects([node.view.accessibilityElements.firstObject accessibilityLabel], @"hello", + @"Container accessibility label override broken %@", + [node.view.accessibilityElements.firstObject accessibilityLabel]); + UIAccessibilityCustomAction *customAction = + [[node.view.accessibilityElements.firstObject accessibilityCustomActions] firstObject]; + XCTAssertEqualObjects(customAction.name, @"world"); +} + +- (void)testThatContainerAccessibilityLabelOverrideStopsAggregation_ButtonTraitSet { + // Setup nodes + ASDisplayNode *node = nil; + ASDisplayNode *innerNode = nil; + node = [[ASDisplayNode alloc] init]; + innerNode = [[ASDisplayNode alloc] init]; + + // Initialize nodes with relevant accessibility data + node.isAccessibilityContainer = YES; + node.accessibilityLabel = @"hello"; + node.accessibilityTraits = UIAccessibilityTraitButton; + innerNode.accessibilityLabel = @"world"; + + // Attach the subnode to the parent node, then ensure the parent's accessibility label does not + // get aggregated with the subnode's label + [node addSubnode:innerNode]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:node]; + [window makeKeyAndVisible]; + + XCTAssertEqualObjects([node.view.accessibilityElements.firstObject accessibilityLabel], @"hello", + @"Container accessibility label override broken %@", + [node.view.accessibilityElements.firstObject accessibilityLabel]); +} + - (void)testAccessibilityLayerBackedContainerWithinAccessibilityContainer { ASDisplayNode *container = [[ASDisplayNode alloc] init]; @@ -189,6 +303,9 @@ - (void)testAccessibilityNonLayerbackedNodesOperationInNonContainer } - (void)testAccessibilityElementsAreNilForWrappedWKWebView { + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalDoNotCacheAccessibilityElements | ASExperimentalEnableNodeIsHiddenFromAcessibility | ASExperimentalEnableAcessibilityElementsReturnNil; + [ASConfigurationManager test_resetWithConfiguration:config]; ASDisplayNode *container = [[ASDisplayNode alloc] init]; UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; [window addSubnode:container]; @@ -216,6 +333,139 @@ - (void)testAccessibilityElementsAreNilForWrappedWKWebView { XCTAssertNil(accessibilityElements); } +/** + * Tests trimming of attributedAccessibilityLabel in the aggregation process within an accessibility + * container with only one subnode that has a one character whitespace set as attributedText. + */ +- (void)testAccessibilityContainerAttributedAccessibilityLabelAggregationEmptyString { + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + container.isAccessibilityContainer = YES; + + ASTextNode *text1 = [[ASTextNode alloc] init]; + text1.attributedText = [[NSAttributedString alloc] initWithString:@" "]; + [container addSubnode:text1]; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + + NSArray *accessibilityElements = container.view.accessibilityElements; + XCTAssertEqual(accessibilityElements.count, 1); + XCTAssertEqualObjects(accessibilityElements[0].accessibilityLabel, @""); +} + +/** + * Tests trimming of attributedAccessibilityLabel in the aggregation process within an accessibility + * container with the first subnode that has a one character whitespace set as attributedText. + */ +- (void)testAccessibilityContainerAttributedAccessibilityLabelAggregationBeginning { + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + container.isAccessibilityContainer = YES; + + ASTextNode *text1 = [[ASTextNode alloc] init]; + text1.attributedText = [[NSAttributedString alloc] initWithString:@" "]; + [container addSubnode:text1]; + + ASTextNode *text2 = [[ASTextNode alloc] init]; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"hello\n"]; + [container addSubnode:text2]; + + ASTextNode *text3 = [[ASTextNode alloc] init]; + text3.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + [container addSubnode:text3]; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + + NSArray *accessibilityElements = container.view.accessibilityElements; + XCTAssertEqual(accessibilityElements.count, 1); + XCTAssertEqualObjects(accessibilityElements[0].accessibilityLabel, @"hello, world"); +} + +/** + * Tests trimming of attributedAccessibilityLabel in the aggregation process within an accessibility + * container with the middle of three subnodes that has a one character whitespace set + * as attributedText. + */ +- (void)testAccessibilityContainerAttributedAccessibilityLabelAggregationMiddle { + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + container.isAccessibilityContainer = YES; + + ASTextNode *text1 = [[ASTextNode alloc] init]; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + [container addSubnode:text1]; + + ASTextNode *text2 = [[ASTextNode alloc] init]; + text2.attributedText = [[NSAttributedString alloc] initWithString:@" "]; + [container addSubnode:text2]; + + ASTextNode *text3 = [[ASTextNode alloc] init]; + text3.attributedText = [[NSAttributedString alloc] initWithString:@"world\n"]; + [container addSubnode:text3]; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + + NSArray *accessibilityElements = container.view.accessibilityElements; + XCTAssertEqual(accessibilityElements.count, 1); + XCTAssertEqualObjects(accessibilityElements[0].accessibilityLabel, @"hello, world"); +} + +/** + * Tests trimming of attributedAccessibilityLabel in the aggregation process within an accessibility + * container with the last of three subnodes has a one character whitespace set as attributedText. + */ +- (void)testAccessibilityContainerAttributedAccessibilityLabelAggregationEnd { + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + container.isAccessibilityContainer = YES; + + ASTextNode *text1 = [[ASTextNode alloc] init]; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + [container addSubnode:text1]; + + ASTextNode *text2 = [[ASTextNode alloc] init]; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + [container addSubnode:text2]; + + ASTextNode *text3 = [[ASTextNode alloc] init]; + text3.attributedText = [[NSAttributedString alloc] initWithString:@" "]; + [container addSubnode:text3]; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + + NSArray *accessibilityElements = container.view.accessibilityElements; + XCTAssertEqual(accessibilityElements.count, 1); + XCTAssertEqualObjects(accessibilityElements[0].accessibilityLabel, @"hello, world"); +} + +- (void)testNestedAccessibilityContainerWithLabelAndTrait { + ASDisplayNode *outerContainerNode = [[ASDisplayNode alloc] init]; + ASDisplayNode *containerNode = [[ASDisplayNode alloc] init]; + outerContainerNode.isAccessibilityContainer = YES; + containerNode.isAccessibilityContainer = NO; + + ASImageNode *imageNode = [[ASImageNode alloc] init]; + imageNode.isAccessibilityContainer = YES; + imageNode.accessibilityLabel = @"hello"; + imageNode.accessibilityTraits = UIAccessibilityTraitButton; + + [containerNode addSubnode:imageNode]; + [outerContainerNode addSubnode:containerNode]; + + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:outerContainerNode]; + [window makeKeyAndVisible]; + + NSArray *accessibilityElements = + outerContainerNode.view.accessibilityElements; + XCTAssertEqual(accessibilityElements.count, 2); + XCTAssertEqualObjects(accessibilityElements[0].accessibilityLabel, @"hello"); +} + #pragma mark - #pragma mark UIAccessibilityAction Forwarding @@ -272,7 +522,7 @@ - (void)testThatAccessibilityElementsWorks { [containerNode addSubnode:label]; [containerNode addSubnode:button]; - + // force load __unused UIView *view = containerNode.view; @@ -282,32 +532,41 @@ - (void)testThatAccessibilityElementsWorks { XCTAssertEqual([elements.lastObject asyncdisplaykit_node], button); } -- (void)testThatAccessibilityElementsOverrideWorks { +- (void)disable_testThatAccessibilityElementsOverrideWorks { + ASDisplayNode *containerNode = [[ASDisplayNode alloc] init]; containerNode.frame = CGRectMake(0, 0, 100, 200); + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:containerNode]; + [window makeKeyAndVisible]; + ASTextNode *label = [[ASTextNode alloc] init]; label.attributedText = [[NSAttributedString alloc] initWithString:@"test label"]; label.frame = CGRectMake(0, 0, 100, 20); - + ASButtonNode *button = [[ASButtonNode alloc] init]; [button setTitle:@"tap me" withFont:[UIFont systemFontOfSize:17] withColor:nil forState:UIControlStateNormal]; [button addTarget:self action:@selector(fakeSelector:) forControlEvents:ASControlNodeEventTouchUpInside]; button.frame = CGRectMake(0, 25, 100, 20); - + [containerNode addSubnode:label]; [containerNode addSubnode:button]; containerNode.accessibilityElements = @[ label ]; - + // force load __unused UIView *view = containerNode.view; - + NSArray *elements = [containerNode.view accessibilityElements]; XCTAssertTrue(elements.count == 1); XCTAssertEqual(elements.firstObject, label); } - (void)testHiddenAccessibilityElements { + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalDoNotCacheAccessibilityElements | ASExperimentalEnableNodeIsHiddenFromAcessibility; + [ASConfigurationManager test_resetWithConfiguration:config]; + ASDisplayNode *containerNode = [[ASDisplayNode alloc] init]; UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; [window addSubnode:containerNode]; @@ -336,6 +595,10 @@ - (void)testHiddenAccessibilityElements { } - (void)testTransparentAccessibilityElements { + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalDoNotCacheAccessibilityElements | ASExperimentalEnableNodeIsHiddenFromAcessibility; + [ASConfigurationManager test_resetWithConfiguration:config]; + ASDisplayNode *containerNode = [[ASDisplayNode alloc] init]; UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; [window addSubnode:containerNode]; @@ -363,7 +626,10 @@ - (void)testTransparentAccessibilityElements { } - (void)testAccessibilityElementsNotInAppWindow { - + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalDoNotCacheAccessibilityElements | ASExperimentalEnableNodeIsHiddenFromAcessibility | ASExperimentalEnableAcessibilityElementsReturnNil; + [ASConfigurationManager test_resetWithConfiguration:config]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 568)]; ASDisplayNode *node = [[ASDisplayNode alloc] init]; node.automaticallyManagesSubnodes = YES; @@ -517,12 +783,11 @@ - (void)testCustomAccessibilitySort { XCTAssertEqual(elements[3], node4); } -- (void)testSubnodeIsModal { - +- (void)DISABLE_testSubnodeIsModal { UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 568)]; ASDisplayNode *node = [[ASDisplayNode alloc] init]; node.automaticallyManagesSubnodes = YES; - + ASDKViewController *vc = [[ASDKViewController alloc] initWithNode:node]; window.rootViewController = vc; [window makeKeyAndVisible]; @@ -532,21 +797,21 @@ - (void)testSubnodeIsModal { label1.attributedText = [[NSAttributedString alloc] initWithString:@"label1"]; label1.frame = CGRectMake(10, 80, 300, 20); [node addSubnode:label1]; - + ASTextNode *label2 = [[ASTextNode alloc] init]; label2.attributedText = [[NSAttributedString alloc] initWithString:@"label2"]; label2.frame = CGRectMake(10, CGRectGetMaxY(label1.frame) + 8, 300, 20); [node addSubnode:label2]; - + ASDisplayNode *modalNode = [[ASDisplayNode alloc] init]; modalNode.frame = CGRectInset(CGRectUnion(label1.frame, label2.frame), -8, -8); - + // This is kind of cheating. When voice over is activated, the modal node will end up reporting that it // has 1 accessibilityElement. But getting that to happen in a unit test doesn't seem possible. id modalMock = OCMPartialMock(modalNode); OCMStub([modalMock accessibilityElementCount]).andReturn(1); [node addSubnode:modalMock]; - + ASTextNode *label3 = [[ASTextNode alloc] init]; label3.attributedText = [[NSAttributedString alloc] initWithString:@"label6"]; label3.frame = CGRectMake(8, 4, 200, 20); @@ -558,12 +823,11 @@ - (void)testSubnodeIsModal { XCTAssertTrue([elements containsObject:modalNode.view]); } -- (void)testMultipleSubnodesAreModal { - +- (void)DISABLE_testMultipleSubnodesAreModal { UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 568)]; ASDisplayNode *node = [[ASDisplayNode alloc] init]; node.automaticallyManagesSubnodes = YES; - + ASDKViewController *vc = [[ASDKViewController alloc] initWithNode:node]; window.rootViewController = vc; [window makeKeyAndVisible]; @@ -573,15 +837,15 @@ - (void)testMultipleSubnodesAreModal { label1.attributedText = [[NSAttributedString alloc] initWithString:@"label1"]; label1.frame = CGRectMake(10, 80, 300, 20); [node addSubnode:label1]; - + ASTextNode *label2 = [[ASTextNode alloc] init]; label2.attributedText = [[NSAttributedString alloc] initWithString:@"label2"]; label2.frame = CGRectMake(10, CGRectGetMaxY(label1.frame) + 8, 300, 20); [node addSubnode:label2]; - + ASDisplayNode *modalNode1 = [[ASDisplayNode alloc] init]; modalNode1.frame = CGRectInset(CGRectUnion(label1.frame, label2.frame), -8, -8); - + // This is kind of cheating. When voice over is activated, the modal node will end up reporting that it // has 1 accessibilityElement. But getting that to happen in a unit test doesn't seem possible. id modalMock1 = OCMPartialMock(modalNode1); @@ -603,7 +867,7 @@ - (void)testMultipleSubnodesAreModal { label4.frame = CGRectMake(8, 4, 200, 20); [modalNode2 addSubnode:label4]; modalNode2.accessibilityViewIsModal = YES; - + // add modalNode1 last, and assert that it is the one that appears in accessibilityElements // (UIKit uses the last modal subview in subviews as the modal element). [node addSubnode:modalMock2]; @@ -612,7 +876,7 @@ - (void)testMultipleSubnodesAreModal { NSArray *elements = [node.view accessibilityElements]; XCTAssertTrue(elements.count == 1); XCTAssertTrue([elements containsObject:modalNode1.view]); - + // let's change which node is modal and make sure the elements get updated. modalNode1.accessibilityViewIsModal = NO; elements = [node.view accessibilityElements]; @@ -621,7 +885,10 @@ - (void)testMultipleSubnodesAreModal { } - (void)testAccessibilityElementsHidden { - + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + config.experimentalFeatures = ASExperimentalDoNotCacheAccessibilityElements | ASExperimentalEnableNodeIsHiddenFromAcessibility; + [ASConfigurationManager test_resetWithConfiguration:config]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 568)]; ASDisplayNode *node = [[ASDisplayNode alloc] init]; node.automaticallyManagesSubnodes = YES; diff --git a/Tests/ASImageNodeSnapshotTests.mm b/Tests/ASImageNodeSnapshotTests.mm index 13364930b..b17b498b3 100644 --- a/Tests/ASImageNodeSnapshotTests.mm +++ b/Tests/ASImageNodeSnapshotTests.mm @@ -10,6 +10,8 @@ #import "ASSnapshotTestCase.h" #import +#import +#import #if AS_AT_LEAST_IOS13 static UIImage* makeImageWithColor(UIColor *color, CGSize size) { @@ -31,9 +33,18 @@ @implementation ASImageNodeSnapshotTests - (void)setUp { [super setUp]; + ASConfiguration *config = [ASConfiguration new]; + config.experimentalFeatures = ASExperimentalFillTemplateImagesWithTintColor; + [ASConfigurationManager test_resetWithConfiguration:config]; + self.recordMode = NO; } +- (void)tearDown +{ + [super tearDown]; +} + - (UIImage *)testImage { NSString *path = [[NSBundle bundleForClass:[self class]] pathForResource:@"logo-square" @@ -102,7 +113,7 @@ - (void)testTintColorOnNodePropertyAlwaysTemplate - (void)testTintColorOnGrayscaleNodePropertyAlwaysTemplate { ASConfiguration *config = [ASConfiguration new]; - config.experimentalFeatures = ASExperimentalDrawingGlobal; + config.experimentalFeatures = ASExperimentalDrawingGlobal | ASExperimentalFillTemplateImagesWithTintColor; [ASConfigurationManager test_resetWithConfiguration:config]; UIImage *test = [self testGrayscaleImage]; @@ -229,7 +240,7 @@ - (void)testUIGraphicsRendererDrawingExperiment { // Test to ensure that rendering with UIGraphicsRenderer don't regress ASConfiguration *config = [ASConfiguration new]; - config.experimentalFeatures = ASExperimentalDrawingGlobal; + config.experimentalFeatures = ASExperimentalDrawingGlobal | ASExperimentalFillTemplateImagesWithTintColor; [ASConfigurationManager test_resetWithConfiguration:config]; ASImageNode *imageNode = [[ASImageNode alloc] init]; @@ -239,9 +250,14 @@ - (void)testUIGraphicsRendererDrawingExperiment } #if AS_AT_LEAST_IOS13 -- (void)testDynamicAssetImage +- (void)disabled_testDynamicAssetImage { if (@available(iOS 13.0, *)) { + // enable experimantal callback for traits change + ASConfiguration *config = [ASConfiguration new]; + config.experimentalFeatures = ASExperimentalTraitCollectionDidChangeWithPreviousCollection; + [ASConfigurationManager test_resetWithConfiguration:config]; + UIImage *image = [UIImage imageNamed:@"light-dark" inBundle:[NSBundle bundleForClass:[self class]] compatibleWithTraitCollection:nil]; ASImageNode *node = [[ASImageNode alloc] init]; node.image = image; @@ -260,6 +276,11 @@ - (void)testDynamicAssetImage - (void)testDynamicTintColor { if (@available(iOS 13.0, *)) { + // enable experimental callback for traits change + ASConfiguration *config = [ASConfiguration new]; + config.experimentalFeatures = ASExperimentalTraitCollectionDidChangeWithPreviousCollection | ASExperimentalFillTemplateImagesWithTintColor; + [ASConfigurationManager test_resetWithConfiguration:config]; + UIImage *image = makeImageWithColor(UIColor.redColor, CGSize{.width = 100, .height = 100}); image = [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; UIColor* tintColor = UIColor.systemBackgroundColor; @@ -280,4 +301,32 @@ - (void)testDynamicTintColor } } #endif // #if AS_AT_LEAST_IOS13 +#if YOGA +- (void)testFlipsForRightToLeftLayoutDirection +{ + ASImageNode *node = [[ASImageNode alloc] init]; + [node enableYoga]; + YGNodeStyleSetDirection([node.style yogaNode], YGDirectionRTL); + + UIImage *test = [self testImage]; + node.image = test; + + UIView *view = [[UIView alloc] initWithFrame:(CGRect){CGPointZero, test.size}]; + [view addSubnode:node]; + ASDisplayNodeSizeToFitSize(node, test.size); + + [view layoutIfNeeded]; + + ASSnapshotVerifyNode(node, @"normal"); + + node.flipsForRightToLeftLayoutDirection = YES; + + ASSnapshotVerifyNode(node, @"flipped"); + + node.flipsForRightToLeftLayoutDirection = NO; + + ASSnapshotVerifyNode(node, @"unflipped"); +} +#endif + @end diff --git a/Tests/ASIntegerMapTests.mm b/Tests/ASIntegerMapTests.mm index d23e5bf49..d3ad5e090 100644 --- a/Tests/ASIntegerMapTests.mm +++ b/Tests/ASIntegerMapTests.mm @@ -7,7 +7,7 @@ // #import "ASTestCase.h" -#import "ASIntegerMap.h" +#import @interface ASIntegerMapTests : ASTestCase diff --git a/Tests/ASLayoutElementStyleTests.mm b/Tests/ASLayoutElementStyleTests.mm index f6f95066e..ec21b7b3b 100644 --- a/Tests/ASLayoutElementStyleTests.mm +++ b/Tests/ASLayoutElementStyleTests.mm @@ -11,21 +11,6 @@ #import "ASXCTExtensions.h" #import -#pragma mark - ASLayoutElementStyleTestsDelegate - -@interface ASLayoutElementStyleTestsDelegate : NSObject -@property (copy, nonatomic) NSString *propertyNameChanged; -@end - -@implementation ASLayoutElementStyleTestsDelegate - -- (void)style:(id)style propertyDidChange:(NSString *)propertyName -{ - self.propertyNameChanged = propertyName; -} - -@end - #pragma mark - ASLayoutElementStyleTests @interface ASLayoutElementStyleTests : XCTestCase @@ -113,15 +98,5 @@ - (void)testSettingSizeViaLayoutSize XCTAssertTrue(ASDimensionEqualToDimension(style.maxLayoutSize.width, layoutSize.width)); XCTAssertTrue(ASDimensionEqualToDimension(style.maxLayoutSize.height, layoutSize.height)); } - -- (void)testSettingPropertiesWillCallDelegate -{ - ASLayoutElementStyleTestsDelegate *delegate = [ASLayoutElementStyleTestsDelegate new]; - ASLayoutElementStyle *style = [[ASLayoutElementStyle alloc] initWithDelegate:delegate]; - XCTAssertTrue(ASDimensionEqualToDimension(style.width, ASDimensionAuto)); - style.width = ASDimensionMake(100); - XCTAssertTrue(ASDimensionEqualToDimension(style.width, ASDimensionMake(100))); - XCTAssertTrue([delegate.propertyNameChanged isEqualToString:ASLayoutElementStyleWidthProperty]); -} @end diff --git a/Tests/ASLayoutEngineTests.mm b/Tests/ASLayoutEngineTests.mm index 7a0b87599..9845bf012 100644 --- a/Tests/ASLayoutEngineTests.mm +++ b/Tests/ASLayoutEngineTests.mm @@ -120,7 +120,7 @@ - (void)testSetNeedsLayoutAndNormalLayoutPass * then to get the measurement completion call on main, * then to get animateLayoutTransition: and didCompleteLayoutTransition: on main. */ -- (void)testLayoutTransitionWithAsyncMeasurement +- (void)disable_testLayoutTransitionWithAsyncMeasurement { [self stubCalculatedLayoutDidChange]; [self runFirstLayoutPassWithFixture:fixture1]; diff --git a/Tests/ASLayoutSpecSnapshotTestsHelper.mm b/Tests/ASLayoutSpecSnapshotTestsHelper.mm index dec82d4c3..9b872d46f 100644 --- a/Tests/ASLayoutSpecSnapshotTestsHelper.mm +++ b/Tests/ASLayoutSpecSnapshotTestsHelper.mm @@ -9,10 +9,11 @@ #import "ASLayoutSpecSnapshotTestsHelper.h" +#import +#import #import -#import #import -#import +#import @interface ASTestNode : ASDisplayNode @property (nonatomic, nullable) ASLayoutSpec *layoutSpecUnderTest; diff --git a/Tests/ASLayoutSpecTests.mm b/Tests/ASLayoutSpecTests.mm index ff13b6655..0497f2247 100644 --- a/Tests/ASLayoutSpecTests.mm +++ b/Tests/ASLayoutSpecTests.mm @@ -39,6 +39,15 @@ @implementation ASLayoutElementStyle (ASDKExtendedLayoutElement) ASDK_STYLE_PROP_OBJ(NSString *, extendedName, setExtendedName); @end +@interface ASLayoutElementStyleYoga (ASDKExtendedLayoutElement) +@end + +@implementation ASLayoutElementStyleYoga (ASDKExtendedLayoutElement) +ASDK_STYLE_PROP_PRIM(CGFloat, extendedWidth, setExtendedWidth, 0); +ASDK_STYLE_PROP_STR(ASDimension, extendedDimension, setExtendedDimension, ASDimensionMake(ASDimensionUnitAuto, 0)); +ASDK_STYLE_PROP_OBJ(NSString *, extendedName, setExtendedName); +@end + /* * As the ASLayoutElementStyle conforms to the ASDKExtendedLayoutElement protocol now, ASDKExtendedLayoutElement properties * can be accessed in ASDKExtendedLayoutSpec diff --git a/Tests/ASLayoutSpecUtilitiesTests.mm b/Tests/ASLayoutSpecUtilitiesTests.mm index fb099fa80..1b05c0a00 100644 --- a/Tests/ASLayoutSpecUtilitiesTests.mm +++ b/Tests/ASLayoutSpecUtilitiesTests.mm @@ -7,7 +7,7 @@ // #import -#import "ASLayoutSpecUtilities.h" +#import @interface ASLayoutSpecUtilitiesTests : XCTestCase diff --git a/Tests/ASNetworkImageNodeTests.mm b/Tests/ASNetworkImageNodeTests.mm index af2d1a35f..c1d15e57d 100644 --- a/Tests/ASNetworkImageNodeTests.mm +++ b/Tests/ASNetworkImageNodeTests.mm @@ -35,6 +35,12 @@ - (void)setUp cache = [OCMockObject partialMockForObject:[[ASTestImageCache alloc] init]]; downloader = [OCMockObject partialMockForObject:[[ASTestImageDownloader alloc] init]]; node = [[ASNetworkImageNode alloc] initWithCache:cache downloader:downloader]; + ASSetEnableImageDownloadSynchronization(NO); +} + +- (void)setUpEnablingExperiments +{ + ASSetEnableImageDownloadSynchronization(YES); } /// Test is flaky: https://github.com/facebook/AsyncDisplayKit/issues/2898 @@ -74,6 +80,15 @@ - (void)testThatProgressBlockIsSetAndClearedCorrectlyOnChangeURL [[downloader expect] setProgressImageBlock:[OCMArg isNotNil] callbackQueue:OCMOCK_ANY withDownloadIdentifier:@1]; node.URL = [NSURL URLWithString:@"http://imageB"]; [downloader verifyWithDelay:5]; + + // Make the node invisible. + [node exitInterfaceState:ASInterfaceStateInHierarchy]; +} + +- (void)testThatProgressBlockIsSetAndClearedCorrectlyOnChangeURLWithExperiments +{ + [self setUpEnablingExperiments]; + [self testThatProgressBlockIsSetAndClearedCorrectlyOnChangeURL]; } - (void)testThatAnimatedImageClearedCorrectlyOnChangeURL @@ -86,6 +101,7 @@ - (void)testThatAnimatedImageClearedCorrectlyOnChangeURL [node setURL:[NSURL URLWithString:@"http://imageA"] resetToDefault:YES]; XCTAssertEqualObjects(nil, node.animatedImage); + [node exitInterfaceState:ASInterfaceStateInHierarchy]; } - (void)testThatSettingAnImageWillStayForEnteringAndExitingPreloadState @@ -103,6 +119,12 @@ - (void)testThatSettingAnImageWillStayForEnteringAndExitingPreloadState XCTAssertEqualObjects(image, networkImageNode.image); } +- (void)testThatSettingAnImageWillStayForEnteringAndExitingPreloadStateWithExperiments +{ + [self setUpEnablingExperiments]; + [self testThatSettingAnImageWillStayForEnteringAndExitingPreloadState]; +} + - (void)testThatSettingADefaultImageWillStayForEnteringAndExitingPreloadState { UIImage *image = [[UIImage alloc] init]; @@ -118,6 +140,12 @@ - (void)testThatSettingADefaultImageWillStayForEnteringAndExitingPreloadState XCTAssertEqualObjects(image, networkImageNode.defaultImage); } +- (void)testThatSettingADefaultImageWillStayForEnteringAndExitingPreloadStateWithExperiments +{ + [self setUpEnablingExperiments]; + [self testThatSettingADefaultImageWillStayForEnteringAndExitingPreloadState]; +} + @end @implementation ASTestImageCache @@ -169,7 +197,7 @@ - (CFTimeInterval)totalDuration - (NSUInteger)frameInterval { - return 0.2; + return 2; } - (size_t)loopCount diff --git a/Tests/ASNodeContextTests.mm b/Tests/ASNodeContextTests.mm new file mode 100644 index 000000000..a50dcb00d --- /dev/null +++ b/Tests/ASNodeContextTests.mm @@ -0,0 +1,74 @@ +// +// ASNodeContextTests.m +// AsyncDisplayKitTests +// +// Created by Adlai Holler on 7/1/19. +// Copyright © 2019 Pinterest. All rights reserved. +// + +#import "ASTestCase.h" + +#import +#import + +@interface ASNodeContextTests : ASTestCase + +@end + +@implementation ASNodeContextTests + +- (void)testBasicStackBehavior +{ + XCTAssertNil(ASNodeContextGet()); + ASNodeContext *ctx = [[ASNodeContext alloc] init]; + ASNodeContextPush(ctx); + XCTAssertEqualObjects(ASNodeContextGet(), ctx); + ASNodeContext *ctx2 = [[ASNodeContext alloc] init]; + ASNodeContextPush(ctx2); + XCTAssertEqualObjects(ASNodeContextGet(), ctx2); + ASNodeContextPop(); + XCTAssertEqualObjects(ASNodeContextGet(), ctx); + ASNodeContextPop(); + XCTAssertNil(ASNodeContextGet()); +} + +- (void)testNodesInheritContext +{ + ASNodeContext *ctx = [[ASNodeContext alloc] init]; + ASNodeContextPush(ctx); + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASNodeContextPop(); + XCTAssertEqualObjects(node.nodeContext, ctx); +} + +- (void)testNodesShareContextLock +{ + ASNodeContext *ctx = [[ASNodeContext alloc] init]; + ASNodeContextPush(ctx); + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASDisplayNode *n2 = [[ASDisplayNode alloc] init]; + ASNodeContextPop(); + XCTAssertEqualObjects(node.nodeContext, ctx); + XCTAssertEqualObjects(n2.nodeContext, ctx); + [node lock]; + // Jump to another thread and try to lock n2. It should fail. + XCTestExpectation *e = [self expectationWithDescription:@"n2 was blocked"]; + [NSThread detachNewThreadWithBlock:^{ + XCTAssertFalse([n2 tryLock]); + [e fulfill]; + }]; + [self waitForExpectationsWithTimeout:3 handler:nil]; + [node unlock]; +} + +- (void)testMixingContextsThrows +{ + ASNodeContext *ctx = [[ASNodeContext alloc] init]; + ASNodeContextPush(ctx); + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASNodeContextPop(); + ASDisplayNode *noContextNode = [[ASDisplayNode alloc] init]; + XCTAssertThrows([node addSubnode:noContextNode]); +} + +@end diff --git a/Tests/ASRunLoopQueueTests.mm b/Tests/ASRunLoopQueueTests.mm index 1396f9043..808df4b73 100644 --- a/Tests/ASRunLoopQueueTests.mm +++ b/Tests/ASRunLoopQueueTests.mm @@ -19,10 +19,16 @@ @interface QueueObject : NSObject @end @implementation QueueObject + +- (BOOL)shouldCoalesceInterfaceStateDuringTransaction { + return NO; +} + - (void)prepareForCATransactionCommit { self.queueObjectProcessed = YES; } + @end @interface ASRunLoopQueueTests : ASTestCase diff --git a/Tests/ASStackLayoutSpecSnapshotTests.mm b/Tests/ASStackLayoutSpecSnapshotTests.mm index ac6a2a254..1e4b2704f 100644 --- a/Tests/ASStackLayoutSpecSnapshotTests.mm +++ b/Tests/ASStackLayoutSpecSnapshotTests.mm @@ -1216,8 +1216,8 @@ - (void)testBaselineAlignmentWithStretchedItem - (void)testFlexWrapWithItemSpacings { ASStackLayoutSpecStyle style = { - .spacing = 50, .direction = ASStackLayoutDirectionHorizontal, + .spacing = 50, .flexWrap = ASStackLayoutFlexWrapWrap, .alignContent = ASStackLayoutAlignContentStart, .lineSpacing = 5, @@ -1246,8 +1246,8 @@ - (void)testFlexWrapWithItemSpacings - (void)testFlexWrapWithItemSpacingsBeingResetOnNewLines { ASStackLayoutSpecStyle style = { - .spacing = 5, .direction = ASStackLayoutDirectionHorizontal, + .spacing = 5, .flexWrap = ASStackLayoutFlexWrapWrap, .alignContent = ASStackLayoutAlignContentStart, .lineSpacing = 5, @@ -1328,8 +1328,8 @@ - (void)testAlignContentStretchAndOtherAlignments ASStackLayoutSpecStyle style = { .direction = ASStackLayoutDirectionHorizontal, .flexWrap = ASStackLayoutFlexWrapWrap, - .alignContent = ASStackLayoutAlignContentStretch, .alignItems = ASStackLayoutAlignItemsStart, + .alignContent = ASStackLayoutAlignContentStretch, }; CGSize subnodeSize = {50, 50}; diff --git a/Tests/ASTableViewTests.mm b/Tests/ASTableViewTests.mm index b908a8751..ae6644c88 100644 --- a/Tests/ASTableViewTests.mm +++ b/Tests/ASTableViewTests.mm @@ -799,7 +799,7 @@ - (void)testThatNilBatchUpdatesCanBeSubmitted [node performBatchAnimated:NO updates:nil completion:nil]; } -- (void)testItemsInsertedIntoThePreloadRangeGetPreloaded +- (void)DISABLED_testItemsInsertedIntoThePreloadRangeGetPreloaded { // Start table node setup ATableViewTestController *testController = [[ATableViewTestController alloc] initWithNibName:nil bundle:nil]; diff --git a/Tests/ASTableViewThrashTests.mm b/Tests/ASTableViewThrashTests.mm index 695bf67ff..ef69c2ab8 100644 --- a/Tests/ASTableViewThrashTests.mm +++ b/Tests/ASTableViewThrashTests.mm @@ -56,14 +56,14 @@ - (void)recordFailureWithDescription:(NSString *)description inFile:(NSString *) #pragma mark Test Methods // Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. -- (void)testInitialDataRead +- (void)disable_testInitialDataRead { ASThrashDataSource *ds = [[ASThrashDataSource alloc] initTableViewDataSourceWithData:[ASThrashTestSection sectionsWithCount:kInitialSectionCount]]; [self verifyDataSource:ds]; } /// Replays the Base64 representation of an ASThrashUpdate from "ASThrashTestRecordedCase" file -- (void)testRecordedThrashCase +- (void)disable_testRecordedThrashCase { NSURL *caseURL = [[NSBundle bundleForClass:[self class]] URLForResource:@"ASThrashTestRecordedCase" withExtension:nil subdirectory:@"TestResources"]; NSString *base64 = [NSString stringWithContentsOfURL:caseURL encoding:NSUTF8StringEncoding error:NULL]; @@ -80,7 +80,7 @@ - (void)testRecordedThrashCase } // Disabled temporarily due to issue where cell nodes are not marked invisible before deallocation. -- (void)testThrashingWildly +- (void)disable_testThrashingWildly { for (NSInteger i = 0; i < kThrashingIterationCount; i++) { [self setUp]; diff --git a/Tests/ASTextKitTests.mm b/Tests/ASTextKitTests.mm index 0f1a3ec72..83078c3c8 100644 --- a/Tests/ASTextKitTests.mm +++ b/Tests/ASTextKitTests.mm @@ -91,6 +91,7 @@ static BOOL checkAttributes(const ASTextKitAttributes &attributes, const CGSize FBSnapshotTestController *controller = [[FBSnapshotTestController alloc] init]; UIImage *labelImage = UITextViewImageWithAttributes(attributes, constrainedSize, linkTextAttributes); UIImage *textKitImage = ASTextKitImageWithAttributes(attributes, constrainedSize); + // This is using a newer than google3 version of FBSnapShotTestController api. return [controller compareReferenceImage:labelImage toImage:textKitImage overallTolerance:0.0 error:nil]; } diff --git a/Tests/ASTextNode2SnapshotTests.mm b/Tests/ASTextNode2SnapshotTests.mm index 0cda2ce45..ef41acbb7 100644 --- a/Tests/ASTextNode2SnapshotTests.mm +++ b/Tests/ASTextNode2SnapshotTests.mm @@ -19,10 +19,11 @@ @interface LineBreakConfig : NSObject @property (nonatomic, assign) NSUInteger numberOfLines; @property (nonatomic, assign) NSLineBreakMode lineBreakMode; +@property (nonatomic) NSString *text; + (NSArray *)configs; -- (instancetype)initWithNumberOfLines:(NSUInteger)numberOfLines lineBreakMode:(NSLineBreakMode)lineBreakMode; +- (instancetype)initWithNumberOfLines:(NSUInteger)numberOfLines lineBreakMode:(NSLineBreakMode)lineBreakMode text:(NSString *)text; - (NSString *)breakModeDescription; @end @@ -39,7 +40,10 @@ @implementation LineBreakConfig for (int i = 0; i <= 3; i++) { for (int j = NSLineBreakByWordWrapping; j <= NSLineBreakByTruncatingMiddle; j++) { if (j == NSLineBreakByClipping) continue; - [setup addObject:[[LineBreakConfig alloc] initWithNumberOfLines:i lineBreakMode:(NSLineBreakMode) j]]; + [setup addObject:[[LineBreakConfig alloc] initWithNumberOfLines:i lineBreakMode:(NSLineBreakMode) j text:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua."]]; + if (j == NSLineBreakByTruncatingMiddle && i == 1) { + [setup addObject:[[LineBreakConfig alloc] initWithNumberOfLines:i lineBreakMode:(NSLineBreakMode) j text:@"Lorem ipsum dolor sit amet, consectetur adipiscing"]]; + } } allConfigs = [NSArray arrayWithArray:setup]; @@ -48,12 +52,13 @@ @implementation LineBreakConfig return allConfigs; } -- (instancetype)initWithNumberOfLines:(NSUInteger)numberOfLines lineBreakMode:(NSLineBreakMode)lineBreakMode +- (instancetype)initWithNumberOfLines:(NSUInteger)numberOfLines lineBreakMode:(NSLineBreakMode)lineBreakMode text:(NSString *)text { self = [super init]; if (self != nil) { _numberOfLines = numberOfLines; _lineBreakMode = lineBreakMode; + _text = text; return self; } @@ -155,12 +160,7 @@ - (void)testTextTruncationModes_ASTextNode2 UILabel *reference = [[UILabel alloc] init]; ASTextNode *textNode = [[ASTextNode alloc] init]; // ASTextNode2 - NSMutableAttributedString *refString = [[NSMutableAttributedString alloc] initWithString:@"Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." - attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:18.0f] }]; - NSMutableAttributedString *asString = [refString mutableCopy]; - reference.attributedText = refString; - textNode.attributedText = asString; CGSize size = (CGSize) {container.bounds.size.width, 120.0}; CGPoint origin = (CGPoint) {CGRectGetWidth(container.bounds) / 2 - size.width / 2, CGRectGetHeight(container.bounds) / 2 - size.height / 2}; // center @@ -195,13 +195,22 @@ - (void)testTextTruncationModes_ASTextNode2 NSArray *c = [LineBreakConfig configs]; for (LineBreakConfig *config in c) { + NSMutableAttributedString *refString = [[NSMutableAttributedString alloc] + initWithString:config.text attributes:@{ NSFontAttributeName : [UIFont systemFontOfSize:18.0f] }]; + NSMutableAttributedString *asString = [refString mutableCopy]; + reference.attributedText = refString; + textNode.attributedText = asString; + reference.lineBreakMode = textNode.truncationMode = config.lineBreakMode; reference.numberOfLines = textNode.maximumNumberOfLines = config.numberOfLines; description.text = config.description; [container setNeedsLayout]; NSString *identifier = [NSString stringWithFormat:@"%@_%luLines", [config breakModeDescription], (unsigned long)config.numberOfLines]; + if (config.text.length < 60) { + identifier = [identifier stringByAppendingString:@"_Short"]; + } [ASSnapshotTestCase hackilySynchronouslyRecursivelyRenderNode:textNode]; - ASSnapshotVerifyViewWithTolerance(container, identifier, 0.01); + ASSnapshotVerifyViewWithTolerance(container, identifier, 0); } } @@ -281,6 +290,41 @@ - (void)testShadowing_ASTextNode2 ASSnapshotVerifyNode(textNode, nil); } +- (void)testThatTruncationTokenDefaultInheritsAttributesFromText +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.style.maxSize = CGSizeMake(20, 80); + + textNode.attributedText = [[NSMutableAttributedString alloc] initWithString:@"Quality is an important thing" attributes:@{ + NSForegroundColorAttributeName : [UIColor grayColor], + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle), + }]; + textNode.truncationMode = NSLineBreakByTruncatingTail; + + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(textNode, nil); +} + +- (void)testThatTruncationTokenAttributesOverwriteThoseInheritedFromTextWhenTruncateTailMode_ASTextNode2 +{ + ASTextNode *textNode = [[ASTextNode alloc] init]; + textNode.style.maxSize = CGSizeMake(20, 80); + + NSMutableAttributedString *mas = [[NSMutableAttributedString alloc] initWithString:@"Quality is an important thing" attributes:@{ + NSForegroundColorAttributeName : [UIColor grayColor], + NSUnderlineStyleAttributeName : @(NSUnderlineStyleSingle), + }]; + textNode.attributedText = mas; + textNode.truncationMode = NSLineBreakByTruncatingTail; + + textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"\u2026" attributes:@{ + NSForegroundColorAttributeName : [UIColor greenColor], + NSUnderlineStyleAttributeName: @(NSUnderlineStyleNone), + }]; + ASDisplayNodeSizeToFitSizeRange(textNode, ASSizeRangeMake(CGSizeZero, CGSizeMake(INFINITY, INFINITY))); + ASSnapshotVerifyNode(textNode, nil); +} + /** * https://github.com/TextureGroup/Texture/issues/822 */ diff --git a/Tests/ASTextNode2Tests.mm b/Tests/ASTextNode2Tests.mm index 70d25cb26..790293f51 100644 --- a/Tests/ASTextNode2Tests.mm +++ b/Tests/ASTextNode2Tests.mm @@ -11,10 +11,13 @@ #import #import -#import #import +#import +#import +#import #import "ASTestCase.h" +#import "ASDisplayNodeTestsHelper.h" @interface ASTextNode2Tests : XCTestCase @@ -28,6 +31,12 @@ @implementation ASTextNode2Tests - (void)setUp { [super setUp]; + + ASSetEnableTextTruncationVisibleRange(NO); + // Reset configuration on every setup + ASConfiguration *config = [[ASConfiguration alloc] initWithDictionary:nil]; + [ASConfigurationManager test_resetWithConfiguration:config]; + _textNode = [[ASTextNode2 alloc] init]; UIFontDescriptor *desc = [UIFontDescriptor fontDescriptorWithName:@"Didot" size:18]; @@ -64,6 +73,19 @@ - (void)setUp _textNode.attributedText = _attributedText; } +- (void)setUpEnablingExperiments { + BOOL prevValue = ASGetEnableImprovedTextTruncationVisibleRange(); + ASSetEnableImprovedTextTruncationVisibleRange(YES); + [self addTeardownBlock:^{ + ASSetEnableImprovedTextTruncationVisibleRange(prevValue); + }]; + BOOL prevLastLineFixValue = ASGetEnableImprovedTextTruncationVisibleRangeLastLineFix(); + ASSetEnableImprovedTextTruncationVisibleRangeLastLineFix(YES); + [self addTeardownBlock:^{ + ASSetEnableImprovedTextTruncationVisibleRangeLastLineFix(prevLastLineFixValue); + }]; +} + - (void)testTruncation { XCTAssertTrue([(ASTextNode *)_textNode shouldTruncateForConstrainedSize:ASSizeRangeMake(CGSizeMake(100, 100))], @"Text Node should truncate"); @@ -72,9 +94,156 @@ - (void)testTruncation XCTAssertTrue(_textNode.isTruncated, @"Text Node should be truncated"); } -- (void)testAccessibility +- (void)testTruncationWithTruncatedLinkAtTheEnd { + [self setUpEnablingExperiments]; + + NSString *link = @"https://medium.com/pinterest-engineering/" + @"introducing-texture-a-new-home-for-asyncdisplaykit-e7c003308f50"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] + initWithString:[NSString stringWithFormat:@"A link: %@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; + textNode.attributedText = attributedText; + textNode.frame = CGRectMake(0, 0, 100, 50); + textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"...Read More"]; + // Call calculateSizeThatFits: to enforce _truncationToken to be set on _textContainer + [textNode calculateSizeThatFits:CGSizeMake(100, 50)]; + // Trigger calculation of layouts on the node manually, otherwise the internal + // text container will not have any size + [textNode layoutThatFits:ASSizeRangeMake(CGSizeMake(99, 49), CGSizeMake(100, 50))]; + + id linkAttribute = nil; + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(50.0, 40.0) + attributeName:nil + range:nil]; + XCTAssertTrue(linkAttribute == nil, @"Should not trigger the truncated link!"); +} + +- (void)testClickOnTruncationTokenWithUntruncatedLinkAtTheEnd { + [self setUpEnablingExperiments]; + + // The truncation token is appended to the third line text directly without truncation on the + // original text. + NSString *link = @"l1\n l2\n l3\n l4"; + NSMutableAttributedString *attributedText = + [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; + textNode.attributedText = attributedText; + textNode.frame = CGRectMake(0, 0, 100, 50); + textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"more"]; + // Call calculateSizeThatFits: to enforce _truncationToken to be set on _textContainer + [textNode calculateSizeThatFits:CGSizeMake(100, 50)]; + // Trigger calculation of layouts on the node manually, otherwise the internal + // text container will not have any size + [textNode layoutThatFits:ASSizeRangeMake(CGSizeMake(99, 49), CGSizeMake(100, 50))]; + + // Click on the truncation token. + id linkAttribute = nil; + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(30.0, 40.0) + attributeName:nil + range:nil]; + XCTAssertTrue(linkAttribute == nil, @"Should not trigger the link!"); + + // Click on the link. + linkAttribute = nil; + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(10.0, 40.0) + attributeName:nil + range:nil]; + XCTAssertFalse(linkAttribute == nil, @"Should trigger the link!"); +} + +- (void)testClickNearBordersOfTruncationToken { + [self setUpEnablingExperiments]; + + NSString *link = @"https://medium.com/pinterest-engineering/" + @"introducing-texture-a-new-home-for-asyncdisplaykit-e7c003308f50"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] + initWithString:[NSString stringWithFormat:@"A link: %@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; + textNode.attributedText = attributedText; + textNode.frame = CGRectMake(0, 0, 100, 50); + textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"...Read More"]; + // Call calculateSizeThatFits: to enforce _truncationToken to be set on _textContainer + [textNode calculateSizeThatFits:CGSizeMake(100, 50)]; + // Trigger calculation of layouts on the node manually, otherwise the internal + // text container will not have any size + [textNode layoutThatFits:ASSizeRangeMake(CGSizeMake(99, 49), CGSizeMake(100, 50))]; + + // The bounds of the last line before the truncation token is: + // origin: (x = 0, y = 32.76) size: (width = 30.02, height = 12) + + id linkAttribute = nil; + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(31.0, 40.0) + attributeName:nil + range:nil]; + + XCTAssertNil(linkAttribute, @"Should not trigger the truncated link, since the click is " + @"inside the truncation token bounds!"); + + linkAttribute = nil; + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(30.0, 40.0) + attributeName:nil + range:nil]; + XCTAssertNotNil( + linkAttribute, + @"Should trigger the truncated link, since the click is left to the truncation token!"); + + linkAttribute = nil; + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(40.0, 33.0) + attributeName:nil + range:nil]; + XCTAssertNil(linkAttribute, @"Should not trigger the truncated link, since the click is " + @"inside the truncation token bounds!"); + + linkAttribute = nil; + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(40.0, 32.0) + attributeName:nil + range:nil]; + XCTAssertNotNil( + linkAttribute, + @"Should trigger the truncated link, since the click is above the truncation token!"); +} + +- (void)testUntruncatedLinkWithTruncationAttributedTextSet { + [self setUpEnablingExperiments]; + + NSString *link = @"https://texturegroup.org/"; + NSMutableAttributedString *attributedText = + [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; + textNode.attributedText = attributedText; + textNode.frame = CGRectMake(0, 0, 100, 100); + textNode.truncationAttributedText = [[NSAttributedString alloc] initWithString:@"...Read More"]; + // Call calculateSizeThatFits: to enforce _truncationToken to be set on _textContainer + [textNode calculateSizeThatFits:CGSizeMake(100, 100)]; + // Trigger calculation of layouts on the node manually, otherwise the internal + // text container will not have any size + [textNode layoutThatFits:ASSizeRangeMake(CGSizeMake(99, 99), CGSizeMake(100, 100))]; + + id linkAttribute = nil; + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(15.0, 10.0) + attributeName:nil + range:nil]; + XCTAssertTrue( + ![textNode isTruncated], + @"The text node should not be truncated even when truncationAttributedText is set!"); + XCTAssertTrue(linkAttribute != nil, @"Should trigger the untruncated link at this point!"); +} + +- (void)testBasicAccessibility { - XCTAssertTrue(_textNode.isAccessibilityElement, @"Should be an accessibility element"); + XCTAssertFalse(_textNode.isAccessibilityElement, @"Should not be an accessibility element"); XCTAssertTrue(_textNode.accessibilityTraits == UIAccessibilityTraitStaticText, @"Should have static text accessibility trait, instead has %llu", _textNode.accessibilityTraits); @@ -91,20 +260,629 @@ - (void)testAccessibility _textNode.defaultAccessibilityLabel, _attributedText.string); } -- (void)testRespectingAccessibilitySetting +- (void)testBasicAccessibilityWithExperiments { + // Create text node explicitly as using the _textNode it's too late for the experiment setup ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; textNode.attributedText = _attributedText; - textNode.isAccessibilityElement = NO; - - textNode.attributedText = [[NSAttributedString alloc] initWithString:@"new string"]; - XCTAssertFalse(textNode.isAccessibilityElement); - - // Ensure removing string on an accessible text node updates the setting. - ASTextNode2 *accessibleTextNode = [ASTextNode2 new]; - accessibleTextNode.attributedText = _attributedText; - accessibleTextNode.attributedText = nil; - XCTAssertFalse(accessibleTextNode.isAccessibilityElement); + XCTAssertFalse(textNode.isAccessibilityElement, @"Is not an accessiblity element as it's a UIAccessibilityContainer"); + XCTAssertTrue(textNode.accessibilityTraits == UIAccessibilityTraitStaticText, + @"Should have static text accessibility trait, instead has %llu", + textNode.accessibilityTraits); + XCTAssertTrue(textNode.defaultAccessibilityTraits == UIAccessibilityTraitStaticText, + @"Default accessibility traits should return static text accessibility trait, " + @"instead returns %llu", + textNode.defaultAccessibilityTraits); + + XCTAssertTrue([textNode.accessibilityLabel isEqualToString:_attributedText.string], + @"Accessibility label is incorrectly set to \n%@\n when it should be \n%@\n", + textNode.accessibilityLabel, _attributedText.string); + XCTAssertTrue([textNode.defaultAccessibilityLabel isEqualToString:_attributedText.string], + @"Default accessibility label incorrectly returns \n%@\n when it should be \n%@\n", + textNode.defaultAccessibilityLabel, _attributedText.string); + + XCTAssertTrue(textNode.accessibilityElements.count == 1, @"Accessibility elements should exist"); + XCTAssertTrue([[textNode.accessibilityElements[0] accessibilityLabel] isEqualToString:_attributedText.string], + @"First accessibility element incorrectly returns \n%@\n when it should be \n%@\n", + [textNode.accessibilityElements[0] accessibilityLabel], textNode.accessibilityLabel); + XCTAssertTrue([[textNode.accessibilityElements[0] accessibilityLabel] isEqualToString:_attributedText.string], + @"First accessibility element incorrectly returns \n%@\n when it should be \n%@\n", + [textNode.accessibilityElements[0] accessibilityLabel], textNode.accessibilityLabel); +} + +- (void)testAccessibilityLayerBackedContainerAndTextNode2 +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + container.frame = CGRectMake(50, 50, 200, 600); + container.backgroundColor = [UIColor grayColor]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + ASDisplayNode *layerBackedContainer = [[ASDisplayNode alloc] init]; + layerBackedContainer.layerBacked = YES; + layerBackedContainer.frame = CGRectMake(50, 50, 200, 600); + layerBackedContainer.backgroundColor = [UIColor grayColor]; + [container addSubnode:layerBackedContainer]; + + ASTextNode2 *text = [[ASTextNode2 alloc] init]; + text.layerBacked = YES; + text.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text.frame = CGRectMake(50, 100, 200, 200); + [layerBackedContainer addSubnode:text]; + + ASTextNode2 *text2 = [[ASTextNode2 alloc] init]; + text2.layerBacked = YES; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + text2.frame = CGRectMake(50, 100, 200, 200); + [layerBackedContainer addSubnode:text2]; + + NSArray *elements = container.view.accessibilityElements; + XCTAssertEqual(elements.count, 2); + XCTAssertEqualObjects([elements[0] accessibilityLabel], @"hello"); + XCTAssertEqualObjects([elements[1] accessibilityLabel], @"world"); +} + +- (void)testAccessibilityLayerBackedContainerAndTextNode2WithExperiments +{ + [self testAccessibilityLayerBackedContainerAndTextNode2]; +} + +- (void)testAccessibilityLayerBackedTextNode2WithExperiments +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + container.frame = CGRectMake(0, 0, 200, 600); + container.backgroundColor = [UIColor grayColor]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + ASTextNode2 *text = [[ASTextNode2 alloc] init]; + text.layerBacked = YES; + text.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text.frame = CGRectMake(50, 100, 200, 200); + [container addSubnode:text]; + + // Trigger calculation of layouts on both nodes manually otherwise the internal + // text container will not have any size and the accessibility elements are not layed out + // properly + (void)[text layoutThatFits:ASSizeRangeMake(CGSizeZero, container.frame.size)]; + (void)[container layoutThatFits:ASSizeRangeMake(CGSizeZero, container.frame.size)]; + [container layoutIfNeeded]; + [container.layer displayIfNeeded]; + + NSArray *elements = container.view.accessibilityElements; + XCTAssertEqual(elements.count, 1); + + UIAccessibilityElement *firstElement = elements.firstObject; + XCTAssertEqualObjects(firstElement.accessibilityLabel, @"hello"); + XCTAssertEqual(YES, CGRectEqualToRect(CGRectMake(50, 102, 26, 13), CGRectIntegral(firstElement.accessibilityFrame))); +} + + +- (void)testThatASTextNode2SubnodeAccessibilityLabelAggregationWorks +{ + // Setup nodes + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASTextNode2 *innerNode1 = [[ASTextNode2 alloc] init]; + ASTextNode2 *innerNode2 = [[ASTextNode2 alloc] init]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:node]; + [window makeKeyAndVisible]; + // Initialize nodes with relevant accessibility data + node.isAccessibilityContainer = YES; + innerNode1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + innerNode2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + + // Attach the subnodes to the parent node, then ensure their accessibility labels have been' + // aggregated to the parent's accessibility label + [node addSubnode:innerNode1]; + [node addSubnode:innerNode2]; + XCTAssertEqualObjects([node.view.accessibilityElements.firstObject accessibilityLabel], + @"hello, world", @"Subnode accessibility label aggregation broken %@", + [node.view.accessibilityElements.firstObject accessibilityLabel]); +} + +- (void)testThatASTextNode2SubnodeAccessibilityLabelAggregationWorksWithExperiments +{ + [self testThatASTextNode2SubnodeAccessibilityLabelAggregationWorks]; +} + +- (void)testThatLayeredBackedASTextNode2SubnodeAccessibilityLabelAggregationWorks +{ + // Setup nodes + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASTextNode2 *innerNode1 = [[ASTextNode2 alloc] init]; + innerNode1.layerBacked = YES; + ASTextNode2 *innerNode2 = [[ASTextNode2 alloc] init]; + innerNode2.layerBacked = YES; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:node]; + [window makeKeyAndVisible]; + // Initialize nodes with relevant accessibility data + node.isAccessibilityContainer = YES; + innerNode1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + innerNode2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + + // Attach the subnodes to the parent node, then ensure their accessibility labels have been' + // aggregated to the parent's accessibility label + [node addSubnode:innerNode1]; + [node addSubnode:innerNode2]; + XCTAssertEqualObjects([node.view.accessibilityElements.firstObject accessibilityLabel], + @"hello, world", @"Subnode accessibility label aggregation broken %@", + [node.view.accessibilityElements.firstObject accessibilityLabel]); + +} + +- (void)testThatLayeredBackedASTextNode2SubnodeAccessibilityLabelAggregationWorksWithExperiments +{ + [self testThatLayeredBackedASTextNode2SubnodeAccessibilityLabelAggregationWorks]; +} + +- (void)testThatASTextNode2SubnodeCustomActionsAreWorking +{ + ASDisplayNode *node = [[ASDisplayNode alloc] init]; + ASTextNode2 *innerNode1 = [[ASTextNode2 alloc] init]; + innerNode1.accessibilityTraits = UIAccessibilityTraitButton; + ASTextNode2 *innerNode2 = [[ASTextNode2 alloc] init]; + innerNode2.accessibilityTraits = UIAccessibilityTraitButton; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:node]; + [window makeKeyAndVisible]; + // Initialize nodes with relevant accessibility data + node.isAccessibilityContainer = YES; + innerNode1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + innerNode2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + + // Attach the subnodes to the parent node, then ensure their accessibility labels have been' + // aggregated to the parent's accessibility label + [node addSubnode:innerNode1]; + [node addSubnode:innerNode2]; + + NSArray *accessibilityElements = node.view.accessibilityElements; + XCTAssertEqual(accessibilityElements.count, 1, @"Container node should have one accessibility element for custom actions"); + + NSArray *accessibilityCustomActions = accessibilityElements.firstObject.accessibilityCustomActions; + XCTAssertEqual(accessibilityCustomActions.count, 2, @"Text nodes should be exposed as a11y custom actions."); +} + +- (void)testThatASTextNode2SubnodeCustomActionsAreWorkingWithExperiments +{ + [self testThatASTextNode2SubnodeCustomActionsAreWorking]; +} + +- (void)testAccessibilityExposeA11YLinksWithExperiments +{ + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Texture Website: %@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + _textNode.attributedText = attributedText; + + NSArray *accessibilityElements = _textNode.accessibilityElements; + XCTAssertEqual(accessibilityElements.count, 2, @"Link should be exposed as accessibility element"); + + XCTAssertEqualObjects([accessibilityElements[0] accessibilityLabel], attributedText.string, @"First accessibility element should be the full text"); + XCTAssertEqualObjects([accessibilityElements[1] accessibilityLabel], link, @"Second accessibility element should be the link"); +} + +- (void)testAccessibilityNonLayerbackedNodesOperationInNonContainer +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + container.frame = CGRectMake(50, 50, 200, 600); + container.backgroundColor = [UIColor grayColor]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + // Do any additional setup after loading the view, typically from a nib. + ASTextNode2 *text1 = [[ASTextNode2 alloc] init]; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text1.frame = CGRectMake(50, 100, 200, 200); + [container addSubnode:text1]; + [container layoutIfNeeded]; + [container.layer displayIfNeeded]; + NSArray *elements = container.view.accessibilityElements; + XCTAssertEqual(elements.count, 1); + XCTAssertEqualObjects([elements.firstObject accessibilityLabel], @"hello"); + ASTextNode2 *text2 = [[ASTextNode2 alloc] init]; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + text2.frame = CGRectMake(50, 300, 200, 200); + [container addSubnode:text2]; + [container layoutIfNeeded]; + [container.layer displayIfNeeded]; + NSArray *updatedElements = container.view.accessibilityElements; + XCTAssertEqual(updatedElements.count, 2); + XCTAssertEqualObjects([updatedElements.firstObject accessibilityLabel], @"hello"); + XCTAssertEqualObjects([updatedElements.lastObject accessibilityLabel], @"world"); + ASTextNode2 *text3 = [[ASTextNode2 alloc] init]; + text3.attributedText = [[NSAttributedString alloc] initWithString:@"!!!!"]; + text3.frame = CGRectMake(50, 400, 200, 100); + [text2 addSubnode:text3]; + [container layoutIfNeeded]; + [container.layer displayIfNeeded]; + NSArray *updatedElements2 = container.view.accessibilityElements; + //text3 won't be read out cause it's overshadowed by text2 + XCTAssertEqual(updatedElements2.count, 2); + XCTAssertEqualObjects([updatedElements2.firstObject accessibilityLabel], @"hello"); + XCTAssertEqualObjects([updatedElements2.lastObject accessibilityLabel], @"world"); +} + +- (void)testAccessibilityNonLayerbackedNodesOperationInNonContainerWithExperiment +{ + [self testAccessibilityNonLayerbackedNodesOperationInNonContainer]; +} + +- (void)testAccessibilityNonLayerbackedNodesOperationInNonContainerWithWindow +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + + container.frame = CGRectMake(50, 50, 200, 600); + container.backgroundColor = [UIColor grayColor]; + // Do any additional setup after loading the view, typically from a nib. + ASTextNode2 *text1 = [[ASTextNode2 alloc] init]; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text1.frame = CGRectMake(50, 100, 200, 200); + [container addSubnode:text1]; + [container layoutIfNeeded]; + [container.layer displayIfNeeded]; + NSArray *elements = container.view.accessibilityElements; + XCTAssertEqual(elements.count, 1); + XCTAssertEqualObjects([elements.firstObject accessibilityLabel], @"hello"); + ASTextNode2 *text2 = [[ASTextNode2 alloc] init]; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + text2.frame = CGRectMake(50, 300, 200, 200); + [container addSubnode:text2]; + [container layoutIfNeeded]; + [container.layer displayIfNeeded]; + ASCATransactionQueueWait(nil); + NSArray *updatedElements = container.view.accessibilityElements; + XCTAssertEqual(updatedElements.count, 2); + XCTAssertEqualObjects([updatedElements.firstObject accessibilityLabel], @"hello"); + XCTAssertEqualObjects([updatedElements.lastObject accessibilityLabel], @"world"); + ASTextNode2 *text3 = [[ASTextNode2 alloc] init]; + text3.attributedText = [[NSAttributedString alloc] initWithString:@"!!!!"]; + text3.frame = CGRectMake(50, 400, 200, 100); + [text2 addSubnode:text3]; + [container layoutIfNeeded]; + [container.layer displayIfNeeded]; + ASCATransactionQueueWait(nil); + NSArray *updatedElements2 = container.view.accessibilityElements; + //text3 won't be read out cause it's overshadowed by text2 + XCTAssertEqual(updatedElements2.count, 2); + XCTAssertEqualObjects([updatedElements2.firstObject accessibilityLabel], @"hello"); + XCTAssertEqualObjects([updatedElements2.lastObject accessibilityLabel], @"world"); +} + +- (void)testAccessibilityNonLayerbackedNodesOperationInNonContainerWithWindowWithExperiment +{ + [self testAccessibilityNonLayerbackedNodesOperationInNonContainerWithWindow]; +} + +- (void)testTextNode2AccessibilityTraits +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + container.accessibilityTraits = UIAccessibilityTraitButton; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + ASTextNode2 *text1 = [[ASTextNode2 alloc] init]; + text1.layerBacked = YES; + text1.attributedText = [[NSAttributedString alloc] initWithString:@"hello"]; + text1.frame = CGRectMake(50, 100, 200, 200); + text1.accessibilityTraits = UIAccessibilityTraitButton; + [container addSubnode:text1]; + NSArray *elements = container.view.accessibilityElements; + + XCTAssertTrue(elements.count == 1); + XCTAssertTrue([[elements objectAtIndex:0] accessibilityTraits] & UIAccessibilityTraitButton); +} + +- (void)testTextNode2AccessibilityTraitsWithExperiments +{ + [self testTextNode2AccessibilityTraits]; +} + +- (void)testExposingLinkCustomActionsForAccessibilityContainer +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + + // Set container as accessibility container to expose the links as accessibility custom actions + container.isAccessibilityContainer = YES; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + ASTextNode2 *text1 = [[ASTextNode2 alloc] init]; + [container addSubnode:text1]; + + // This text node is explicitly marked as not layer backed as links are existing + text1.layerBacked = NO; + + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Texture Website: %@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + text1.attributedText = attributedText; + + NSArray *elements = container.view.accessibilityElements; + XCTAssertEqual(elements.count, 1, @"First element should be a representation of the ASTextNode"); + XCTAssertEqualObjects(elements.firstObject.accessibilityLabel, attributedText.string); + + NSArray *accessibilityActions = elements.firstObject.accessibilityCustomActions; + XCTAssertEqual(accessibilityActions.count, 1, @"Link should be exposed as accessibility custom action on the ASTextNode accessibility element"); + XCTAssertEqualObjects(accessibilityActions.firstObject.name, link); + XCTAssertTrue([accessibilityActions[0] isKindOfClass:[ASAccessibilityCustomAction class]]); + XCTAssertEqualObjects(((ASAccessibilityCustomAction*)accessibilityActions[0]).value, link); +} + +- (void)testExposingLinkAccessibleElementsForNonAccessibilityContainer +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + + // This container is explicitly no accessibility container to expose the links within the + // text node as UIAccessibilityElement + container.isAccessibilityContainer = NO; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + ASTextNode2 *text = [[ASTextNode2 alloc] init]; + [container addSubnode:text]; + + // This text node is explicitly marked as not layer backed as links are existing + text.layerBacked = NO; + + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Texture Website: %@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + text.attributedText = attributedText; + + NSArray *elements = container.view.accessibilityElements; + XCTAssertEqual(elements.count, 1, @"TBD"); + + NSArray *textElements = elements.firstObject.accessibilityElements; + XCTAssertEqual(textElements.count, 2, @"Link should be exposed as accessibility element"); + + // First one should be whole text node + XCTAssertEqualObjects(textElements.firstObject.accessibilityLabel, attributedText.string); + + // Second should represent the link + XCTAssertEqualObjects(textElements[1].accessibilityLabel, link); + XCTAssertEqual(textElements[1].accessibilityTraits, UIAccessibilityTraitLink); +} + +// Please note: This test is disabled as it needs to be run with the Accessibility Inspector started +// at least once. This is most of the time not the case for CIs +- (void)disabled_testAccessibilityTwoTextNodesAndOneLayerBackedAndOneWithLinks +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + container.frame = CGRectMake(50, 50, 200, 600); + container.isAccessibilityContainer = NO; + + ASDisplayNode *subContainer = [[ASDisplayNode alloc] init]; + // As text has a link this node can not be layer backed + subContainer.frame = CGRectMake(50, 50, 200, 600); + subContainer.layerBacked = NO; + subContainer.isAccessibilityContainer = NO; + [container addSubnode:subContainer]; + + ASTextNode2 *text = [[ASTextNode2 alloc] init]; + [subContainer addSubnode:text]; + + // This text node is explicitly marked as not layer backed as links are existing + text.layerBacked = NO; + + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Texture Website: %@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + text.attributedText = attributedText; + + ASTextNode2 *text2 = [[ASTextNode2 alloc] init]; + text2.layerBacked = YES; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + [subContainer addSubnode:text2]; + + NSArray *elements = container.view.accessibilityElements; + // The first element will be the view of the subContainer + XCTAssertEqual(elements.count, 1); + + elements = [elements.firstObject accessibilityElements]; + // The first element will be the view of the text1, the second is the UIAccessibilityElement representation of the text2 + XCTAssertEqual(elements.count, 2); + XCTAssertEqualObjects([elements[1] accessibilityLabel], @"world"); + + elements = [elements.firstObject accessibilityElements]; + // The first element will be the UIAccessibilityElement for the whole text of text, the second element will be the UIAccessibilityElement representation for the link + XCTAssertEqual(elements.count, 2); + XCTAssertEqualObjects([elements[0] accessibilityLabel], attributedText.string); + XCTAssertEqualObjects([elements[1] accessibilityLabel], link); + XCTAssertTrue(([elements[1] accessibilityTraits] & UIAccessibilityTraitLink), @"Accessibility elements need to have an element with a UIAccessibilityTraitLink trait set on"); +} + +- (void)testAccessibilityContainerTwoTextNodesAndOneLayerBackedAndOneWithLinks +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + + // Main container is an accessibility container + container.isAccessibilityContainer = YES; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + ASDisplayNode *subContainer = [[ASDisplayNode alloc] init]; + // As text has a link this node can not be layer backed + subContainer.layerBacked = NO; + subContainer.backgroundColor = [UIColor grayColor]; + subContainer.isAccessibilityContainer = NO; + [container addSubnode:subContainer]; + + ASTextNode2 *text = [[ASTextNode2 alloc] init]; + [subContainer addSubnode:text]; + + // This text node is explicitly marked as not layer backed as links are existing + text.layerBacked = NO; + + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Texture Website: %@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + text.attributedText = attributedText; + + ASTextNode2 *text2 = [[ASTextNode2 alloc] init]; + text2.layerBacked = YES; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + [subContainer addSubnode:text2]; + + NSArray *elements = container.view.accessibilityElements; + // The first element will be ASAccessibilityElement representation of the container with the aggregated accessibilityLabels + XCTAssertEqual(elements.count, 1); + XCTAssertEqualObjects([elements[0] accessibilityLabel], @"Texture Website: https://texturegroup.com, world"); + + NSArray *elementCustomActions = elements.firstObject.accessibilityCustomActions; + + // The first action represents the link of text + XCTAssertEqual(elementCustomActions.count, 1); + XCTAssertEqualObjects(elementCustomActions[0].name, link); + XCTAssertTrue([elementCustomActions[0] isKindOfClass:[ASAccessibilityCustomAction class]]); + XCTAssertEqualObjects(((ASAccessibilityCustomAction*)elementCustomActions[0]).value, link); +} + +- (void)testAccessibilityMultipleContainerTwoTextNodesAndOneLayerBackedAndOneWithLinks +{ + ASDisplayNode *container = [[ASDisplayNode alloc] init]; + + // Main container is an accessibility container + container.isAccessibilityContainer = YES; + UIWindow *window = [[UIWindow alloc] initWithFrame:CGRectMake(0, 0, 320, 560)]; + [window addSubnode:container]; + [window makeKeyAndVisible]; + ASDisplayNode *subContainer = [[ASDisplayNode alloc] init]; + // As text has a link this node can not be layer backed + subContainer.layerBacked = NO; + subContainer.backgroundColor = [UIColor grayColor]; + [container addSubnode:subContainer]; + + // Sub container is an accessibility container + subContainer.isAccessibilityContainer = YES; + + ASTextNode2 *text = [[ASTextNode2 alloc] init]; + [subContainer addSubnode:text]; + + // This text node is explicitly marked as not layer backed as links are existing + text.layerBacked = NO; + + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Texture Website: %@", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + text.attributedText = attributedText; + + ASTextNode2 *text2 = [[ASTextNode2 alloc] init]; + text2.layerBacked = YES; + text2.attributedText = [[NSAttributedString alloc] initWithString:@"world"]; + [subContainer addSubnode:text2]; + + NSArray *elements = container.view.accessibilityElements; + + // Everything is promoted to the container + XCTAssertEqual(elements.count, 2); + XCTAssertEqualObjects([elements[0] accessibilityLabel], @"Texture Website: https://texturegroup.com, world"); + + NSArray *elementCustomActions = elements[0].accessibilityCustomActions; + // The first action represents the link of text + XCTAssertEqual(elementCustomActions.count, 1); + XCTAssertEqualObjects(elementCustomActions[0].name, link); + XCTAssertEqualObjects(((ASAccessibilityCustomAction*)elementCustomActions[0]).value, link); +} + +- (void)testTappingLinkAtTheBeginningSingleline +{ + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ - Texture Website", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; + textNode.attributedText = attributedText; + [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + + id linkAttribute = nil; + + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(2.0, 5.0) attributeName:nil range:nil]; + XCTAssertTrue(linkAttribute != nil); + + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(200.0, 5.0) attributeName:nil range:nil]; + XCTAssertTrue(linkAttribute == nil); +} + +- (void)testTappingLinkAtTheBeginningMultiline +{ + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ - Texture Website" + @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor " + @"incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; + textNode.attributedText = attributedText; + [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(200.0, CGFLOAT_MAX))]; + + XCTAssertTrue(textNode.lineCount > 1); + + id linkAttribute = nil; + + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(2.0, 5.0) attributeName:nil range:nil]; + XCTAssertTrue(linkAttribute != nil); + + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(200.0, 2.0) attributeName:nil range:nil]; + XCTAssertTrue(linkAttribute == nil); +} + +- (void)testTappingLinkInTheMiddleSingleline +{ + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"Texture %@ Website", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; + textNode.attributedText = attributedText; + [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(CGFLOAT_MAX, CGFLOAT_MAX))]; + + id linkAttribute = nil; + + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(40.0, 5.0) attributeName:nil range:nil]; + XCTAssertTrue(linkAttribute != nil); + + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(2.0, 5.0) attributeName:nil range:nil]; + XCTAssertTrue(linkAttribute == nil); +} + +- (void)testTappingNonLinkAfterFirstLine +{ + NSString *link = @"https://texturegroup.com"; + NSMutableAttributedString *attributedText = [[NSMutableAttributedString alloc] initWithString:[NSString stringWithFormat:@"%@ - Texture Website" + @"Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor " + @"incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud", link]]; + NSRange linkRange = [attributedText.string rangeOfString:link]; + [attributedText addAttribute:NSLinkAttributeName value:link range:linkRange]; + + ASTextNode2 *textNode = [[ASTextNode2 alloc] init]; + textNode.attributedText = attributedText; + [textNode layoutThatFits:ASSizeRangeMake(CGSizeZero, CGSizeMake(200.0, CGFLOAT_MAX))]; + + XCTAssertTrue(textNode.lineCount > 1); + + id linkAttribute = nil; + + linkAttribute = [textNode linkAttributeValueAtPoint:CGPointMake(2.0, 50.0) attributeName:nil range:nil]; + XCTAssertTrue(linkAttribute == nil); } - (void)testSupportsLayerBacking diff --git a/Tests/ASTextNodePerformanceTests.mm b/Tests/ASTextNodePerformanceTests.mm index 961d192aa..a145b949c 100644 --- a/Tests/ASTextNodePerformanceTests.mm +++ b/Tests/ASTextNodePerformanceTests.mm @@ -50,7 +50,7 @@ @implementation ASTextNodePerformanceTests return array; } -- (void)testPerformance_RealisticData +- (void)disable_testPerformance_RealisticData { NSArray *data = [self.class realisticDataSet]; @@ -82,7 +82,7 @@ - (void)testPerformance_RealisticData ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseASDK, 0.2, 0.5); } -- (void)testPerformance_TwoParagraphLatinNoTruncation +- (void)disable_testPerformance_TwoParagraphLatinNoTruncation { NSAttributedString *text = [ASTextNodePerformanceTests twoParagraphLatinText]; @@ -112,7 +112,7 @@ - (void)testPerformance_TwoParagraphLatinNoTruncation ASXCTAssertRelativePerformanceInRange(ctx, kTestCaseASDK, 0.5, 0.9); } -- (void)testPerformance_OneParagraphLatinWithTruncation +- (void)disable_testPerformance_OneParagraphLatinWithTruncation { NSAttributedString *text = [ASTextNodePerformanceTests oneParagraphLatinText]; diff --git a/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testFlipsForRightToLeftLayoutDirection_flipped@2x.png b/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testFlipsForRightToLeftLayoutDirection_flipped@2x.png new file mode 100644 index 000000000..2ac10dbec Binary files /dev/null and b/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testFlipsForRightToLeftLayoutDirection_flipped@2x.png differ diff --git a/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testFlipsForRightToLeftLayoutDirection_normal@2x.png b/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testFlipsForRightToLeftLayoutDirection_normal@2x.png new file mode 100644 index 000000000..3b19513ef Binary files /dev/null and b/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testFlipsForRightToLeftLayoutDirection_normal@2x.png differ diff --git a/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testFlipsForRightToLeftLayoutDirection_unflipped@2x.png b/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testFlipsForRightToLeftLayoutDirection_unflipped@2x.png new file mode 100644 index 000000000..3b19513ef Binary files /dev/null and b/Tests/ReferenceImages_64/ASImageNodeSnapshotTests/testFlipsForRightToLeftLayoutDirection_unflipped@2x.png differ diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testShadowing_ASTextNode2@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testShadowing_ASTextNode2@2x.png index 7a336db72..969ca3bc9 100644 Binary files a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testShadowing_ASTextNode2@2x.png and b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testShadowing_ASTextNode2@2x.png differ diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2@2x.png index 84ef8a2fc..e0ef83dbd 100644 Binary files a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2@2x.png and b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextContainerInsetIsIncludedWithSmallerConstrainedSize_ASTextNode2@2x.png differ diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_1Lines_Short@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_1Lines_Short@2x.png new file mode 100644 index 000000000..e6de8dc23 Binary files /dev/null and b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByTruncatingMiddle_1Lines_Short@2x.png differ diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_0Lines@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_0Lines@2x.png index beeeedb32..b3b5c62d2 100644 Binary files a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_0Lines@2x.png and b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTextTruncationModes_ASTextNode2_NSLineBreakByWordWrapping_0Lines@2x.png differ diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatDefaultTruncationTokenAttributesAreInheritedFromTextWhenTruncated_ASTextNode2@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatDefaultTruncationTokenAttributesAreInheritedFromTextWhenTruncated_ASTextNode2@2x.png index 18266450f..faae04ac5 100644 Binary files a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatDefaultTruncationTokenAttributesAreInheritedFromTextWhenTruncated_ASTextNode2@2x.png and b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatDefaultTruncationTokenAttributesAreInheritedFromTextWhenTruncated_ASTextNode2@2x.png differ diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatTruncationTokenAttributesOverwriteThoseInheritedFromTextWhenTruncateTailMode_ASTextNode2@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatTruncationTokenAttributesOverwriteThoseInheritedFromTextWhenTruncateTailMode_ASTextNode2@2x.png new file mode 100644 index 000000000..97fec59cd Binary files /dev/null and b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatTruncationTokenAttributesOverwriteThoseInheritedFromTextWhenTruncateTailMode_ASTextNode2@2x.png differ diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatTruncationTokenDefaultInheritsAttributesFromText@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatTruncationTokenDefaultInheritsAttributesFromText@2x.png new file mode 100644 index 000000000..f1146b2fe Binary files /dev/null and b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testThatTruncationTokenDefaultInheritsAttributesFromText@2x.png differ diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTintColorHierarchyChange_green_tint_from_parent@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTintColorHierarchyChange_green_tint_from_parent@2x.png index 966591c52..7807e4ed2 100644 Binary files a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTintColorHierarchyChange_green_tint_from_parent@2x.png and b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTintColorHierarchyChange_green_tint_from_parent@2x.png differ diff --git a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTintColorHierarchyChange_red_tint_from_parent@2x.png b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTintColorHierarchyChange_red_tint_from_parent@2x.png index 0d7f5ab4a..9bbd3bd67 100644 Binary files a/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTintColorHierarchyChange_red_tint_from_parent@2x.png and b/Tests/ReferenceImages_64/ASTextNode2SnapshotTests/testTintColorHierarchyChange_red_tint_from_parent@2x.png differ diff --git a/examples/ASDKgram/Sample/FeedHeaderNode.m b/examples/ASDKgram/Sample/FeedHeaderNode.m index f6d922c27..003cbf241 100644 --- a/examples/ASDKgram/Sample/FeedHeaderNode.m +++ b/examples/ASDKgram/Sample/FeedHeaderNode.m @@ -44,8 +44,6 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize - (void)setupYogaLayoutIfNeeded { #if YOGA_LAYOUT - [self.style yogaNodeCreateIfNeeded]; - [self.textNode.style yogaNodeCreateIfNeeded]; [self addYogaChild:self.textNode]; self.style.padding = ASEdgeInsetsMake(kFeedHeaderInset); diff --git a/examples/ASDKgram/Sample/PhotoCellNode.m b/examples/ASDKgram/Sample/PhotoCellNode.m index 2dfe84648..1d471651a 100644 --- a/examples/ASDKgram/Sample/PhotoCellNode.m +++ b/examples/ASDKgram/Sample/PhotoCellNode.m @@ -280,15 +280,6 @@ - (ASTextNode *)createLayerBackedTextNodeWithString:(NSAttributedString *)attrib - (void)setupYogaLayoutIfNeeded { #if YOGA_LAYOUT - [self.style yogaNodeCreateIfNeeded]; - [_userAvatarImageNode.style yogaNodeCreateIfNeeded]; - [_userNameLabel.style yogaNodeCreateIfNeeded]; - [_photoImageNode.style yogaNodeCreateIfNeeded]; - [_photoLikesLabel.style yogaNodeCreateIfNeeded]; - [_photoDescriptionLabel.style yogaNodeCreateIfNeeded]; - [_photoLocationLabel.style yogaNodeCreateIfNeeded]; - [_photoTimeIntervalSincePostLabel.style yogaNodeCreateIfNeeded]; - ASDisplayNode *headerStack = [ASDisplayNode yogaHorizontalStack]; headerStack.style.margin = ASEdgeInsetsMake(InsetForHeader); headerStack.style.alignItems = ASStackLayoutAlignItemsCenter; diff --git a/examples/ASDKgram/Sample/TailLoadingNode.m b/examples/ASDKgram/Sample/TailLoadingNode.m index 63c462ab9..584f09492 100644 --- a/examples/ASDKgram/Sample/TailLoadingNode.m +++ b/examples/ASDKgram/Sample/TailLoadingNode.m @@ -42,8 +42,6 @@ - (ASLayoutSpec *)layoutSpecThatFits:(ASSizeRange)constrainedSize - (void)setupYogaLayoutIfNeeded { #if YOGA_LAYOUT - [self.style yogaNodeCreateIfNeeded]; - [self.activityIndicatorNode.style yogaNodeCreateIfNeeded]; [self addYogaChild:self.activityIndicatorNode]; self.style.justifyContent = ASStackLayoutJustifyContentCenter; diff --git a/examples/CatDealsCollectionView/Sample/ViewController.m b/examples/CatDealsCollectionView/Sample/ViewController.m index c1f9111ad..040d10990 100644 --- a/examples/CatDealsCollectionView/Sample/ViewController.m +++ b/examples/CatDealsCollectionView/Sample/ViewController.m @@ -148,7 +148,10 @@ - (void)reloadTapped - (ASCellNodeBlock)collectionNode:(ASCollectionNode *)collectionNode nodeBlockForItemAtIndexPath:(NSIndexPath *)indexPath { return ^{ - return [[ItemNode alloc] init]; + ASNodeContextPush([[ASNodeContext alloc] init]); + ItemNode *result = [[ItemNode alloc] init]; + ASNodeContextPop(); + return result; }; } @@ -159,11 +162,14 @@ - (id)collectionNode:(ASCollectionNode *)collectionNode nodeModelForItemAtIndexP - (ASCellNode *)collectionNode:(ASCollectionNode *)collectionNode nodeForSupplementaryElementOfKind:(NSString *)kind atIndexPath:(NSIndexPath *)indexPath { + ASCellNode *node; + ASNodeContextPush([[ASNodeContext alloc] init]); if ([kind isEqualToString:UICollectionElementKindSectionHeader] && indexPath.section == 0) { - return [[BlurbNode alloc] init]; + node = [[BlurbNode alloc] init]; } else if ([kind isEqualToString:UICollectionElementKindSectionFooter] && indexPath.section == 0) { - return [[LoadingNode alloc] init]; + node = [[LoadingNode alloc] init]; } + ASNodeContextPop(); return nil; } diff --git a/examples_extra/TextStressTest/Sample/TextCellNode.m b/examples_extra/TextStressTest/Sample/TextCellNode.m index 4b777b567..6698e4c51 100644 --- a/examples_extra/TextStressTest/Sample/TextCellNode.m +++ b/examples_extra/TextStressTest/Sample/TextCellNode.m @@ -62,10 +62,6 @@ - (instancetype)initWithText1:(NSString *)text1 text2:(NSString *)text2 */ - (void)simpleSetupYogaLayout { - [self.style yogaNodeCreateIfNeeded]; - [_label1.style yogaNodeCreateIfNeeded]; - [_label2.style yogaNodeCreateIfNeeded]; - _label1.style.flexGrow = 0; _label1.style.flexShrink = 1; _label1.backgroundColor = [UIColor lightGrayColor]; diff --git a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.h b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.h index 75aff7fc1..8d58a13cb 100644 --- a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.h +++ b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.h @@ -2,7 +2,8 @@ // AppDelegate.h // Texture // -// Copyright (c) Pinterest, Inc. All rights reserved. +// 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 // diff --git a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.m b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.m index d6890ff2e..d0fd66f77 100644 --- a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.m +++ b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/AppDelegate.m @@ -2,7 +2,8 @@ // AppDelegate.m // Texture // -// Copyright (c) Pinterest, Inc. All rights reserved. +// 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 // diff --git a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.h b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.h index 261e9ff48..4627e2928 100644 --- a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.h +++ b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.h @@ -2,7 +2,8 @@ // ViewController.h // Texture // -// Copyright (c) Pinterest, Inc. All rights reserved. +// 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 // diff --git a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.m b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.m index 106ee524f..2715fdef7 100644 --- a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.m +++ b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/ViewController.m @@ -2,7 +2,8 @@ // ViewController.m // Texture // -// Copyright (c) Pinterest, Inc. All rights reserved. +// 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 // diff --git a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/main.m b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/main.m index ec43dab25..65850400e 100644 --- a/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/main.m +++ b/smoke-tests/Life Without CocoaPods/Life Without CocoaPods/main.m @@ -2,7 +2,8 @@ // main.m // Texture // -// Copyright (c) Pinterest, Inc. All rights reserved. +// 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 //