diff --git a/AsyncDisplayKit.xcodeproj/project.pbxproj b/AsyncDisplayKit.xcodeproj/project.pbxproj index 4995f00798..655b14c92b 100644 --- a/AsyncDisplayKit.xcodeproj/project.pbxproj +++ b/AsyncDisplayKit.xcodeproj/project.pbxproj @@ -145,6 +145,24 @@ 251B8EFA1BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m in Sources */ = {isa = PBXBuildFile; fileRef = 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */; }; 251B8EFB1BBB3D690087C538 /* ASDataController+Subclasses.h in Headers */ = {isa = PBXBuildFile; fileRef = 251B8EF61BBB3D690087C538 /* ASDataController+Subclasses.h */; }; 2538B6F31BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */; }; + 2577547A1BED252700737CA5 /* CKTextKitAttributes.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754671BED252700737CA5 /* CKTextKitAttributes.h */; }; + 2577547B1BED252700737CA5 /* CKTextKitAttributes.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754681BED252700737CA5 /* CKTextKitAttributes.mm */; }; + 2577547C1BED252700737CA5 /* CKTextKitContext.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754691BED252700737CA5 /* CKTextKitContext.h */; }; + 2577547D1BED252700737CA5 /* CKTextKitContext.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2577546A1BED252700737CA5 /* CKTextKitContext.mm */; }; + 2577547E1BED252700737CA5 /* CKTextKitEntityAttribute.h in Headers */ = {isa = PBXBuildFile; fileRef = 2577546B1BED252700737CA5 /* CKTextKitEntityAttribute.h */; }; + 2577547F1BED252700737CA5 /* CKTextKitEntityAttribute.m in Sources */ = {isa = PBXBuildFile; fileRef = 2577546C1BED252700737CA5 /* CKTextKitEntityAttribute.m */; }; + 257754801BED252700737CA5 /* CKTextKitRenderer.h in Headers */ = {isa = PBXBuildFile; fileRef = 2577546D1BED252700737CA5 /* CKTextKitRenderer.h */; }; + 257754811BED252700737CA5 /* CKTextKitRenderer.mm in Sources */ = {isa = PBXBuildFile; fileRef = 2577546E1BED252700737CA5 /* CKTextKitRenderer.mm */; }; + 257754821BED252700737CA5 /* CKTextKitRenderer+Positioning.h in Headers */ = {isa = PBXBuildFile; fileRef = 2577546F1BED252700737CA5 /* CKTextKitRenderer+Positioning.h */; }; + 257754831BED252700737CA5 /* CKTextKitRenderer+Positioning.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754701BED252700737CA5 /* CKTextKitRenderer+Positioning.mm */; }; + 257754841BED252700737CA5 /* CKTextKitRenderer+TextChecking.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754711BED252700737CA5 /* CKTextKitRenderer+TextChecking.h */; }; + 257754851BED252700737CA5 /* CKTextKitRenderer+TextChecking.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754721BED252700737CA5 /* CKTextKitRenderer+TextChecking.mm */; }; + 257754881BED252700737CA5 /* CKTextKitShadower.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754751BED252700737CA5 /* CKTextKitShadower.h */; }; + 257754891BED252700737CA5 /* CKTextKitShadower.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754761BED252700737CA5 /* CKTextKitShadower.mm */; }; + 2577548A1BED252700737CA5 /* CKTextKitTailTruncater.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754771BED252700737CA5 /* CKTextKitTailTruncater.h */; }; + 2577548B1BED252700737CA5 /* CKTextKitTailTruncater.mm in Sources */ = {isa = PBXBuildFile; fileRef = 257754781BED252700737CA5 /* CKTextKitTailTruncater.mm */; }; + 2577548C1BED252700737CA5 /* CKTextKitTruncating.h in Headers */ = {isa = PBXBuildFile; fileRef = 257754791BED252700737CA5 /* CKTextKitTruncating.h */; }; + 2577548E1BED278B00737CA5 /* CKEqualityHashHelpers.h in Sources */ = {isa = PBXBuildFile; fileRef = 2577548D1BED278B00737CA5 /* CKEqualityHashHelpers.h */; }; 2767E9411BB19BD600EA9B77 /* ASViewController.h in Headers */ = {isa = PBXBuildFile; fileRef = ACC945A81BA9E7A0005E1FB8 /* ASViewController.h */; settings = {ATTRIBUTES = (Public, ); }; }; 2767E9421BB19BD600EA9B77 /* ASViewController.m in Sources */ = {isa = PBXBuildFile; fileRef = ACC945AA1BA9E7C1005E1FB8 /* ASViewController.m */; }; 2911485C1A77147A005D0878 /* ASControlNodeTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 2911485B1A77147A005D0878 /* ASControlNodeTests.m */; }; @@ -583,6 +601,24 @@ 251B8EF51BBB3D690087C538 /* ASCollectionViewFlowLayoutInspector.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspector.m; sourceTree = ""; }; 251B8EF61BBB3D690087C538 /* ASDataController+Subclasses.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = "ASDataController+Subclasses.h"; sourceTree = ""; }; 2538B6F21BC5D2A2003CA0B4 /* ASCollectionViewFlowLayoutInspectorTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASCollectionViewFlowLayoutInspectorTests.m; sourceTree = ""; }; + 257754671BED252700737CA5 /* CKTextKitAttributes.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CKTextKitAttributes.h; path = TextKit/CKTextKitAttributes.h; sourceTree = ""; }; + 257754681BED252700737CA5 /* CKTextKitAttributes.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CKTextKitAttributes.mm; path = TextKit/CKTextKitAttributes.mm; sourceTree = ""; }; + 257754691BED252700737CA5 /* CKTextKitContext.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CKTextKitContext.h; path = TextKit/CKTextKitContext.h; sourceTree = ""; }; + 2577546A1BED252700737CA5 /* CKTextKitContext.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CKTextKitContext.mm; path = TextKit/CKTextKitContext.mm; sourceTree = ""; }; + 2577546B1BED252700737CA5 /* CKTextKitEntityAttribute.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CKTextKitEntityAttribute.h; path = TextKit/CKTextKitEntityAttribute.h; sourceTree = ""; }; + 2577546C1BED252700737CA5 /* CKTextKitEntityAttribute.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = CKTextKitEntityAttribute.m; path = TextKit/CKTextKitEntityAttribute.m; sourceTree = ""; }; + 2577546D1BED252700737CA5 /* CKTextKitRenderer.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CKTextKitRenderer.h; path = TextKit/CKTextKitRenderer.h; sourceTree = ""; }; + 2577546E1BED252700737CA5 /* CKTextKitRenderer.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CKTextKitRenderer.mm; path = TextKit/CKTextKitRenderer.mm; sourceTree = ""; }; + 2577546F1BED252700737CA5 /* CKTextKitRenderer+Positioning.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "CKTextKitRenderer+Positioning.h"; path = "TextKit/CKTextKitRenderer+Positioning.h"; sourceTree = ""; }; + 257754701BED252700737CA5 /* CKTextKitRenderer+Positioning.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "CKTextKitRenderer+Positioning.mm"; path = "TextKit/CKTextKitRenderer+Positioning.mm"; sourceTree = ""; }; + 257754711BED252700737CA5 /* CKTextKitRenderer+TextChecking.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = "CKTextKitRenderer+TextChecking.h"; path = "TextKit/CKTextKitRenderer+TextChecking.h"; sourceTree = ""; }; + 257754721BED252700737CA5 /* CKTextKitRenderer+TextChecking.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = "CKTextKitRenderer+TextChecking.mm"; path = "TextKit/CKTextKitRenderer+TextChecking.mm"; sourceTree = ""; }; + 257754751BED252700737CA5 /* CKTextKitShadower.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CKTextKitShadower.h; path = TextKit/CKTextKitShadower.h; sourceTree = ""; }; + 257754761BED252700737CA5 /* CKTextKitShadower.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CKTextKitShadower.mm; path = TextKit/CKTextKitShadower.mm; sourceTree = ""; }; + 257754771BED252700737CA5 /* CKTextKitTailTruncater.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CKTextKitTailTruncater.h; path = TextKit/CKTextKitTailTruncater.h; sourceTree = ""; }; + 257754781BED252700737CA5 /* CKTextKitTailTruncater.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; name = CKTextKitTailTruncater.mm; path = TextKit/CKTextKitTailTruncater.mm; sourceTree = ""; }; + 257754791BED252700737CA5 /* CKTextKitTruncating.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CKTextKitTruncating.h; path = TextKit/CKTextKitTruncating.h; sourceTree = ""; }; + 2577548D1BED278B00737CA5 /* CKEqualityHashHelpers.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = CKEqualityHashHelpers.h; path = TextKit/CKEqualityHashHelpers.h; sourceTree = ""; }; 2911485B1A77147A005D0878 /* ASControlNodeTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ASControlNodeTests.m; sourceTree = ""; }; 292C59991A956527007E5DD6 /* ASLayoutRangeType.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASLayoutRangeType.h; sourceTree = ""; }; 292C599A1A956527007E5DD6 /* ASRangeHandlerPreload.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ASRangeHandlerPreload.h; sourceTree = ""; }; @@ -836,6 +872,7 @@ 058D09E1195D050800B7D73C /* Details */, 058D0A01195D050800B7D73C /* Private */, AC6456051B0A333200CF11B8 /* Layout */, + 257754661BED245B00737CA5 /* TextKit */, 058D09B2195D04C000B7D73C /* Supporting Files */, ); path = AsyncDisplayKit; @@ -1044,6 +1081,31 @@ path = Base; sourceTree = SOURCE_ROOT; }; + 257754661BED245B00737CA5 /* TextKit */ = { + isa = PBXGroup; + children = ( + 257754671BED252700737CA5 /* CKTextKitAttributes.h */, + 257754681BED252700737CA5 /* CKTextKitAttributes.mm */, + 257754691BED252700737CA5 /* CKTextKitContext.h */, + 2577546A1BED252700737CA5 /* CKTextKitContext.mm */, + 2577546B1BED252700737CA5 /* CKTextKitEntityAttribute.h */, + 2577546C1BED252700737CA5 /* CKTextKitEntityAttribute.m */, + 2577546D1BED252700737CA5 /* CKTextKitRenderer.h */, + 2577546E1BED252700737CA5 /* CKTextKitRenderer.mm */, + 2577546F1BED252700737CA5 /* CKTextKitRenderer+Positioning.h */, + 257754701BED252700737CA5 /* CKTextKitRenderer+Positioning.mm */, + 257754711BED252700737CA5 /* CKTextKitRenderer+TextChecking.h */, + 257754721BED252700737CA5 /* CKTextKitRenderer+TextChecking.mm */, + 257754751BED252700737CA5 /* CKTextKitShadower.h */, + 257754761BED252700737CA5 /* CKTextKitShadower.mm */, + 257754771BED252700737CA5 /* CKTextKitTailTruncater.h */, + 257754781BED252700737CA5 /* CKTextKitTailTruncater.mm */, + 257754791BED252700737CA5 /* CKTextKitTruncating.h */, + 2577548D1BED278B00737CA5 /* CKEqualityHashHelpers.h */, + ); + name = TextKit; + sourceTree = ""; + }; AC6456051B0A333200CF11B8 /* Layout */ = { isa = PBXGroup; children = ( @@ -1120,6 +1182,7 @@ AC026B691BD57D6F00BBC17E /* ASChangeSetDataController.h in Headers */, 058D0A71195D05F800B7D73C /* _AS-objc-internal.h in Headers */, 058D0A68195D05EC00B7D73C /* _ASAsyncTransaction.h in Headers */, + 257754881BED252700737CA5 /* CKTextKitShadower.h in Headers */, 058D0A6A195D05EC00B7D73C /* _ASAsyncTransactionContainer+Private.h in Headers */, 058D0A6B195D05EC00B7D73C /* _ASAsyncTransactionContainer.h in Headers */, 058D0A6D195D05EC00B7D73C /* _ASAsyncTransactionGroup.h in Headers */, @@ -1145,10 +1208,12 @@ AC3C4A511A1139C100143C57 /* ASCollectionView.h in Headers */, 205F0E1D1B373A2C007741D0 /* ASCollectionViewLayoutController.h in Headers */, AC3C4A541A113EEC00143C57 /* ASCollectionViewProtocols.h in Headers */, + 2577547E1BED252700737CA5 /* CKTextKitEntityAttribute.h in Headers */, 058D0A49195D05CB00B7D73C /* ASControlNode+Subclasses.h in Headers */, 058D0A47195D05CB00B7D73C /* ASControlNode.h in Headers */, 464052201A3F83C40061C0BA /* ASDataController.h in Headers */, 05A6D05A19D0EB64002DD95E /* ASDealloc2MainObject.h in Headers */, + 2577547C1BED252700737CA5 /* CKTextKitContext.h in Headers */, ACF6ED201B17843500DA7C62 /* ASDimension.h in Headers */, 058D0A78195D05F900B7D73C /* ASDisplayNode+DebugTiming.h in Headers */, DECBD6E71BE56E1900CF4905 /* ASButtonNode.h in Headers */, @@ -1164,6 +1229,7 @@ 058D0A57195D05DC00B7D73C /* ASHighlightOverlayLayer.h in Headers */, 058D0A7C195D05F900B7D73C /* ASImageNode+CGExtras.h in Headers */, 058D0A4F195D05CB00B7D73C /* ASImageNode.h in Headers */, + 2577547A1BED252700737CA5 /* CKTextKitAttributes.h in Headers */, 05F20AA41A15733C00DCA68A /* ASImageProtocols.h in Headers */, 430E7C8F1B4C23F100697A4C /* ASIndexPath.h in Headers */, ACF6ED221B17843500DA7C62 /* ASInsetLayoutSpec.h in Headers */, @@ -1181,7 +1247,9 @@ ACF6ED261B17843500DA7C62 /* ASLayoutSpec.h in Headers */, ACF6ED4D1B17847A00DA7C62 /* ASLayoutSpecUtilities.h in Headers */, AC026B6F1BD57DBF00BBC17E /* _ASHierarchyChangeSet.h in Headers */, + 2577548C1BED252700737CA5 /* CKTextKitTruncating.h in Headers */, 0516FA3D1A15563400B4EBED /* ASLog.h in Headers */, + 257754821BED252700737CA5 /* CKTextKitRenderer+Positioning.h in Headers */, 0442850D1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.h in Headers */, 0516FA401A1563D200B4EBED /* ASMultiplexImageNode.h in Headers */, 058D0A59195D05DC00B7D73C /* ASMutableAttributedStringBuilder.h in Headers */, @@ -1191,6 +1259,7 @@ 292C59A21A956527007E5DD6 /* ASRangeHandler.h in Headers */, 292C59A01A956527007E5DD6 /* ASRangeHandlerPreload.h in Headers */, 292C59A31A956527007E5DD6 /* ASRangeHandlerRender.h in Headers */, + 2577548A1BED252700737CA5 /* CKTextKitTailTruncater.h in Headers */, ACF6ED2D1B17843500DA7C62 /* ASRatioLayoutSpec.h in Headers */, AC47D9451B3BB41900AAEE9D /* ASRelativeSize.h in Headers */, 291B63FB1AA53A7A000A71B3 /* ASScrollDirection.h in Headers */, @@ -1202,8 +1271,10 @@ CC7FD9DE1BB5E962005CCB2B /* ASPhotosFrameworkImageRequest.h in Headers */, ACF6ED2F1B17843500DA7C62 /* ASStackLayoutSpec.h in Headers */, ACF6ED4E1B17847A00DA7C62 /* ASStackLayoutSpecUtilities.h in Headers */, + 257754801BED252700737CA5 /* CKTextKitRenderer.h in Headers */, ACF6ED4F1B17847A00DA7C62 /* ASStackPositionedLayout.h in Headers */, ACF6ED511B17847A00DA7C62 /* ASStackUnpositionedLayout.h in Headers */, + 257754841BED252700737CA5 /* CKTextKitRenderer+TextChecking.h in Headers */, 9C6BB3B21B8CC9C200F13F52 /* ASStaticLayoutable.h in Headers */, ACF6ED311B17843500DA7C62 /* ASStaticLayoutSpec.h in Headers */, 055F1A3419ABD3E3004DAFF1 /* ASTableView.h in Headers */, @@ -1557,11 +1628,15 @@ 058D0A15195D050800B7D73C /* ASDisplayNodeExtras.mm in Sources */, 0587F9BE1A7309ED00AFF0BA /* ASEditableTextNode.mm in Sources */, 464052231A3F83C40061C0BA /* ASFlowLayoutController.mm in Sources */, + 2577548B1BED252700737CA5 /* CKTextKitTailTruncater.mm in Sources */, + 2577547F1BED252700737CA5 /* CKTextKitEntityAttribute.m in Sources */, 058D0A1A195D050800B7D73C /* ASHighlightOverlayLayer.mm in Sources */, 058D0A2B195D050800B7D73C /* ASImageNode+CGExtras.m in Sources */, 058D0A16195D050800B7D73C /* ASImageNode.mm in Sources */, + 2577547B1BED252700737CA5 /* CKTextKitAttributes.mm in Sources */, 430E7C911B4C23F100697A4C /* ASIndexPath.m in Sources */, ACF6ED231B17843500DA7C62 /* ASInsetLayoutSpec.mm in Sources */, + 2577548E1BED278B00737CA5 /* CKEqualityHashHelpers.h in Sources */, ACF6ED4C1B17847A00DA7C62 /* ASInternalHelpers.mm in Sources */, ACF6ED251B17843500DA7C62 /* ASLayout.mm in Sources */, 9C5FA3531B8F6ADF00A62714 /* ASLayoutOptions.mm in Sources */, @@ -1572,6 +1647,8 @@ DECBD6E91BE56E1900CF4905 /* ASButtonNode.mm in Sources */, 058D0A1B195D050800B7D73C /* ASMutableAttributedStringBuilder.m in Sources */, 055B9FA91A1C154B00035D6D /* ASNetworkImageNode.mm in Sources */, + 2577547D1BED252700737CA5 /* CKTextKitContext.mm in Sources */, + 257754891BED252700737CA5 /* CKTextKitShadower.mm in Sources */, ACF6ED2C1B17843500DA7C62 /* ASOverlayLayoutSpec.mm in Sources */, 0442850F1BAA64EC00D16268 /* ASMultidimensionalArrayUtils.mm in Sources */, 055F1A3919ABD413004DAFF1 /* ASRangeController.mm in Sources */, @@ -1581,6 +1658,7 @@ ACF6ED2E1B17843500DA7C62 /* ASRatioLayoutSpec.mm in Sources */, AC47D9461B3BB41900AAEE9D /* ASRelativeSize.mm in Sources */, 205F0E121B371BD7007741D0 /* ASScrollDirection.m in Sources */, + 257754811BED252700737CA5 /* CKTextKitRenderer.mm in Sources */, D785F6631A74327E00291744 /* ASScrollNode.m in Sources */, 058D0A2C195D050800B7D73C /* ASSentinel.m in Sources */, 9C8221971BA237B80037F19A /* ASStackBaselinePositionedLayout.mm in Sources */, @@ -1588,6 +1666,7 @@ ACF6ED301B17843500DA7C62 /* ASStackLayoutSpec.mm in Sources */, ACF6ED501B17847A00DA7C62 /* ASStackPositionedLayout.mm in Sources */, ACF6ED521B17847A00DA7C62 /* ASStackUnpositionedLayout.mm in Sources */, + 257754831BED252700737CA5 /* CKTextKitRenderer+Positioning.mm in Sources */, ACF6ED321B17843500DA7C62 /* ASStaticLayoutSpec.mm in Sources */, AC026B6B1BD57D6F00BBC17E /* ASChangeSetDataController.m in Sources */, 055F1A3519ABD3E3004DAFF1 /* ASTableView.mm in Sources */, @@ -1597,6 +1676,7 @@ 058D0A1D195D050800B7D73C /* ASTextNodeRenderer.mm in Sources */, 058D0A1E195D050800B7D73C /* ASTextNodeShadower.m in Sources */, 058D0A1F195D050800B7D73C /* ASTextNodeTextKitHelpers.mm in Sources */, + 257754851BED252700737CA5 /* CKTextKitRenderer+TextChecking.mm in Sources */, 058D0A20195D050800B7D73C /* ASTextNodeWordKerner.m in Sources */, ACC945AB1BA9E7C1005E1FB8 /* ASViewController.m in Sources */, B0F8805B1BEAEC7500D17647 /* ASTableNode.m in Sources */, diff --git a/AsyncDisplayKit/TextKit/CKEqualityHashHelpers.h b/AsyncDisplayKit/TextKit/CKEqualityHashHelpers.h new file mode 100644 index 0000000000..cac0b2a849 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKEqualityHashHelpers.h @@ -0,0 +1,171 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +#import + +// From folly: +// This is the Hash128to64 function from Google's cityhash (available +// under the MIT License). We use it to reduce multiple 64 bit hashes +// into a single hash. +inline uint64_t CKHashCombine(const uint64_t upper, const uint64_t lower) { + // Murmur-inspired hashing. + const uint64_t kMul = 0x9ddfea08eb382d69ULL; + uint64_t a = (lower ^ upper) * kMul; + a ^= (a >> 47); + uint64_t b = (upper ^ a) * kMul; + b ^= (b >> 47); + b *= kMul; + return b; +} + +#if __LP64__ +inline size_t CKHash64ToNative(uint64_t key) { + return key; +} +#else + +// Thomas Wang downscaling hash function +inline size_t CKHash64ToNative(uint64_t key) { + key = (~key) + (key << 18); + key = key ^ (key >> 31); + key = key * 21; + key = key ^ (key >> 11); + key = key + (key << 6); + key = key ^ (key >> 22); + return (uint32_t) key; +} +#endif + +NSUInteger CKIntegerArrayHash(const NSUInteger *subhashes, NSUInteger count); + +namespace CK { + // Default is not an ObjC class + template + struct is_objc_class : std::false_type { }; + + // Conditionally enable this template specialization on whether T is convertible to id, makes the is_objc_class a true_type + template + struct is_objc_class::value, bool>::type> : std::true_type { }; + + // CKUtils::hash()(value) -> either std::hash if c++ or [o hash] if ObjC object. + template struct hash; + + // For non-objc types, defer to std::hash + template struct hash::value>::type> { + size_t operator ()(const T& a) { + return std::hash()(a); + } + }; + + // For objc types, call [o hash] + template struct hash::value>::type> { + size_t operator ()(id o) { + return [o hash]; + } + }; + + template struct is_equal; + + // For non-objc types use == operator + template struct is_equal::value>::type> { + bool operator ()(const T& a, const T& b) { + return a == b; + } + }; + + // For objc types, check pointer equality, then use -isEqual: + template struct is_equal::value>::type> { + bool operator ()(id a, id b) { + return a == b || [a isEqual:b]; + } + }; + +}; + +namespace CKTupleOperations +{ + // Recursive case (hash up to Index) + template ::value - 1> + struct _hash_helper + { + static size_t hash(Tuple const& tuple) + { + size_t prev = _hash_helper::hash(tuple); + using TypeForIndex = typename std::tuple_element::type; + size_t thisHash = CK::hash()(std::get(tuple)); + return CKHashCombine(prev, thisHash); + } + }; + + // Base case (hash 0th element) + template + struct _hash_helper + { + static size_t hash(Tuple const& tuple) + { + using TypeForIndex = typename std::tuple_element<0,Tuple>::type; + return CK::hash()(std::get<0>(tuple)); + } + }; + + // Recursive case (elements equal up to Index) + template ::value - 1> + struct _eq_helper + { + static bool equal(Tuple const& a, Tuple const& b) + { + bool prev = _eq_helper::equal(a, b); + using TypeForIndex = typename std::tuple_element::type; + auto aValue = std::get(a); + auto bValue = std::get(b); + return prev && CK::is_equal()(aValue, bValue); + } + }; + + // Base case (0th elements equal) + template + struct _eq_helper + { + static bool equal(Tuple const& a, Tuple const& b) + { + using TypeForIndex = typename std::tuple_element<0,Tuple>::type; + auto& aValue = std::get<0>(a); + auto& bValue = std::get<0>(b); + return CK::is_equal()(aValue, bValue); + } + }; + + + template struct hash; + + template + struct hash> + { + size_t operator()(std::tuple const& tt) const + { + return _hash_helper>::hash(tt); + } + }; + + + template struct equal_to; + + template + struct equal_to> + { + bool operator()(std::tuple const& a, std::tuple const& b) const + { + return _eq_helper>::equal(a, b); + } + }; + +} \ No newline at end of file diff --git a/AsyncDisplayKit/TextKit/CKTextKitAttributes.h b/AsyncDisplayKit/TextKit/CKTextKitAttributes.h new file mode 100755 index 0000000000..0590010d24 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitAttributes.h @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +#ifndef ComponentKit_CKTextKitAttributes_h +#define ComponentKit_CKTextKitAttributes_h + +@protocol CKTextKitTruncating; + +extern NSString *const CKTextKitTruncationAttributeName; +/** + Use CKTextKitEntityAttribute as the value of this attribute to embed a link or other interactable content inside the + text. + */ +extern NSString *const CKTextKitEntityAttributeName; + +static inline BOOL _objectsEqual(id obj1, id obj2) +{ + return obj1 == obj2 ? YES : [obj1 isEqual:obj2]; +} + +/** + All NSObject values in this struct should be copied when passed into the TextComponent. + */ +struct CKTextKitAttributes { + /** + The string to be drawn. CKTextKit will not augment this string with default colors, etc. so this must be complete. + */ + NSAttributedString *attributedString; + /** + The string to use as the truncation string, usually just "...". If you have a range of text you would like to + restrict highlighting to (for instance if you have "... Continue Reading", use the CKTextKitTruncationAttributeName + to mark the specific range of the string that should be highlightable. + */ + NSAttributedString *truncationAttributedString; + /** + This is the character set that CKTextKit should attempt to avoid leaving as a trailing character before your + truncation token. By default this set includes "\s\t\n\r.,!?:;" so you don't end up with ugly looking truncation + text like "Hey, this is some fancy Truncation!\n\n...". Instead it would be truncated as "Hey, this is some fancy + truncation...". This is not always possible. + + Set this to the empty charset if you want to just use the "dumb" truncation behavior. A nil value will be + substituted with the default described above. + */ + NSCharacterSet *avoidTailTruncationSet; + /** + The line-break mode to apply to the text. Since this also impacts how TextKit will attempt to truncate the text + in your string, we only support NSLineBreakByWordWrapping and NSLineBreakByCharWrapping. + */ + NSLineBreakMode lineBreakMode; + /** + The maximum number of lines to draw in the drawable region. Leave blank or set to 0 to define no maximum. + */ + NSUInteger maximumNumberOfLines; + /** + The shadow offset for any shadows applied to the text. The coordinate space for this is the same as UIKit, so a + positive width means towards the right, and a positive height means towards the bottom. + */ + CGSize shadowOffset; + /** + The color to use in drawing the text's shadow. + */ + UIColor *shadowColor; + /** + The opacity of the shadow from 0 to 1. + */ + CGFloat shadowOpacity; + /** + The radius that should be applied to the shadow blur. Larger values mean a larger, more blurred shadow. + */ + CGFloat shadowRadius; + /** + A pointer to a function that that returns a custom layout manager subclass. If nil, defaults to NSLayoutManager. + */ + NSLayoutManager *(*layoutManagerFactory)(void); + + /** + We provide an explicit copy function so we can use aggregate initializer syntax while providing copy semantics for + the NSObjects inside. + */ + const CKTextKitAttributes copy() const + { + return { + [attributedString copy], + [truncationAttributedString copy], + [avoidTailTruncationSet copy], + lineBreakMode, + maximumNumberOfLines, + shadowOffset, + [shadowColor copy], + shadowOpacity, + shadowRadius, + layoutManagerFactory + }; + }; + + bool operator==(const CKTextKitAttributes &other) const + { + // These comparisons are in a specific order to reduce the overall cost of this function. + return lineBreakMode == other.lineBreakMode + && maximumNumberOfLines == other.maximumNumberOfLines + && shadowOpacity == other.shadowOpacity + && shadowRadius == other.shadowRadius + && layoutManagerFactory == other.layoutManagerFactory + && CGSizeEqualToSize(shadowOffset, other.shadowOffset) + && _objectsEqual(avoidTailTruncationSet, other.avoidTailTruncationSet) + && _objectsEqual(shadowColor, other.shadowColor) + && _objectsEqual(attributedString, other.attributedString) + && _objectsEqual(truncationAttributedString, other.truncationAttributedString); + } + + size_t hash() const; +}; + +#endif diff --git a/AsyncDisplayKit/TextKit/CKTextKitAttributes.mm b/AsyncDisplayKit/TextKit/CKTextKitAttributes.mm new file mode 100755 index 0000000000..fab1035c91 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitAttributes.mm @@ -0,0 +1,36 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "CKTextKitAttributes.h" + +#import "CKEqualityHashHelpers.h" + +#include + +NSString *const CKTextKitTruncationAttributeName = @"ck_truncation"; +NSString *const CKTextKitEntityAttributeName = @"ck_entity"; + +size_t CKTextKitAttributes::hash() const +{ + NSUInteger subhashes[] = { + [attributedString hash], + [truncationAttributedString hash], + [avoidTailTruncationSet hash], + std::hash()((NSUInteger) layoutManagerFactory), + std::hash()(lineBreakMode), + std::hash()(maximumNumberOfLines), + std::hash()(shadowOffset.width), + std::hash()(shadowOffset.height), + [shadowColor hash], + std::hash()(shadowOpacity), + std::hash()(shadowRadius), + }; + return CKIntegerArrayHash(subhashes, sizeof(subhashes) / sizeof(subhashes[0])); +} diff --git a/AsyncDisplayKit/TextKit/CKTextKitContext.h b/AsyncDisplayKit/TextKit/CKTextKitContext.h new file mode 100755 index 0000000000..64f32a71c0 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitContext.h @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +/** + A threadsafe container for the TextKit components that CKTextKit uses to lay out and truncate its text. + + This container is the sole owner and manager of the TextKit classes. This is an important model because of major + thread safety issues inside vanilla TextKit. It provides a central locking location for accessing TextKit methods. + */ +@interface CKTextKitContext : NSObject + +/** + Initializes a context and its associated TextKit components. + + Initialization of TextKit components is a globally locking operation so be careful of bottlenecks with this class. + */ +- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString + lineBreakMode:(NSLineBreakMode)lineBreakMode + maximumNumberOfLines:(NSUInteger)maximumNumberOfLines + constrainedSize:(CGSize)constrainedSize + layoutManagerFactory:(NSLayoutManager*(*)(void))layoutManagerFactory; + +/** + All operations on TextKit values MUST occur within this locked context. Simultaneous access (even non-mutative) to + TextKit components may cause crashes. + + The block provided MUST not call out to client code from within its scope or it is possible for this to cause deadlocks + in your application. Use with EXTREME care. + + Callers MUST NOT keep a ref to these internal objects and use them later. This WILL cause crashes in your application. + */ +- (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *layoutManager, + NSTextStorage *textStorage, + NSTextContainer *textContainer))block; + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitContext.mm b/AsyncDisplayKit/TextKit/CKTextKitContext.mm new file mode 100755 index 0000000000..3e9bc586ba --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitContext.mm @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +#import "CKTextKitContext.h" + +@implementation CKTextKitContext +{ + // All TextKit operations (even non-mutative ones) must be executed serially. + std::mutex _textKitMutex; + + NSLayoutManager *_layoutManager; + NSTextStorage *_textStorage; + NSTextContainer *_textContainer; +} + +- (instancetype)initWithAttributedString:(NSAttributedString *)attributedString + lineBreakMode:(NSLineBreakMode)lineBreakMode + maximumNumberOfLines:(NSUInteger)maximumNumberOfLines + constrainedSize:(CGSize)constrainedSize + layoutManagerFactory:(NSLayoutManager*(*)(void))layoutManagerFactory +{ + if (self = [super init]) { + // Concurrently initialising TextKit components crashes (rdar://18448377) so we use a global lock. + static std::mutex __static_mutex; + std::lock_guard l(__static_mutex); + // Create the TextKit component stack with our default configuration. + _textStorage = (attributedString ? [[NSTextStorage alloc] initWithAttributedString:attributedString] : [[NSTextStorage alloc] init]); + _layoutManager = layoutManagerFactory ? layoutManagerFactory() : [[NSLayoutManager alloc] init]; + _layoutManager.usesFontLeading = NO; + [_textStorage addLayoutManager:_layoutManager]; + _textContainer = [[NSTextContainer alloc] initWithSize:constrainedSize]; + // We want the text laid out up to the very edges of the container. + _textContainer.lineFragmentPadding = 0; + _textContainer.lineBreakMode = lineBreakMode; + _textContainer.maximumNumberOfLines = maximumNumberOfLines; + [_layoutManager addTextContainer:_textContainer]; + } + return self; +} + +- (void)performBlockWithLockedTextKitComponents:(void (^)(NSLayoutManager *, + NSTextStorage *, + NSTextContainer *))block +{ + std::lock_guard l(_textKitMutex); + block(_layoutManager, _textStorage, _textContainer); +} + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitEntityAttribute.h b/AsyncDisplayKit/TextKit/CKTextKitEntityAttribute.h new file mode 100755 index 0000000000..3f588ace32 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitEntityAttribute.h @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +/** + The object that should be embedded with CKTextKitEntityAttributeName. Please note that the entity you provide MUST + implement a proper hash and isEqual function or your application performance will grind to a halt due to + NSMutableAttributedString's usage of a global hash table of all attributes. This means the entity should NOT be a + Foundation Collection (NSArray, NSDictionary, NSSet, etc.) since their hash function is a simple count of the values + in the collection, which causes pathological performance problems deep inside NSAttributedString's implementation. + + rdar://19352367 + */ +@interface CKTextKitEntityAttribute : NSObject + +@property (nonatomic, strong, readonly) id entity; + +- (instancetype)initWithEntity:(id)entity; + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitEntityAttribute.m b/AsyncDisplayKit/TextKit/CKTextKitEntityAttribute.m new file mode 100755 index 0000000000..bb03948173 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitEntityAttribute.m @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "CKTextKitEntityAttribute.h" + +@implementation CKTextKitEntityAttribute + +- (instancetype)initWithEntity:(id)entity +{ + if (self = [super init]) { + _entity = entity; + } + return self; +} + +- (NSUInteger)hash +{ + return [_entity hash]; +} + +- (BOOL)isEqual:(id)object +{ + if (self == object) { + return YES; + } + if (![object isKindOfClass:[self class]]) { + return NO; + } + CKTextKitEntityAttribute *other = (CKTextKitEntityAttribute *)object; + return _entity == other.entity || [_entity isEqual:other.entity]; +} + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitRenderer+Positioning.h b/AsyncDisplayKit/TextKit/CKTextKitRenderer+Positioning.h new file mode 100755 index 0000000000..efc18a92d1 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitRenderer+Positioning.h @@ -0,0 +1,103 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "CKTextKitRenderer.h" + +typedef void (^ck_text_component_index_block_t)(NSUInteger characterIndex, + CGRect glyphBoundingRect, + BOOL *stop); + +/** + Measure options are used to specify which type of line height measurement to use. + + ASTextNodeRendererMeasureOptionLineHeight is faster and will give the height from the baseline to the next line. + + ASTextNodeRendererMeasureOptionCapHeight is a more nuanced measure of the glyphs in the given range that attempts to + produce a visually balanced rectangle above and below the glyphs to produce nice looking text highlights. + + ASTextNodeRendererMeasureOptionBlock uses the cap height option to generate each glyph index, but combines all but the + first and last line rect into a single block. Looks nice for multiline selection. + */ +typedef NS_ENUM(NSUInteger, CKTextKitRendererMeasureOption) { + CKTextKitRendererMeasureOptionLineHeight, + CKTextKitRendererMeasureOptionCapHeight, + CKTextKitRendererMeasureOptionBlock +}; + +@interface CKTextKitRenderer (Positioning) + +/** + Returns the bounding rect for the given character range. + + @param textRange The character range for which the bounding rect will be computed. Should be within the range of the + attributedString of this renderer. + + @discussion In the external, shadowed coordinate space. + */ +- (CGRect)frameForTextRange:(NSRange)textRange; + +/** + Returns an array of rects representing the lines in the given character range + + @param textRange The character range for which the rects will be computed. Should be within the range of the + attributedString of this renderer. + @param measureOption The measure option to use for construction of the rects. See CKTextKitRendererMeasureOption + docs for usage. + + @discussion This method is useful for providing highlighting text. Returned rects are in the coordinate space of the + renderer. + + Triggers initialization of textkit components, truncation, and sizing. + */ +- (NSArray *)rectsForTextRange:(NSRange)textRange + measureOption:(CKTextKitRendererMeasureOption)measureOption; + +/** + Enumerate the text character indexes at a position within the coordinate space of the renderer. + + @param position The point in the shadowed coordinate space at which text indexes will be enumerated. + @param block The block that will be executed for each index identified that may correspond to the given position. The + block is given the character index that corresponds to the glyph at each index in question, as well as the bounding + rect for that glyph. + + @discussion Glyph location based on a touch point is not an exact science because user touches are not well-represented + by a simple point, especially in the context of link-heavy text. So we have this method to make it a bit easier. This + method checks a grid of candidate positions around the touch point you give it, and computes the bounding rect of the + glyph corresponding to the character index given. + + The bounding rect of the glyph can be used to identify the best glyph index that corresponds to your touch. For + instance, comparing centroidal distance from the glyph bounding rect to the touch center is useful for identifying + which link a user actually intended to select. + + Triggers initialization of textkit components, truncation, and sizing. + */ +- (void)enumerateTextIndexesAtPosition:(CGPoint)position + usingBlock:(ck_text_component_index_block_t)block; + +/** + Returns the single text index whose glyph's centroid is closest to the given position. + + @param position The point in the shadowed coordinate space that should be checked. + + @discussion This will use the grid enumeration function above, `enumerateTextIndexesAtPosition...`, in order to find + the closest glyph, so it is possible that a glyph could be missed, but ultimately unlikely. + */ +- (NSUInteger)nearestTextIndexAtPosition:(CGPoint)position; + +/** + Returns the trailing rect unused by the renderer in the last rendered line. + + @discussion In the external shadowed coordinate space. + + Triggers initialization of textkit components, truncation, and sizing. + */ +- (CGRect)trailingRect; + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitRenderer+Positioning.mm b/AsyncDisplayKit/TextKit/CKTextKitRenderer+Positioning.mm new file mode 100755 index 0000000000..1b65cad815 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitRenderer+Positioning.mm @@ -0,0 +1,374 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "CKTextKitRenderer+Positioning.h" + +#import + +#import "ASAssert.h" + +#import "CKTextKitContext.h" +#import "CKTextKitShadower.h" + +static const CGFloat CKTextKitRendererGlyphTouchHitSlop = 5.0; +static const CGFloat CKTextKitRendererTextCapHeightPadding = 1.3; + +@implementation CKTextKitRenderer (Tracking) + +- (NSArray *)rectsForTextRange:(NSRange)textRange + measureOption:(CKTextKitRendererMeasureOption)measureOption +{ + __block NSArray *textRects = @[]; + [self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + BOOL textRangeIsValid = (NSMaxRange(textRange) <= [textStorage length]); + ASDisplayNodeCAssertTrue(textRangeIsValid); + if (!textRangeIsValid) { + return; + } + + // Used for block measure option + __block CGRect firstRect = CGRectNull; + __block CGRect lastRect = CGRectNull; + __block CGRect blockRect = CGRectNull; + NSMutableArray *mutableTextRects = [NSMutableArray array]; + + NSString *string = textStorage.string; + + NSRange totalGlyphRange = [layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL]; + + [layoutManager enumerateLineFragmentsForGlyphRange:totalGlyphRange usingBlock:^(CGRect rect, + CGRect usedRect, + NSTextContainer *innerTextContainer, + NSRange glyphRange, + BOOL *stop) { + + CGRect lineRect = CGRectNull; + // If we're empty, don't bother looping through glyphs, use the default. + if (CGRectIsEmpty(usedRect)) { + lineRect = usedRect; + } else { + // TextKit's bounding rect computations are just a touch off, so we actually + // compose the rects by hand from the center of the given TextKit bounds and + // imposing the font attributes returned by the glyph's font. + NSRange lineGlyphRange = NSIntersectionRange(totalGlyphRange, glyphRange); + for (NSUInteger i = lineGlyphRange.location; i < NSMaxRange(lineGlyphRange) && i < string.length; i++) { + // We grab the properly sized rect for the glyph + CGRect properGlyphRect = [self _internalRectForGlyphAtIndex:i + measureOption:measureOption + layoutManager:layoutManager + textContainer:textContainer + textStorage:textStorage]; + + // Don't count empty glyphs towards our line rect. + if (!CGRectIsEmpty(properGlyphRect)) { + lineRect = CGRectIsNull(lineRect) ? properGlyphRect + : CGRectUnion(lineRect, properGlyphRect); + } + } + } + + if (!CGRectIsNull(lineRect)) { + if (measureOption == CKTextKitRendererMeasureOptionBlock) { + // For the block measurement option we store the first & last rect as + // special cases, then merge everything else into a single block rect + if (CGRectIsNull(firstRect)) { + // We don't have a firstRect, so we must be on the first line. + firstRect = lineRect; + } else if(CGRectIsNull(lastRect)) { + // We don't have a lastRect, but we do have a firstRect, so we must + // be on the second line. No need to merge in the blockRect just yet + lastRect = lineRect; + } else if(CGRectIsNull(blockRect)) { + // We have both a first and last rect, so we must be on the third line + // we don't have any blockRect to merge it into, so we just set it + // directly. + blockRect = lastRect; + lastRect = lineRect; + } else { + // Everything is already set, so we just merge this line into the + // block. + blockRect = CGRectUnion(blockRect, lastRect); + lastRect = lineRect; + } + } else { + // If the block option isn't being used then each line is being treated + // individually. + [mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:lineRect]]]; + } + } + }]; + + if (measureOption == CKTextKitRendererMeasureOptionBlock) { + // Block measure option is handled differently with just 3 vars for the entire range. + if (!CGRectIsNull(firstRect)) { + if (!CGRectIsNull(blockRect)) { + CGFloat rightEdge = MAX(CGRectGetMaxX(blockRect), CGRectGetMaxX(lastRect)); + if (rightEdge > CGRectGetMaxX(firstRect)) { + // Force the right side of the first rect to properly align with the + // right side of the rightmost of the block and last rect + firstRect.size.width += rightEdge - CGRectGetMaxX(firstRect); + } + + // Force the left side of the block rect to properly align with the + // left side of the leftmost of the first and last rect + blockRect.origin.x = MIN(CGRectGetMinX(firstRect), CGRectGetMinX(lastRect)); + // Force the right side of the block rect to properly align with the + // right side of the rightmost of the first and last rect + blockRect.size.width += MAX(CGRectGetMaxX(firstRect), CGRectGetMaxX(lastRect)) - CGRectGetMaxX(blockRect); + } + if (!CGRectIsNull(lastRect)) { + // Force the left edge of the last rect to properly align with the + // left side of the leftmost of the first and block rect, if necessary. + CGFloat leftEdge = MIN(CGRectGetMinX(blockRect), CGRectGetMinX(firstRect)); + CGFloat lastRectNudgeAmount = MAX(CGRectGetMinX(lastRect) - leftEdge, 0); + lastRect.origin.x = MIN(leftEdge, CGRectGetMinX(lastRect)); + lastRect.size.width += lastRectNudgeAmount; + } + + [mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:firstRect]]]; + } + if (!CGRectIsNull(blockRect)) { + [mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:blockRect]]]; + } + if (!CGRectIsNull(lastRect)) { + [mutableTextRects addObject:[NSValue valueWithCGRect:[self.shadower offsetRectWithInternalRect:lastRect]]]; + } + } + textRects = mutableTextRects; + }]; + + return textRects; +} + +- (NSUInteger)nearestTextIndexAtPosition:(CGPoint)position +{ + // Check in a 9-point region around the actual touch point so we make sure + // we get the best attribute for the touch. + __block CGFloat minimumGlyphDistance = CGFLOAT_MAX; + __block NSUInteger minimumGlyphCharacterIndex = NSNotFound; + + [self enumerateTextIndexesAtPosition:position usingBlock:^(NSUInteger characterIndex, CGRect glyphBoundingRect, BOOL *stop) { + CGPoint glyphLocation = CGPointMake(CGRectGetMidX(glyphBoundingRect), CGRectGetMidY(glyphBoundingRect)); + CGFloat currentDistance = sqrtf(powf(position.x - glyphLocation.x, 2.f) + powf(position.y - glyphLocation.y, 2.f)); + if (currentDistance < minimumGlyphDistance) { + minimumGlyphDistance = currentDistance; + minimumGlyphCharacterIndex = characterIndex; + } + }]; + return minimumGlyphCharacterIndex; +} + +/** + Measured from the internal coordinate space of the context, not accounting for shadow offsets. Actually uses CoreText + as an approximation to work around problems in TextKit's glyph sizing. + */ +- (CGRect)_internalRectForGlyphAtIndex:(NSUInteger)glyphIndex + measureOption:(CKTextKitRendererMeasureOption)measureOption + layoutManager:(NSLayoutManager *)layoutManager + textContainer:(NSTextContainer *)textContainer + textStorage:(NSTextStorage *)textStorage +{ + NSUInteger charIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex]; + CGGlyph glyph = [layoutManager glyphAtIndex:glyphIndex]; + CTFontRef font = (__bridge_retained CTFontRef)[textStorage attribute:NSFontAttributeName + atIndex:charIndex + effectiveRange:NULL]; + if (font == nil) { + font = (__bridge_retained CTFontRef)[UIFont systemFontOfSize:12.0]; + } + + // Glyph Advance + // +-------------------------+ + // | | + // | | + // +------------------------+--|-------------------------|--+-----------+-----+ What TextKit returns sometimes + // | | | XXXXXXXXXXX + | | | (approx. correct height, but + // | ---------|--+---------+ XXX XXXX +|-----------|-----| sometimes inaccurate bounding + // | | | XXX XXXXX| | | widths) + // | | | XX XX | | | + // | | | XX | | | + // | | | XXX | | | + // | | | XX | | | + // | | | XXXXXXXXXXX | | | + // | Cap Height->| | XX | | | + // | | | XX | Ascent-->| | + // | | | XX | | | + // | | | XX | | | + // | | | X | | | + // | | | X | | | + // | | | X | | | + // | | | XX | | | + // | | | X | | | + // | ---------|-------+ X +-------------------------------------| + // | | XX | | + // | | X | | + // | | XX Descent------>| | + // | | XXXXXX | | + // | | XXX | | + // +------------------------+-------------------------------------------------+ + // | + // +--+Actual bounding box + + CGRect glyphRect = [layoutManager boundingRectForGlyphRange:NSMakeRange(glyphIndex, 1) + inTextContainer:textContainer]; + + // If it is a NSTextAttachment, we don't have the matched glyph and use width of glyphRect instead of advance. + CGFloat advance = (glyph == kCGFontIndexInvalid) ? glyphRect.size.width : CTFontGetAdvancesForGlyphs(font, kCTFontOrientationHorizontal, &glyph, NULL, 1); + + // We treat the center of the glyph's bounding box as the center of our new rect + CGPoint glyphCenter = CGPointMake(CGRectGetMidX(glyphRect), CGRectGetMidY(glyphRect)); + + CGRect properGlyphRect; + if (measureOption == CKTextKitRendererMeasureOptionCapHeight + || measureOption == CKTextKitRendererMeasureOptionBlock) { + CGFloat ascent = CTFontGetAscent(font); + CGFloat descent = CTFontGetDescent(font); + CGFloat capHeight = CTFontGetCapHeight(font); + CGFloat leading = CTFontGetLeading(font); + CGFloat glyphHeight = ascent + descent; + + // For visual balance, we add the cap height padding above the cap, and + // below the baseline, we scale by the descent so it grows with the size of + // the text. + CGFloat topPadding = CKTextKitRendererTextCapHeightPadding * descent; + CGFloat bottomPadding = topPadding; + + properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5, + glyphCenter.y - glyphHeight * 0.5 + (ascent - capHeight) - topPadding + leading, + advance, + capHeight + topPadding + bottomPadding); + } else { + // We are just measuring the line heights here, so we can use the + // heights used by TextKit, which tend to be pretty good. + properGlyphRect = CGRectMake(glyphCenter.x - advance * 0.5, + glyphRect.origin.y, + advance, + glyphRect.size.height); + } + + CFRelease(font); + + return properGlyphRect; +} + +- (void)enumerateTextIndexesAtPosition:(CGPoint)externalPosition usingBlock:(ck_text_component_index_block_t)block +{ + // This method is a little complex because it has to call out to client code from inside an enumeration that needs + // to achieve a lock on the textkit components. It cannot call out to client code from within that lock so we just + // perform the textkit-locked ops inside the locked context. + CKTextKitContext *lockingContext = self.context; + CGPoint internalPosition = [self.shadower offsetPointWithExternalPoint:externalPosition]; + __block BOOL invalidPosition = NO; + [lockingContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + invalidPosition = internalPosition.x > textContainer.size.width + || internalPosition.y > textContainer.size.height + || block == NULL; + }]; + if (invalidPosition) { + // Short circuit if the position is outside the size of this renderer, or if the block is null. + return; + } + + // We break it up into a 44pt box for the touch, and find the closest link attribute-containing glyph to the center of + // the touch. + CGFloat squareSide = 44.f; + // Should be odd if you want to test the center of the touch. + NSInteger pointsOnASide = 3; + + // The distance between any 2 of the adjacent points + CGFloat pointSeparation = squareSide / pointsOnASide; + // These are for tracking which point we're on. We start with -pointsOnASide/2 and go to pointsOnASide/2. So if + // pointsOnASide=3, we go from -1 to 1. + NSInteger endIndex = pointsOnASide / 2; + NSInteger startIndex = -endIndex; + + BOOL stop = NO; + for (NSInteger i = startIndex; i <= endIndex && !stop; i++) { + for (NSInteger j = startIndex; j <= endIndex && !stop; j++) { + CGPoint currentPoint = CGPointMake(internalPosition.x + i * pointSeparation, + internalPosition.y + j * pointSeparation); + + __block NSUInteger characterIndex = NSNotFound; + __block BOOL isValidGlyph = NO; + __block CGRect glyphRect = CGRectNull; + + [lockingContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + // We ask the layout manager for the proper glyph at the touch point + NSUInteger glyphIndex = [layoutManager glyphIndexForPoint:currentPoint + inTextContainer:textContainer]; + + // If it's an invalid glyph, quit. + + [layoutManager glyphAtIndex:glyphIndex isValidIndex:&isValidGlyph]; + if (!isValidGlyph) { + return; + } + + characterIndex = [layoutManager characterIndexForGlyphAtIndex:glyphIndex]; + + glyphRect = [self _internalRectForGlyphAtIndex:glyphIndex + measureOption:CKTextKitRendererMeasureOptionLineHeight + layoutManager:layoutManager + textContainer:textContainer + textStorage:textStorage]; + }]; + + // Sometimes TextKit plays jokes on us and returns glyphs that really aren't close to the point in question. + // Silly TextKit... + if (!isValidGlyph || !CGRectContainsPoint(CGRectInset(glyphRect, -CKTextKitRendererGlyphTouchHitSlop, -CKTextKitRendererGlyphTouchHitSlop), currentPoint)) { + continue; + } + + block(characterIndex, [self.shadower offsetRectWithInternalRect:glyphRect], &stop); + } + } +} + +- (CGRect)trailingRect +{ + __block CGRect trailingRect = CGRectNull; + [self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + CGSize calculatedSize = textContainer.size; + // If have an empty string, then our whole bounds constitute trailing space. + if ([textStorage length] == 0) { + trailingRect = CGRectMake(0, 0, calculatedSize.width, calculatedSize.height); + return; + } + + // Take everything after our final character as trailing space. + NSArray *finalRects = [self rectsForTextRange:NSMakeRange([textStorage length] - 1, 1) measureOption:CKTextKitRendererMeasureOptionLineHeight]; + CGRect finalGlyphRect = [[finalRects lastObject] CGRectValue]; + CGPoint origin = CGPointMake(CGRectGetMaxX(finalGlyphRect), CGRectGetMinY(finalGlyphRect)); + CGSize size = CGSizeMake(calculatedSize.width - origin.x, calculatedSize.height - origin.y); + trailingRect = (CGRect){origin, size}; + }]; + return trailingRect; +} + +- (CGRect)frameForTextRange:(NSRange)textRange +{ + __block CGRect textRect = CGRectNull; + [self.context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + // Bail on invalid range. + if (NSMaxRange(textRange) > [textStorage length]) { + ASDisplayNodeCFailAssert(@"Invalid range"); + return; + } + + // Force glyph generation and layout. + [layoutManager ensureLayoutForTextContainer:textContainer]; + + NSRange glyphRange = [layoutManager glyphRangeForCharacterRange:textRange actualCharacterRange:NULL]; + textRect = [layoutManager boundingRectForGlyphRange:glyphRange inTextContainer:textContainer]; + }]; + return textRect; +} + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitRenderer+TextChecking.h b/AsyncDisplayKit/TextKit/CKTextKitRenderer+TextChecking.h new file mode 100755 index 0000000000..4ed56e5237 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitRenderer+TextChecking.h @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "CKTextKitRenderer.h" + +/** + Application extensions to NSTextCheckingType. We're allowed to do this (see NSTextCheckingAllCustomTypes). + */ +static uint64_t const CKTextKitTextCheckingTypeEntity = 1ULL << 33; +static uint64_t const CKTextKitTextCheckingTypeTruncation = 1ULL << 34; + +@class CKTextKitEntityAttribute; + +@interface CKTextKitTextCheckingResult : NSTextCheckingResult +@property (nonatomic, strong, readonly) CKTextKitEntityAttribute *entityAttribute; +@end + +@interface CKTextKitRenderer (TextChecking) + +- (NSTextCheckingResult *)textCheckingResultAtPoint:(CGPoint)point; + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitRenderer+TextChecking.mm b/AsyncDisplayKit/TextKit/CKTextKitRenderer+TextChecking.mm new file mode 100755 index 0000000000..1c87c4ac38 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitRenderer+TextChecking.mm @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "CKTextKitRenderer+TextChecking.h" + +#import "CKTextKitAttributes.h" +#import "CKTextKitEntityAttribute.h" +#import "CKTextKitRenderer+Positioning.h" +#import "CKTextKitTailTruncater.h" + +@implementation CKTextKitTextCheckingResult + +{ + // Be explicit about the fact that we are overriding the super class' implementation of -range and -resultType + // and substituting our own custom values. (We could use @synthesize to make these ivars, but our linter correctly + // complains; it's weird to use @synthesize for properties that are redeclared on top of an original declaration in + // the superclass. We only do it here because NSTextCheckingResult doesn't expose an initializer, which is silly.) + NSRange _rangeOverride; + NSTextCheckingType _resultTypeOverride; +} + +- (instancetype)initWithType:(NSTextCheckingType)type + entityAttribute:(CKTextKitEntityAttribute *)entityAttribute + range:(NSRange)range +{ + if ((self = [super init])) { + _resultTypeOverride = type; + _rangeOverride = range; + _entityAttribute = entityAttribute; + } + return self; +} + +- (NSTextCheckingType)resultType +{ + return _resultTypeOverride; +} + +- (NSRange)range +{ + return _rangeOverride; +} + +@end + +@implementation CKTextKitRenderer (TextChecking) + +- (NSTextCheckingResult *)textCheckingResultAtPoint:(CGPoint)point +{ + __block NSTextCheckingResult *result = nil; + NSAttributedString *attributedString = self.attributes.attributedString; + NSAttributedString *truncationAttributedString = self.attributes.truncationAttributedString; + + // get the index of the last character, so we can handle text in the truncation token + NSRange visibleRange = self.truncater.visibleRanges[0]; + __block NSRange truncationTokenRange = { NSNotFound, 0 }; + + [truncationAttributedString enumerateAttribute:CKTextKitTruncationAttributeName inRange:NSMakeRange(0, truncationAttributedString.length) + options:0 + usingBlock:^(id value, NSRange range, BOOL *stop) { + if (value != nil && range.length > 0) { + truncationTokenRange = range; + } + }]; + + if (truncationTokenRange.location == NSNotFound) { + // The truncation string didn't specify a substring which should be highlighted, so we just highlight it all + truncationTokenRange = { 0, self.attributes.truncationAttributedString.length }; + } + + truncationTokenRange.location += NSMaxRange(visibleRange); + + [self enumerateTextIndexesAtPosition:point usingBlock:^(NSUInteger index, CGRect glyphBoundingRect, BOOL *stop){ + if (index >= truncationTokenRange.location) { + result = [[CKTextKitTextCheckingResult alloc] initWithType:CKTextKitTextCheckingTypeTruncation + entityAttribute:nil + range:truncationTokenRange]; + } else { + NSRange range; + NSDictionary *attributes = [attributedString attributesAtIndex:index effectiveRange:&range]; + CKTextKitEntityAttribute *entityAttribute = attributes[CKTextKitEntityAttributeName]; + if (entityAttribute) { + result = [[CKTextKitTextCheckingResult alloc] initWithType:CKTextKitTextCheckingTypeEntity + entityAttribute:entityAttribute + range:range]; + } + } + if (result != nil) { + *stop = YES; + } + }]; + return result; +} + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitRenderer.h b/AsyncDisplayKit/TextKit/CKTextKitRenderer.h new file mode 100755 index 0000000000..abac746ec1 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitRenderer.h @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +#import + +#import "CKTextKitAttributes.h" + +@class CKTextKitContext; +@class CKTextKitShadower; +@protocol CKTextKitTruncating; + +/** + CKTextKitRenderer is a modular object that is responsible for laying out and drawing text. + + A renderer will hold onto the TextKit layouts for the given attributes after initialization. This may constitute a + large amount of memory for large enough applications, so care must be taken when keeping many of these around in-memory + at once. + + This object is designed to be modular and simple. All complex maintenance of state should occur in sub-objects or be + derived via pure functions or categories. No touch-related handling belongs in this class. + + ALL sizing and layout information from this class is in the external coordinate space of the TextKit components. This + is an important distinction because all internal sizing and layout operations are carried out within the shadowed + coordinate space. Padding will be added for you in order to ensure clipping does not occur, and additional information + on this transform is available via the shadower should you need it. + */ +@interface CKTextKitRenderer : NSObject + +/** + Designated Initializer +dvlkferufedgjnhjjfhldjedlunvtdtv + @discussion Sizing will occur as a result of initialization, so be careful when/where you use this. + */ +- (instancetype)initWithTextKitAttributes:(const CKTextKitAttributes &)textComponentAttributes + constrainedSize:(const CGSize)constrainedSize; + +@property (nonatomic, strong, readonly) CKTextKitContext *context; + +@property (nonatomic, strong, readonly) id truncater; + +@property (nonatomic, strong, readonly) CKTextKitShadower *shadower; + +@property (nonatomic, assign, readonly) CKTextKitAttributes attributes; + +@property (nonatomic, assign, readonly) CGSize constrainedSize; + +#pragma mark - Drawing +/* + Draw the renderer's text content into the bounds provided. + + @param bounds The rect in which to draw the contents of the renderer. + */ +- (void)drawInContext:(CGContextRef)context bounds:(CGRect)bounds; + +#pragma mark - Layout + +/* + Returns the computed size of the renderer given the constrained size and other parameters in the initializer. + */ +- (CGSize)size; + +#pragma mark - Text Ranges + +/* + The character range from the original attributedString that is displayed by the renderer given the parameters in the + initializer. + */ +- (std::vector)visibleRanges; + +/* + The number of lines shown in the string. + */ +- (NSUInteger)lineCount; + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitRenderer.mm b/AsyncDisplayKit/TextKit/CKTextKitRenderer.mm new file mode 100755 index 0000000000..8bdfabbb36 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitRenderer.mm @@ -0,0 +1,140 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "CKTextKitRenderer.h" + +#import "ASAssert.h" + +#import "CKTextKitContext.h" +#import "CKTextKitShadower.h" +#import "CKTextKitTailTruncater.h" +#import "CKTextKitTruncating.h" + +static NSCharacterSet *_defaultAvoidTruncationCharacterSet() +{ + static NSCharacterSet *truncationCharacterSet; + static dispatch_once_t onceToken; + dispatch_once(&onceToken, ^{ + NSMutableCharacterSet *mutableCharacterSet = [[NSMutableCharacterSet alloc] init]; + [mutableCharacterSet formUnionWithCharacterSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]]; + [mutableCharacterSet addCharactersInString:@".,!?:;"]; + truncationCharacterSet = mutableCharacterSet; + }); + return truncationCharacterSet; +} + +@implementation CKTextKitRenderer { + CGSize _calculatedSize; +} + +#pragma mark - Initialization + +- (instancetype)initWithTextKitAttributes:(const CKTextKitAttributes &)attributes + constrainedSize:(const CGSize)constrainedSize +{ + if (self = [super init]) { + _constrainedSize = constrainedSize; + _attributes = attributes; + + _shadower = [[CKTextKitShadower alloc] initWithShadowOffset:attributes.shadowOffset + shadowColor:attributes.shadowColor + shadowOpacity:attributes.shadowOpacity + shadowRadius:attributes.shadowRadius]; + + // We must inset the constrained size by the size of the shadower. + CGSize shadowConstrainedSize = [_shadower insetSizeWithConstrainedSize:_constrainedSize]; + + _context = [[CKTextKitContext alloc] initWithAttributedString:attributes.attributedString + lineBreakMode:attributes.lineBreakMode + maximumNumberOfLines:attributes.maximumNumberOfLines + constrainedSize:shadowConstrainedSize + layoutManagerFactory:attributes.layoutManagerFactory]; + + _truncater = [[CKTextKitTailTruncater alloc] initWithContext:_context + truncationAttributedString:attributes.truncationAttributedString + avoidTailTruncationSet:attributes.avoidTailTruncationSet ?: _defaultAvoidTruncationCharacterSet() + constrainedSize:shadowConstrainedSize]; + + [self _calculateSize]; + } + return self; +} + +#pragma mark - Sizing + +- (void)_calculateSize +{ + // Force glyph generation and layout, which may not have happened yet (and isn't triggered by + // -usedRectForTextContainer:). + [_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + [layoutManager ensureLayoutForTextContainer:textContainer]; + }]; + + + CGRect constrainedRect = {CGPointZero, _constrainedSize}; + __block CGRect boundingRect; + [_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + boundingRect = [layoutManager usedRectForTextContainer:textContainer]; + }]; + + // TextKit often returns incorrect glyph bounding rects in the horizontal direction, so we clip to our bounding rect + // to make sure our width calculations aren't being offset by glyphs going beyond the constrained rect. + boundingRect = CGRectIntersection(boundingRect, {.size = constrainedRect.size}); + + _calculatedSize = [_shadower outsetSizeWithInsetSize:boundingRect.size]; +} + +- (CGSize)size +{ + return _calculatedSize; +} + +#pragma mark - Drawing + +- (void)drawInContext:(CGContextRef)context bounds:(CGRect)bounds; +{ + // We add an assertion so we can track the rare conditions where a graphics context is not present + ASDisplayNodeAssertNotNil(context, @"This is no good without a context."); + + CGRect shadowInsetBounds = [_shadower insetRectWithConstrainedRect:bounds]; + + CGContextSaveGState(context); + [_shadower setShadowInContext:context]; + UIGraphicsPushContext(context); + + [_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + NSRange glyphRange = [layoutManager glyphRangeForTextContainer:textContainer]; + [layoutManager drawBackgroundForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; + [layoutManager drawGlyphsForGlyphRange:glyphRange atPoint:shadowInsetBounds.origin]; + }]; + + UIGraphicsPopContext(); + CGContextRestoreGState(context); +} + +#pragma mark - String Ranges + +- (NSUInteger)lineCount +{ + __block NSUInteger lineCount = 0; + [_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + for (NSRange lineRange = { 0, 0 }; NSMaxRange(lineRange) < [layoutManager numberOfGlyphs]; lineCount++) { + [layoutManager lineFragmentRectForGlyphAtIndex:NSMaxRange(lineRange) effectiveRange:&lineRange]; + } + }]; + return lineCount; +} + +- (std::vector)visibleRanges +{ + return _truncater.visibleRanges; +} + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitShadower.h b/AsyncDisplayKit/TextKit/CKTextKitShadower.h new file mode 100755 index 0000000000..f1a40e0754 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitShadower.h @@ -0,0 +1,70 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +/** + * @abstract an immutable class for calculating shadow padding drawing a shadowed background for text + */ +@interface CKTextKitShadower : NSObject + +- (instancetype)initWithShadowOffset:(CGSize)shadowOffset + shadowColor:(UIColor *)shadowColor + shadowOpacity:(CGFloat)shadowOpacity + shadowRadius:(CGFloat)shadowRadius; + +/** + * @abstract The offset from the top-left corner at which the shadow starts. + * @discussion A positive width will move the shadow to the right. + * A positive height will move the shadow downwards. + */ +@property (nonatomic, readonly, assign) CGSize shadowOffset; + +//! CGColor in which the shadow is drawn +@property (nonatomic, readonly, strong) UIColor *shadowColor; + +//! Alpha of the shadow +@property (nonatomic, readonly, assign) CGFloat shadowOpacity; + +//! Radius, in pixels +@property (nonatomic, readonly, assign) CGFloat shadowRadius; + +/** + * @abstract The edge insets which represent shadow padding + * @discussion Each edge inset is less than or equal to zero. + * + * Example: + * CGRect boundsWithoutShadowPadding; // Large enough to fit text, not large enough to fit the shadow as well + * UIEdgeInsets shadowPadding = [shadower shadowPadding]; + * CGRect boundsWithShadowPadding = UIEdgeInsetsRect(boundsWithoutShadowPadding, shadowPadding); + */ +- (UIEdgeInsets)shadowPadding; + +- (CGSize)insetSizeWithConstrainedSize:(CGSize)constrainedSize; + +- (CGRect)insetRectWithConstrainedRect:(CGRect)constrainedRect; + +- (CGSize)outsetSizeWithInsetSize:(CGSize)insetSize; + +- (CGRect)outsetRectWithInsetRect:(CGRect)insetRect; + +- (CGRect)offsetRectWithInternalRect:(CGRect)internalRect; + +- (CGPoint)offsetPointWithInternalPoint:(CGPoint)internalPoint; + +- (CGPoint)offsetPointWithExternalPoint:(CGPoint)externalPoint; + +/** + * @abstract draws the shadow for text in the provided CGContext + * @discussion Call within the text node's +drawRect method + */ +- (void)setShadowInContext:(CGContextRef)context; + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitShadower.mm b/AsyncDisplayKit/TextKit/CKTextKitShadower.mm new file mode 100755 index 0000000000..a9362549fd --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitShadower.mm @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "CKTextKitShadower.h" + +static inline CGSize _insetSize(CGSize size, UIEdgeInsets insets) +{ + return UIEdgeInsetsInsetRect({.size = size}, insets).size; +} + +static inline UIEdgeInsets _invertInsets(UIEdgeInsets insets) +{ + return { + .top = -insets.top, + .left = -insets.left, + .bottom = -insets.bottom, + .right = -insets.right + }; +} + +@implementation CKTextKitShadower { + UIEdgeInsets _calculatedShadowPadding; +} + +- (instancetype)initWithShadowOffset:(CGSize)shadowOffset + shadowColor:(UIColor *)shadowColor + shadowOpacity:(CGFloat)shadowOpacity + shadowRadius:(CGFloat)shadowRadius +{ + if (self = [super init]) { + _shadowOffset = shadowOffset; + _shadowColor = shadowColor; + _shadowOpacity = shadowOpacity; + _shadowRadius = shadowRadius; + _calculatedShadowPadding = UIEdgeInsetsMake(-INFINITY, -INFINITY, INFINITY, INFINITY); + } + return self; +} + +/* + * This method is duplicated here because it gets called frequently, and we were + * wasting valuable time constructing a state object to ask it. + */ +- (BOOL)_shouldDrawShadow +{ + return _shadowOpacity != 0.0 && _shadowColor != nil && (_shadowRadius != 0 || !CGSizeEqualToSize(_shadowOffset, CGSizeZero)); +} + +- (void)setShadowInContext:(CGContextRef)context +{ + if ([self _shouldDrawShadow]) { + CGColorRef textShadowColor = CGColorRetain(_shadowColor.CGColor); + CGSize textShadowOffset = _shadowOffset; + CGFloat textShadowOpacity = _shadowOpacity; + CGFloat textShadowRadius = _shadowRadius; + + if (textShadowOpacity != 1.0) { + CGFloat inherentAlpha = CGColorGetAlpha(textShadowColor); + + CGColorRef oldTextShadowColor = textShadowColor; + textShadowColor = CGColorCreateCopyWithAlpha(textShadowColor, inherentAlpha * textShadowOpacity); + CGColorRelease(oldTextShadowColor); + } + + CGContextSetShadowWithColor(context, textShadowOffset, textShadowRadius, textShadowColor); + + CGColorRelease(textShadowColor); + } +} + + +- (UIEdgeInsets)shadowPadding +{ + if (_calculatedShadowPadding.top == -INFINITY) { + if (![self _shouldDrawShadow]) { + return UIEdgeInsetsZero; + } + + UIEdgeInsets shadowPadding = UIEdgeInsetsZero; + + // min values are expected to be negative for most typical shadowOffset and + // blurRadius settings: + shadowPadding.top = fminf(0.0f, _shadowOffset.height - _shadowRadius); + shadowPadding.left = fminf(0.0f, _shadowOffset.width - _shadowRadius); + + shadowPadding.bottom = fminf(0.0f, -_shadowOffset.height - _shadowRadius); + shadowPadding.right = fminf(0.0f, -_shadowOffset.width - _shadowRadius); + + _calculatedShadowPadding = shadowPadding; + } + + return _calculatedShadowPadding; +} + +- (CGSize)insetSizeWithConstrainedSize:(CGSize)constrainedSize +{ + return _insetSize(constrainedSize, _invertInsets([self shadowPadding])); +} + +- (CGRect)insetRectWithConstrainedRect:(CGRect)constrainedRect +{ + return UIEdgeInsetsInsetRect(constrainedRect, _invertInsets([self shadowPadding])); +} + +- (CGSize)outsetSizeWithInsetSize:(CGSize)insetSize +{ + return _insetSize(insetSize, [self shadowPadding]); +} + +- (CGRect)outsetRectWithInsetRect:(CGRect)insetRect +{ + return UIEdgeInsetsInsetRect(insetRect, [self shadowPadding]); +} + +- (CGRect)offsetRectWithInternalRect:(CGRect)internalRect +{ + return (CGRect){ + .origin = [self offsetPointWithInternalPoint:internalRect.origin], + .size = internalRect.size + }; +} + +- (CGPoint)offsetPointWithInternalPoint:(CGPoint)internalPoint +{ + UIEdgeInsets shadowPadding = [self shadowPadding]; + return (CGPoint){ + internalPoint.x + shadowPadding.left, + internalPoint.y + shadowPadding.top + }; +} + +- (CGPoint)offsetPointWithExternalPoint:(CGPoint)externalPoint +{ + UIEdgeInsets shadowPadding = [self shadowPadding]; + return (CGPoint){ + externalPoint.x - shadowPadding.left, + externalPoint.y - shadowPadding.top + }; +} + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitTailTruncater.h b/AsyncDisplayKit/TextKit/CKTextKitTailTruncater.h new file mode 100755 index 0000000000..df8ddf07f4 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitTailTruncater.h @@ -0,0 +1,17 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +#import "CKTextKitTruncating.h" + +@interface CKTextKitTailTruncater : NSObject + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitTailTruncater.mm b/AsyncDisplayKit/TextKit/CKTextKitTailTruncater.mm new file mode 100755 index 0000000000..14a8621d05 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitTailTruncater.mm @@ -0,0 +1,190 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import "ASAssert.h" + +#import "CKTextKitContext.h" +#import "CKTextKitTailTruncater.h" + +@implementation CKTextKitTailTruncater +{ + __weak CKTextKitContext *_context; + NSAttributedString *_truncationAttributedString; + NSCharacterSet *_avoidTailTruncationSet; + CGSize _constrainedSize; +} +@synthesize visibleRanges = _visibleRanges; +@synthesize truncationStringRect = _truncationStringRect; + +- (instancetype)initWithContext:(CKTextKitContext *)context + truncationAttributedString:(NSAttributedString *)truncationAttributedString + avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet + constrainedSize:(CGSize)constrainedSize +{ + if (self = [super init]) { + _context = context; + _truncationAttributedString = truncationAttributedString; + _avoidTailTruncationSet = avoidTailTruncationSet; + _constrainedSize = constrainedSize; + + [self _truncate]; + } + return self; +} + +/** + Calculates the intersection of the truncation message within the end of the last line. + */ +- (NSUInteger)_calculateCharacterIndexBeforeTruncationMessage:(NSLayoutManager *)layoutManager + textStorage:(NSTextStorage *)textStorage + textContainer:(NSTextContainer *)textContainer +{ + CGRect constrainedRect = (CGRect){ .size = textContainer.size }; + + NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:constrainedRect + inTextContainer:textContainer]; + NSInteger lastVisibleGlyphIndex = (NSMaxRange(visibleGlyphRange) - 1); + + if (lastVisibleGlyphIndex < 0) { + return NSNotFound; + } + + CGRect lastLineRect = [layoutManager lineFragmentRectForGlyphAtIndex:lastVisibleGlyphIndex + effectiveRange:NULL]; + CGRect lastLineUsedRect = [layoutManager lineFragmentUsedRectForGlyphAtIndex:lastVisibleGlyphIndex + effectiveRange:NULL]; + NSParagraphStyle *paragraphStyle = [textStorage attributesAtIndex:[layoutManager characterIndexForGlyphAtIndex:lastVisibleGlyphIndex] + effectiveRange:NULL][NSParagraphStyleAttributeName]; + // We assume LTR so long as the writing direction is not + BOOL rtlWritingDirection = paragraphStyle ? paragraphStyle.baseWritingDirection == NSWritingDirectionRightToLeft : NO; + // We only want to treat the trunction rect as left-aligned in the case that we are right-aligned and our writing + // direction is RTL. + BOOL leftAligned = CGRectGetMinX(lastLineRect) == CGRectGetMinX(lastLineUsedRect) || !rtlWritingDirection; + + // Calculate the bounding rectangle for the truncation message + CKTextKitContext *truncationContext = [[CKTextKitContext alloc] initWithAttributedString:_truncationAttributedString + lineBreakMode:NSLineBreakByWordWrapping + maximumNumberOfLines:1 + constrainedSize:constrainedRect.size + layoutManagerFactory:nil]; + + __block CGRect truncationUsedRect; + + [truncationContext performBlockWithLockedTextKitComponents:^(NSLayoutManager *truncationLayoutManager, NSTextStorage *truncationTextStorage, NSTextContainer *truncationTextContainer) { + // Size the truncation message + [truncationLayoutManager ensureLayoutForTextContainer:truncationTextContainer]; + NSRange truncationGlyphRange = [truncationLayoutManager glyphRangeForTextContainer:truncationTextContainer]; + truncationUsedRect = [truncationLayoutManager boundingRectForGlyphRange:truncationGlyphRange + inTextContainer:truncationTextContainer]; + }]; + CGFloat truncationOriginX = (leftAligned ? + CGRectGetMaxX(constrainedRect) - truncationUsedRect.size.width : + CGRectGetMinX(constrainedRect)); + CGRect translatedTruncationRect = CGRectMake(truncationOriginX, + CGRectGetMinY(lastLineRect), + truncationUsedRect.size.width, + truncationUsedRect.size.height); + + // Determine which glyph is the first to be clipped / overlaps the truncation message. + CGFloat truncationMessageX = (leftAligned ? + CGRectGetMinX(translatedTruncationRect) : + CGRectGetMaxX(translatedTruncationRect)); + CGPoint beginningOfTruncationMessage = CGPointMake(truncationMessageX, + CGRectGetMidY(translatedTruncationRect)); + NSUInteger firstClippedGlyphIndex = [layoutManager glyphIndexForPoint:beginningOfTruncationMessage + inTextContainer:textContainer + fractionOfDistanceThroughGlyph:NULL]; + // If it didn't intersect with any text then it should just return the last visible character index, since the + // truncation rect can fully fit on the line without clipping any other text. + if (firstClippedGlyphIndex == NSNotFound) { + return [layoutManager characterIndexForGlyphAtIndex:lastVisibleGlyphIndex]; + } + NSUInteger firstCharacterIndexToReplace = [layoutManager characterIndexForGlyphAtIndex:firstClippedGlyphIndex]; + + // Break on word boundaries + return [self _findTruncationInsertionPointAtOrBeforeCharacterIndex:firstCharacterIndexToReplace + layoutManager:layoutManager + textStorage:textStorage]; +} + +/** + Finds the first whitespace at or before the character index do we don't truncate in the middle of words + If there are multiple whitespaces together (say a space and a newline), this will backtrack to the first one + */ +- (NSUInteger)_findTruncationInsertionPointAtOrBeforeCharacterIndex:(NSUInteger)firstCharacterIndexToReplace + layoutManager:(NSLayoutManager *)layoutManager + textStorage:(NSTextStorage *)textStorage +{ + // Don't attempt to truncate beyond the end of the string + if (firstCharacterIndexToReplace >= textStorage.length) { + return 0; + } + + // Find the glyph range of the line fragment containing the first character to replace. + NSRange lineGlyphRange; + [layoutManager lineFragmentRectForGlyphAtIndex:[layoutManager glyphIndexForCharacterAtIndex:firstCharacterIndexToReplace] + effectiveRange:&lineGlyphRange]; + + // Look for the first whitespace from the end of the line, starting from the truncation point + NSUInteger startingSearchIndex = [layoutManager characterIndexForGlyphAtIndex:lineGlyphRange.location]; + NSUInteger endingSearchIndex = firstCharacterIndexToReplace; + NSRange rangeToSearch = NSMakeRange(startingSearchIndex, (endingSearchIndex - startingSearchIndex)); + + NSRange rangeOfLastVisibleAvoidedChars = { .location = NSNotFound }; + if (_avoidTailTruncationSet) { + rangeOfLastVisibleAvoidedChars = [textStorage.string rangeOfCharacterFromSet:_avoidTailTruncationSet + options:NSBackwardsSearch + range:rangeToSearch]; + } + + // Couldn't find a good place to truncate. Might be because there is no whitespace in the text, or we're dealing + // with a foreign language encoding. Settle for truncating at the original place, which may be mid-word. + if (rangeOfLastVisibleAvoidedChars.location == NSNotFound) { + return firstCharacterIndexToReplace; + } else { + return rangeOfLastVisibleAvoidedChars.location; + } +} + +- (void)_truncate +{ + [_context performBlockWithLockedTextKitComponents:^(NSLayoutManager *layoutManager, NSTextStorage *textStorage, NSTextContainer *textContainer) { + NSUInteger originalStringLength = textStorage.length; + + [layoutManager ensureLayoutForTextContainer:textContainer]; + + NSRange visibleGlyphRange = [layoutManager glyphRangeForBoundingRect:{ .size = textContainer.size } + inTextContainer:textContainer]; + NSRange visibleCharacterRange = [layoutManager characterRangeForGlyphRange:visibleGlyphRange + actualGlyphRange:NULL]; + + // Check if text is truncated, and if so apply our truncation string + if (visibleCharacterRange.length < originalStringLength && _truncationAttributedString.length > 0) { + NSInteger firstCharacterIndexToReplace = [self _calculateCharacterIndexBeforeTruncationMessage:layoutManager + textStorage:textStorage + textContainer:textContainer]; + if (firstCharacterIndexToReplace == 0 || firstCharacterIndexToReplace == NSNotFound) { + return; + } + + // Update/truncate the visible range of text + visibleCharacterRange = NSMakeRange(0, firstCharacterIndexToReplace); + NSRange truncationReplacementRange = NSMakeRange(firstCharacterIndexToReplace, + textStorage.length - firstCharacterIndexToReplace); + // Replace the end of the visible message with the truncation string + [textStorage replaceCharactersInRange:truncationReplacementRange + withAttributedString:_truncationAttributedString]; + } + + _visibleRanges = { visibleCharacterRange }; + }]; +} + +@end diff --git a/AsyncDisplayKit/TextKit/CKTextKitTruncating.h b/AsyncDisplayKit/TextKit/CKTextKitTruncating.h new file mode 100755 index 0000000000..b54ff16ce1 --- /dev/null +++ b/AsyncDisplayKit/TextKit/CKTextKitTruncating.h @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2014-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + * + */ + +#import + +#import + +#import "CKTextKitRenderer.h" + +@protocol CKTextKitTruncating + +@property (nonatomic, assign, readonly) std::vector visibleRanges; +@property (nonatomic, assign, readonly) CGRect truncationStringRect; + +/** + A truncater object is initialized with the full state of the text. It is a Single Responsibility Object that is + mutative. It configures the state of the TextKit components (layout manager, text container, text storage) to achieve + the intended truncation, then it stores the resulting state for later fetching. + + The truncater may mutate the state of the text storage such that only the drawn string is actually present in the + text storage itself. + + The truncater should not store a strong reference to the context to prevent retain cycles. + */ +- (instancetype)initWithContext:(CKTextKitContext *)context + truncationAttributedString:(NSAttributedString *)truncationAttributedString + avoidTailTruncationSet:(NSCharacterSet *)avoidTailTruncationSet + constrainedSize:(CGSize)constrainedSize; + +@end